diff --git a/.DS_Store b/.DS_Store index cc313d7..1d57d72 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index 7e6b035..0b848f4 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Rate limiting protects the protocol across both routes. All USD values below are - Fee abstraction caps: `setCapsUSD(minCapUsd, maxCapUsd)` - Per-block budget: `setBlockUsdCap(cap1e18)` -- Per-token epoch thresholds: `setTokenLimitThresholds(tokens[], thresholds[])`, `updateTokenLimitThreshold(tokens[], thresholds[])` +- Per-token epoch thresholds: `setTokenLimitThresholds(tokens[], thresholds[])` - Epoch duration: `updateEpochDuration(newDurationSec)` ### Additional Safety @@ -175,7 +175,7 @@ Administrative setters (all `onlyRole(DEFAULT_ADMIN_ROLE)` and `whenNotPaused` u - Caps: `setCapsUSD(min, max)`; block budget: `setBlockUsdCap(cap1e18)` - Uniswap: `setRouters(factory, router)`, `setV3FeeOrder(a, b, c)`, `setDefaultSwapDeadline(deadlineSec)` - Chainlink: `setEthUsdFeed(addr)`, `setChainlinkStalePeriod(sec)`, `setL2SequencerFeed(addr)`, `setL2SequencerGracePeriod(sec)` -- Rate limits: `setTokenLimitThresholds(tokens[], thresholds[])`, `updateTokenLimitThreshold(tokens[], thresholds[])`, `updateEpochDuration(sec)` +- Rate limits: `setTokenLimitThresholds(tokens[], thresholds[])`, `updateEpochDuration(sec)` Withdrawals (TSS only): - `withdrawFunds(recipient, token, amount)` diff --git a/contracts/.DS_Store b/contracts/.DS_Store index a367d09..a14410a 100644 Binary files a/contracts/.DS_Store and b/contracts/.DS_Store differ diff --git a/contracts/evm-gateway/README.md b/contracts/evm-gateway/README.md index d8d3cd9..0608e33 100644 --- a/contracts/evm-gateway/README.md +++ b/contracts/evm-gateway/README.md @@ -89,7 +89,7 @@ From `IUniversalGateway.sol`: Core structs and enums in `src/libraries/Types.sol`: - `TX_TYPE`: `GAS`, `GAS_AND_PAYLOAD`, `FUNDS`, `FUNDS_AND_PAYLOAD` -- `RevertInstructions { address fundRecipient; bytes revertContext; }` +- `RevertInstructions { address revertRecipient; bytes revertMsg; }` - `UniversalPayload { to, value, data, gasLimit, maxFeePerGas, maxPriorityFeePerGas, nonce, deadline, vType }` - `EpochUsage { uint64 epoch; uint192 used; }` @@ -130,7 +130,7 @@ Rate limiting protects the protocol across both routes. All USD values below are - Fee abstraction caps: `setCapsUSD(minCapUsd, maxCapUsd)` - Per-block budget: `setBlockUsdCap(cap1e18)` -- Per-token epoch thresholds: `setTokenLimitThresholds(tokens[], thresholds[])`, `updateTokenLimitThreshold(tokens[], thresholds[])` +- Per-token epoch thresholds: `setTokenLimitThresholds(tokens[], thresholds[])` - Epoch duration: `updateEpochDuration(newDurationSec)` ### Additional Safety @@ -174,7 +174,7 @@ Administrative setters (all `onlyRole(DEFAULT_ADMIN_ROLE)` and `whenNotPaused` u - Caps: `setCapsUSD(min, max)`; block budget: `setBlockUsdCap(cap1e18)` - Uniswap: `setRouters(factory, router)`, `setV3FeeOrder(a, b, c)`, `setDefaultSwapDeadline(deadlineSec)` - Chainlink: `setEthUsdFeed(addr)`, `setChainlinkStalePeriod(sec)`, `setL2SequencerFeed(addr)`, `setL2SequencerGracePeriod(sec)` -- Rate limits: `setTokenLimitThresholds(tokens[], thresholds[])`, `updateTokenLimitThreshold(tokens[], thresholds[])`, `updateEpochDuration(sec)` +- Rate limits: `setTokenLimitThresholds(tokens[], thresholds[])`, `updateEpochDuration(sec)` Withdrawals (TSS only): - `withdrawFunds(recipient, token, amount)` diff --git a/contracts/evm-gateway/coverage.lcov b/contracts/evm-gateway/coverage.lcov new file mode 100644 index 0000000..7b633d9 --- /dev/null +++ b/contracts/evm-gateway/coverage.lcov @@ -0,0 +1,816 @@ +Warning: This is a nightly build of Foundry. It is recommended to use the latest stable version. Visit https://book.getfoundry.sh/announcements for more information. +To mute this warning set `FOUNDRY_DISABLE_NIGHTLY_WARNING` in your environment. + +Warning: `--ir-minimum` enables `viaIR` with minimum optimization, which can result in inaccurate source mappings. +Only use this flag as a workaround if you are experiencing "stack too deep" errors. +Note that `viaIR` is production ready since Solidity 0.8.13 and above. +See more: https://github.com/foundry-rs/foundry/issues/3357 +Compiling 110 files with Solc 0.8.26 +Solc 0.8.26 finished in 90.90s +Compiler run successful with warnings: +Warning (3420): Source file does not specify required compiler version! Consider adding "pragma solidity ^0.8.26;" +--> test/mocks/MockAggregatorV3.sol + +Warning (3420): Source file does not specify required compiler version! Consider adding "pragma solidity ^0.8.26;" +--> test/mocks/MockSequencerUptimeFeed.sol + +Warning (2072): Unused local variable. + --> src/UniversalGatewayV0.sol:258:9: + | +258 | uint24[3] memory old = v3FeeOrder; + | ^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> src/UniversalGatewayV0.sol:343:25: + | +343 | (uint256 price, uint8 decimals) = getEthUsdPrice_old(); + | ^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> src/UniversalGatewayV0.sol:1198:10: + | +1198 | (IUniswapV3Pool pool, uint24 fee) = _findV3PoolWithNative(tokenIn); + | ^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> src/UniversalGateway.sol:913:10: + | +913 | (IUniswapV3Pool pool, uint24 fee) = _findV3PoolWithNative(tokenIn); + | ^^^^^^^^^^^^^^^^^^^ + +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + --> test/mocks/MockERC20.sol:177:38: + | +177 | function simulateTransferFailure(address to, uint256 amount) external pure returns (bool) { + | ^^^^^^^^^^ + +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + --> test/mocks/MockERC20.sol:177:50: + | +177 | function simulateTransferFailure(address to, uint256 amount) external pure returns (bool) { + | ^^^^^^^^^^^^^^ + +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + --> test/mocks/MockERC20.sol:188:38: + | +188 | function simulateApprovalFailure(address spender, uint256 amount) external pure returns (bool) { + | ^^^^^^^^^^^^^^^ + +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + --> test/mocks/MockERC20.sol:188:55: + | +188 | function simulateApprovalFailure(address spender, uint256 amount) external pure returns (bool) { + | ^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/11_executeUniversalTx.t.sol:458:9: + | +458 | bytes memory tokenPayload = + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:849:9: + | +849 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:896:9: + | +896 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:1048:9: + | +1048 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:1087:9: + | +1087 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:1119:9: + | +1119 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:1155:9: + | +1155 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:1189:9: + | +1189 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:1337:9: + | +1337 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:1390:9: + | +1390 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:1478:9: + | +1478 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:1610:9: + | +1610 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/13_rateLimit_EpochBased.t.sol:1645:9: + | +1645 | RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + --> test/mocks/MockUniversalCoreReal.sol:138:9: + | +138 | uint256 minPCOut, + | ^^^^^^^^^^^^^^^^ + +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + --> test/mocks/MockUniversalCoreReal.sol:247:27: + | +247 | function getSwapQuote(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn) public returns (uint256) { + | ^^^^^^^^^^^^^^^ + +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + --> test/mocks/MockUniversalCoreReal.sol:247:44: + | +247 | function getSwapQuote(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn) public returns (uint256) { + | ^^^^^^^^^^^^^^^^ + +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + --> test/mocks/MockUniversalCoreReal.sol:247:62: + | +247 | function getSwapQuote(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn) public returns (uint256) { + | ^^^^^^^^^^ + +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + --> test/mocks/MockReentrantContract.sol:113:27: + | +113 | function transferFrom(address from, address to, uint256 amount) external returns (bool) { + | ^^^^^^^^^^^^ + +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + --> test/mocks/MockReentrantContract.sol:113:41: + | +113 | function transferFrom(address from, address to, uint256 amount) external returns (bool) { + | ^^^^^^^^^^ + +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + --> test/mocks/MockReentrantContract.sol:113:53: + | +113 | function transferFrom(address from, address to, uint256 amount) external returns (bool) { + | ^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/mocks/MockReentrantContract.sol:115:10: + | +115 | (bool success,) = + | ^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/7_sendUniversalTxWithFUNDS_FundsTxType_Case2_2.t.sol:252:9: + | +252 | uint256 expectedGasAmount = 0.001 ether; // $2 at $2000/ETH (within caps) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/7_sendUniversalTxWithFUNDS_FundsTxType_Case2_2.t.sol:867:9: + | +867 | address explicitRecipient = address(0x999); + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/7_sendUniversalTxWithFUNDS_FundsTxType_Case2_2.t.sol:1030:9: + | +1030 | uint256 expectedGasAmount = 0.002 ether; // $4 (within USD caps) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/gateway/8_sendUniversalTxWithFUNDS_FundsTxType_Case2_3.t.sol:925:9: + | +925 | address explicitRecipient = address(0x999); + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/oracle/OracleTest.t.sol:360:10: + | +360 | (uint256 price, uint8 decimals) = gatewayInstance.getEthUsdPrice(); + | ^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/oracle/OracleTest.t.sol:388:10: + | +388 | (bool success,) = payable(address(gateway)).call{ value: amount }(""); + | ^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/oracle/OracleTest.t.sol:470:26: + | +470 | (uint256 minEth, uint256 maxEth) = gateway.getMinMaxValueForNative(); + | ^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/oracle/OracleTest.t.sol:486:10: + | +486 | (uint256 minEth, uint256 maxEth) = gateway.getMinMaxValueForNative(); + | ^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/oracle/OracleTest.t.sol:501:26: + | +501 | (uint256 minEth, uint256 maxEth) = gateway.getMinMaxValueForNative(); + | ^^^^^^^^^^^^^^ + +Warning (2072): Unused local variable. + --> test/oracle/OracleTest.t.sol:516:10: + | +516 | (uint256 minEth, uint256 maxEth) = gateway.getMinMaxValueForNative(); + | ^^^^^^^^^^^^^^ + +Warning (2018): Function state mutability can be restricted to view + --> test/BaseTest.t.sol:198:5: + | +198 | function _initializeGateway() internal { + | ^ (Relevant source part starts here and spans across multiple lines). + +Warning (2018): Function state mutability can be restricted to pure + --> test/BaseTest.t.sol:339:5: + | +339 | function buildDefaultPayload() internal view returns (UniversalPayload memory) { + | ^ (Relevant source part starts here and spans across multiple lines). + +Warning (2018): Function state mutability can be restricted to pure + --> test/BaseTest.t.sol:495:5: + | +495 | function assertDualEmitOrder(bytes32 firstTopic0, bytes32 secondTopic0, Vm.Log[] memory logs) internal { + | ^ (Relevant source part starts here and spans across multiple lines). + +Warning (2018): Function state mutability can be restricted to pure + --> test/gateway/12_rateLimit_BlockBased.t.sol:480:5: + | +480 | function _getEthAmountFromUsd(uint256 usdAmount1e18) internal view returns (uint256) { + | ^ (Relevant source part starts here and spans across multiple lines). + +Warning (2018): Function state mutability can be restricted to pure + --> test/mocks/MockUniversalCoreReal.sol:247:5: + | +247 | function getSwapQuote(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn) public returns (uint256) { + | ^ (Relevant source part starts here and spans across multiple lines). + +Warning (2018): Function state mutability can be restricted to pure + --> test/gateway/14_gatewayPC.t.sol:85:5: + | +85 | function calculateExpectedGasFee(uint256 gasLimit) internal view returns (uint256) { + | ^ (Relevant source part starts here and spans across multiple lines). + +Warning (2018): Function state mutability can be restricted to view + --> test/oracle/OracleTest.t.sol:417:5: + | +417 | function testUniswapInitialization_ValidAddresses_SetsCorrectly() public { + | ^ (Relevant source part starts here and spans across multiple lines). + +Warning (2018): Function state mutability can be restricted to view + --> test/oracle/OracleTest.t.sol:427:5: + | +427 | function testFeeTiers_AllTiersTested() public { + | ^ (Relevant source part starts here and spans across multiple lines). + +Analysing contracts... +Running tests... + +Ran 30 tests for test/gateway/10_withdrawTokens.t.sol:GatewayTSSFunctionsTest +[PASS] testOnlyTSS_NonTSSShouldRevert() (gas: 34517) +[PASS] testOnlyTSS_TSSShouldSucceed() (gas: 78768) +[PASS] testRevertUniversalTxToken_ReplayProtection() (gas: 101204) +[PASS] testRevertUniversalTx_ReplayProtection_Native() (gas: 97841) +[PASS] testRevertWithdrawFunds_ERC20Token_Success() (gas: 109751) +[PASS] testRevertWithdrawFunds_InsufficientBalance_Revert() (gas: 47336) +[PASS] testRevertWithdrawFunds_InvalidAmount_Revert() (gas: 39697) +[PASS] testRevertWithdrawFunds_InvalidRecipient_Revert() (gas: 37665) +[PASS] testRevertWithdrawFunds_MultipleTokens() (gas: 155759) +[PASS] testRevertWithdrawFunds_NativeETH_Success() (gas: 84677) +[PASS] testRevertWithdrawFunds_WhenPaused_Revert() (gas: 63591) +[PASS] testWithdrawFunds_ERC20InsufficientBalance_Revert() (gas: 46449) +[PASS] testWithdrawFunds_ERC20Token_Success() (gas: 110939) +[PASS] testWithdrawFunds_InsufficientBalance_Revert() (gas: 47212) +[PASS] testWithdrawFunds_InvalidAmount_Revert() (gas: 39708) +[PASS] testWithdrawFunds_InvalidRecipient_Revert() (gas: 36112) +[PASS] testWithdrawFunds_MultipleTokens() (gas: 231735) +[PASS] testWithdrawFunds_NativeETH_Success() (gas: 85346) +[PASS] testWithdrawFunds_ReentrancyProtection() (gas: 78282) +[PASS] testWithdrawFunds_WhenPaused_Revert() (gas: 63619) +[PASS] testWithdraw_Native_AmountMismatch_Reverts() (gas: 48468) +[PASS] testWithdraw_Native_EmitsCorrectEvent() (gas: 79563) +[PASS] testWithdraw_Native_MultipleSequentialWithdrawals() (gas: 128595) +[PASS] testWithdraw_Native_OnlyTSS() (gas: 43302) +[PASS] testWithdraw_Native_ReplayProtection() (gas: 96789) +[PASS] testWithdraw_Native_Success() (gas: 82789) +[PASS] testWithdraw_Native_WhenPaused_Reverts() (gas: 73045) +[PASS] testWithdraw_Native_ZeroAmount_Reverts() (gas: 39981) +[PASS] testWithdraw_Native_ZeroOriginCaller_Reverts() (gas: 45927) +[PASS] testWithdraw_Native_ZeroRecipient_Reverts() (gas: 46209) +Suite result: ok. 30 passed; 0 failed; 0 skipped; finished in 10.54ms (6.41ms CPU time) + +Ran 23 tests for test/gateway/3_sendUniversalTx_token.t.sol:GatewaySendUniversalTxTokenGasTest +[PASS] test_TokenGas_AcceptsMsgValue() (gas: 296504) +[PASS] test_TokenGas_BuildsCorrectUniversalTxRequest() (gas: 404) +[PASS] test_TokenGas_InferFUNDS_AND_PAYLOAD_Type() (gas: 302920) +[PASS] test_TokenGas_InferFUNDS_Type() (gas: 294131) +[PASS] test_TokenGas_InferGAS_AND_PAYLOAD_Type() (gas: 296415) +[PASS] test_TokenGas_InferGAS_Type() (gas: 292108) +[PASS] test_TokenGas_MaximumValues() (gas: 83642) +[PASS] test_TokenGas_MsgValueDoesNotAffectNativeValue() (gas: 479070) +[PASS] test_TokenGas_PreservesRevertInstruction() (gas: 308912) +[PASS] test_TokenGas_PreservesSignatureData() (gas: 289582) +[PASS] test_TokenGas_RevertOn_ExpiredDeadline() (gas: 39640) +[PASS] test_TokenGas_RevertOn_InsufficientAllowance() (gas: 83368) +[PASS] test_TokenGas_RevertOn_InsufficientBalance() (gas: 125131) +[PASS] test_TokenGas_RevertOn_NoPoolFound() (gas: 2208243) +[PASS] test_TokenGas_RevertOn_Paused() (gas: 68593) +[PASS] test_TokenGas_RevertOn_SlippageExceeded() (gas: 304361) +[PASS] test_TokenGas_RevertOn_UniswapNotConfigured() (gas: 8047386) +[PASS] test_TokenGas_RevertOn_ZeroAmountOutMinETH() (gas: 38194) +[PASS] test_TokenGas_RevertOn_ZeroGasAmount() (gas: 39174) +[PASS] test_TokenGas_RevertOn_ZeroGasToken() (gas: 35932) +[PASS] test_TokenGas_RoutesCorrectly() (gas: 1715) +[PASS] test_TokenGas_WETHFastPath_Success() (gas: 185918) +[PASS] test_TokenGas_ZeroDeadlineUsesDefault() (gas: 289365) +Suite result: ok. 23 passed; 0 failed; 0 skipped; finished in 34.00ms (17.77ms CPU time) + +Ran 21 tests for test/gateway/5_sendUniversalTx_FundsTxType_Case_1.t.sol:GatewaySendUniversalTxWithFundsTest +[PASS] test_SendTxWithFunds_FUNDS_ERC20_HappyPath() (gas: 163901) +[PASS] test_SendTxWithFunds_FUNDS_ERC20_RevertOn_InsufficientAllowance() (gas: 124592) +[PASS] test_SendTxWithFunds_FUNDS_ERC20_RevertOn_InsufficientBalance() (gas: 120682) +[PASS] test_SendTxWithFunds_FUNDS_ERC20_RevertOn_NonZeroMsgValue() (gas: 45994) +[PASS] test_SendTxWithFunds_FUNDS_ERC20_RevertOn_RateLimitExceeded() (gas: 73313) +[PASS] test_SendTxWithFunds_FUNDS_ERC20_RevertOn_UnsupportedToken() (gas: 2184487) +[PASS] test_SendTxWithFunds_FUNDS_ERC20_SeparateRateLimitsPerToken() (gas: 282888) +[PASS] test_SendTxWithFunds_FUNDS_EventPreservesrevertMsg() (gas: 102392) +[PASS] test_SendTxWithFunds_FUNDS_MultipleUsersIndependent() (gas: 187387) +[PASS] test_SendTxWithFunds_FUNDS_Native_AllowsZeroRecipient() (gas: 98938) +[PASS] test_SendTxWithFunds_FUNDS_Native_GatewayDoesNotAccumulate() (gas: 96475) +[PASS] test_SendTxWithFunds_FUNDS_Native_HappyPath() (gas: 123078) +[PASS] test_SendTxWithFunds_FUNDS_Native_RateLimitResetsInNewEpoch() (gas: 174776) +[PASS] test_SendTxWithFunds_FUNDS_Native_RevertOn_CumulativeRateLimitExceeded() (gas: 143807) +[PASS] test_SendTxWithFunds_FUNDS_Native_RevertOn_MsgValueMismatch_TooLow() (gas: 48426) +[PASS] test_SendTxWithFunds_FUNDS_Native_RevertOn_RateLimitExceeded() (gas: 76123) +[PASS] test_SendTxWithFunds_FUNDS_Native_RevertOn_ZeroAmount() (gas: 37032) +[PASS] test_SendTxWithFunds_FUNDS_RevertOn_NonEmptyPayload() (gas: 102381) +[PASS] test_SendTxWithFunds_FUNDS_RevertOn_ZerorevertRecipient() (gas: 46333) +Suite result: ok. 21 passed; 0 failed; 0 skipped; finished in 23.86ms (10.80ms CPU time) + +Ran 44 tests for test/gateway/13_rateLimit_EpochBased.t.sol:GatewayGlobalRateLimitTest +[PASS] testBelowThresholdSucceeds() (gas: 185192) +[PASS] testChangeEpochDurationMidEpoch() (gas: 360732) +[PASS] testCurrentTokenUsageAfterEpochRollover() (gas: 201825) +[PASS] testCurrentTokenUsageView() (gas: 212276) +[PASS] testCurrentTokenUsageWithZeroEpochDuration() (gas: 72779) +[PASS] testCurrentTokenUsageWithZeroThreshold() (gas: 28305) +[PASS] testEpochDurationUpdatedEvent() (gas: 33792) +[PASS] testEpochResetBehavior() (gas: 265266) +[PASS] testEpochRollover() (gas: 292226) +[PASS] testEpochRolloverRobustness() (gas: 311418) +[PASS] testExactThresholdInclusive() (gas: 210514) +[PASS] testExactThresholdSucceeds() (gas: 183900) +[PASS] testExceedingLimitWithSendFunds() (gas: 199961) +[PASS] testExceedingThresholdReverts() (gas: 91069) +[PASS] testInitialTokenThresholds() (gas: 62152) +[PASS] testMinimumThreshold() (gas: 211171) +[PASS] testMultipleEpochRollovers() (gas: 403655) +[PASS] testMultipleTokenThresholdEvents() (gas: 101515) +[PASS] testMultipleTokensWithDifferentThresholds() (gas: 592024) +[PASS] testMultipleTransactionsAccumulate() (gas: 325188) +[PASS] testNativeTokenRateLimit() (gas: 158208) +[PASS] testOnlyAdminCanSetThresholds() (gas: 69215) +[PASS] testOnlyAdminCanUpdateEpochDuration() (gas: 42188) +[PASS] testPartialUsageThenRollover() (gas: 315238) +[PASS] testPausedFundsRouteReverts() (gas: 206332) +[PASS] testSendFundsWithNative() (gas: 125814) +[PASS] testSendTxWithFundsTokenGas() (gas: 94391) +[PASS] testSetTokenLimitThresholds() (gas: 122198) +[PASS] testSetTokenLimitThresholdsAllowsUpdating() (gas: 75549) +[PASS] testSetTokenLimitThresholdsArrayMismatch() (gas: 32531) +[PASS] testSetTokenLimitThresholdsWhenPaused() (gas: 89976) +[PASS] testToggleThresholdToZero() (gas: 213306) +[PASS] testTokenLimitThresholdUpdatedEvent() (gas: 70638) +[PASS] testUnsupportedNativeToken() (gas: 76857) +[PASS] testUnsupportedTokenReverts() (gas: 48724) +[PASS] testUnsupportedTokenWithSendFunds() (gas: 48816) +[PASS] testUpdateEpochDuration() (gas: 38994) +[PASS] testUpdateEpochDurationWhenPaused() (gas: 64524) +[PASS] testUpdateThresholdMidEpochDecrease() (gas: 237385) +[PASS] testUpdateThresholdMidEpochIncrease() (gas: 267458) +[PASS] testUpdateTokenLimitThresholdWhenPaused() (gas: 100494) +[PASS] testViewHelpersFunctionality() (gas: 271521) +[PASS] testZeroEpochDuration() (gas: 217932) +[PASS] testZeroEpochDurationReverts() (gas: 91901) +Suite result: ok. 44 passed; 0 failed; 0 skipped; finished in 53.43ms (48.36ms CPU time) + +Ran 18 tests for test/gateway/6_sendUniversalTxWithFUNDS_FundsTxType_Case2_1.t.sol:GatewaySendUniversalTxWithFunds_PAYLOAD_Case2_1_Test +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_AllowsZeroRecipient() (gas: 146225) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_CumulativeRateLimit() (gas: 214317) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_ERC20_NoBatching_HappyPath() (gas: 180294) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_EventPreservesSignatureData() (gas: 150425) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_EventPreservesrevertMsg() (gas: 150208) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_GatewayDoesNotAccumulateETH() (gas: 231662) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_LargePayload() (gas: 7714167) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_MultipleERC20Tokens() (gas: 264123) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_PayloadPreserved() (gas: 152640) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_EmptyPayload() (gas: 138335) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_InsufficientAllowance() (gas: 130593) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_InsufficientBalance() (gas: 126040) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_NativeToken_NoBatching() (gas: 40819) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_RateLimitExceeded() (gas: 77934) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_UnsupportedToken() (gas: 2188834) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_ZeroAmount() (gas: 84495) +[PASS] test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_ZerorevertRecipient() (gas: 47088) +Suite result: ok. 18 passed; 0 failed; 0 skipped; finished in 68.65ms (57.90ms CPU time) + +Ran 16 tests for test/gateway/12_rateLimit_BlockBased.t.sol:GatewayBlockRateLimitTest +[PASS] testAccumulateUnderCap() (gas: 377544) +[PASS] testCrossSenderGlobalBudget() (gas: 242305) +[PASS] testDisableBlockCap() (gas: 362982) +[PASS] testExactlyEqualToBlockCap() (gas: 184829) +[PASS] testFundsOnlyRouteNotThrottled() (gas: 290165) +[PASS] testJustUnderJustOver() (gas: 228980) +[PASS] testNativeGasLegInSendTxWithFunds() (gas: 350392) +[PASS] testOverflowOnNthCall() (gas: 361139) +[PASS] testPartialUsageThenNextBlock() (gas: 567758) +[PASS] testPausedStateAndCap() (gas: 346348) +[PASS] testPerTxCapFailsFirst() (gas: 116609) +[PASS] testResetOnNextBlock() (gas: 256667) +[PASS] testRevertAfterCapCheckDoesntLeakUsage() (gas: 477966) +[PASS] testSetBlockUsdCap_HappyPath() (gas: 52598) +[PASS] testSetBlockUsdCap_OnlyAdmin() (gas: 60515) +[PASS] testSingleCallExceedsBlockCap() (gas: 156881) +Suite result: ok. 16 passed; 0 failed; 0 skipped; finished in 20.96ms (15.40ms CPU time) + +Ran 23 tests for test/gateway/4_sendUniversalTx_GasTxType.t.sol:GatewaySendUniversalTxWithGasTest +[PASS] test_SendTxWithGas_BlockCap_Disabled_AllowsUnlimited() (gas: 296022) +[PASS] test_SendTxWithGas_BlockCap_RevertOn_CumulativeExceedsCap() (gas: 223155) +[PASS] test_SendTxWithGas_BlockCap_RevertOn_SingleCallExceedsCap() (gas: 140742) +[PASS] test_SendTxWithGas_Event_WithEmpty_SignatureData() (gas: 101665) +[PASS] test_SendTxWithGas_GAS_AND_PAYLOAD_AllowsZeroGas() (gas: 68972) +[PASS] test_SendTxWithGas_GAS_AND_PAYLOAD_EmitsCorrect_UniversalTxEvent() (gas: 113766) +[PASS] test_SendTxWithGas_GAS_AND_PAYLOAD_ForwardsNative_ToTSS() (gas: 108696) +[PASS] test_SendTxWithGas_GAS_AND_PAYLOAD_RevertOn_EmptyPayload() (gas: 92594) +[PASS] test_SendTxWithGas_GAS_AND_PAYLOAD_RevertOn_ZerorevertRecipient() (gas: 50418) +[PASS] test_SendTxWithGas_GAS_AND_PAYLOAD_SucceedsWith_NonEmptyPayload() (gas: 105335) +[PASS] test_SendTxWithGas_GAS_And_GAS_AND_PAYLOAD_ShareBlockCap() (gas: 227793) +[PASS] test_SendTxWithGas_GAS_EmitsCorrect_UniversalTxEvent() (gas: 102843) +[PASS] test_SendTxWithGas_GAS_ForwardsNative_ToTSS() (gas: 100440) +[PASS] test_SendTxWithGas_GAS_RevertOn_NonEmptyPayload() (gas: 101034) +[PASS] test_SendTxWithGas_GAS_RevertOn_ZeroAmount() (gas: 36917) +[PASS] test_SendTxWithGas_GAS_RevertOn_ZerorevertRecipient() (gas: 46362) +[PASS] test_SendTxWithGas_GAS_SucceedsWith_EmptyPayload() (gas: 98091) +[PASS] test_SendTxWithGas_Gateway_DoesNotAccumulate_NativeETH() (gas: 287133) +[PASS] test_SendTxWithGas_LargePayload_DoesNotAffect_USDCaps() (gas: 7668521) +[PASS] test_SendTxWithGas_MultipleUsers_ShareBlockCap() (gas: 344256) +[PASS] test_SendTxWithGas_RevertOn_AboveMaxCap() (gas: 75179) +[PASS] test_SendTxWithGas_RevertOn_BelowMinCap() (gas: 72975) +[PASS] test_SendTxWithGas_SucceedsAt_ExactMinCap() (gas: 96886) +Suite result: ok. 23 passed; 0 failed; 0 skipped; finished in 75.90ms (70.12ms CPU time) + +Ran 37 tests for test/gateway/8_sendUniversalTxWithFUNDS_FundsTxType_Case2_3.t.sol:GatewaySendUniversalTxWithFunds_PAYLOAD_Case2_3_Test +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_Batching_HappyPath() (gas: 263098) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_CumulativeERC20RateLimit() (gas: 301871) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_DifferentRecipients_Work() (gas: 274049) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_DifferentTokensSameTx() (gas: 352048) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_EmitsTwoEvents_Always() (gas: 214540) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_EventsPreserveSignatureData() (gas: 194459) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_EventsPreserverevertMsg() (gas: 194101) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_FullMsgValueToGasRoute() (gas: 197854) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_GasEvent_HasEmptyPayload() (gas: 202522) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_GasEvent_NativeToken_FundsEvent_ERC20Token() (gas: 214655) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_GasEvent_RecipientAlwaysZero() (gas: 213643) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_Gateway_DoesNotAccumulate() (gas: 518404) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_IndependentAmounts() (gas: 206393) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_LargePayload_DoesNotAffectGasCaps() (gas: 7766692) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_MaximalGasAmount_AtMaxCap() (gas: 199013) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_MinimalGasAmount() (gas: 198001) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_MultipleCallsSameBlock_GasBlockCap() (gas: 410427) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_MultipleUsers() (gas: 382176) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_NativeNotConsumedInRateLimit() (gas: 524072) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_NativeToTSS_ERC20ToVault() (gas: 220692) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_PayloadPreserved() (gas: 215672) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RateLimitResetsInNewEpoch() (gas: 314671) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_CumulativeERC20RateLimitExceeded() (gas: 271927) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_ERC20RateLimitExceeded() (gas: 134096) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_EmptyPayload() (gas: 45905) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_GasAmountAboveMaxUSDCap() (gas: 85292) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_GasAmountBelowMinUSDCap() (gas: 82513) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_GasAmountExceedsBlockCap() (gas: 150395) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_InsufficientAllowance() (gas: 185474) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_InsufficientBalance() (gas: 182255) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_UnsupportedToken() (gas: 2243903) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_ZeroAmount() (gas: 114868) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_ZerorevertRecipient() (gas: 53742) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_SeparateRateLimits() (gas: 230353) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_SmallGasLargeFunds() (gas: 206154) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_VeryLargeERC20Amount_WithinRateLimit() (gas: 201934) +[PASS] test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_ZeroMsgValue_RoutesToCase2_1() (gas: 144983) +Suite result: ok. 37 passed; 0 failed; 0 skipped; finished in 79.70ms (76.34ms CPU time) + +Ran 34 tests for test/gateway/7_sendUniversalTxWithFUNDS_FundsTxType_Case2_2.t.sol:GatewaySendUniversalTxWithFunds_PAYLOAD_Case2_2_Test +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_Batching_HappyPath() (gas: 190911) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_CumulativeRateLimit() (gas: 253714) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_DifferentRecipients_Work() (gas: 225390) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_EmitsOneEvent_WhenGasAmountZero() (gas: 113349) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_EmitsTwoEvents_WhenGasAmountPositive() (gas: 169036) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_EventsPreserveSignatureData() (gas: 148589) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_EventsPreserverevertMsg() (gas: 149725) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_ExactAmount_AllToFunds() (gas: 106613) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_ExactAmount_NoGas() (gas: 119297) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_FundSplit_CorrectDistribution() (gas: 154125) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_GasEvent_HasEmptyPayload() (gas: 157363) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_GasEvent_RecipientAlwaysZero() (gas: 168277) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_Gateway_DoesNotAccumulate() (gas: 449221) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_LargePayload_DoesNotAffectGasCaps() (gas: 7722371) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_MaximalGasAmount_AtMaxCap() (gas: 153001) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_MinimalGasAmount_AtMinCap() (gas: 154220) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_MultipleCallsSameBlock_GasBlockCap() (gas: 362796) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_MultipleUsers() (gas: 303932) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_PayloadPreserved() (gas: 169127) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RateLimitOnlyForFunds() (gas: 163584) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RateLimitResetsInNewEpoch() (gas: 264925) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_CumulativeRateLimitExceeded() (gas: 226634) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_EmptyPayload() (gas: 102157) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_FundsExceedRateLimit() (gas: 129852) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_GasAmountAboveMaxUSDCap() (gas: 82908) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_GasAmountBelowMinUSDCap() (gas: 80934) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_GasAmountExceedsBlockCap() (gas: 148149) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_MsgValueLessThanAmount() (gas: 55050) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_ZeroAmount() (gas: 80110) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_ZerorevertRecipient() (gas: 50752) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_ZeroMsgValue() (gas: 41233) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_SmallGasLargeFunds() (gas: 153452) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_TSS_ReceivesFullMsgValue() (gas: 156615) +[PASS] test_Case2_2_FUNDS_AND_PAYLOAD_Native_VeryLargeFunds_WithinRateLimit() (gas: 155099) +Suite result: ok. 34 passed; 0 failed; 0 skipped; finished in 71.07ms (67.45ms CPU time) + +Ran 28 tests for test/gateway/9_sendUniversalTxFetchTxType.t.sol:GatewayFetchTxTypeTest +[PASS] test_FAP_erc20_plus_gas_basic() (gas: 195155) +[PASS] test_FAP_native_batching_basic() (gas: 150657) +[PASS] test_FAP_native_batching_missingNativeValue_revert() (gas: 42331) +[PASS] test_FAP_native_batching_zeroExtraGas_ok() (gas: 119527) +[PASS] test_FAP_nobatching_addNativeValue_becomes_FAP_erc20_plus_gas() (gas: 194396) +[PASS] test_FAP_nobatching_erc20_basic() (gas: 155631) +[PASS] test_FAP_nobatching_missingFunds_revert() (gas: 55361) +[PASS] test_FUNDS_erc20_basic() (gas: 139772) +[PASS] test_FUNDS_erc20_noFunds_revert() (gas: 39618) +[PASS] test_FUNDS_erc20_withNativeValue_revert() (gas: 46991) +[PASS] test_FUNDS_native_basic() (gas: 103259) +[PASS] test_FUNDS_native_missingNativeValue_revert() (gas: 38104) +[PASS] test_GAS_AND_PAYLOAD_basic() (gas: 118091) +[PASS] test_GAS_AND_PAYLOAD_mutation_addFunds_native_becomes_FUNDS_AND_PAYLOAD() (gas: 119964) +[PASS] test_GAS_AND_PAYLOAD_nonNativeToken_ignored() (gas: 120448) +[PASS] test_GAS_AND_PAYLOAD_payload_only_native0() (gas: 75991) +[PASS] test_GAS_basic_native() (gas: 102380) +[PASS] test_GAS_basic_nonNativeToken_ignored() (gas: 104349) +[PASS] test_GAS_mutation_hasFunds_becomes_FUNDS_native() (gas: 103742) +[PASS] test_GAS_mutation_hasPayload_becomes_GAS_AND_PAYLOAD() (gas: 117286) +[PASS] test_GAS_mutation_noNativeValue_revert() (gas: 37355) +[PASS] test_Invalid_allZero() (gas: 39963) +[PASS] test_Invalid_erc20Funds_withNoPayload_andNativeValue() (gas: 46439) +[PASS] test_Invalid_nativeFunds_noNativeValue() (gas: 37690) +[PASS] test_Invalid_payload_only_noNative() (gas: 53558) +[PASS] test_Invariance_revertInstruction_onlyRecipientMatters() (gas: 278299) +[PASS] test_Invariance_signatureData_ignored() (gas: 277647) +Suite result: ok. 28 passed; 0 failed; 0 skipped; finished in 84.32ms (13.52ms CPU time) + +Ran 52 tests for test/gateway/1_adminActions.t.sol:GatewayAdminSettersTest +[PASS] testGetMinMaxValueReflectsNewCaps() (gas: 67603) +[PASS] testModifySupportForToken() (gas: 78561) +[PASS] testModifySupportForTokenInvalidInput() (gas: 26690) +[PASS] testModifySupportForTokenOnlyAdmin() (gas: 80034) +[PASS] testPauseBlocksAllStateChangingFunctions() (gas: 110144) +[PASS] testPauseOnlyAdmin() (gas: 68393) +[PASS] testPauseUnpause() (gas: 51389) +[PASS] testSetCapsUSD() (gas: 48257) +[PASS] testSetCapsUSDInvalidRange() (gas: 26179) +[PASS] testSetCapsUSDOnlyAdmin() (gas: 57703) +[PASS] testSetCapsUSDWhenPaused() (gas: 53348) +[PASS] testSetChainlinkStalePeriod() (gas: 34806) +[PASS] testSetChainlinkStalePeriodOnlyAdmin() (gas: 22337) +[PASS] testSetChainlinkStalePeriodWhenPaused() (gas: 53173) +[PASS] testSetDefaultSwapDeadline() (gas: 35728) +[PASS] testSetDefaultSwapDeadlineOnlyAdmin() (gas: 23349) +[PASS] testSetDefaultSwapDeadlineWhenPaused() (gas: 53403) +[PASS] testSetDefaultSwapDeadlineZeroReverts() (gas: 27162) +[PASS] testSetEthUsdFeed() (gas: 617519) +[PASS] testSetEthUsdFeedOnlyAdmin() (gas: 570298) +[PASS] testSetEthUsdFeedWhenPaused() (gas: 600427) +[PASS] testSetEthUsdFeedZeroAddressReverts() (gas: 25144) +[PASS] testSetL2SequencerFeed() (gas: 472395) +[PASS] testSetL2SequencerFeedOnlyAdmin() (gas: 449903) +[PASS] testSetL2SequencerFeedWhenPaused() (gas: 479457) +[PASS] testSetL2SequencerGracePeriod() (gas: 53217) +[PASS] testSetL2SequencerGracePeriodMultipleUpdates() (gas: 58603) +[PASS] testSetL2SequencerGracePeriodOnlyAdmin() (gas: 23280) +[PASS] testSetL2SequencerGracePeriodWhenPaused() (gas: 53564) +[PASS] testSetL2SequencerGracePeriodZeroAllowed() (gas: 32877) +[PASS] testSetRouters() (gas: 81218) +[PASS] testSetRoutersOnlyAdmin() (gas: 91385) +[PASS] testSetRoutersWhenPaused() (gas: 52613) +[PASS] testSetRoutersZeroAddress() (gas: 41240) +[PASS] testSetTSSAddress() (gas: 87187) +[PASS] testSetTSSAddressOnlyAdmin() (gas: 80423) +[PASS] testSetTSSAddressWhenPaused() (gas: 94711) +[PASS] testSetTSSAddressZeroAddress() (gas: 24653) +[PASS] testSetV3FeeOrder() (gas: 66333) +[PASS] testSetV3FeeOrderOnlyAdmin() (gas: 23974) +[PASS] testSetV3FeeOrderWhenPaused() (gas: 53804) +[PASS] testUnpauseOnlyAdmin() (gas: 54160) +[PASS] testUpdateEpochDuration() (gas: 38195) +[PASS] testUpdateEpochDurationCanBeCalledWhenPaused() (gas: 62989) +[PASS] testUpdateEpochDurationMultipleUpdates() (gas: 55729) +[PASS] testUpdateEpochDurationOnlyAdmin() (gas: 48281) +[PASS] testUpdateEpochDurationZeroDuration() (gas: 28895) +[PASS] testUpdateVault() (gas: 121917) +[PASS] testUpdateVaultOnlyAdmin() (gas: 107319) +[PASS] testUpdateVaultRequiresPaused() (gas: 111844) +[PASS] testUpdateVaultRoleTransfer() (gas: 163485) +[PASS] testUpdateVaultZeroAddressReverts() (gas: 53555) +Suite result: ok. 52 passed; 0 failed; 0 skipped; finished in 13.61ms (12.39ms CPU time) + +Ran 50 tests for test/gateway/14_gatewayPC.t.sol:UniversalGatewayPCTest +[PASS] testAdminFunctionsWorkWhenPaused() (gas: 45880) +[PASS] testGasFeeCalculationAccuracy() (gas: 542612) +[PASS] testInitializeRevertDoubleInit() (gas: 3880332) +[PASS] testInitializeRevertZeroAdmin() (gas: 2787453) +[PASS] testInitializeRevertZeroPauser() (gas: 2786742) +[PASS] testInitializeRevertZeroUniversalCore() (gas: 2786645) +[PASS] testInitializeRevertZeroVaultPC() (gas: 2786462) +[PASS] testInitializeSuccess() (gas: 3897652) +[PASS] testInvalidFeeQuoteZeroGasFee() (gas: 2176404) +[PASS] testInvalidFeeQuoteZeroGasToken() (gas: 2138457) +[PASS] testMaxGasLimit() (gas: 174862) +[PASS] testPauseRevertAlreadyPaused() (gas: 50831) +[PASS] testPauseRevertNonPauser() (gas: 22625) +[PASS] testPauseSuccess() (gas: 55313) +[PASS] testReentrancyProtection() (gas: 1550217) +[PASS] testReentrancyProtectionWithExecute() (gas: 1551545) +[PASS] testSetVaultPCRevertNonAdmin() (gas: 22312) +[PASS] testSetVaultPCRevertWhenPaused() (gas: 56304) +[PASS] testSetVaultPCRevertZeroAddress() (gas: 25720) +[PASS] testSetVaultPCSuccess() (gas: 43851) +[PASS] testSetVaultPCToZeroReverts() (gas: 25467) +[PASS] testTokenBurnFailure() (gas: 2183712) +[PASS] testUnpauseRevertNonPauser() (gas: 56172) +[PASS] testUnpauseRevertNotPaused() (gas: 29597) +[PASS] testUnpauseSuccess() (gas: 45125) +[PASS] testWithdrawAndExecuteDifferentPayloadSizes() (gas: 252720) +[PASS] testWithdrawAndExecuteEventEmission() (gas: 181715) +[PASS] testWithdrawAndExecuteRevertEmptyTarget() (gas: 37274) +[PASS] testWithdrawAndExecuteRevertInsufficientGasTokenAllowance() (gas: 117142) +[PASS] testWithdrawAndExecuteRevertInsufficientGasTokenBalance() (gas: 137878) +[PASS] testWithdrawAndExecuteRevertInsufficientPrc20Balance() (gas: 122880) +[PASS] testWithdrawAndExecuteRevertInvalidRecipient() (gas: 39434) +[PASS] testWithdrawAndExecuteRevertWhenPaused() (gas: 63628) +[PASS] testWithdrawAndExecuteRevertZeroAmount() (gas: 38027) +[PASS] testWithdrawAndExecuteRevertZeroToken() (gas: 36819) +[PASS] testWithdrawAndExecuteSuccessWithComplexPayload() (gas: 182898) +[PASS] testWithdrawAndExecuteSuccessWithCustomGasLimit() (gas: 182442) +[PASS] testWithdrawAndExecuteSuccessWithDefaultGasLimit() (gas: 185667) +[PASS] testWithdrawAndExecuteSuccessWithEmptyPayload() (gas: 179797) +[PASS] testWithdrawEventEmission() (gas: 181073) +[PASS] testWithdrawRevertEmptyTarget() (gas: 36679) +[PASS] testWithdrawRevertInsufficientGasTokenAllowance() (gas: 115239) +[PASS] testWithdrawRevertInsufficientGasTokenBalance() (gas: 135561) +[PASS] testWithdrawRevertInsufficientPrc20Balance() (gas: 120471) +[PASS] testWithdrawRevertInvalidRecipient() (gas: 36884) +[PASS] testWithdrawRevertWhenPaused() (gas: 62087) +[PASS] testWithdrawRevertZeroAmount() (gas: 37248) +[PASS] testWithdrawRevertZeroToken() (gas: 34798) +[PASS] testWithdrawSuccessWithCustomGasLimit() (gas: 182470) +[PASS] testWithdrawSuccessWithDefaultGasLimit() (gas: 183312) +Suite result: ok. 50 passed; 0 failed; 0 skipped; finished in 40.50ms (42.46ms CPU time) + +Ran 5 tests for test/gateway/2_sendUniversalTx.t.sol:GatewaySendUniversalTxTest +[PASS] test_SendUniversalTx_FUNDS_AND_PAYLOAD_NoBatching_HappyPath() (gas: 145926) +[PASS] test_SendUniversalTx_FUNDS_ERC20_HappyPath() (gas: 137654) +[PASS] test_SendUniversalTx_FUNDS_Native_HappyPath() (gas: 106225) +[PASS] test_SendUniversalTx_GAS_AND_PAYLOAD_HappyPath() (gas: 117899) +[PASS] test_SendUniversalTx_GAS_HappyPath() (gas: 105687) +Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 9.83ms (4.12ms CPU time) + +Ran 67 tests for test/gateway/11_executeUniversalTx.t.sol:GatewayExecuteUniversalTxTest +[PASS] testExecuteCall_GasExhaustion_Reverts() (gas: 1040432847) +[PASS] testExecuteCall_NonPayableTargetWithValue_Reverts() (gas: 71747) +[PASS] testExecuteCall_SuccessNoValue() (gas: 157832) +[PASS] testExecuteCall_SuccessWithValue() (gas: 126910) +[PASS] testExecuteCall_TargetReverts_Reverts() (gas: 141393) +[PASS] testExecuteUniversalTx_DuplicateTxID_Reverts() (gas: 156900) +[PASS] testExecuteUniversalTx_ERC20Path_InsufficientBalance_Reverts() (gas: 81707) +[PASS] testExecuteUniversalTx_ERC20Path_NoRemainingTokens() (gas: 156235) +[PASS] testExecuteUniversalTx_ERC20Path_OnlyVault_Reverts() (gas: 169196) +[PASS] testExecuteUniversalTx_ERC20Path_RemainingTokensReturnedToVault() (gas: 248018) +[PASS] testExecuteUniversalTx_ERC20Path_ResetApprovalFails_Reverts() (gas: 153439) +[PASS] testExecuteUniversalTx_ERC20Path_SafeApproveFails_Reverts() (gas: 142315) +[PASS] testExecuteUniversalTx_ERC20Path_Success() (gas: 235000) +[PASS] testExecuteUniversalTx_ERC20Path_TargetReverts_Reverts() (gas: 147214) +[PASS] testExecuteUniversalTx_ERC20Path_WithValue_Reverts() (gas: 43408) +[PASS] testExecuteUniversalTx_ERC20WithArbitraryCall() (gas: 154536) +[PASS] testExecuteUniversalTx_EmptyPayload_Succeeds() (gas: 153692) +[PASS] testExecuteUniversalTx_EventEmittedCorrectly() (gas: 153303) +[PASS] testExecuteUniversalTx_MultipleExecutionsDifferentTxIDs() (gas: 256271) +[PASS] testExecuteUniversalTx_NativePath_ComplexPayload_Succeeds() (gas: 130990) +[PASS] testExecuteUniversalTx_NativePath_DuplicateTxID_Reverts() (gas: 137612) +[PASS] testExecuteUniversalTx_NativePath_EmptyPayload_Succeeds() (gas: 126834) +[PASS] testExecuteUniversalTx_NativePath_EventEmission_Complete() (gas: 123872) +[PASS] testExecuteUniversalTx_NativePath_GasExhaustion_Reverts() (gas: 2988621) +[PASS] testExecuteUniversalTx_NativePath_NonPayableTarget_Reverts() (gas: 73104) +[PASS] testExecuteUniversalTx_NativePath_OnlyTSS_Reverts() (gas: 150490) +[PASS] testExecuteUniversalTx_NativePath_StateNotUpdatedOnRevert() (gas: 188926) +[PASS] testExecuteUniversalTx_NativePath_Success() (gas: 132187) +[PASS] testExecuteUniversalTx_NativePath_TargetReverts_Reverts() (gas: 79392) +[PASS] testExecuteUniversalTx_NativePath_WhenPaused_Reverts() (gas: 66979) +[PASS] testExecuteUniversalTx_NativePath_WrongValue_Reverts() (gas: 73469) +[PASS] testExecuteUniversalTx_NativePath_ZeroAmount_Reverts() (gas: 41820) +[PASS] testExecuteUniversalTx_NativePath_ZeroOriginCaller_Reverts() (gas: 42053) +[PASS] testExecuteUniversalTx_NativePath_ZeroTarget_Reverts() (gas: 38715) +[PASS] testExecuteUniversalTx_NotTSS_Reverts() (gas: 32129) +[PASS] testExecuteUniversalTx_StateUpdatedCorrectly() (gas: 154564) +[PASS] testExecuteUniversalTx_ValidTSS_Succeeds() (gas: 150136) +[PASS] testExecuteUniversalTx_WhenPaused_Reverts() (gas: 59663) +[PASS] testExecuteUniversalTx_ZeroAmount_Reverts() (gas: 79306) +[PASS] testExecuteUniversalTx_ZeroOriginCaller_Reverts() (gas: 79096) +[PASS] testExecuteUniversalTx_ZeroTarget_Reverts() (gas: 75574) +[PASS] testIsExecutedMapping() (gas: 155043) +[PASS] testResetApproval_NoReturnData_Success() (gas: 269267) +[PASS] testResetApproval_ReturnsFalse_Reverts() (gas: 159722) +[PASS] testResetApproval_RevertsOnZeroApproval_Success() (gas: 258270) +[PASS] testResetApproval_StandardERC20_Success() (gas: 178408) +[PASS] testResetApproval_USDTStyle_Success() (gas: 181591) +[PASS] testSafeApprove_Idempotency_Success() (gas: 190260) +[PASS] testSafeApprove_NoReturnData_Success() (gas: 268449) +[PASS] testSafeApprove_ReturnsFalse_Reverts() (gas: 151247) +[PASS] testSafeApprove_RevertsOnApprove_Reverts() (gas: 132594) +[PASS] testSafeApprove_StandardERC20_Success() (gas: 163342) +[PASS] testSafeApprove_USDTStyle_FromNonZero_Reverts() (gas: 2252760) +[PASS] testSafeApprove_USDTStyle_FromZero_Success() (gas: 165945) +[PASS] testWithdrawFunds_DuplicateTxID_Reverts() (gas: 146370) +[PASS] testWithdrawFunds_EventEmission_Complete() (gas: 116617) +[PASS] testWithdrawFunds_InsufficientBalance_Reverts() (gas: 81481) +[PASS] testWithdrawFunds_MultipleTokens_Success() (gas: 222579) +[PASS] testWithdrawFunds_OnlyVault_Reverts() (gas: 139086) +[PASS] testWithdrawFunds_PartialWithdrawal_Success() (gas: 155498) +[PASS] testWithdrawFunds_StateNotUpdatedOnRevert() (gas: 147868) +[PASS] testWithdrawFunds_Success() (gas: 136716) +[PASS] testWithdrawFunds_WhenPaused_Reverts() (gas: 102826) +[PASS] testWithdrawFunds_ZeroAmount_Reverts() (gas: 79283) +[PASS] testWithdrawFunds_ZeroOriginCaller_Reverts() (gas: 78420) +[PASS] testWithdrawFunds_ZeroRecipient_Reverts() (gas: 74714) +[PASS] testWithdrawFunds_ZeroToken_Reverts() (gas: 77414) +Suite result: ok. 67 passed; 0 failed; 0 skipped; finished in 792.92ms (843.86ms CPU time) + +Ran 24 tests for test/gateway/3_sendUniversalTx_token_fork.t.sol:GatewaySendUniversalTxTokenGasForkTest +[PASS] test_TokenGas_AcceptsMsgValue() (gas: 306084) +[PASS] test_TokenGas_InferFUNDS_AND_PAYLOAD_Type() (gas: 347344) +[PASS] test_TokenGas_InferFUNDS_Type() (gas: 329885) +[PASS] test_TokenGas_InferGAS_AND_PAYLOAD_Type() (gas: 303061) +[PASS] test_TokenGas_InferGAS_Type() (gas: 303781) +[PASS] test_TokenGas_MaximumValues() (gas: 77506) +[PASS] test_TokenGas_MsgValueDoesNotAffectNativeValue() (gas: 460424) +[PASS] test_TokenGas_PreservesRevertInstruction() (gas: 321494) +[PASS] test_TokenGas_PreservesSignatureData() (gas: 298304) +[PASS] test_TokenGas_RevertOn_ExpiredDeadline() (gas: 120882) +[PASS] test_TokenGas_RevertOn_InsufficientAllowance() (gas: 118424) +[PASS] test_TokenGas_RevertOn_InsufficientBalance() (gas: 106030) +[PASS] test_TokenGas_RevertOn_NoPoolFound() (gas: 2203417) +[PASS] test_TokenGas_RevertOn_Paused() (gas: 150364) +[PASS] test_TokenGas_RevertOn_SlippageExceeded() (gas: 299043) +[PASS] test_TokenGas_RevertOn_UniswapNotConfigured() (gas: 8126336) +[PASS] test_TokenGas_RevertOn_ZeroAmountOutMinETH() (gas: 119968) +[PASS] test_TokenGas_RevertOn_ZeroGasAmount() (gas: 120902) +[PASS] test_TokenGas_RevertOn_ZeroGasToken() (gas: 35932) +[PASS] test_TokenGas_SwapDAI_Success() (gas: 289714) +[PASS] test_TokenGas_SwapUSDC_Success() (gas: 310582) +[PASS] test_TokenGas_SwapUSDT_Success() (gas: 307312) +[PASS] test_TokenGas_WETHFastPath_Success() (gas: 169629) +[PASS] test_TokenGas_ZeroDeadlineUsesDefault() (gas: 298540) +Suite result: ok. 24 passed; 0 failed; 0 skipped; finished in 15.17s (39.36s CPU time) + +Ran 15 test suites in 15.21s (16.55s CPU time): 472 tests passed, 0 failed, 0 skipped (472 total tests) +Wrote LCOV report. diff --git a/contracts/evm-gateway/foundry.toml b/contracts/evm-gateway/foundry.toml index e33f653..fa7fe57 100644 --- a/contracts/evm-gateway/foundry.toml +++ b/contracts/evm-gateway/foundry.toml @@ -10,7 +10,7 @@ script = "script" optimizer = true optimizer_runs = 99999 via_ir = true -evm_version = "shanghai" +evm_version = "cancun" solc_version = "0.8.26" # gas & performance diff --git a/contracts/evm-gateway/lcov.info b/contracts/evm-gateway/lcov.info index 736ec94..1437cd9 100644 --- a/contracts/evm-gateway/lcov.info +++ b/contracts/evm-gateway/lcov.info @@ -261,505 +261,570 @@ BRH:0 end_of_record TN: SF:src/UniversalGateway.sol -DA:119,356 -FN:119,UniversalGateway.initialize -FNDA:356,UniversalGateway.initialize -DA:130,356 -BRDA:130,0,0,4 -DA:131,4 -DA:134,0 -DA:135,0 -DA:136,0 -DA:137,0 -DA:139,352 -DA:140,352 -DA:141,352 -DA:142,352 -DA:144,352 -DA:145,352 -DA:146,352 -DA:147,352 -DA:149,352 -DA:150,352 -BRDA:150,1,0,2 -DA:151,2 -DA:152,2 -DA:155,352 -DA:157,352 -DA:159,352 -DA:163,13 -FN:163,UniversalGateway.onlyTSS -FNDA:13,UniversalGateway.onlyTSS -DA:164,13 -BRDA:164,2,0,1 -DA:171,104 -FN:171,UniversalGateway.pause -FNDA:104,UniversalGateway.pause -DA:172,0 -DA:174,86 -FN:174,UniversalGateway.unpause -FNDA:86,UniversalGateway.unpause -DA:175,0 -DA:180,7 -FN:180,UniversalGateway.setTSS +DA:115,637 +FN:115,UniversalGateway.initialize +FNDA:637,UniversalGateway.initialize +DA:125,637 +BRDA:125,0,0,- +DA:126,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:134,637 +DA:135,637 +DA:136,637 +DA:138,637 +DA:139,637 +DA:140,637 +DA:141,637 +DA:143,637 +DA:144,637 +BRDA:144,1,0,47 +DA:145,47 +DA:146,47 +DA:149,637 +DA:151,637 +DA:153,637 +DA:157,11 +FN:157,UniversalGateway.onlyTSS +FNDA:11,UniversalGateway.onlyTSS +DA:158,11 +BRDA:158,2,0,1 +DA:165,33 +FN:165,UniversalGateway.pause +FNDA:33,UniversalGateway.pause +DA:166,0 +DA:168,8 +FN:168,UniversalGateway.unpause +FNDA:8,UniversalGateway.unpause +DA:169,0 +DA:174,7 +FN:174,UniversalGateway.setTSS FNDA:7,UniversalGateway.setTSS -DA:181,6 -BRDA:181,3,0,1 +DA:175,6 +BRDA:175,3,0,1 +DA:176,5 +DA:179,5 +BRDA:179,4,0,5 +DA:180,5 DA:182,5 -DA:185,5 -BRDA:185,4,0,5 -DA:186,5 -DA:188,5 -DA:193,78 -FN:193,UniversalGateway.updateVault -FNDA:78,UniversalGateway.updateVault -DA:194,78 -BRDA:194,5,0,- -DA:195,78 -DA:198,78 -BRDA:198,6,0,78 -DA:199,78 -DA:201,78 -DA:202,78 -DA:208,9 -FN:208,UniversalGateway.setCapsUSD -FNDA:9,UniversalGateway.setCapsUSD -DA:209,6 -BRDA:209,7,0,1 -DA:211,5 -DA:212,5 -DA:213,5 -DA:217,18 -FN:217,UniversalGateway.setBlockUsdCap -FNDA:18,UniversalGateway.setBlockUsdCap -DA:218,17 -DA:223,5 -FN:223,UniversalGateway.setDefaultSwapDeadline +DA:187,8 +FN:187,UniversalGateway.updateVault +FNDA:8,UniversalGateway.updateVault +DA:188,6 +BRDA:188,5,0,1 +DA:189,5 +DA:192,5 +BRDA:192,6,0,5 +DA:193,5 +DA:195,5 +DA:196,5 +DA:202,8 +FN:202,UniversalGateway.setCapsUSD +FNDA:8,UniversalGateway.setCapsUSD +DA:203,5 +BRDA:203,7,0,1 +DA:205,4 +DA:206,4 +DA:207,4 +DA:211,26 +FN:211,UniversalGateway.setBlockUsdCap +FNDA:26,UniversalGateway.setBlockUsdCap +DA:212,25 +DA:217,5 +FN:217,UniversalGateway.setDefaultSwapDeadline FNDA:5,UniversalGateway.setDefaultSwapDeadline -DA:224,2 -BRDA:224,8,0,1 -DA:225,1 -DA:231,74 -FN:231,UniversalGateway.setRouters -FNDA:74,UniversalGateway.setRouters -DA:232,71 -BRDA:232,9,0,2 -DA:233,69 -DA:234,69 -DA:240,422 -FN:240,UniversalGateway.setTokenLimitThresholds -FNDA:422,UniversalGateway.setTokenLimitThresholds -DA:244,419 -BRDA:244,10,0,2 -DA:245,417 -DA:246,805 -DA:247,805 -DA:254,10 -FN:254,UniversalGateway.updateTokenLimitThreshold -FNDA:10,UniversalGateway.updateTokenLimitThreshold -DA:258,8 -BRDA:258,11,0,1 -DA:259,7 -DA:260,7 -DA:261,7 -DA:267,11 -FN:267,UniversalGateway.updateEpochDuration -FNDA:11,UniversalGateway.updateEpochDuration -DA:268,9 -DA:269,9 -DA:270,9 -DA:277,56 -FN:277,UniversalGateway.setV3FeeOrder -FNDA:56,UniversalGateway.setV3FeeOrder -DA:278,53 -DA:283,266 -FN:283,UniversalGateway.setEthUsdFeed -FNDA:266,UniversalGateway.setEthUsdFeed -DA:284,264 -BRDA:284,12,0,1 -DA:285,263 -DA:287,263 -DA:288,263 -DA:289,263 -DA:294,25 -FN:294,UniversalGateway.setChainlinkStalePeriod -FNDA:25,UniversalGateway.setChainlinkStalePeriod -DA:295,22 -DA:301,25 -FN:301,UniversalGateway.setL2SequencerFeed -FNDA:25,UniversalGateway.setL2SequencerFeed -DA:302,22 -DA:307,4 -FN:307,UniversalGateway.setL2SequencerGracePeriod -FNDA:4,UniversalGateway.setL2SequencerGracePeriod -DA:308,1 -DA:316,67 -FN:316,UniversalGateway.sendTxWithGas -FNDA:67,UniversalGateway.sendTxWithGas -DA:322,67 -DA:328,13 -FN:328,UniversalGateway.sendTxWithGas -FNDA:13,UniversalGateway.sendTxWithGas -DA:337,13 -BRDA:337,13,0,1 -DA:338,12 -BRDA:338,14,0,1 -DA:339,11 -BRDA:339,15,0,1 -DA:340,10 -BRDA:340,16,0,1 -DA:343,9 -DA:345,9 -DA:352,86 -FN:352,UniversalGateway._sendTxWithGas -FNDA:86,UniversalGateway._sendTxWithGas -DA:360,86 -BRDA:360,17,0,1 -DA:363,85 -DA:364,65 -DA:365,55 -DA:367,85 -DA:384,84 -FN:384,UniversalGateway.sendFunds -FNDA:84,UniversalGateway.sendFunds -DA:390,84 -BRDA:390,18,0,2 -DA:392,82 -BRDA:392,19,0,13 -BRDA:392,19,1,2 -DA:393,13 -BRDA:393,20,0,2 -DA:394,11 -DA:395,9 -DA:397,69 -BRDA:397,21,0,2 -DA:398,67 -DA:399,53 -DA:402,11 -DA:415,7 -FN:415,UniversalGateway.sendTxWithFunds -FNDA:7,UniversalGateway.sendTxWithFunds -DA:422,7 -BRDA:422,22,0,- -DA:423,7 -DA:424,7 -BRDA:424,23,0,- -DA:426,7 -DA:429,3 -DA:430,3 -DA:432,7 -DA:445,18 -FN:445,UniversalGateway.sendTxWithFunds -FNDA:18,UniversalGateway.sendTxWithFunds -DA:456,18 -BRDA:456,24,0,1 -DA:457,17 -BRDA:457,25,0,2 -DA:458,15 -BRDA:458,26,0,2 -DA:461,13 -DA:463,8 -DA:466,5 -DA:467,4 -DA:468,13 -DA:482,69 -FN:482,UniversalGateway._sendTxWithFunds -FNDA:69,UniversalGateway._sendTxWithFunds -DA:492,69 -BRDA:492,27,0,- -DA:494,69 -BRDA:494,28,0,7 -DA:495,7 -BRDA:495,29,0,- -DA:496,0 -DA:500,69 -DA:513,9 -FN:513,UniversalGateway.revertUniversalTxToken -FNDA:9,UniversalGateway.revertUniversalTxToken -DA:519,8 -BRDA:519,30,0,- -DA:520,8 -BRDA:520,31,0,- -DA:522,8 -DA:524,8 -DA:528,12 -FN:528,UniversalGateway.revertUniversalTx -FNDA:12,UniversalGateway.revertUniversalTx -DA:535,12 -BRDA:535,32,0,2 -DA:536,10 -BRDA:536,33,0,4 -DA:538,6 -DA:539,6 +DA:218,2 +BRDA:218,8,0,1 +DA:219,1 +DA:225,70 +FN:225,UniversalGateway.setRouters +FNDA:70,UniversalGateway.setRouters +DA:226,67 +BRDA:226,9,0,2 +DA:227,65 +DA:228,65 +DA:234,746 +FN:234,UniversalGateway.setTokenLimitThresholds +FNDA:746,UniversalGateway.setTokenLimitThresholds +DA:238,742 +BRDA:238,10,0,2 +DA:239,740 +DA:240,1576 +DA:241,1576 +DA:247,19 +FN:247,UniversalGateway.updateEpochDuration +FNDA:19,UniversalGateway.updateEpochDuration +DA:248,16 +DA:249,16 +DA:250,16 +DA:257,51 +FN:257,UniversalGateway.setV3FeeOrder +FNDA:51,UniversalGateway.setV3FeeOrder +DA:258,48 +DA:263,588 +FN:263,UniversalGateway.setEthUsdFeed +FNDA:588,UniversalGateway.setEthUsdFeed +DA:264,586 +BRDA:264,11,0,1 +DA:265,585 +DA:267,585 +DA:268,585 +DA:269,585 +DA:274,4 +FN:274,UniversalGateway.setChainlinkStalePeriod +FNDA:4,UniversalGateway.setChainlinkStalePeriod +DA:275,1 +DA:281,5 +FN:281,UniversalGateway.setL2SequencerFeed +FNDA:5,UniversalGateway.setL2SequencerFeed +DA:282,2 +DA:287,8 +FN:287,UniversalGateway.setL2SequencerGracePeriod +FNDA:8,UniversalGateway.setL2SequencerGracePeriod +DA:288,5 +DA:296,318 +FN:296,UniversalGateway.sendUniversalTx +FNDA:318,UniversalGateway.sendUniversalTx +DA:297,318 +DA:298,318 +DA:299,318 +DA:303,45 +FN:303,UniversalGateway.sendUniversalTx +FNDA:45,UniversalGateway.sendUniversalTx +DA:305,45 +BRDA:305,12,0,2 +DA:306,43 +BRDA:306,13,0,2 +DA:307,41 +BRDA:307,14,0,2 +DA:308,39 +BRDA:308,15,0,2 +DA:311,37 +DA:314,25 +DA:323,25 +DA:324,25 +DA:341,201 +FN:341,UniversalGateway._sendTxWithGas +FNDA:201,UniversalGateway._sendTxWithGas +DA:350,201 +BRDA:350,16,0,196 +DA:352,196 +DA:353,187 +DA:354,169 +DA:358,201 +DA:368,215 +FN:368,UniversalGateway._sendTxWithFunds +FNDA:215,UniversalGateway._sendTxWithFunds +DA:371,215 +BRDA:371,17,0,87 +DA:372,87 +DA:374,87 +BRDA:374,18,0,25 +BRDA:374,18,1,- +DA:375,25 +BRDA:375,19,0,2 +DA:376,23 +DA:380,62 +BRDA:380,20,0,- +DA:381,62 +DA:384,85 +DA:385,66 +DA:387,23 +DA:414,192 +BRDA:414,21,0,128 +DA:415,128 +DA:417,128 +BRDA:417,22,0,21 +BRDA:417,22,1,59 +DA:418,21 +BRDA:418,23,0,- +DA:420,21 +DA:423,107 +BRDA:423,24,0,48 +BRDA:423,24,1,59 +DA:424,48 +BRDA:424,25,0,1 +DA:426,47 +DA:428,47 +BRDA:428,26,0,40 +DA:429,40 +DA:433,43 +DA:436,59 +BRDA:436,27,0,59 +DA:437,0 +DA:439,59 +DA:443,59 +DA:446,118 +DA:447,110 +DA:448,21 +DA:470,343 +FN:470,UniversalGateway._emitUniversalTx +FNDA:343,UniversalGateway._emitUniversalTx +DA:480,343 +DA:498,8 +FN:498,UniversalGateway.revertUniversalTxToken +FNDA:8,UniversalGateway.revertUniversalTxToken +DA:509,1 +BRDA:509,28,0,1 +DA:511,6 +BRDA:511,29,0,- +DA:512,6 +BRDA:512,30,0,- +DA:514,6 +DA:515,6 +DA:517,6 +DA:521,14 +FN:521,UniversalGateway.revertUniversalTx +FNDA:14,UniversalGateway.revertUniversalTx +DA:532,1 +BRDA:532,31,0,1 +DA:534,13 +BRDA:534,32,0,2 +DA:535,11 +BRDA:535,33,0,4 +DA:537,7 +DA:538,7 +DA:539,7 BRDA:539,34,0,- -DA:541,6 -DA:548,13 -FN:548,UniversalGateway.withdrawToken -FNDA:13,UniversalGateway.withdrawToken -DA:555,0 -BRDA:555,35,0,- -DA:557,13 -BRDA:557,36,0,- -DA:558,13 -BRDA:558,37,0,- -DA:559,13 -BRDA:559,38,0,- -DA:561,13 -BRDA:561,39,0,- -DA:563,13 -DA:564,13 -DA:565,13 -DA:579,39 -FN:579,UniversalGateway.executeUniversalTx -FNDA:39,UniversalGateway.executeUniversalTx -DA:587,1 -BRDA:587,40,0,1 -DA:589,37 -BRDA:589,41,0,2 -DA:590,35 -BRDA:590,42,0,1 -DA:591,34 -BRDA:591,43,0,- -DA:593,34 -BRDA:593,44,0,1 -DA:595,33 -DA:597,33 -DA:598,33 -DA:599,33 -DA:600,33 -DA:603,33 -DA:604,25 -BRDA:604,45,0,17 -DA:605,17 -DA:608,25 -DA:618,9 -FN:618,UniversalGateway.executeUniversalTx -FNDA:9,UniversalGateway.executeUniversalTx -DA:625,0 -BRDA:625,46,0,- -DA:627,9 -BRDA:627,47,0,- -DA:628,9 -BRDA:628,48,0,- -DA:629,9 -BRDA:629,49,0,3 -DA:631,6 -DA:633,6 -DA:635,6 -DA:643,32 -FN:643,UniversalGateway.isSupportedToken -FNDA:32,UniversalGateway.isSupportedToken -DA:644,32 -DA:648,15 -FN:648,UniversalGateway.getMinMaxValueForNative -FNDA:15,UniversalGateway.getMinMaxValueForNative -DA:649,15 -DA:650,15 -DA:651,15 -DA:660,153 -FN:660,UniversalGateway.getEthUsdPrice -FNDA:153,UniversalGateway.getEthUsdPrice -DA:661,153 -BRDA:661,50,0,- -DA:664,153 -BRDA:664,51,0,- +DA:541,7 +DA:549,10 +FN:549,UniversalGateway.withdraw +FNDA:10,UniversalGateway.withdraw +DA:555,1 +BRDA:555,35,0,1 +DA:557,9 +BRDA:557,36,0,2 +DA:558,7 +BRDA:558,37,0,1 +DA:559,6 +BRDA:559,38,0,1 +DA:561,5 +DA:562,5 +DA:563,5 +BRDA:563,39,0,- +DA:565,5 +DA:569,16 +FN:569,UniversalGateway.withdrawFunds +FNDA:16,UniversalGateway.withdrawFunds +DA:576,1 +BRDA:576,40,0,1 +DA:578,14 +BRDA:578,41,0,2 +DA:579,12 +BRDA:579,42,0,1 +DA:580,11 +BRDA:580,43,0,1 +DA:582,10 +BRDA:582,44,0,2 +DA:584,8 +DA:585,8 +DA:586,8 +DA:600,37 +FN:600,UniversalGateway.executeUniversalTx +FNDA:37,UniversalGateway.executeUniversalTx +DA:608,1 +BRDA:608,45,0,1 +DA:610,34 +BRDA:610,46,0,2 +DA:611,32 +BRDA:611,47,0,1 +DA:612,31 +BRDA:612,48,0,- +DA:614,31 +BRDA:614,49,0,1 +DA:616,30 +DA:618,30 +DA:619,30 +DA:620,30 +DA:621,30 +DA:624,30 +DA:625,22 +BRDA:625,50,0,18 +DA:626,18 +DA:629,22 +DA:639,21 +FN:639,UniversalGateway.executeUniversalTx +FNDA:21,UniversalGateway.executeUniversalTx +DA:646,1 +BRDA:646,51,0,1 +DA:648,19 +BRDA:648,52,0,2 +DA:649,17 +BRDA:649,53,0,1 +DA:650,16 +BRDA:650,54,0,3 +DA:652,13 +DA:654,13 +DA:656,13 +DA:664,0 +FN:664,UniversalGateway.isSupportedToken +FNDA:0,UniversalGateway.isSupportedToken DA:665,0 -DA:671,0 -DA:674,0 -BRDA:674,52,0,- -DA:677,0 -BRDA:677,53,0,- -DA:678,0 -DA:682,153 -DA:685,153 -BRDA:685,54,0,4 -DA:686,149 -BRDA:686,55,0,1 -DA:687,148 -BRDA:687,56,0,2 -DA:688,2 -DA:691,146 -DA:692,146 -BRDA:692,57,0,1 -DA:693,1 -BRDA:693,58,0,1 -DA:694,0 -DA:695,1 -BRDA:695,58,1,1 -DA:697,1 -DA:701,146 -DA:704,146 -BRDA:704,59,0,1 -DA:705,145 -DA:707,145 -DA:714,129 -FN:714,UniversalGateway.quoteEthAmountInUsd1e18 -FNDA:129,UniversalGateway.quoteEthAmountInUsd1e18 -DA:715,129 -DA:716,127 -DA:718,127 -DA:728,89 -FN:728,UniversalGateway._checkUSDCaps -FNDA:89,UniversalGateway._checkUSDCaps -DA:729,89 -DA:730,85 -BRDA:730,61,0,8 -DA:731,77 -BRDA:731,62,0,10 -DA:738,65 -FN:738,UniversalGateway._checkBlockUSDCap -FNDA:65,UniversalGateway._checkBlockUSDCap -DA:739,65 -DA:740,65 -DA:742,32 -BRDA:742,64,0,15 -DA:743,15 -DA:744,15 -DA:747,32 -DA:749,32 -BRDA:749,65,0,1 -DA:752,31 -DA:753,31 -BRDA:753,66,0,9 -DA:754,22 -DA:761,64 -FN:761,UniversalGateway._handleNativeDeposit -FNDA:64,UniversalGateway._handleNativeDeposit -DA:762,64 -DA:763,64 -BRDA:763,67,0,1 -DA:764,0 -DA:771,60 -FN:771,UniversalGateway._handleTokenDeposit -FNDA:60,UniversalGateway._handleTokenDeposit -DA:772,60 -BRDA:772,68,0,- -DA:773,60 -DA:779,58 -FN:779,UniversalGateway._resetApproval -FNDA:58,UniversalGateway._resetApproval -DA:780,58 -DA:781,58 -DA:782,58 -BRDA:782,69,0,3 -DA:784,3 -DA:787,55 -BRDA:787,70,0,55 -DA:788,55 -DA:789,55 -BRDA:789,71,0,4 -DA:795,29 -FN:795,UniversalGateway._safeApprove -FNDA:29,UniversalGateway._safeApprove -DA:796,29 -DA:797,29 -DA:798,29 -BRDA:798,72,0,1 -DA:799,1 -DA:801,28 -BRDA:801,73,0,28 -DA:802,28 -DA:803,28 -BRDA:803,74,0,- -DA:804,0 -DA:812,34 -FN:812,UniversalGateway._executeCall -FNDA:34,UniversalGateway._executeCall -DA:813,34 -DA:814,34 -BRDA:814,75,0,7 -DA:815,0 -DA:823,86 -FN:823,UniversalGateway._consumeRateLimit -FNDA:86,UniversalGateway._consumeRateLimit -DA:824,86 -DA:825,86 -BRDA:825,76,0,6 -DA:827,80 -DA:828,80 -BRDA:828,77,0,2 -DA:830,78 -DA:831,78 -DA:833,78 -BRDA:833,78,0,22 -DA:834,22 -DA:835,22 -DA:839,78 -DA:840,78 -BRDA:840,79,0,9 -DA:841,69 -DA:849,60 -FN:849,UniversalGateway.currentTokenUsage -FNDA:60,UniversalGateway.currentTokenUsage -DA:850,60 -DA:851,60 -DA:853,56 -DA:854,56 -DA:856,55 -DA:857,55 -DA:858,55 -DA:860,55 -DA:861,55 -DA:874,22 -FN:874,UniversalGateway.swapToNative -FNDA:22,UniversalGateway.swapToNative -DA:878,22 -BRDA:878,82,0,- -DA:880,22 -BRDA:880,83,0,1 -BRDA:880,83,1,1 -DA:881,1 -DA:882,21 -BRDA:882,84,0,1 -DA:883,1 -DA:885,21 -BRDA:885,85,0,1 -DA:887,20 -BRDA:887,86,0,3 -DA:889,3 -DA:891,3 -DA:892,3 -DA:893,3 -DA:896,3 -BRDA:896,87,0,- -DA:897,3 -DA:899,17 -DA:901,12 -DA:902,10 -DA:904,17 -DA:915,17 -DA:917,9 -DA:919,9 -DA:920,9 -DA:921,9 -DA:924,9 -BRDA:924,88,0,- -DA:927,17 -FN:927,UniversalGateway._findV3PoolWithNative -FNDA:17,UniversalGateway._findV3PoolWithNative -DA:928,17 -BRDA:928,89,0,- -DA:929,17 -BRDA:929,90,0,- -DA:931,0 -DA:935,17 -DA:936,27 -DA:937,27 -DA:938,27 -BRDA:938,91,0,12 -DA:939,12 -DA:942,5 -DA:947,14 -FN:947,UniversalGateway.receive -FNDA:14,UniversalGateway.receive -DA:949,14 -BRDA:949,92,0,1 -FNF:46 -FNH:46 -LF:309 -LH:290 -BRF:92 -BRH:64 +DA:669,1 +FN:669,UniversalGateway.getMinMaxValueForNative +FNDA:1,UniversalGateway.getMinMaxValueForNative +DA:670,1 +DA:671,1 +DA:672,1 +DA:681,246 +FN:681,UniversalGateway.getEthUsdPrice +FNDA:246,UniversalGateway.getEthUsdPrice +DA:682,246 +BRDA:682,55,0,- +DA:685,246 +BRDA:685,56,0,- +DA:686,0 +DA:692,0 +DA:695,0 +BRDA:695,57,0,- +DA:698,0 +BRDA:698,58,0,- +DA:699,0 +DA:703,246 +DA:706,246 +BRDA:706,59,0,- +DA:707,246 +BRDA:707,60,0,- +DA:708,246 +BRDA:708,61,0,- +DA:709,0 +DA:712,246 +DA:713,246 +BRDA:713,62,0,- +DA:714,0 +BRDA:714,63,0,- +DA:715,0 +DA:716,0 +BRDA:716,63,1,- +DA:718,0 +DA:722,246 +DA:725,246 +BRDA:725,64,0,- +DA:726,246 +DA:728,246 +DA:735,245 +FN:735,UniversalGateway.quoteEthAmountInUsd1e18 +FNDA:245,UniversalGateway.quoteEthAmountInUsd1e18 +DA:736,245 +DA:737,245 +DA:739,245 +DA:743,89 +FN:743,UniversalGateway.currentTokenUsage +FNDA:89,UniversalGateway.currentTokenUsage +DA:744,89 +DA:745,89 +DA:747,85 +DA:748,85 +DA:750,84 +DA:751,84 +DA:752,84 +DA:754,84 +DA:755,84 +DA:765,196 +FN:765,UniversalGateway._checkUSDCaps +FNDA:196,UniversalGateway._checkUSDCaps +DA:766,196 +DA:767,196 +BRDA:767,68,0,3 +DA:768,193 +BRDA:768,69,0,6 +DA:775,345 +FN:775,UniversalGateway._handleDeposits +FNDA:345,UniversalGateway._handleDeposits +DA:776,345 +BRDA:776,70,0,229 +BRDA:776,70,1,- +DA:778,229 +DA:779,229 +BRDA:779,71,0,1 +DA:782,116 +BRDA:782,72,0,- +DA:783,116 +DA:788,52 +FN:788,UniversalGateway._resetApproval +FNDA:52,UniversalGateway._resetApproval +DA:789,52 +DA:790,52 +DA:791,52 +BRDA:791,73,0,3 +DA:793,3 +DA:796,49 +BRDA:796,74,0,49 +DA:797,49 +DA:798,49 +BRDA:798,75,0,4 +DA:804,26 +FN:804,UniversalGateway._safeApprove +FNDA:26,UniversalGateway._safeApprove +DA:805,26 +DA:806,26 +DA:807,26 +BRDA:807,76,0,1 +DA:808,1 +DA:810,25 +BRDA:810,77,0,25 +DA:811,25 +DA:812,25 +BRDA:812,78,0,- +DA:813,0 +DA:821,38 +FN:821,UniversalGateway._executeCall +FNDA:38,UniversalGateway._executeCall +DA:822,38 +DA:823,38 +BRDA:823,79,0,8 +DA:824,0 +DA:831,187 +FN:831,UniversalGateway._checkBlockUSDCap +FNDA:187,UniversalGateway._checkBlockUSDCap +DA:832,187 +DA:833,187 +DA:835,49 +BRDA:835,81,0,23 +DA:836,23 +DA:837,23 +DA:840,49 +DA:842,49 +BRDA:842,82,0,4 +DA:845,45 +DA:846,45 +BRDA:846,83,0,14 +DA:847,31 +DA:856,203 +FN:856,UniversalGateway._consumeRateLimit +FNDA:203,UniversalGateway._consumeRateLimit +DA:857,203 +DA:858,203 +BRDA:858,84,0,8 +DA:860,195 +DA:861,195 +BRDA:861,85,0,2 +DA:863,193 +DA:864,193 +DA:866,193 +BRDA:866,86,0,12 +DA:867,12 +DA:868,12 +DA:872,193 +DA:873,193 +BRDA:873,87,0,17 +DA:874,176 +DA:888,37 +FN:888,UniversalGateway.swapToNative +FNDA:37,UniversalGateway.swapToNative +DA:892,37 +BRDA:892,88,0,- +DA:894,37 +BRDA:894,89,0,2 +BRDA:894,89,1,- +DA:895,2 +DA:896,35 +BRDA:896,90,0,- +DA:897,0 +DA:899,37 +BRDA:899,91,0,2 +DA:901,35 +BRDA:901,92,0,2 +DA:903,2 +DA:905,2 +DA:906,2 +DA:907,2 +DA:910,2 +BRDA:910,93,0,- +DA:911,2 +DA:913,33 +DA:915,31 +DA:916,25 +DA:918,33 +DA:929,33 +DA:931,24 +DA:933,24 +DA:934,24 +DA:935,24 +DA:938,24 +BRDA:938,94,0,1 +DA:941,33 +FN:941,UniversalGateway._findV3PoolWithNative +FNDA:33,UniversalGateway._findV3PoolWithNative +DA:942,33 +BRDA:942,95,0,- +DA:943,33 +BRDA:943,96,0,- +DA:945,0 +DA:949,33 +DA:950,37 +DA:951,37 +DA:952,37 +BRDA:952,97,0,31 +DA:953,31 +DA:956,2 +DA:975,343 +FN:975,UniversalGateway._fetchTxType +FNDA:343,UniversalGateway._fetchTxType +DA:980,343 +DA:981,343 +DA:982,343 +DA:983,343 +DA:987,343 +BRDA:987,98,0,49 +DA:988,49 +DA:994,294 +BRDA:994,99,0,57 +DA:995,57 +DA:999,237 +BRDA:999,100,0,96 +DA:1002,96 +BRDA:1002,101,0,27 +DA:1003,27 +DA:1007,69 +BRDA:1007,102,0,63 +DA:1008,63 +DA:1010,6 +DA:1014,141 +BRDA:1014,103,0,136 +DA:1016,136 +BRDA:1016,104,0,23 +DA:1017,23 +DA:1020,113 +BRDA:1020,105,0,49 +DA:1021,49 +DA:1024,64 +BRDA:1024,106,0,61 +DA:1025,61 +DA:1027,3 +DA:1030,5 +DA:1038,329 +FN:1038,UniversalGateway._routeUniversalTx +FNDA:329,UniversalGateway._routeUniversalTx +DA:1044,0 +DA:1047,329 +BRDA:1047,107,0,8 +DA:1048,8 +DA:1052,321 +BRDA:1052,108,0,102 +BRDA:1052,108,1,4 +DA:1053,102 +DA:1056,219 +BRDA:1056,109,0,219 +BRDA:1056,109,1,4 +DA:1058,219 +BRDA:1058,110,0,4 +DA:1059,4 +DA:1061,215 +DA:1065,0 +DA:1071,26 +FN:1071,UniversalGateway.receive +FNDA:26,UniversalGateway.receive +DA:1073,26 +BRDA:1073,111,0,- +FNF:45 +FNH:44 +LF:352 +LH:326 +BRF:116 +BRH:86 end_of_record TN: SF:src/UniversalGatewayPC.sol @@ -859,766 +924,705 @@ BRH:7 end_of_record TN: SF:src/UniversalGatewayV0.sol -DA:132,0 -FN:132,UniversalGatewayV0.updateVault -FNDA:0,UniversalGatewayV0.updateVault -DA:133,0 -BRDA:133,0,0,- -DA:134,0 -DA:137,0 -BRDA:137,1,0,- -DA:138,0 -DA:140,0 -DA:141,0 -DA:144,0 -FN:144,UniversalGatewayV0.initialize +DA:136,0 +FN:136,UniversalGatewayV0.initialize FNDA:0,UniversalGatewayV0.initialize +DA:149,0 +DA:150,0 +DA:151,0 +DA:152,0 +BRDA:152,0,0,- +DA:154,0 +DA:155,0 +DA:156,0 DA:157,0 -DA:158,0 DA:159,0 DA:160,0 -BRDA:160,2,0,- -DA:162,0 +DA:161,0 DA:163,0 DA:164,0 DA:165,0 DA:167,0 DA:168,0 +BRDA:168,1,0,- DA:169,0 -DA:171,0 -DA:172,0 -DA:173,0 -DA:175,0 -DA:176,0 -BRDA:176,3,0,- +DA:170,0 +DA:174,0 DA:177,0 DA:178,0 -DA:182,0 +DA:179,0 +DA:180,0 DA:185,0 -DA:186,0 -DA:187,0 -DA:188,0 -DA:193,0 -FN:193,UniversalGatewayV0.onlyTSS +FN:185,UniversalGatewayV0.onlyTSS FNDA:0,UniversalGatewayV0.onlyTSS -DA:194,0 -BRDA:194,4,0,- -DA:198,0 -FN:198,UniversalGatewayV0.version +DA:186,0 +BRDA:186,2,0,- +DA:190,0 +FN:190,UniversalGatewayV0.version FNDA:0,UniversalGatewayV0.version -DA:199,0 -DA:205,0 -FN:205,UniversalGatewayV0.pause +DA:191,0 +DA:197,0 +FN:197,UniversalGatewayV0.pause FNDA:0,UniversalGatewayV0.pause -DA:206,0 -DA:209,0 -FN:209,UniversalGatewayV0.unpause +DA:198,0 +DA:201,0 +FN:201,UniversalGatewayV0.unpause FNDA:0,UniversalGatewayV0.unpause +DA:202,0 +DA:208,0 +FN:208,UniversalGatewayV0.setTSSAddress +FNDA:0,UniversalGatewayV0.setTSSAddress +DA:209,0 +BRDA:209,3,0,- DA:210,0 +DA:213,0 +BRDA:213,4,0,- +DA:214,0 DA:216,0 -FN:216,UniversalGatewayV0.setTSSAddress -FNDA:0,UniversalGatewayV0.setTSSAddress -DA:217,0 -BRDA:217,5,0,- -DA:218,0 -DA:221,0 -BRDA:221,6,0,- DA:222,0 -DA:224,0 -DA:230,0 -FN:230,UniversalGatewayV0.setCapsUSD +FN:222,UniversalGatewayV0.setCapsUSD FNDA:0,UniversalGatewayV0.setCapsUSD -DA:231,0 -BRDA:231,7,0,- -DA:233,0 -DA:234,0 -DA:235,0 -DA:240,0 -FN:240,UniversalGatewayV0.setDefaultSwapDeadline +DA:223,0 +BRDA:223,5,0,- +DA:225,0 +DA:226,0 +DA:227,0 +DA:238,0 +FN:238,UniversalGatewayV0.setDefaultSwapDeadline FNDA:0,UniversalGatewayV0.setDefaultSwapDeadline -DA:241,0 -BRDA:241,8,0,- -DA:242,0 -DA:248,0 -FN:248,UniversalGatewayV0.setRouters +DA:239,0 +BRDA:239,6,0,- +DA:240,0 +DA:246,0 +FN:246,UniversalGatewayV0.setRouters FNDA:0,UniversalGatewayV0.setRouters +DA:247,0 +BRDA:247,7,0,- +DA:248,0 DA:249,0 -BRDA:249,9,0,- -DA:250,0 -DA:251,0 +DA:256,0 +FN:256,UniversalGatewayV0.setV3FeeOrder +FNDA:0,UniversalGatewayV0.setV3FeeOrder DA:258,0 -FN:258,UniversalGatewayV0.modifySupportForToken -FNDA:0,UniversalGatewayV0.modifySupportForToken DA:259,0 -BRDA:259,10,0,- -DA:260,0 -DA:261,0 -DA:269,0 -FN:269,UniversalGatewayV0.setV3FeeOrder -FNDA:0,UniversalGatewayV0.setV3FeeOrder -DA:271,0 -DA:272,0 -DA:276,0 -FN:276,UniversalGatewayV0.setEthUsdFeed +DA:263,0 +FN:263,UniversalGatewayV0.setEthUsdFeed FNDA:0,UniversalGatewayV0.setEthUsdFeed -DA:277,0 -BRDA:277,11,0,- -DA:278,0 +DA:264,0 +BRDA:264,8,0,- +DA:265,0 +DA:267,0 +DA:268,0 +DA:269,0 +DA:274,0 +FN:274,UniversalGatewayV0.setChainlinkStalePeriod +FNDA:0,UniversalGatewayV0.setChainlinkStalePeriod +DA:275,0 DA:280,0 +FN:280,UniversalGatewayV0.setL2SequencerFeed +FNDA:0,UniversalGatewayV0.setL2SequencerFeed DA:281,0 -DA:282,0 +DA:286,0 +FN:286,UniversalGatewayV0.setL2SequencerGracePeriod +FNDA:0,UniversalGatewayV0.setL2SequencerGracePeriod DA:287,0 -FN:287,UniversalGatewayV0.setChainlinkStalePeriod -FNDA:0,UniversalGatewayV0.setChainlinkStalePeriod -DA:288,0 DA:293,0 -FN:293,UniversalGatewayV0.setL2SequencerFeed -FNDA:0,UniversalGatewayV0.setL2SequencerFeed -DA:294,0 +FN:293,UniversalGatewayV0.setTokenLimitThresholds +FNDA:0,UniversalGatewayV0.setTokenLimitThresholds +DA:297,0 +BRDA:297,9,0,- +DA:298,0 DA:299,0 -FN:299,UniversalGatewayV0.setL2SequencerGracePeriod -FNDA:0,UniversalGatewayV0.setL2SequencerGracePeriod DA:300,0 -DA:304,0 -FN:304,UniversalGatewayV0.setBlockUsdCap -FNDA:0,UniversalGatewayV0.setBlockUsdCap -DA:305,0 -DA:311,0 -FN:311,UniversalGatewayV0.setTokenLimitThresholds -FNDA:0,UniversalGatewayV0.setTokenLimitThresholds -DA:315,0 -BRDA:315,12,0,- -DA:316,0 -DA:317,0 -DA:318,0 -DA:325,0 -FN:325,UniversalGatewayV0.updateTokenLimitThreshold -FNDA:0,UniversalGatewayV0.updateTokenLimitThreshold -DA:329,0 -BRDA:329,13,0,- -DA:330,0 DA:331,0 +FN:331,UniversalGatewayV0.addFunds +FNDA:0,UniversalGatewayV0.addFunds DA:332,0 +DA:335,0 +FN:335,UniversalGatewayV0._addFunds +FNDA:0,UniversalGatewayV0._addFunds DA:338,0 -FN:338,UniversalGatewayV0.updateEpochDuration -FNDA:0,UniversalGatewayV0.updateEpochDuration DA:339,0 DA:340,0 -DA:341,0 -DA:362,0 -FN:362,UniversalGatewayV0.addFunds -FNDA:0,UniversalGatewayV0.addFunds -DA:363,0 +DA:343,0 +DA:346,0 +DA:347,0 +DA:348,0 +DA:349,0 +DA:351,0 +DA:361,0 +DA:364,0 +DA:365,0 DA:366,0 -FN:366,UniversalGatewayV0._addFunds -FNDA:0,UniversalGatewayV0._addFunds +DA:367,0 DA:369,0 -DA:370,0 -DA:371,0 DA:374,0 -DA:377,0 -DA:378,0 -DA:379,0 -DA:380,0 -DA:382,0 +DA:385,0 +FN:385,UniversalGatewayV0.sendTxWithFunds +FNDA:0,UniversalGatewayV0.sendTxWithFunds DA:392,0 -DA:395,0 -DA:396,0 -DA:397,0 +BRDA:392,10,0,- +DA:393,0 +DA:394,0 +BRDA:394,11,0,- DA:398,0 -DA:400,0 -DA:405,0 -DA:410,0 -FN:410,UniversalGatewayV0.sendTxWithGas -FNDA:0,UniversalGatewayV0.sendTxWithGas -DA:416,0 -DA:421,0 -FN:421,UniversalGatewayV0.sendTxWithGas -FNDA:0,UniversalGatewayV0.sendTxWithGas -DA:430,0 -BRDA:430,14,0,- +DA:401,0 +DA:402,0 +DA:415,0 +FN:415,UniversalGatewayV0.sendTxWithFunds +FNDA:0,UniversalGatewayV0.sendTxWithFunds +DA:426,0 +BRDA:426,12,0,- +DA:427,0 +BRDA:427,13,0,- +DA:428,0 +BRDA:428,14,0,- DA:431,0 -BRDA:431,15,0,- -DA:432,0 -BRDA:432,16,0,- DA:434,0 -BRDA:434,17,0,- +DA:436,0 DA:437,0 -DA:439,0 -DA:456,0 -FN:456,UniversalGatewayV0._sendTxWithGas -FNDA:0,UniversalGatewayV0._sendTxWithGas +DA:452,0 +FN:452,UniversalGatewayV0._sendTxWithFunds_old +FNDA:0,UniversalGatewayV0._sendTxWithFunds_old +DA:462,0 +BRDA:462,15,0,- DA:464,0 -BRDA:464,18,0,- -DA:469,0 +BRDA:464,16,0,- +DA:465,0 +BRDA:465,17,0,- +DA:466,0 DA:470,0 -DA:472,0 -DA:489,0 -FN:489,UniversalGatewayV0.sendFunds -FNDA:0,UniversalGatewayV0.sendFunds +DA:485,0 +FN:485,UniversalGatewayV0.sendUniversalTx +FNDA:0,UniversalGatewayV0.sendUniversalTx +DA:486,0 +DA:487,0 +DA:488,0 +DA:491,0 +FN:491,UniversalGatewayV0.sendUniversalTx +FNDA:0,UniversalGatewayV0.sendUniversalTx +DA:492,0 +BRDA:492,18,0,- +DA:493,0 +BRDA:493,19,0,- +DA:494,0 +BRDA:494,20,0,- DA:495,0 -BRDA:495,19,0,- -DA:497,0 -BRDA:497,20,0,- -BRDA:497,20,1,- +BRDA:495,21,0,- DA:498,0 -BRDA:498,21,0,- -DA:500,0 -DA:502,0 -BRDA:502,22,0,- -DA:504,0 -DA:507,0 -DA:520,0 -FN:520,UniversalGatewayV0.sendTxWithFunds -FNDA:0,UniversalGatewayV0.sendTxWithFunds -DA:527,0 -BRDA:527,23,0,- +DA:501,0 +DA:510,0 +DA:511,0 +DA:516,0 +FN:516,UniversalGatewayV0._sendTxWithGas +FNDA:0,UniversalGatewayV0._sendTxWithGas +DA:524,0 +BRDA:524,22,0,- DA:528,0 -DA:529,0 -BRDA:529,24,0,- -DA:533,0 +DA:531,0 DA:536,0 -DA:537,0 -DA:550,0 -FN:550,UniversalGatewayV0.sendTxWithFunds_new -FNDA:0,UniversalGatewayV0.sendTxWithFunds_new -DA:557,0 -BRDA:557,25,0,- -DA:558,0 -DA:559,0 -BRDA:559,26,0,- -DA:561,0 -DA:565,0 -DA:567,0 -DA:581,0 -FN:581,UniversalGatewayV0.sendTxWithFunds -FNDA:0,UniversalGatewayV0.sendTxWithFunds +FN:536,UniversalGatewayV0._sendTxWithFunds +FNDA:0,UniversalGatewayV0._sendTxWithFunds +DA:539,0 +BRDA:539,23,0,- +DA:540,0 +DA:542,0 +BRDA:542,24,0,- +BRDA:542,24,1,- +DA:543,0 +BRDA:543,25,0,- +DA:544,0 +DA:548,0 +BRDA:548,26,0,- +DA:549,0 +DA:553,0 +DA:555,0 +DA:582,0 +BRDA:582,27,0,- +DA:583,0 +DA:585,0 +BRDA:585,28,0,- +BRDA:585,28,1,- +DA:586,0 +BRDA:586,29,0,- +DA:588,0 +DA:591,0 +BRDA:591,30,0,- +BRDA:591,30,1,- DA:592,0 -BRDA:592,27,0,- -DA:593,0 -BRDA:593,28,0,- +BRDA:592,31,0,- DA:594,0 -BRDA:594,29,0,- +DA:596,0 +BRDA:596,32,0,- DA:597,0 -DA:600,0 -DA:602,0 -DA:603,0 -DA:617,0 -FN:617,UniversalGatewayV0.sendTxWithFunds_new -FNDA:0,UniversalGatewayV0.sendTxWithFunds_new -DA:628,0 -BRDA:628,30,0,- -DA:629,0 -BRDA:629,31,0,- -DA:630,0 -BRDA:630,32,0,- -DA:633,0 -DA:635,0 +DA:601,0 +DA:604,0 +BRDA:604,33,0,- +DA:605,0 +DA:607,0 +DA:611,0 +DA:615,0 +DA:616,0 DA:639,0 -DA:640,0 -DA:661,0 -FN:661,UniversalGatewayV0._sendTxWithFunds -FNDA:0,UniversalGatewayV0._sendTxWithFunds -DA:671,0 -BRDA:671,33,0,- +FN:639,UniversalGatewayV0.sendTxWithGas +FNDA:0,UniversalGatewayV0.sendTxWithGas +DA:644,0 +DA:663,0 +FN:663,UniversalGatewayV0.sendTxWithGas +FNDA:0,UniversalGatewayV0.sendTxWithGas +DA:672,0 +BRDA:672,34,0,- DA:673,0 -BRDA:673,34,0,- +BRDA:673,35,0,- +DA:674,0 +BRDA:674,36,0,- DA:675,0 -DA:676,0 -DA:677,0 -BRDA:677,35,0,- +BRDA:675,37,0,- DA:678,0 -DA:682,0 -DA:699,0 -FN:699,UniversalGatewayV0.withdrawFunds -FNDA:0,UniversalGatewayV0.withdrawFunds -DA:704,0 -BRDA:704,36,0,- -DA:705,0 -BRDA:705,37,0,- -DA:707,0 -BRDA:707,38,0,- -BRDA:707,38,1,- -DA:708,0 -DA:710,0 -DA:713,0 -DA:717,0 -FN:717,UniversalGatewayV0.revertWithdrawFunds -FNDA:0,UniversalGatewayV0.revertWithdrawFunds -DA:722,0 -BRDA:722,39,0,- -DA:723,0 -BRDA:723,40,0,- -DA:725,0 -BRDA:725,41,0,- -BRDA:725,41,1,- -DA:726,0 +DA:680,0 +DA:695,0 +FN:695,UniversalGatewayV0.sendFunds +FNDA:0,UniversalGatewayV0.sendFunds +DA:702,0 +DA:711,0 +DA:720,0 +FN:720,UniversalGatewayV0.sendTxWithFunds_new +FNDA:0,UniversalGatewayV0.sendTxWithFunds_new DA:728,0 -DA:731,0 -DA:738,0 -FN:738,UniversalGatewayV0.revertTokens -FNDA:0,UniversalGatewayV0.revertTokens -DA:744,0 -BRDA:744,42,0,- -DA:745,0 -BRDA:745,43,0,- -DA:747,0 -DA:749,0 -DA:755,0 -FN:755,UniversalGatewayV0.revertNative -FNDA:0,UniversalGatewayV0.revertNative +DA:737,0 +DA:751,0 +FN:751,UniversalGatewayV0.sendTxWithFunds_new +FNDA:0,UniversalGatewayV0.sendTxWithFunds_new DA:762,0 -BRDA:762,44,0,- +BRDA:762,38,0,- DA:763,0 -BRDA:763,45,0,- +BRDA:763,39,0,- +DA:764,0 +BRDA:764,40,0,- DA:765,0 +BRDA:765,41,0,- DA:766,0 -BRDA:766,46,0,- -DA:768,0 -DA:778,0 -FN:778,UniversalGatewayV0.isTokenSupported -FNDA:0,UniversalGatewayV0.isTokenSupported +BRDA:766,42,0,- +DA:769,0 +DA:771,0 DA:780,0 -DA:787,0 -FN:787,UniversalGatewayV0.getMinMaxValueForNative -FNDA:0,UniversalGatewayV0.getMinMaxValueForNative -DA:788,0 -DA:792,0 -DA:793,0 -DA:804,0 -FN:804,UniversalGatewayV0.getEthUsdPrice -FNDA:0,UniversalGatewayV0.getEthUsdPrice +DA:789,0 +FN:789,UniversalGatewayV0.revertUniversalTx +FNDA:0,UniversalGatewayV0.revertUniversalTx +DA:800,0 +BRDA:800,43,0,- +DA:802,0 +BRDA:802,44,0,- +DA:803,0 +BRDA:803,45,0,- DA:805,0 -BRDA:805,47,0,- -DA:808,0 -BRDA:808,48,0,- +DA:806,0 +DA:807,0 +BRDA:807,46,0,- DA:809,0 -DA:815,0 -DA:818,0 -BRDA:818,49,0,- -DA:821,0 -BRDA:821,50,0,- -DA:822,0 +DA:813,0 +FN:813,UniversalGatewayV0.revertUniversalTxToken +FNDA:0,UniversalGatewayV0.revertUniversalTxToken +DA:824,0 +BRDA:824,47,0,- DA:826,0 +BRDA:826,48,0,- +DA:827,0 +BRDA:827,49,0,- +DA:829,0 +DA:830,0 DA:832,0 -DA:835,0 -BRDA:835,51,0,- -DA:836,0 -BRDA:836,52,0,- -DA:837,0 -BRDA:837,53,0,- -DA:838,0 DA:841,0 -DA:844,0 -BRDA:844,54,0,- -DA:845,0 -BRDA:845,55,0,- -DA:846,0 +FN:841,UniversalGatewayV0.withdraw +FNDA:0,UniversalGatewayV0.withdraw DA:847,0 -BRDA:847,55,1,- +BRDA:847,50,0,- DA:849,0 +BRDA:849,51,0,- +DA:850,0 +BRDA:850,52,0,- +DA:851,0 +BRDA:851,53,0,- +DA:853,0 DA:854,0 +DA:855,0 +BRDA:855,54,0,- DA:857,0 -BRDA:857,56,0,- -DA:858,0 DA:860,0 -DA:864,0 -FN:864,UniversalGatewayV0.getEthUsdPrice_old -FNDA:0,UniversalGatewayV0.getEthUsdPrice_old -DA:865,0 -DA:866,0 -DA:868,0 -BRDA:868,57,0,- -BRDA:868,57,1,- +FN:860,UniversalGatewayV0.withdrawTokens +FNDA:0,UniversalGatewayV0.withdrawTokens +DA:867,0 +BRDA:867,55,0,- DA:869,0 +BRDA:869,56,0,- +DA:870,0 +BRDA:870,57,0,- +DA:871,0 +BRDA:871,58,0,- +DA:873,0 +BRDA:873,59,0,- +DA:875,0 +DA:876,0 DA:877,0 -FN:877,UniversalGatewayV0.quoteEthAmountInUsd1e18 -FNDA:0,UniversalGatewayV0.quoteEthAmountInUsd1e18 -DA:878,0 -DA:879,0 -DA:882,0 -DA:892,0 -FN:892,UniversalGatewayV0._checkUSDCaps -FNDA:0,UniversalGatewayV0._checkUSDCaps -DA:893,0 -DA:894,0 -BRDA:894,59,0,- -DA:895,0 -BRDA:895,60,0,- -DA:902,0 -FN:902,UniversalGatewayV0._checkBlockUSDCap -FNDA:0,UniversalGatewayV0._checkBlockUSDCap +DA:890,0 +FN:890,UniversalGatewayV0.isSupportedToken +FNDA:0,UniversalGatewayV0.isSupportedToken +DA:891,0 +DA:898,0 +FN:898,UniversalGatewayV0.getMinMaxValueForNative +FNDA:0,UniversalGatewayV0.getMinMaxValueForNative +DA:899,0 DA:903,0 DA:904,0 -DA:906,0 -BRDA:906,62,0,- -DA:907,0 -DA:908,0 -DA:911,0 -DA:913,0 -BRDA:913,63,0,- +DA:915,0 +FN:915,UniversalGatewayV0.getEthUsdPrice +FNDA:0,UniversalGatewayV0.getEthUsdPrice DA:916,0 -DA:917,0 -BRDA:917,64,0,- -DA:918,0 -DA:923,0 -FN:923,UniversalGatewayV0._handleNativeDeposit -FNDA:0,UniversalGatewayV0._handleNativeDeposit -DA:924,0 -DA:925,0 -BRDA:925,65,0,- +BRDA:916,60,0,- +DA:919,0 +BRDA:919,61,0,- +DA:920,0 DA:926,0 +DA:929,0 +BRDA:929,62,0,- +DA:932,0 +BRDA:932,63,0,- DA:933,0 -FN:933,UniversalGatewayV0._handleTokenDeposit -FNDA:0,UniversalGatewayV0._handleTokenDeposit -DA:934,0 -BRDA:934,66,0,- -DA:935,0 -BRDA:935,67,0,- -BRDA:935,67,1,- DA:937,0 -DA:940,0 -DA:945,0 -FN:945,UniversalGatewayV0._handleNativeWithdraw -FNDA:0,UniversalGatewayV0._handleNativeWithdraw +DA:943,0 DA:946,0 +BRDA:946,64,0,- DA:947,0 -BRDA:947,68,0,- +BRDA:947,65,0,- +DA:948,0 +BRDA:948,66,0,- +DA:949,0 +DA:952,0 DA:955,0 -FN:955,UniversalGatewayV0._handleTokenWithdraw -FNDA:0,UniversalGatewayV0._handleTokenWithdraw -DA:959,0 -BRDA:959,69,0,- +BRDA:955,67,0,- +DA:956,0 +BRDA:956,68,0,- +DA:957,0 +DA:958,0 +BRDA:958,68,1,- DA:960,0 +DA:965,0 DA:968,0 -FN:968,UniversalGatewayV0._consumeRateLimit -FNDA:0,UniversalGatewayV0._consumeRateLimit +BRDA:968,69,0,- DA:969,0 -DA:970,0 -BRDA:970,70,0,- -DA:972,0 -DA:973,0 -BRDA:973,71,0,- +DA:971,0 DA:975,0 +FN:975,UniversalGatewayV0.getEthUsdPrice_old +FNDA:0,UniversalGatewayV0.getEthUsdPrice_old DA:976,0 -DA:978,0 -BRDA:978,72,0,- +DA:977,0 DA:979,0 +BRDA:979,70,0,- +BRDA:979,70,1,- DA:980,0 -DA:984,0 -DA:985,0 -BRDA:985,73,0,- -DA:986,0 -DA:994,0 -FN:994,UniversalGatewayV0.currentTokenUsage -FNDA:0,UniversalGatewayV0.currentTokenUsage -DA:995,0 -DA:996,0 -DA:998,0 -DA:999,0 -DA:1001,0 -DA:1002,0 +DA:988,0 +FN:988,UniversalGatewayV0.quoteEthAmountInUsd1e18 +FNDA:0,UniversalGatewayV0.quoteEthAmountInUsd1e18 +DA:989,0 +DA:990,0 +DA:993,0 DA:1003,0 +FN:1003,UniversalGatewayV0._checkUSDCaps +FNDA:0,UniversalGatewayV0._checkUSDCaps +DA:1004,0 DA:1005,0 +BRDA:1005,72,0,- DA:1006,0 +BRDA:1006,73,0,- +DA:1010,0 +FN:1010,UniversalGatewayV0._emitUniversalTx +FNDA:0,UniversalGatewayV0._emitUniversalTx DA:1020,0 -FN:1020,UniversalGatewayV0.swapToNative -FNDA:0,UniversalGatewayV0.swapToNative -DA:1026,0 -BRDA:1026,76,0,- -DA:1028,0 -BRDA:1028,77,0,- -BRDA:1028,77,1,- -DA:1029,0 -DA:1030,0 -BRDA:1030,78,0,- -DA:1031,0 -DA:1033,0 -BRDA:1033,79,0,- -DA:1035,0 -BRDA:1035,80,0,- DA:1037,0 -DA:1039,0 -DA:1040,0 -DA:1041,0 -DA:1044,0 -BRDA:1044,81,0,- -DA:1045,0 -DA:1049,0 -DA:1053,0 -DA:1054,0 -DA:1057,0 -DA:1068,0 -DA:1071,0 -DA:1074,0 -DA:1075,0 -DA:1076,0 -DA:1079,0 -BRDA:1079,82,0,- -DA:1086,0 -FN:1086,UniversalGatewayV0._findV3PoolWithNative -FNDA:0,UniversalGatewayV0._findV3PoolWithNative -DA:1089,0 -BRDA:1089,83,0,- -DA:1090,0 -BRDA:1090,84,0,- -DA:1092,0 -DA:1096,0 -DA:1097,0 +FN:1037,UniversalGatewayV0._routeUniversalTx +FNDA:0,UniversalGatewayV0._routeUniversalTx +DA:1043,0 +DA:1046,0 +BRDA:1046,74,0,- +DA:1047,0 +DA:1051,0 +BRDA:1051,75,0,- +BRDA:1051,75,1,- +DA:1052,0 +DA:1055,0 +BRDA:1055,76,0,- +BRDA:1055,76,1,- +DA:1060,0 +DA:1064,0 DA:1098,0 +FN:1098,UniversalGatewayV0._handleDeposits +FNDA:0,UniversalGatewayV0._handleDeposits DA:1099,0 -BRDA:1099,85,0,- -DA:1100,0 +BRDA:1099,77,0,- +BRDA:1099,77,1,- +DA:1101,0 +DA:1102,0 +BRDA:1102,78,0,- +DA:1105,0 +BRDA:1105,79,0,- DA:1106,0 -DA:1129,0 -FN:1129,UniversalGatewayV0.executeUniversalTx -FNDA:0,UniversalGatewayV0.executeUniversalTx -DA:1137,0 -BRDA:1137,86,0,- -DA:1139,0 -BRDA:1139,87,0,- -DA:1140,0 -BRDA:1140,88,0,- -DA:1141,0 -BRDA:1141,89,0,- -DA:1143,0 -BRDA:1143,90,0,- -DA:1145,0 -DA:1147,0 -DA:1148,0 -DA:1149,0 -DA:1150,0 -DA:1153,0 -DA:1154,0 -BRDA:1154,91,0,- -DA:1155,0 -DA:1158,0 -DA:1168,0 -FN:1168,UniversalGatewayV0.executeUniversalTx -FNDA:0,UniversalGatewayV0.executeUniversalTx +DA:1169,0 +FN:1169,UniversalGatewayV0.swapToNative +FNDA:0,UniversalGatewayV0.swapToNative DA:1175,0 -BRDA:1175,92,0,- +BRDA:1175,80,0,- DA:1177,0 -BRDA:1177,93,0,- +BRDA:1177,81,0,- +BRDA:1177,81,1,- DA:1178,0 -BRDA:1178,94,0,- DA:1179,0 -BRDA:1179,95,0,- -DA:1181,0 -DA:1183,0 -DA:1185,0 +BRDA:1179,82,0,- +DA:1180,0 +DA:1182,0 +BRDA:1182,83,0,- +DA:1184,0 +BRDA:1184,84,0,- +DA:1186,0 +DA:1188,0 DA:1189,0 -FN:1189,UniversalGatewayV0._resetApproval -FNDA:0,UniversalGatewayV0._resetApproval DA:1190,0 -DA:1191,0 -DA:1192,0 -BRDA:1192,96,0,- +DA:1193,0 +BRDA:1193,85,0,- DA:1194,0 -DA:1197,0 -BRDA:1197,97,0,- DA:1198,0 -DA:1199,0 -BRDA:1199,98,0,- -DA:1205,0 -FN:1205,UniversalGatewayV0._safeApprove -FNDA:0,UniversalGatewayV0._safeApprove +DA:1202,0 +DA:1203,0 DA:1206,0 -DA:1207,0 -DA:1208,0 -BRDA:1208,99,0,- -DA:1209,0 -DA:1211,0 -BRDA:1211,100,0,- -DA:1212,0 -DA:1213,0 -BRDA:1213,101,0,- -DA:1214,0 -DA:1222,0 -FN:1222,UniversalGatewayV0._executeCall -FNDA:0,UniversalGatewayV0._executeCall +DA:1217,0 +DA:1220,0 DA:1223,0 DA:1224,0 -BRDA:1224,102,0,- DA:1225,0 -DA:1229,0 -FN:1229,UniversalGatewayV0.receive +DA:1228,0 +BRDA:1228,86,0,- +DA:1235,0 +FN:1235,UniversalGatewayV0._findV3PoolWithNative +FNDA:0,UniversalGatewayV0._findV3PoolWithNative +DA:1238,0 +BRDA:1238,87,0,- +DA:1239,0 +BRDA:1239,88,0,- +DA:1241,0 +DA:1245,0 +DA:1246,0 +DA:1247,0 +DA:1248,0 +BRDA:1248,89,0,- +DA:1249,0 +DA:1255,0 +DA:1273,0 +FN:1273,UniversalGatewayV0._fetchTxType +FNDA:0,UniversalGatewayV0._fetchTxType +DA:1278,0 +DA:1279,0 +DA:1280,0 +DA:1281,0 +DA:1285,0 +BRDA:1285,90,0,- +DA:1286,0 +DA:1292,0 +BRDA:1292,91,0,- +DA:1293,0 +DA:1297,0 +BRDA:1297,92,0,- +DA:1300,0 +BRDA:1300,93,0,- +DA:1301,0 +DA:1305,0 +BRDA:1305,94,0,- +DA:1306,0 +DA:1308,0 +DA:1312,0 +BRDA:1312,95,0,- +DA:1314,0 +BRDA:1314,96,0,- +DA:1315,0 +DA:1318,0 +BRDA:1318,97,0,- +DA:1319,0 +DA:1322,0 +BRDA:1322,98,0,- +DA:1323,0 +DA:1325,0 +DA:1328,0 +DA:1335,0 +FN:1335,UniversalGatewayV0.receive FNDA:0,UniversalGatewayV0.receive -DA:1231,0 -BRDA:1231,103,0,- -FNF:56 +DA:1337,0 +BRDA:1337,99,0,- +FNF:46 FNH:0 -LF:368 +LF:331 LH:0 -BRF:107 +BRF:108 BRH:0 end_of_record TN: SF:src/Vault.sol -DA:55,83 +DA:55,0 FN:55,Vault.initialize -FNDA:83,Vault.initialize -DA:56,83 -BRDA:56,0,0,4 -DA:57,4 +FNDA:0,Vault.initialize +DA:56,0 +BRDA:56,0,0,- +DA:57,0 DA:60,0 DA:61,0 DA:62,0 DA:63,0 -DA:65,79 -DA:66,79 -DA:67,79 -DA:69,79 -DA:70,79 -DA:71,79 -DA:72,79 -DA:80,12 +DA:65,0 +DA:66,0 +DA:67,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:80,0 FN:80,Vault.pause -FNDA:12,Vault.pause +FNDA:0,Vault.pause DA:81,0 -DA:86,3 +DA:86,0 FN:86,Vault.unpause -FNDA:3,Vault.unpause +FNDA:0,Vault.unpause DA:87,0 -DA:93,8 +DA:93,0 FN:93,Vault.setGateway -FNDA:8,Vault.setGateway -DA:94,7 -BRDA:94,1,0,1 -DA:95,6 -DA:96,6 -DA:97,6 -DA:103,9 +FNDA:0,Vault.setGateway +DA:94,0 +BRDA:94,1,0,- +DA:95,0 +DA:96,0 +DA:97,0 +DA:103,0 FN:103,Vault.setTSS -FNDA:9,Vault.setTSS -DA:104,8 -BRDA:104,2,0,1 -DA:105,7 -DA:108,7 -BRDA:108,3,0,7 -DA:109,7 -DA:111,7 -DA:112,7 -DA:120,6 +FNDA:0,Vault.setTSS +DA:104,0 +BRDA:104,2,0,- +DA:105,0 +DA:108,0 +BRDA:108,3,0,- +DA:109,0 +DA:111,0 +DA:112,0 +DA:120,0 FN:120,Vault.sweep -FNDA:6,Vault.sweep -DA:121,5 -BRDA:121,4,0,2 -DA:122,3 -DA:129,23 +FNDA:0,Vault.sweep +DA:121,0 +BRDA:121,4,0,- +DA:122,0 +DA:129,0 FN:129,Vault.withdraw -FNDA:23,Vault.withdraw -DA:135,21 -BRDA:135,5,0,2 -DA:136,19 -BRDA:136,6,0,1 -DA:137,18 -DA:138,14 -BRDA:138,7,0,1 -DA:140,13 -DA:141,13 -DA:142,13 -DA:146,12 +FNDA:0,Vault.withdraw +DA:135,0 +BRDA:135,5,0,- +DA:136,0 +BRDA:136,6,0,- +DA:137,0 +DA:138,0 +BRDA:138,7,0,- +DA:140,0 +DA:141,0 +DA:142,0 +DA:146,0 FN:146,Vault.withdrawAndExecute -FNDA:12,Vault.withdrawAndExecute -DA:152,11 -BRDA:152,8,0,2 -DA:153,9 -BRDA:153,9,0,1 -DA:154,8 -DA:155,7 -BRDA:155,10,0,1 -DA:158,6 -DA:161,6 -DA:163,6 -DA:167,9 +FNDA:0,Vault.withdrawAndExecute +DA:152,0 +BRDA:152,8,0,- +DA:153,0 +BRDA:153,9,0,- +DA:154,0 +DA:155,0 +BRDA:155,10,0,- +DA:158,0 +DA:161,0 +DA:163,0 +DA:167,0 FN:167,Vault.revertWithdraw -FNDA:9,Vault.revertWithdraw -DA:173,8 -BRDA:173,11,0,2 -DA:174,6 -BRDA:174,12,0,1 -DA:175,5 -DA:176,4 -BRDA:176,13,0,1 -DA:178,3 -DA:179,3 -DA:181,3 -DA:187,31 +FNDA:0,Vault.revertWithdraw +DA:173,0 +BRDA:173,11,0,- +DA:174,0 +BRDA:174,12,0,- +DA:175,0 +DA:176,0 +BRDA:176,13,0,- +DA:178,0 +DA:179,0 +DA:181,0 +DA:187,0 FN:187,Vault._enforceSupported -FNDA:31,Vault._enforceSupported -DA:189,31 -BRDA:189,14,0,6 +FNDA:0,Vault._enforceSupported +DA:189,0 +BRDA:189,14,0,- FNF:10 -FNH:10 +FNH:0 LF:59 -LH:53 +LH:0 BRF:15 -BRH:15 +BRH:0 end_of_record TN: SF:src/VaultPC.sol -DA:48,59 -FN:48,VaultPC.initialize -FNDA:59,VaultPC.initialize -DA:49,59 -BRDA:49,0,0,4 +DA:43,0 +FN:43,VaultPC.initialize +FNDA:0,VaultPC.initialize +DA:44,0 +BRDA:44,0,0,- +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 DA:51,0 DA:52,0 DA:53,0 -DA:54,0 -DA:56,55 -DA:57,55 -DA:58,55 -DA:60,55 -DA:66,12 -FN:66,VaultPC.pause -FNDA:12,VaultPC.pause -DA:67,0 -DA:69,4 -FN:69,VaultPC.unpause -FNDA:4,VaultPC.unpause -DA:70,0 -DA:73,10 -FN:73,VaultPC.updateUniversalCore -FNDA:10,VaultPC.updateUniversalCore -DA:74,9 -BRDA:74,1,0,1 -DA:75,8 -DA:79,11 -FN:79,VaultPC.sweep -FNDA:11,VaultPC.sweep -DA:80,10 -BRDA:80,2,0,2 -DA:81,8 -DA:88,32 -FN:88,VaultPC.withdraw -FNDA:32,VaultPC.withdraw -DA:94,31 -DA:95,24 -BRDA:95,3,0,1 -DA:96,23 -BRDA:96,4,0,1 -DA:97,22 -BRDA:97,5,0,1 -DA:99,21 -DA:109,31 -FN:109,VaultPC._enforceSupportedToken -FNDA:31,VaultPC._enforceSupportedToken -DA:111,31 -BRDA:111,6,0,7 -FNF:7 -FNH:7 -LF:28 -LH:22 -BRF:7 -BRH:7 +DA:60,0 +FN:60,VaultPC.pause +FNDA:0,VaultPC.pause +DA:61,0 +DA:63,0 +FN:63,VaultPC.unpause +FNDA:0,VaultPC.unpause +DA:64,0 +DA:72,0 +FN:72,VaultPC.withdraw +FNDA:0,VaultPC.withdraw +DA:78,0 +BRDA:78,1,0,- +DA:79,0 +BRDA:79,2,0,- +DA:80,0 +BRDA:80,3,0,- +DA:82,0 +DA:83,0 +BRDA:83,4,0,- +DA:85,0 +DA:89,0 +FN:89,VaultPC.withdrawToken +FNDA:0,VaultPC.withdrawToken +DA:95,0 +BRDA:95,5,0,- +DA:96,0 +BRDA:96,6,0,- +DA:97,0 +BRDA:97,7,0,- +DA:99,0 +DA:100,0 +FNF:5 +FNH:0 +LF:26 +LH:0 +BRF:8 +BRH:0 end_of_record TN: SF:test/BaseTest.t.sol -DA:91,192 +DA:91,355 FN:91,BaseTest.setUp -FNDA:192,BaseTest.setUp +FNDA:355,BaseTest.setUp DA:92,0 DA:93,0 DA:94,0 @@ -1629,282 +1633,299 @@ DA:98,0 DA:99,0 DA:100,0 DA:101,0 -DA:107,192 +DA:107,355 FN:107,BaseTest._createActors -FNDA:192,BaseTest._createActors -DA:108,192 -DA:109,192 -DA:110,192 -DA:111,192 -DA:112,192 -DA:113,192 -DA:114,192 -DA:115,192 -DA:116,192 -DA:117,192 -DA:119,192 -DA:120,192 -DA:121,192 -DA:122,192 -DA:123,192 -DA:124,192 -DA:125,192 -DA:126,192 -DA:127,192 -DA:128,192 -DA:131,192 +FNDA:355,BaseTest._createActors +DA:108,355 +DA:109,355 +DA:110,355 +DA:111,355 +DA:112,355 +DA:113,355 +DA:114,355 +DA:115,355 +DA:116,355 +DA:117,355 +DA:119,355 +DA:120,355 +DA:121,355 +DA:122,355 +DA:123,355 +DA:124,355 +DA:125,355 +DA:126,355 +DA:127,355 +DA:128,355 +DA:131,355 FN:131,BaseTest._fundActors -FNDA:192,BaseTest._fundActors -DA:132,192 -DA:133,192 -DA:134,192 -DA:135,192 -DA:136,192 -DA:137,192 -DA:138,192 -DA:145,192 +FNDA:355,BaseTest._fundActors +DA:132,355 +DA:133,355 +DA:134,355 +DA:135,355 +DA:136,355 +DA:137,355 +DA:138,355 +DA:145,355 FN:145,BaseTest._deployMocks -FNDA:192,BaseTest._deployMocks -DA:146,192 -DA:147,192 -DA:148,192 -DA:150,192 -DA:151,192 -DA:152,192 -DA:158,192 +FNDA:355,BaseTest._deployMocks +DA:146,355 +DA:147,355 +DA:148,355 +DA:150,355 +DA:151,355 +DA:152,355 +DA:158,355 FN:158,BaseTest._deployUniswapPlaceholders -FNDA:192,BaseTest._deployUniswapPlaceholders -DA:161,192 -DA:162,192 -DA:168,192 +FNDA:355,BaseTest._deployUniswapPlaceholders +DA:161,355 +DA:162,355 +DA:168,355 FN:168,BaseTest._deployGateway -FNDA:192,BaseTest._deployGateway -DA:170,192 -DA:173,192 -DA:176,192 -DA:189,192 -DA:192,192 -DA:194,192 -DA:195,192 -DA:196,192 -DA:199,192 -FN:199,BaseTest._initializeGateway -FNDA:192,BaseTest._initializeGateway -DA:202,192 -DA:203,192 -DA:204,192 -DA:205,192 -DA:211,192 -FN:211,BaseTest._deployOracles -FNDA:192,BaseTest._deployOracles -DA:212,192 -DA:214,192 -DA:217,192 -DA:219,192 -DA:222,192 -FN:222,BaseTest._wireOraclesToGateway -FNDA:192,BaseTest._wireOraclesToGateway -DA:224,192 -DA:225,192 -DA:229,192 -FN:229,BaseTest._setupNativeTokenSupport -FNDA:192,BaseTest._setupNativeTokenSupport -DA:231,192 -DA:232,192 -DA:233,192 -DA:234,192 -DA:236,192 -DA:237,192 -DA:241,0 -FN:241,BaseTest.setEthUsdPrice1e8 +FNDA:355,BaseTest._deployGateway +DA:170,355 +DA:173,355 +DA:176,355 +DA:188,355 +DA:191,355 +DA:193,355 +DA:194,355 +DA:195,355 +DA:198,355 +FN:198,BaseTest._initializeGateway +FNDA:355,BaseTest._initializeGateway +DA:201,355 +DA:202,355 +DA:203,355 +DA:204,355 +DA:210,355 +FN:210,BaseTest._deployOracles +FNDA:355,BaseTest._deployOracles +DA:211,355 +DA:213,355 +DA:216,355 +DA:218,355 +DA:221,355 +FN:221,BaseTest._wireOraclesToGateway +FNDA:355,BaseTest._wireOraclesToGateway +DA:223,355 +DA:224,355 +DA:228,355 +FN:228,BaseTest._setupNativeTokenSupport +FNDA:355,BaseTest._setupNativeTokenSupport +DA:230,355 +DA:231,355 +DA:232,355 +DA:233,355 +DA:235,355 +DA:236,355 +DA:240,0 +FN:240,BaseTest.setEthUsdPrice1e8 FNDA:0,BaseTest.setEthUsdPrice1e8 -DA:242,0 -DA:245,0 -FN:245,BaseTest.setChainlinkStalePeriod +DA:241,0 +DA:244,0 +FN:244,BaseTest.setChainlinkStalePeriod FNDA:0,BaseTest.setChainlinkStalePeriod +DA:245,0 DA:246,0 -DA:247,0 -DA:250,0 -FN:250,BaseTest.enableSequencerFeed +DA:249,0 +FN:249,BaseTest.enableSequencerFeed FNDA:0,BaseTest.enableSequencerFeed +DA:250,0 DA:251,0 -DA:252,0 -DA:255,0 -FN:255,BaseTest.setSequencerStatusDown +DA:254,0 +FN:254,BaseTest.setSequencerStatusDown FNDA:0,BaseTest.setSequencerStatusDown -DA:256,0 -DA:259,0 -FN:259,BaseTest.setSequencerGracePeriod +DA:255,0 +DA:258,0 +FN:258,BaseTest.setSequencerGracePeriod FNDA:0,BaseTest.setSequencerGracePeriod +DA:259,0 DA:260,0 -DA:261,0 -DA:267,192 -FN:267,BaseTest._mintAndApproveTokens -FNDA:192,BaseTest._mintAndApproveTokens -DA:268,192 -DA:269,192 -DA:270,192 -DA:271,192 -DA:272,192 -DA:273,192 -DA:276,192 -DA:277,960 -DA:281,192 -DA:282,960 -DA:286,192 -DA:287,960 -DA:288,960 -DA:289,960 -DA:296,0 -FN:296,BaseTest.buildMinimalPayload +DA:266,355 +FN:266,BaseTest._mintAndApproveTokens +FNDA:355,BaseTest._mintAndApproveTokens +DA:267,355 +DA:268,355 +DA:269,355 +DA:270,355 +DA:271,355 +DA:272,355 +DA:275,355 +DA:276,1775 +DA:280,355 +DA:281,1775 +DA:285,355 +DA:286,1775 +DA:287,1775 +DA:288,1775 +DA:295,0 +FN:295,BaseTest.buildMinimalPayload FNDA:0,BaseTest.buildMinimalPayload -DA:301,0 -DA:312,0 -DA:315,14 -FN:315,BaseTest.buildValuePayload -FNDA:14,BaseTest.buildValuePayload -DA:320,14 -DA:331,14 -DA:334,29 -FN:334,BaseTest.revertCfg -FNDA:29,BaseTest.revertCfg -DA:335,29 -DA:340,41 -FN:340,BaseTest.buildDefaultPayload -FNDA:41,BaseTest.buildDefaultPayload -DA:341,41 -DA:356,41 -FN:356,BaseTest.buildDefaultRevertInstructions -FNDA:41,BaseTest.buildDefaultRevertInstructions -DA:357,41 -DA:363,1920 -FN:363,BaseTest.mintAndApprove -FNDA:1920,BaseTest.mintAndApprove -DA:365,1920 -BRDA:365,0,0,960 -BRDA:365,0,1,- -DA:366,960 -DA:367,960 -BRDA:367,1,0,960 -BRDA:367,1,1,- -DA:368,960 -DA:370,0 -DA:374,1920 -DA:375,1920 -DA:378,960 -FN:378,BaseTest.mintWETH -FNDA:960,BaseTest.mintWETH -DA:379,960 -DA:380,960 -DA:381,960 -DA:388,0 -FN:388,BaseTest.setV3FeeOrder +DA:300,0 +DA:311,0 +DA:314,0 +FN:314,BaseTest.buildValuePayload +FNDA:0,BaseTest.buildValuePayload +DA:319,0 +DA:330,0 +DA:333,7 +FN:333,BaseTest.revertCfg +FNDA:7,BaseTest.revertCfg +DA:334,7 +DA:339,160 +FN:339,BaseTest.buildDefaultPayload +FNDA:160,BaseTest.buildDefaultPayload +DA:340,160 +DA:355,40 +FN:355,BaseTest.buildDefaultRevertInstructions +FNDA:40,BaseTest.buildDefaultRevertInstructions +DA:356,40 +DA:367,38 +FN:367,BaseTest._buildGasTxRequest +FNDA:38,BaseTest._buildGasTxRequest +DA:368,38 +DA:369,38 +DA:383,0 +FN:383,BaseTest._buildFundsTxRequest +FNDA:0,BaseTest._buildFundsTxRequest +DA:384,0 +DA:392,59 +FN:392,BaseTest._buildFundsTxRequest +FNDA:59,BaseTest._buildFundsTxRequest +DA:398,59 +DA:415,2 +FN:415,BaseTest._buildFundsAndPayloadTxRequest +FNDA:2,BaseTest._buildFundsAndPayloadTxRequest +DA:421,2 +DA:434,3550 +FN:434,BaseTest.mintAndApprove +FNDA:3550,BaseTest.mintAndApprove +DA:436,3550 +BRDA:436,0,0,1775 +BRDA:436,0,1,- +DA:437,1775 +DA:438,1775 +BRDA:438,1,0,1775 +BRDA:438,1,1,- +DA:439,1775 +DA:441,0 +DA:445,3550 +DA:446,3550 +DA:449,1775 +FN:449,BaseTest.mintWETH +FNDA:1775,BaseTest.mintWETH +DA:450,1775 +DA:451,1775 +DA:452,1775 +DA:459,0 +FN:459,BaseTest.setV3FeeOrder FNDA:0,BaseTest.setV3FeeOrder -DA:389,0 -DA:390,0 -DA:393,0 -FN:393,BaseTest.setRouters +DA:460,0 +DA:461,0 +DA:464,0 +FN:464,BaseTest.setRouters FNDA:0,BaseTest.setRouters -DA:394,0 -DA:395,0 -DA:398,0 -FN:398,BaseTest.setCaps +DA:465,0 +DA:466,0 +DA:469,0 +FN:469,BaseTest.setCaps FNDA:0,BaseTest.setCaps -DA:399,0 -DA:400,0 -DA:403,0 -FN:403,BaseTest.toggleSupport +DA:470,0 +DA:471,0 +DA:474,0 +FN:474,BaseTest.toggleSupport FNDA:0,BaseTest.toggleSupport -DA:404,0 -DA:405,0 -DA:406,0 -DA:407,0 -DA:409,0 -DA:411,0 -DA:412,0 -DA:413,0 -DA:419,0 -FN:419,BaseTest.recordAndGetLogs -FNDA:0,BaseTest.recordAndGetLogs -DA:420,0 -DA:421,0 -DA:424,0 -FN:424,BaseTest.assertDualEmitOrder -FNDA:0,BaseTest.assertDualEmitOrder -DA:425,0 -DA:426,0 -DA:428,0 -DA:429,0 -BRDA:429,2,0,- -DA:430,0 -DA:432,0 -BRDA:432,3,0,- -DA:433,0 -DA:434,0 -BRDA:434,4,0,- -BRDA:434,4,1,- -DA:438,0 -BRDA:438,5,0,- -BRDA:438,5,1,- -DA:456,4 -FN:456,BaseTest.buildERC20Payload -FNDA:4,BaseTest.buildERC20Payload -DA:462,4 -DA:474,4 -DA:476,4 +DA:475,0 +DA:476,0 +DA:477,0 +DA:478,0 +DA:480,0 +DA:482,0 DA:483,0 -FN:483,BaseTest.fundUserWithMainnetTokens -FNDA:0,BaseTest.fundUserWithMainnetTokens -DA:485,0 -DA:486,0 -BRDA:486,6,0,- -BRDA:486,6,1,- -DA:488,0 -DA:489,0 -BRDA:489,7,0,- -BRDA:489,7,1,- +DA:484,0 +DA:490,0 +FN:490,BaseTest.recordAndGetLogs +FNDA:0,BaseTest.recordAndGetLogs DA:491,0 DA:492,0 -BRDA:492,8,0,- -BRDA:492,8,1,- -DA:494,0 DA:495,0 -BRDA:495,9,0,- -BRDA:495,9,1,- +FN:495,BaseTest.assertDualEmitOrder +FNDA:0,BaseTest.assertDualEmitOrder +DA:496,0 DA:497,0 -DA:498,0 -BRDA:498,10,0,- -BRDA:498,10,1,- +DA:499,0 DA:500,0 +BRDA:500,2,0,- DA:501,0 -BRDA:501,11,0,- -BRDA:501,11,1,- DA:503,0 +BRDA:503,3,0,- +DA:504,0 DA:505,0 +BRDA:505,4,0,- +BRDA:505,4,1,- DA:509,0 -DA:510,0 -BRDA:510,12,0,- -BRDA:510,12,1,- -DA:513,0 -DA:514,0 -BRDA:514,13,0,- -BRDA:514,13,1,- -DA:516,0 -DA:518,0 -DA:520,0 -FNF:31 -FNH:18 -LF:188 +BRDA:509,5,0,- +BRDA:509,5,1,- +DA:527,0 +FN:527,BaseTest.buildERC20Payload +FNDA:0,BaseTest.buildERC20Payload +DA:533,0 +DA:545,0 +DA:547,0 +DA:554,0 +FN:554,BaseTest.fundUserWithMainnetTokens +FNDA:0,BaseTest.fundUserWithMainnetTokens +DA:556,0 +DA:557,0 +BRDA:557,6,0,- +BRDA:557,6,1,- +DA:559,0 +DA:560,0 +BRDA:560,7,0,- +BRDA:560,7,1,- +DA:562,0 +DA:563,0 +BRDA:563,8,0,- +BRDA:563,8,1,- +DA:565,0 +DA:566,0 +BRDA:566,9,0,- +BRDA:566,9,1,- +DA:568,0 +DA:569,0 +BRDA:569,10,0,- +BRDA:569,10,1,- +DA:571,0 +DA:572,0 +BRDA:572,11,0,- +BRDA:572,11,1,- +DA:574,0 +DA:576,0 +DA:580,0 +DA:581,0 +BRDA:581,12,0,- +BRDA:581,12,1,- +DA:584,0 +DA:585,0 +BRDA:585,13,0,- +BRDA:585,13,1,- +DA:587,0 +DA:589,0 +DA:591,0 +FNF:35 +FNH:19 +LF:197 LH:108 BRF:26 BRH:2 end_of_record TN: -SF:test/gateway/5_GatewayBlockRateLimit.t.sol -DA:541,1 -FN:541,RevertingReceiver.receive +SF:test/gateway/12_rateLimit_BlockBased.t.sol +DA:494,1 +FN:494,RevertingReceiver.receive FNDA:1,RevertingReceiver.receive -DA:542,1 +DA:495,1 FNF:1 FNH:1 LF:2 @@ -1914,28 +1935,28 @@ BRH:0 end_of_record TN: SF:test/mocks/MockAggregatorV3.sol -DA:9,213 +DA:9,374 FN:9,MockAggregatorV3.constructor -FNDA:213,MockAggregatorV3.constructor -DA:10,213 -DA:11,213 -DA:12,213 -DA:13,213 -DA:16,215 +FNDA:374,MockAggregatorV3.constructor +DA:10,374 +DA:11,374 +DA:12,374 +DA:13,374 +DA:16,374 FN:16,MockAggregatorV3.setAnswer -FNDA:215,MockAggregatorV3.setAnswer -DA:17,215 -DA:18,215 -DA:19,215 -DA:20,215 -DA:23,211 +FNDA:374,MockAggregatorV3.setAnswer +DA:17,374 +DA:18,374 +DA:19,374 +DA:20,374 +DA:23,561 FN:23,MockAggregatorV3.decimals -FNDA:211,MockAggregatorV3.decimals -DA:24,211 -DA:27,100 +FNDA:561,MockAggregatorV3.decimals +DA:24,561 +DA:27,234 FN:27,MockAggregatorV3.latestRoundData -FNDA:100,MockAggregatorV3.latestRoundData -DA:32,100 +FNDA:234,MockAggregatorV3.latestRoundData +DA:32,234 FNF:4 FNH:4 LF:14 @@ -1945,27 +1966,27 @@ BRH:0 end_of_record TN: SF:test/mocks/MockERC20.sol -DA:30,810 +DA:30,977 FN:30,MockERC20.constructor -FNDA:810,MockERC20.constructor -DA:31,810 -DA:32,810 -BRDA:32,0,0,423 -DA:33,423 +FNDA:977,MockERC20.constructor +DA:31,977 +DA:32,977 +BRDA:32,0,0,262 +DA:33,262 DA:40,0 FN:40,MockERC20.decimals FNDA:0,MockERC20.decimals DA:41,0 -DA:49,2444 +DA:49,3940 FN:49,MockERC20.mint -FNDA:2444,MockERC20.mint -DA:50,2444 +FNDA:3940,MockERC20.mint +DA:50,3940 BRDA:50,1,0,- BRDA:50,1,1,- -DA:51,2444 +DA:51,3940 BRDA:51,2,0,- BRDA:51,2,1,- -DA:52,2444 +DA:52,3940 DA:60,0 FN:60,MockERC20.burn FNDA:0,MockERC20.burn @@ -1986,32 +2007,32 @@ DA:72,0 BRDA:72,6,0,- BRDA:72,6,1,- DA:73,0 -DA:79,94 +DA:79,81 FN:79,MockERC20.transfer -FNDA:94,MockERC20.transfer -DA:80,94 +FNDA:81,MockERC20.transfer +DA:80,81 BRDA:80,7,0,- BRDA:80,7,1,- -DA:81,94 +DA:81,81 BRDA:81,8,0,- BRDA:81,8,1,- -DA:82,94 +DA:82,81 BRDA:82,9,0,- BRDA:82,9,1,- -DA:83,94 -DA:89,48 +DA:83,81 +DA:89,146 FN:89,MockERC20.transferFrom -FNDA:48,MockERC20.transferFrom -DA:90,48 +FNDA:146,MockERC20.transferFrom +DA:90,146 BRDA:90,10,0,- BRDA:90,10,1,- -DA:91,48 +DA:91,146 BRDA:91,11,0,- BRDA:91,11,1,- -DA:92,48 +DA:92,146 BRDA:92,12,0,- BRDA:92,12,1,- -DA:93,48 +DA:93,146 DA:99,0 FN:99,MockERC20.pauseTransfers FNDA:0,MockERC20.pauseTransfers @@ -2115,20 +2136,20 @@ FNDA:0,MockPRC20.onlyUniversalExecutor DA:58,0 BRDA:58,0,0,- BRDA:58,0,1,- -DA:65,217 +DA:65,103 FN:65,MockPRC20.constructor -FNDA:217,MockPRC20.constructor -DA:75,217 +FNDA:103,MockPRC20.constructor +DA:75,103 BRDA:75,1,0,- BRDA:75,1,1,- -DA:77,217 -DA:78,217 -DA:79,217 -DA:81,217 -DA:82,217 -DA:83,217 -DA:84,217 -DA:85,217 +DA:77,103 +DA:78,103 +DA:79,103 +DA:81,103 +DA:82,103 +DA:83,103 +DA:84,103 +DA:85,103 DA:89,0 FN:89,MockPRC20.name FNDA:0,MockPRC20.name @@ -2145,19 +2166,19 @@ DA:101,0 FN:101,MockPRC20.totalSupply FNDA:0,MockPRC20.totalSupply DA:102,0 -DA:105,99 +DA:105,44 FN:105,MockPRC20.balanceOf -FNDA:99,MockPRC20.balanceOf -DA:106,99 +FNDA:44,MockPRC20.balanceOf +DA:106,44 DA:109,0 FN:109,MockPRC20.allowance FNDA:0,MockPRC20.allowance DA:110,0 -DA:114,31 +DA:114,2 FN:114,MockPRC20.transfer -FNDA:31,MockPRC20.transfer -DA:115,31 -DA:116,31 +FNDA:2,MockPRC20.transfer +DA:115,2 +DA:116,2 DA:119,46 FN:119,MockPRC20.transferFrom FNDA:46,MockPRC20.transferFrom @@ -2189,91 +2210,91 @@ FNDA:0,MockPRC20.deposit DA:152,0 BRDA:152,4,0,- BRDA:152,4,1,- +DA:154,0 +DA:156,0 DA:157,0 -DA:159,0 -DA:160,0 -DA:166,0 -FN:166,MockPRC20.GAS_LIMIT +DA:163,0 +FN:163,MockPRC20.GAS_LIMIT FNDA:0,MockPRC20.GAS_LIMIT -DA:167,0 -DA:171,0 -FN:171,MockPRC20.withdrawGasFeeWithGasLimit +DA:164,0 +DA:168,0 +FN:168,MockPRC20.withdrawGasFeeWithGasLimit FNDA:0,MockPRC20.withdrawGasFeeWithGasLimit -DA:172,0 -DA:179,0 -FN:179,MockPRC20.updateUniversalCore +DA:169,0 +DA:176,0 +FN:176,MockPRC20.updateUniversalCore FNDA:0,MockPRC20.updateUniversalCore -DA:180,0 -BRDA:180,5,0,- -BRDA:180,5,1,- -DA:181,0 -DA:182,0 -DA:186,0 -FN:186,MockPRC20.updateProtocolFlatFee +DA:177,0 +BRDA:177,5,0,- +BRDA:177,5,1,- +DA:178,0 +DA:179,0 +DA:183,0 +FN:183,MockPRC20.updateProtocolFlatFee FNDA:0,MockPRC20.updateProtocolFlatFee -DA:187,0 -DA:188,0 -DA:192,0 -FN:192,MockPRC20.setName +DA:184,0 +DA:185,0 +DA:189,0 +FN:189,MockPRC20.setName FNDA:0,MockPRC20.setName -DA:193,0 -DA:197,0 -FN:197,MockPRC20.setSymbol +DA:190,0 +DA:194,0 +FN:194,MockPRC20.setSymbol FNDA:0,MockPRC20.setSymbol -DA:198,0 -DA:210,77 -FN:210,MockPRC20._transfer -FNDA:77,MockPRC20._transfer -DA:211,77 -BRDA:211,6,0,77 -BRDA:211,6,1,77 -DA:213,77 -DA:214,77 -BRDA:214,7,0,- -BRDA:214,7,1,- -DA:217,77 -DA:218,77 -DA:221,77 -DA:231,329 -FN:231,MockPRC20._mint -FNDA:329,MockPRC20._mint -DA:232,329 -BRDA:232,8,0,- -BRDA:232,8,1,- -DA:233,329 -BRDA:233,9,0,- -BRDA:233,9,1,- -DA:236,329 -DA:237,329 -DA:239,329 -DA:248,18 -FN:248,MockPRC20._burn +DA:195,0 +DA:207,48 +FN:207,MockPRC20._transfer +FNDA:48,MockPRC20._transfer +DA:208,48 +BRDA:208,6,0,48 +BRDA:208,6,1,48 +DA:210,48 +DA:211,48 +BRDA:211,7,0,- +BRDA:211,7,1,- +DA:214,48 +DA:215,48 +DA:218,48 +DA:228,215 +FN:228,MockPRC20._mint +FNDA:215,MockPRC20._mint +DA:229,215 +BRDA:229,8,0,- +BRDA:229,8,1,- +DA:230,215 +BRDA:230,9,0,- +BRDA:230,9,1,- +DA:233,215 +DA:234,215 +DA:236,215 +DA:245,18 +FN:245,MockPRC20._burn FNDA:18,MockPRC20._burn +DA:246,18 +BRDA:246,10,0,- +BRDA:246,10,1,- +DA:247,18 +BRDA:247,11,0,- +BRDA:247,11,1,- DA:249,18 -BRDA:249,10,0,- -BRDA:249,10,1,- DA:250,18 -BRDA:250,11,0,- -BRDA:250,11,1,- -DA:252,18 +BRDA:250,12,0,- +BRDA:250,12,1,- DA:253,18 -BRDA:253,12,0,- -BRDA:253,12,1,- +DA:254,18 DA:256,18 -DA:257,18 -DA:259,18 -DA:263,329 -FN:263,MockPRC20.mint -FNDA:329,MockPRC20.mint -DA:264,329 -DA:267,1 -FN:267,MockPRC20.setBalance +DA:260,215 +FN:260,MockPRC20.mint +FNDA:215,MockPRC20.mint +DA:261,215 +DA:264,1 +FN:264,MockPRC20.setBalance FNDA:1,MockPRC20.setBalance -DA:268,1 -DA:271,0 -FN:271,MockPRC20.setAllowance +DA:265,1 +DA:268,0 +FN:268,MockPRC20.setAllowance FNDA:0,MockPRC20.setAllowance -DA:272,0 +DA:269,0 FNF:25 FNH:11 LF:89 @@ -2283,12 +2304,12 @@ BRH:2 end_of_record TN: SF:test/mocks/MockReentrantContract.sol -DA:25,135 +DA:25,2 FN:25,MockReentrantContract.constructor -FNDA:135,MockReentrantContract.constructor -DA:26,135 -DA:27,135 -DA:28,135 +FNDA:2,MockReentrantContract.constructor +DA:26,2 +DA:27,2 +DA:28,2 DA:35,1 FN:35,MockReentrantContract.attemptReentrancy FNDA:1,MockReentrantContract.attemptReentrancy @@ -2297,76 +2318,78 @@ DA:44,1 FN:44,MockReentrantContract.attemptReentrancyWithExecute FNDA:1,MockReentrantContract.attemptReentrancyWithExecute DA:51,1 -DA:65,78 -FN:65,MockReentrantContract.setVault -FNDA:78,MockReentrantContract.setVault -DA:66,78 -DA:69,55 -FN:69,MockReentrantContract.setVaultPC -FNDA:55,MockReentrantContract.setVaultPC -DA:70,55 -DA:73,0 -FN:73,MockReentrantContract.enableVaultReentry +DA:58,0 +FN:58,MockReentrantContract.setVault +FNDA:0,MockReentrantContract.setVault +DA:59,0 +DA:62,0 +FN:62,MockReentrantContract.setVaultPC +FNDA:0,MockReentrantContract.setVaultPC +DA:63,0 +DA:66,0 +FN:66,MockReentrantContract.enableVaultReentry FNDA:0,MockReentrantContract.enableVaultReentry -DA:74,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:72,0 +FN:72,MockReentrantContract.pullTokens +FNDA:0,MockReentrantContract.pullTokens +DA:73,0 DA:75,0 +BRDA:75,0,0,- DA:76,0 DA:79,0 -FN:79,MockReentrantContract.pullTokens -FNDA:0,MockReentrantContract.pullTokens -DA:80,0 +BRDA:79,1,0,- +BRDA:79,1,1,- +DA:81,0 DA:82,0 -BRDA:82,0,0,- DA:83,0 +BRDA:83,2,0,- +BRDA:83,2,1,- +DA:84,0 +BRDA:84,3,0,- +BRDA:84,3,1,- DA:86,0 -BRDA:86,1,0,- -BRDA:86,1,1,- -DA:88,0 DA:91,0 -BRDA:91,2,0,- -BRDA:91,2,1,- +BRDA:91,4,0,- +BRDA:91,4,1,- DA:92,0 -BRDA:92,3,0,- -BRDA:92,3,1,- +BRDA:92,5,0,- DA:94,0 DA:97,0 -BRDA:97,4,0,- -BRDA:97,4,1,- -DA:98,0 -BRDA:98,5,0,- -DA:100,0 -DA:103,0 -BRDA:103,6,0,- -BRDA:103,6,1,- -DA:112,0 -FN:112,MockReentrantContract.attackVaultPCWithdraw +BRDA:97,6,0,- +BRDA:97,6,1,- +DA:106,0 +FN:106,MockReentrantContract.attackVaultPCWithdraw FNDA:0,MockReentrantContract.attackVaultPCWithdraw -DA:114,0 -DA:117,0 -BRDA:117,7,0,- -BRDA:117,7,1,- -DA:121,0 -FN:121,MockReentrantContract.transferFrom +DA:108,0 +DA:109,0 +BRDA:109,7,0,- +BRDA:109,7,1,- +DA:113,0 +FN:113,MockReentrantContract.transferFrom FNDA:0,MockReentrantContract.transferFrom -DA:123,0 -DA:127,0 -DA:130,0 -FN:130,MockReentrantContract.onERC721Received +DA:115,0 +DA:116,0 +DA:118,0 +DA:121,0 +FN:121,MockReentrantContract.onERC721Received FNDA:0,MockReentrantContract.onERC721Received -DA:131,0 +DA:122,0 FNF:10 -FNH:5 -LF:37 -LH:12 +FNH:3 +LF:39 +LH:8 BRF:14 BRH:0 end_of_record TN: SF:test/mocks/MockRevertingTarget.sol -DA:15,3 +DA:15,4 FN:15,MockRevertingTarget.receiveFunds -FNDA:3,MockRevertingTarget.receiveFunds -DA:16,3 +FNDA:4,MockRevertingTarget.receiveFunds +DA:16,4 DA:23,2 FN:23,MockRevertingTarget.receiveFundsGasHeavy FNDA:2,MockRevertingTarget.receiveFundsGasHeavy @@ -2400,12 +2423,12 @@ BRH:0 end_of_record TN: SF:test/mocks/MockSequencerUptimeFeed.sol -DA:7,193 +DA:7,355 FN:7,MockSequencerUptimeFeed.setStatus -FNDA:193,MockSequencerUptimeFeed.setStatus -DA:8,193 -DA:9,193 -DA:10,193 +FNDA:355,MockSequencerUptimeFeed.setStatus +DA:8,355 +DA:9,355 +DA:10,355 DA:13,0 FN:13,MockSequencerUptimeFeed.decimals FNDA:0,MockSequencerUptimeFeed.decimals @@ -2423,28 +2446,28 @@ BRH:0 end_of_record TN: SF:test/mocks/MockTarget.sol -DA:12,15 +DA:12,22 FN:12,MockTarget.receiveFunds -FNDA:15,MockTarget.receiveFunds -DA:13,15 -DA:14,15 -DA:18,8 +FNDA:22,MockTarget.receiveFunds +DA:13,22 +DA:14,22 +DA:18,5 FN:18,MockTarget.receiveToken -FNDA:8,MockTarget.receiveToken -DA:19,8 -DA:20,8 -DA:21,8 -DA:24,8 +FNDA:5,MockTarget.receiveToken +DA:19,5 +DA:20,5 +DA:21,5 +DA:24,5 DA:28,0 FN:28,MockTarget.fallback FNDA:0,MockTarget.fallback DA:29,0 DA:30,0 -DA:34,3 +DA:34,2 FN:34,MockTarget.receive -FNDA:3,MockTarget.receive -DA:35,3 -DA:36,3 +FNDA:2,MockTarget.receive +DA:35,2 +DA:36,2 FNF:4 FNH:3 LF:14 @@ -2454,14 +2477,14 @@ BRH:0 end_of_record TN: SF:test/mocks/MockTokenApprovalVariants.sol -DA:27,119 +DA:27,67 FN:27,MockTokenApprovalVariants.constructor -FNDA:119,MockTokenApprovalVariants.constructor -DA:28,119 -DA:35,9 +FNDA:67,MockTokenApprovalVariants.constructor +DA:28,67 +DA:35,8 FN:35,MockTokenApprovalVariants.setApprovalBehavior -FNDA:9,MockTokenApprovalVariants.setApprovalBehavior -DA:36,9 +FNDA:8,MockTokenApprovalVariants.setApprovalBehavior +DA:36,8 DA:45,21 FN:45,MockTokenApprovalVariants.approve FNDA:21,MockTokenApprovalVariants.approve @@ -2501,13 +2524,101 @@ BRF:1 BRH:1 end_of_record TN: +SF:test/mocks/MockUniswapV3.sol +DA:18,23 +FN:18,MockUniswapV3Factory.constructor +FNDA:23,MockUniswapV3Factory.constructor +DA:19,23 +DA:22,23 +FN:22,MockUniswapV3Factory.setPool +FNDA:23,MockUniswapV3Factory.setPool +DA:23,23 +BRDA:23,0,0,- +BRDA:23,0,1,- +DA:24,23 +DA:25,23 +DA:28,17 +FN:28,MockUniswapV3Factory.getPool +FNDA:17,MockUniswapV3Factory.getPool +DA:29,17 +DA:32,23 +FN:32,MockUniswapV3Factory.owner +FNDA:23,MockUniswapV3Factory.owner +DA:33,23 +DA:36,0 +FN:36,MockUniswapV3Factory.feeAmountTickSpacing +FNDA:0,MockUniswapV3Factory.feeAmountTickSpacing +DA:37,0 +DA:40,0 +FN:40,MockUniswapV3Factory.createPool +FNDA:0,MockUniswapV3Factory.createPool +DA:41,0 +DA:44,0 +FN:44,MockUniswapV3Factory.setOwner +FNDA:0,MockUniswapV3Factory.setOwner +DA:45,0 +DA:48,0 +FN:48,MockUniswapV3Factory.enableFeeAmount +FNDA:0,MockUniswapV3Factory.enableFeeAmount +DA:49,0 +DA:62,23 +FN:62,MockUniswapV3Router.constructor +FNDA:23,MockUniswapV3Router.constructor +DA:63,23 +DA:67,23 +FN:67,MockUniswapV3Router.setSwapRate +FNDA:23,MockUniswapV3Router.setSwapRate +DA:68,23 +DA:71,11 +FN:71,MockUniswapV3Router.exactInputSingle +FNDA:11,MockUniswapV3Router.exactInputSingle +DA:77,11 +BRDA:77,0,0,- +BRDA:77,0,1,- +DA:80,11 +DA:81,11 +BRDA:81,1,0,- +DA:82,0 +DA:84,11 +DA:87,11 +DA:91,11 +DA:92,11 +BRDA:92,2,0,11 +DA:93,11 +DA:95,11 +DA:99,11 +DA:101,11 +DA:104,0 +FN:104,MockUniswapV3Router.exactInput +FNDA:0,MockUniswapV3Router.exactInput +DA:105,0 +DA:108,0 +FN:108,MockUniswapV3Router.exactOutputSingle +FNDA:0,MockUniswapV3Router.exactOutputSingle +DA:109,0 +DA:112,0 +FN:112,MockUniswapV3Router.exactOutput +FNDA:0,MockUniswapV3Router.exactOutput +DA:113,0 +DA:116,0 +FN:116,MockUniswapV3Router.uniswapV3SwapCallback +FNDA:0,MockUniswapV3Router.uniswapV3SwapCallback +DA:118,0 +FNF:15 +FNH:7 +LF:43 +LH:26 +BRF:6 +BRH:1 +end_of_record +TN: SF:test/mocks/MockUniversalCoreReal.sol -DA:77,114 +DA:77,50 FN:77,MockUniversalCoreReal.constructor -FNDA:114,MockUniversalCoreReal.constructor -DA:78,114 -DA:79,114 -DA:80,114 +FNDA:50,MockUniversalCoreReal.constructor +DA:78,50 +DA:79,50 +DA:80,50 DA:84,0 FN:84,MockUniversalCoreReal.onlyUEModule FNDA:0,MockUniversalCoreReal.onlyUEModule @@ -2526,10 +2637,10 @@ FNDA:0,MockUniversalCoreReal.whenNotPaused DA:95,0 BRDA:95,2,0,- BRDA:95,2,1,- -DA:100,216 +DA:100,101 FN:100,MockUniversalCoreReal.hasRole -FNDA:216,MockUniversalCoreReal.hasRole -DA:101,216 +FNDA:101,MockUniversalCoreReal.hasRole +DA:101,101 DA:104,0 FN:104,MockUniversalCoreReal.grantRole FNDA:0,MockUniversalCoreReal.grantRole @@ -2538,14 +2649,14 @@ DA:108,0 FN:108,MockUniversalCoreReal.revokeRole FNDA:0,MockUniversalCoreReal.revokeRole DA:109,0 -DA:113,31 +DA:113,0 FN:113,MockUniversalCoreReal.isSupportedToken -FNDA:31,MockUniversalCoreReal.isSupportedToken -DA:114,31 -DA:117,115 +FNDA:0,MockUniversalCoreReal.isSupportedToken +DA:114,0 +DA:117,0 FN:117,MockUniversalCoreReal.setSupportedToken -FNDA:115,MockUniversalCoreReal.setSupportedToken -DA:118,115 +FNDA:0,MockUniversalCoreReal.setSupportedToken +DA:118,0 DA:122,0 FN:122,MockUniversalCoreReal.depositPRC20Token FNDA:0,MockUniversalCoreReal.depositPRC20Token @@ -2701,232 +2812,236 @@ DA:261,0 DA:264,27 FN:264,MockUniversalCoreReal.withdrawGasFeeWithGasLimit FNDA:27,MockUniversalCoreReal.withdrawGasFeeWithGasLimit -DA:265,27 -DA:267,27 -DA:268,27 -BRDA:268,26,0,- -BRDA:268,26,1,- -DA:270,27 -DA:271,26 -BRDA:271,27,0,- -BRDA:271,27,1,- -DA:273,27 -DA:278,0 -FN:278,MockUniversalCoreReal.updateBaseGasLimit +DA:269,27 +DA:271,27 +DA:272,27 +BRDA:272,26,0,- +BRDA:272,26,1,- +DA:274,27 +DA:275,26 +BRDA:275,27,0,- +BRDA:275,27,1,- +DA:277,27 +DA:282,0 +FN:282,MockUniversalCoreReal.updateBaseGasLimit FNDA:0,MockUniversalCoreReal.updateBaseGasLimit -DA:279,0 -DA:280,0 -DA:281,0 +DA:283,0 +DA:284,0 DA:285,0 -FN:285,MockUniversalCoreReal.setUniversalExecutorModule +DA:289,0 +FN:289,MockUniversalCoreReal.setUniversalExecutorModule FNDA:0,MockUniversalCoreReal.setUniversalExecutorModule FNF:29 -FNH:8 +FNH:6 LF:106 -LH:26 +LH:22 BRF:54 BRH:0 end_of_record TN: SF:test/mocks/MockWETH.sol -DA:41,0 -FN:41,MockWETH.decimals +DA:44,0 +FN:44,MockWETH.decimals FNDA:0,MockWETH.decimals -DA:42,0 -DA:48,960 -FN:48,MockWETH.deposit -FNDA:960,MockWETH.deposit -DA:49,960 -BRDA:49,0,0,- -BRDA:49,0,1,- -DA:50,960 -BRDA:50,1,0,- -BRDA:50,1,1,- -DA:51,960 -BRDA:51,2,0,- -BRDA:51,2,1,- -DA:53,960 -DA:54,960 -DA:61,0 -FN:61,MockWETH.withdraw -FNDA:0,MockWETH.withdraw -DA:62,0 -BRDA:62,3,0,- -BRDA:62,3,1,- -DA:63,0 -BRDA:63,4,0,- -BRDA:63,4,1,- -DA:64,0 -BRDA:64,5,0,- -BRDA:64,5,1,- -DA:65,0 -BRDA:65,6,0,- -BRDA:65,6,1,- -DA:67,0 -DA:71,0 -DA:80,960 -FN:80,MockWETH.transfer -FNDA:960,MockWETH.transfer -DA:81,960 -BRDA:81,7,0,- -BRDA:81,7,1,- -DA:82,960 -BRDA:82,8,0,- -BRDA:82,8,1,- -DA:83,960 -BRDA:83,9,0,- -BRDA:83,9,1,- -DA:84,960 -DA:90,0 -FN:90,MockWETH.transferFrom -FNDA:0,MockWETH.transferFrom -DA:91,0 -BRDA:91,10,0,- -BRDA:91,10,1,- -DA:92,0 -BRDA:92,11,0,- -BRDA:92,11,1,- -DA:93,0 -BRDA:93,12,0,- -BRDA:93,12,1,- -DA:94,0 -DA:102,0 -FN:102,MockWETH.mint -FNDA:0,MockWETH.mint -DA:103,0 -BRDA:103,13,0,- -BRDA:103,13,1,- +DA:45,0 +DA:51,1787 +FN:51,MockWETH.deposit +FNDA:1787,MockWETH.deposit +DA:52,1787 +BRDA:52,0,0,- +BRDA:52,0,1,- +DA:53,1787 +BRDA:53,1,0,- +BRDA:53,1,1,- +DA:54,1787 +BRDA:54,2,0,- +BRDA:54,2,1,- +DA:56,1787 +DA:57,1787 +DA:64,12 +FN:64,MockWETH.withdraw +FNDA:12,MockWETH.withdraw +DA:65,12 +BRDA:65,3,0,- +BRDA:65,3,1,- +DA:66,12 +BRDA:66,4,0,- +BRDA:66,4,1,- +DA:67,12 +BRDA:67,5,0,- +BRDA:67,5,1,- +DA:68,12 +BRDA:68,6,0,- +BRDA:68,6,1,- +DA:70,12 +DA:72,12 +DA:75,12 +DA:76,12 +BRDA:76,7,0,- +BRDA:76,7,1,- +DA:82,1786 +FN:82,MockWETH.transfer +FNDA:1786,MockWETH.transfer +DA:83,1786 +BRDA:83,8,0,- +BRDA:83,8,1,- +DA:84,1786 +BRDA:84,9,0,- +BRDA:84,9,1,- +DA:85,1786 +BRDA:85,10,0,- +BRDA:85,10,1,- +DA:86,1786 +DA:92,1 +FN:92,MockWETH.transferFrom +FNDA:1,MockWETH.transferFrom +DA:93,1 +BRDA:93,11,0,- +BRDA:93,11,1,- +DA:94,1 +BRDA:94,12,0,- +BRDA:94,12,1,- +DA:95,1 +BRDA:95,13,0,- +BRDA:95,13,1,- +DA:96,1 DA:104,0 -DA:112,0 -FN:112,MockWETH.burn -FNDA:0,MockWETH.burn -DA:113,0 -BRDA:113,14,0,- -BRDA:113,14,1,- +FN:104,MockWETH.mint +FNDA:0,MockWETH.mint +DA:105,0 +BRDA:105,14,0,- +BRDA:105,14,1,- +DA:106,0 DA:114,0 -DA:121,0 -FN:121,MockWETH.burn +FN:114,MockWETH.burn FNDA:0,MockWETH.burn -DA:122,0 -BRDA:122,15,0,- -BRDA:122,15,1,- +DA:115,0 +BRDA:115,15,0,- +BRDA:115,15,1,- +DA:116,0 DA:123,0 -DA:129,0 -FN:129,MockWETH.pauseDeposits -FNDA:0,MockWETH.pauseDeposits -DA:130,0 +FN:123,MockWETH.burn +FNDA:0,MockWETH.burn +DA:124,0 +BRDA:124,16,0,- +BRDA:124,16,1,- +DA:125,0 DA:131,0 -DA:137,0 -FN:137,MockWETH.unpauseDeposits -FNDA:0,MockWETH.unpauseDeposits -DA:138,0 +FN:131,MockWETH.pauseDeposits +FNDA:0,MockWETH.pauseDeposits +DA:132,0 +DA:133,0 DA:139,0 -DA:145,0 -FN:145,MockWETH.pauseWithdrawals -FNDA:0,MockWETH.pauseWithdrawals -DA:146,0 +FN:139,MockWETH.unpauseDeposits +FNDA:0,MockWETH.unpauseDeposits +DA:140,0 +DA:141,0 DA:147,0 -DA:153,0 -FN:153,MockWETH.unpauseWithdrawals -FNDA:0,MockWETH.unpauseWithdrawals -DA:154,0 +FN:147,MockWETH.pauseWithdrawals +FNDA:0,MockWETH.pauseWithdrawals +DA:148,0 +DA:149,0 DA:155,0 -DA:161,0 -FN:161,MockWETH.pauseTransfers -FNDA:0,MockWETH.pauseTransfers -DA:162,0 +FN:155,MockWETH.unpauseWithdrawals +FNDA:0,MockWETH.unpauseWithdrawals +DA:156,0 +DA:157,0 DA:163,0 -DA:169,0 -FN:169,MockWETH.unpauseTransfers -FNDA:0,MockWETH.unpauseTransfers -DA:170,0 +FN:163,MockWETH.pauseTransfers +FNDA:0,MockWETH.pauseTransfers +DA:164,0 +DA:165,0 DA:171,0 -DA:178,0 -FN:178,MockWETH.blacklist -FNDA:0,MockWETH.blacklist -DA:179,0 +FN:171,MockWETH.unpauseTransfers +FNDA:0,MockWETH.unpauseTransfers +DA:172,0 +DA:173,0 DA:180,0 -DA:187,0 -FN:187,MockWETH.unblacklist -FNDA:0,MockWETH.unblacklist -DA:188,0 +FN:180,MockWETH.blacklist +FNDA:0,MockWETH.blacklist +DA:181,0 +DA:182,0 DA:189,0 -DA:197,0 -FN:197,MockWETH.isBlacklisted +FN:189,MockWETH.unblacklist +FNDA:0,MockWETH.unblacklist +DA:190,0 +DA:191,0 +DA:199,0 +FN:199,MockWETH.isBlacklisted FNDA:0,MockWETH.isBlacklisted -DA:198,0 -DA:204,0 -FN:204,MockWETH.simulateDepositFailure +DA:200,0 +DA:206,0 +FN:206,MockWETH.simulateDepositFailure FNDA:0,MockWETH.simulateDepositFailure -DA:205,0 -DA:211,0 -FN:211,MockWETH.simulateWithdrawalFailure +DA:207,0 +DA:213,0 +FN:213,MockWETH.simulateWithdrawalFailure FNDA:0,MockWETH.simulateWithdrawalFailure -DA:212,0 -DA:220,0 -FN:220,MockWETH.forceSetBalance -FNDA:0,MockWETH.forceSetBalance -DA:221,0 +DA:214,0 DA:222,0 -BRDA:222,16,0,- -BRDA:222,16,1,- +FN:222,MockWETH.forceSetBalance +FNDA:0,MockWETH.forceSetBalance DA:223,0 DA:224,0 BRDA:224,17,0,- +BRDA:224,17,1,- DA:225,0 -DA:235,0 -FN:235,MockWETH.forceSetAllowance +DA:226,0 +BRDA:226,18,0,- +DA:227,0 +DA:237,0 +FN:237,MockWETH.forceSetAllowance FNDA:0,MockWETH.forceSetAllowance -DA:236,0 -DA:244,0 -FN:244,MockWETH.getETHBalance +DA:238,0 +DA:246,0 +FN:246,MockWETH.getETHBalance FNDA:0,MockWETH.getETHBalance -DA:245,0 -DA:253,0 -FN:253,MockWETH.wethToEth +DA:247,0 +DA:255,0 +FN:255,MockWETH.wethToEth FNDA:0,MockWETH.wethToEth -DA:254,0 -DA:262,0 -FN:262,MockWETH.ethToWeth +DA:256,0 +DA:264,0 +FN:264,MockWETH.ethToWeth FNDA:0,MockWETH.ethToWeth -DA:263,0 -DA:270,0 -FN:270,MockWETH.getTotalSupplyInETH +DA:265,0 +DA:272,0 +FN:272,MockWETH.getTotalSupplyInETH FNDA:0,MockWETH.getTotalSupplyInETH -DA:271,0 -DA:279,0 -FN:279,MockWETH.simulateETHTransfer -FNDA:0,MockWETH.simulateETHTransfer -DA:280,0 -BRDA:280,18,0,- -BRDA:280,18,1,- +DA:273,0 DA:281,0 +FN:281,MockWETH.simulateETHTransfer +FNDA:0,MockWETH.simulateETHTransfer DA:282,0 -DA:289,0 -FN:289,MockWETH.getContractInfo +BRDA:282,19,0,- +BRDA:282,19,1,- +DA:283,0 +DA:284,0 +DA:291,0 +FN:291,MockWETH.getContractInfo FNDA:0,MockWETH.getContractInfo -DA:290,0 -DA:300,0 -FN:300,MockWETH._toString -FNDA:0,MockWETH._toString -DA:301,0 -BRDA:301,19,0,- +DA:292,0 DA:302,0 +FN:302,MockWETH._toString +FNDA:0,MockWETH._toString +DA:303,0 +BRDA:303,20,0,- DA:304,0 -DA:305,0 DA:306,0 DA:307,0 DA:308,0 +DA:309,0 DA:310,0 -DA:311,0 DA:312,0 DA:313,0 DA:314,0 +DA:315,0 DA:316,0 +DA:318,0 FNF:28 -FNH:2 -LF:100 -LH:11 -BRF:38 +FNH:4 +LF:102 +LH:25 +BRF:40 BRH:0 end_of_record diff --git a/contracts/evm-gateway/lib/base64/LICENSE b/contracts/evm-gateway/lib/base64/LICENSE new file mode 100644 index 0000000..7dab2e7 --- /dev/null +++ b/contracts/evm-gateway/lib/base64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Brecht Devos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/contracts/evm-gateway/lib/base64/README.md b/contracts/evm-gateway/lib/base64/README.md new file mode 100644 index 0000000..8524b19 --- /dev/null +++ b/contracts/evm-gateway/lib/base64/README.md @@ -0,0 +1,15 @@ +# base64 + +base64 encoding/decoding in solidity. + +Also available as a package. Add it to `package.json`: + +``` +"base64-sol": "1.1.0" +``` + +and import it in solidity: + +``` +import 'base64-sol/base64.sol'; +``` diff --git a/contracts/evm-gateway/lib/base64/base64.sol b/contracts/evm-gateway/lib/base64/base64.sol new file mode 100644 index 0000000..c434a9e --- /dev/null +++ b/contracts/evm-gateway/lib/base64/base64.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0; + +/// @title Base64 +/// @author Brecht Devos - +/// @notice Provides functions for encoding/decoding base64 +library Base64 { + string internal constant TABLE_ENCODE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + bytes internal constant TABLE_DECODE = hex"0000000000000000000000000000000000000000000000000000000000000000" + hex"00000000000000000000003e0000003f3435363738393a3b3c3d000000000000" + hex"00000102030405060708090a0b0c0d0e0f101112131415161718190000000000" + hex"001a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132330000000000"; + + function encode(bytes memory data) internal pure returns (string memory) { + if (data.length == 0) return ''; + + // load the table into memory + string memory table = TABLE_ENCODE; + + // multiply by 4/3 rounded up + uint256 encodedLen = 4 * ((data.length + 2) / 3); + + // add some extra buffer at the end required for the writing + string memory result = new string(encodedLen + 32); + + assembly { + // set the actual output length + mstore(result, encodedLen) + + // prepare the lookup table + let tablePtr := add(table, 1) + + // input ptr + let dataPtr := data + let endPtr := add(dataPtr, mload(data)) + + // result ptr, jump over length + let resultPtr := add(result, 32) + + // run over the input, 3 bytes at a time + for {} lt(dataPtr, endPtr) {} + { + // read 3 bytes + dataPtr := add(dataPtr, 3) + let input := mload(dataPtr) + + // write 4 characters + mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) + resultPtr := add(resultPtr, 1) + mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) + resultPtr := add(resultPtr, 1) + mstore8(resultPtr, mload(add(tablePtr, and(shr( 6, input), 0x3F)))) + resultPtr := add(resultPtr, 1) + mstore8(resultPtr, mload(add(tablePtr, and( input, 0x3F)))) + resultPtr := add(resultPtr, 1) + } + + // padding with '=' + switch mod(mload(data), 3) + case 1 { mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) } + case 2 { mstore(sub(resultPtr, 1), shl(248, 0x3d)) } + } + + return result; + } + + function decode(string memory _data) internal pure returns (bytes memory) { + bytes memory data = bytes(_data); + + if (data.length == 0) return new bytes(0); + require(data.length % 4 == 0, "invalid base64 decoder input"); + + // load the table into memory + bytes memory table = TABLE_DECODE; + + // every 4 characters represent 3 bytes + uint256 decodedLen = (data.length / 4) * 3; + + // add some extra buffer at the end required for the writing + bytes memory result = new bytes(decodedLen + 32); + + assembly { + // padding with '=' + let lastBytes := mload(add(data, mload(data))) + if eq(and(lastBytes, 0xFF), 0x3d) { + decodedLen := sub(decodedLen, 1) + if eq(and(lastBytes, 0xFFFF), 0x3d3d) { + decodedLen := sub(decodedLen, 1) + } + } + + // set the actual output length + mstore(result, decodedLen) + + // prepare the lookup table + let tablePtr := add(table, 1) + + // input ptr + let dataPtr := data + let endPtr := add(dataPtr, mload(data)) + + // result ptr, jump over length + let resultPtr := add(result, 32) + + // run over the input, 4 characters at a time + for {} lt(dataPtr, endPtr) {} + { + // read 4 characters + dataPtr := add(dataPtr, 4) + let input := mload(dataPtr) + + // write 3 bytes + let output := add( + add( + shl(18, and(mload(add(tablePtr, and(shr(24, input), 0xFF))), 0xFF)), + shl(12, and(mload(add(tablePtr, and(shr(16, input), 0xFF))), 0xFF))), + add( + shl( 6, and(mload(add(tablePtr, and(shr( 8, input), 0xFF))), 0xFF)), + and(mload(add(tablePtr, and( input , 0xFF))), 0xFF) + ) + ) + mstore(resultPtr, shl(232, output)) + resultPtr := add(resultPtr, 3) + } + } + + return result; + } +} diff --git a/contracts/evm-gateway/lib/base64/package.json b/contracts/evm-gateway/lib/base64/package.json new file mode 100644 index 0000000..75657ee --- /dev/null +++ b/contracts/evm-gateway/lib/base64/package.json @@ -0,0 +1,23 @@ +{ + "name": "base64-sol", + "version": "1.1.0", + "description": "base64 implementation in solidity", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Brechtpd/base64.git" + }, + "keywords": [ + "base64", + "solidity" + ], + "author": "Brecht Devos", + "license": "MIT", + "bugs": { + "url": "https://github.com/Brechtpd/base64/issues" + }, + "homepage": "https://github.com/Brechtpd/base64#readme" +} diff --git a/contracts/evm-gateway/lib/chainlink-brownie-contracts b/contracts/evm-gateway/lib/chainlink-brownie-contracts index 67887b8..5cb41fb 160000 --- a/contracts/evm-gateway/lib/chainlink-brownie-contracts +++ b/contracts/evm-gateway/lib/chainlink-brownie-contracts @@ -1 +1 @@ -Subproject commit 67887b84d3add02a25ef4145fc014e2f549509da +Subproject commit 5cb41fbc9b525338b6098da5ea7dd0b7e92f89e4 diff --git a/contracts/evm-gateway/lib/forge-std b/contracts/evm-gateway/lib/forge-std index 60acb7a..7117c90 160000 --- a/contracts/evm-gateway/lib/forge-std +++ b/contracts/evm-gateway/lib/forge-std @@ -1 +1 @@ -Subproject commit 60acb7aaadcce2d68e52986a0a66fe79f07d138f +Subproject commit 7117c90c8cf6c68e5acce4f09a6b24715cea4de6 diff --git a/contracts/evm-gateway/lib/openzeppelin-contracts b/contracts/evm-gateway/lib/openzeppelin-contracts index e4f7021..fcbae53 160000 --- a/contracts/evm-gateway/lib/openzeppelin-contracts +++ b/contracts/evm-gateway/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 +Subproject commit fcbae5394ae8ad52d8e580a3477db99814b9d565 diff --git a/contracts/evm-gateway/lib/openzeppelin-contracts-upgradeable b/contracts/evm-gateway/lib/openzeppelin-contracts-upgradeable index 60b305a..2d081f2 160000 --- a/contracts/evm-gateway/lib/openzeppelin-contracts-upgradeable +++ b/contracts/evm-gateway/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 60b305a8f3ff0c7688f02ac470417b6bbf1c4d27 +Subproject commit 2d081f24cac1a867f6f73d512f2022e1fa987854 diff --git a/contracts/evm-gateway/lib/v3-core b/contracts/evm-gateway/lib/v3-core index 6562c52..e3589b1 160000 --- a/contracts/evm-gateway/lib/v3-core +++ b/contracts/evm-gateway/lib/v3-core @@ -1 +1 @@ -Subproject commit 6562c52e8f75f0c10f9deaf44861847585fc8129 +Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899 diff --git a/contracts/evm-gateway/lib/v3-periphery b/contracts/evm-gateway/lib/v3-periphery index b325bb0..80f26c8 160000 --- a/contracts/evm-gateway/lib/v3-periphery +++ b/contracts/evm-gateway/lib/v3-periphery @@ -1 +1 @@ -Subproject commit b325bb0905d922ae61fcc7df85ee802e8df5e96c +Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 diff --git a/contracts/evm-gateway/src/PC20.sol b/contracts/evm-gateway/src/PC20.sol new file mode 100644 index 0000000..3596b28 --- /dev/null +++ b/contracts/evm-gateway/src/PC20.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Errors } from "./libraries/Errors.sol"; + +/// @title PC20 +/// @notice Push Chain ERC20-style wrapper for external assets +/// @dev +/// - Name / symbol / decimals are set at construction. +/// - `gateway` is the ONLY address allowed to mint / burn. +/// - Used together with PC20Factory + UniversalGateway. +contract PC20 is ERC20 { + /// @notice Address that can mint / burn (typically UniversalGateway). + address public immutable gateway; + + /// @notice Origin token address on Push Chain that this PC20 represents. + address public immutable originToken; + + /// @notice Decimals for this token (immutable). + uint8 private immutable _decimals; + + error OnlyGateway(); + error InvalidAmount(); + + modifier onlyGateway() { + if (msg.sender != gateway) revert OnlyGateway(); + _; + } + + /// @param name_ ERC20 name + /// @param symbol_ ERC20 symbol + /// @param decimals_ token decimals + /// @param gateway_ minter / burner (UniversalGateway) + /// @param originToken_ origin token address on Push Chain + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + address gateway_, + address originToken_ + ) ERC20(name_, symbol_) { + if (gateway_ == address(0)) revert Errors.ZeroAddress(); + if (originToken_ == address(0)) revert Errors.ZeroAddress(); + gateway = gateway_; + originToken = originToken_; + _decimals = decimals_; + } + + /// @notice Returns decimals for this PC20 token. + function decimals() public view override returns (uint8) { + return _decimals; + } + + /// @notice Mint tokens to `to`. + /// @dev Only callable by `gateway`. + function mint(address to, uint256 amount) external onlyGateway { + if (to == address(0)) revert Errors.ZeroAddress(); + if (amount == 0) revert InvalidAmount(); + _mint(to, amount); + } + + /// @notice Burn tokens from `from`. + /// @dev Only callable by `gateway`. + function burn(address from, uint256 amount) external onlyGateway { + if (from == address(0)) revert Errors.ZeroAddress(); + if (amount == 0) revert InvalidAmount(); + _burn(from, amount); + } +} diff --git a/contracts/evm-gateway/src/PC20Factory.sol b/contracts/evm-gateway/src/PC20Factory.sol new file mode 100644 index 0000000..34660dc --- /dev/null +++ b/contracts/evm-gateway/src/PC20Factory.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { Errors } from "./libraries/Errors.sol"; +import { PC20 } from "./PC20.sol"; + +/// @title PC20Factory +/// @notice Deploys PC20 tokens at deterministic addresses using CREATE2, +/// keyed only by the origin token address. +/// - One PC20 per origin token. +/// - `gateway` (UniversalGateway) is the sole creator and the minter/burner in PC20. +contract PC20Factory { + /// @notice Address allowed to create PC20 wrappers (typically UniversalGateway). + address public immutable gateway; + + /// @notice Mapping from origin token (Push Chain address) to its PC20 wrapper on this chain. + mapping(address => address) public pc20Mapping; + + /// @dev Emitted when a new PC20 wrapper is deployed. + event PC20Deployed( + address indexed originToken, + address indexed pc20Token, + string name, + string symbol, + uint8 decimals + ); + + error OnlyGateway(); + error PC20DeploymentFailed(); + error InvalidMetadata(); + + modifier onlyGateway() { + if (msg.sender != gateway) revert OnlyGateway(); + _; + } + + constructor(address _gateway) { + if (_gateway == address(0)) revert Errors.ZeroAddress(); + gateway = _gateway; + } + + /// @notice Returns the PC20 wrapper for a given origin token, or address(0) if not deployed. + function getPC20(address originToken) external view returns (address) { + return pc20Mapping[originToken]; + } + + /// @notice Deploy a PC20 wrapper for a given origin token if not already deployed. + /// @dev Deterministic per originToken: + /// - salt = keccak256(abi.encodePacked(originToken)) + /// If already created, returns the existing wrapper. + function createPC20( + address originToken, + string calldata name, + string calldata symbol, + uint8 decimals + ) external onlyGateway returns (address pc20Token) { + if (originToken == address(0)) revert Errors.ZeroAddress(); + if (bytes(name).length == 0 || bytes(symbol).length == 0) revert InvalidMetadata(); + + pc20Token = pc20Mapping[originToken]; + if (pc20Token != address(0)) { + // Already deployed for this origin token + return pc20Token; + } + + // Deterministic salt per origin token + bytes32 salt = keccak256(abi.encodePacked(originToken)); + + // Constructor args baked into init code + bytes memory bytecode = abi.encodePacked( + type(PC20).creationCode, + abi.encode(name, symbol, decimals, gateway, originToken) + ); + + address deployed; + assembly { + let encodedData := add(bytecode, 0x20) + let encodedSize := mload(bytecode) + deployed := create2(0, encodedData, encodedSize, salt) + } + + if (deployed == address(0)) revert PC20DeploymentFailed(); + + pc20Token = deployed; + pc20Mapping[originToken] = pc20Token; + + emit PC20Deployed(originToken, pc20Token, name, symbol, decimals); + } +} diff --git a/contracts/evm-gateway/src/PC721.sol b/contracts/evm-gateway/src/PC721.sol new file mode 100644 index 0000000..4567739 --- /dev/null +++ b/contracts/evm-gateway/src/PC721.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import { Errors } from "./libraries/Errors.sol"; + +/// @title PC721 +/// @notice Push Chain ERC721 style wrapper for external NFTs +/// @dev +/// - Name and symbol are set at construction. +/// - `gateway` is the ONLY address allowed to mint and burn. +/// - Used together with PC721Factory and UniversalGateway. +contract PC721 is ERC721 { + /// @notice Address that can mint and burn (typically UniversalGateway). + address public immutable gateway; + + /// @notice Origin token address on Push Chain that this PC721 represents. + address public immutable originToken; + + error OnlyGateway(); + error InvalidTokenId(); + + modifier onlyGateway() { + if (msg.sender != gateway) revert OnlyGateway(); + _; + } + + /// @param name_ ERC721 name + /// @param symbol_ ERC721 symbol + /// @param gateway_ minter and burner (UniversalGateway) + /// @param originToken_ origin token address on Push Chain + constructor( + string memory name_, + string memory symbol_, + address gateway_, + address originToken_ + ) ERC721(name_, symbol_) { + if (gateway_ == address(0)) revert Errors.ZeroAddress(); + if (originToken_ == address(0)) revert Errors.ZeroAddress(); + gateway = gateway_; + originToken = originToken_; + } + + /// @notice Mint tokenId to `to`. + /// @dev Only callable by `gateway`. + function mint(address to, uint256 tokenId) external onlyGateway { + if (to == address(0)) revert Errors.ZeroAddress(); + if (tokenId == 0) revert InvalidTokenId(); // optional, but keeps things strict + _mint(to, tokenId); + } + + /// @notice Burn `tokenId`. + /// @dev Only callable by `gateway`. Does not check msg.sender ownership, + /// since the gateway is trusted by design. + function burn(uint256 tokenId) external onlyGateway { + if (tokenId == 0) revert InvalidTokenId(); + _burn(tokenId); + } +} diff --git a/contracts/evm-gateway/src/PC721Factory.sol b/contracts/evm-gateway/src/PC721Factory.sol new file mode 100644 index 0000000..5dc658c --- /dev/null +++ b/contracts/evm-gateway/src/PC721Factory.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { Errors } from "./libraries/Errors.sol"; +import { PC721 } from "./PC721.sol"; + +/// @title PC721Factory +/// @notice Deploys PC721 tokens at deterministic addresses using CREATE2, +/// keyed only by the origin token address. +/// - One PC721 per origin token. +/// - `gateway` (UniversalGateway) is the sole creator and the minter/burner in PC721. +contract PC721Factory { + /// @notice Address allowed to create PC721 wrappers (typically UniversalGateway). + address public immutable gateway; + + /// @notice Mapping from origin token (Push Chain address) to its PC721 wrapper on this chain. + mapping(address => address) public pc721Mapping; + + /// @dev Emitted when a new PC721 wrapper is deployed. + event PC721Deployed( + address indexed originToken, + address indexed pc721Token, + string name, + string symbol + ); + + error OnlyGateway(); + error PC721DeploymentFailed(); + error InvalidMetadata(); + + modifier onlyGateway() { + if (msg.sender != gateway) revert OnlyGateway(); + _; + } + + constructor(address _gateway) { + if (_gateway == address(0)) revert Errors.ZeroAddress(); + gateway = _gateway; + } + + /// @notice Returns the PC721 wrapper for a given origin token, or address(0) if not deployed. + function getPC721(address originToken) external view returns (address) { + return pc721Mapping[originToken]; + } + + /// @notice Deploy a PC721 wrapper for a given origin token if not already deployed. + /// @dev Deterministic per originToken: + /// - salt = keccak256(abi.encodePacked(originToken)) + /// If already created, returns the existing wrapper. + function createPC721( + address originToken, + string calldata name, + string calldata symbol + ) external onlyGateway returns (address pc721Token) { + if (originToken == address(0)) revert Errors.ZeroAddress(); + if (bytes(name).length == 0 || bytes(symbol).length == 0) revert InvalidMetadata(); + + pc721Token = pc721Mapping[originToken]; + if (pc721Token != address(0)) { + // Already deployed for this origin token + return pc721Token; + } + + // Deterministic salt per origin token + bytes32 salt = keccak256(abi.encodePacked(originToken)); + + // Constructor args baked into init code + bytes memory bytecode = abi.encodePacked( + type(PC721).creationCode, + abi.encode(name, symbol, gateway, originToken) + ); + + address deployed; + assembly { + let encodedData := add(bytecode, 0x20) + let encodedSize := mload(bytecode) + deployed := create2(0, encodedData, encodedSize, salt) + } + + if (deployed == address(0)) revert PC721DeploymentFailed(); + + pc721Token = deployed; + pc721Mapping[originToken] = pc721Token; + + emit PC721Deployed(originToken, pc721Token, name, symbol); + } +} diff --git a/contracts/evm-gateway/src/UniversalGateway.sol b/contracts/evm-gateway/src/UniversalGateway.sol index 43fa61a..01f2825 100644 --- a/contracts/evm-gateway/src/UniversalGateway.sol +++ b/contracts/evm-gateway/src/UniversalGateway.sol @@ -37,10 +37,15 @@ pragma solidity 0.8.26; import { IWETH } from "./interfaces/IWETH.sol"; import { Errors } from "./libraries/Errors.sol"; import { IUniversalGateway } from "./interfaces/IUniversalGateway.sol"; +import { PC20Factory } from "./PC20Factory.sol"; +import { PC721Factory } from "./PC721Factory.sol"; +import { IPC20 } from "./interfaces/IPC20.sol"; +import { IPC721 } from "./interfaces/IPC721.sol"; import { RevertInstructions, - UniversalPayload, TX_TYPE, - EpochUsage } from "./libraries/Types.sol"; + EpochUsage, + UniversalTxRequest, + UniversalTokenTxRequest } from "./libraries/Types.sol"; import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; @@ -51,8 +56,8 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { ContextUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; -import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; contract UniversalGateway is @@ -89,10 +94,7 @@ contract UniversalGateway is ISwapRouterV3 public uniV3Router; // Uniswap V3 router. IUniswapV3Factory public uniV3Factory; // Uniswap V3 factory. uint256 public defaultSwapDeadlineSec; // Default swap deadline window (industry common ~10 minutes). - uint24[3] public v3FeeOrder = // Fee order for Uniswap V3 router. - [uint24(500), - uint24(3000), - uint24(10000)]; + uint24[3] public v3FeeOrder; // Fee order for Uniswap V3 router. /// @notice Chainlink Oracle Configs uint256 public chainlinkStalePeriod; // Chainlink stale period. @@ -104,6 +106,16 @@ contract UniversalGateway is /// @notice Map to track if a payload has been executed mapping(bytes32 => bool) public isExecuted; + /// @notice PC20 and PC721 Factories for wrapped tokens + PC20Factory public pc20Factory; + PC721Factory public pc721Factory; + + // Magic Marker Constants + bytes4 private constant MAGIC_PCAS = 0x50434153; // "PCAS" + uint8 private constant META_VERSION = 1; + uint8 private constant META_KIND_PC20 = 1; + uint8 private constant META_KIND_PC721 = 2; + /** * @notice Initialize the UniversalGateway contract * @param admin DEFAULT_ADMIN_ROLE holder @@ -162,7 +174,7 @@ contract UniversalGateway is } // ========================= - // ADMIN ACTIONS + // UG_1: ADMIN ACTIONS // ========================= function pause() external whenNotPaused onlyRole(DEFAULT_ADMIN_ROLE) { _pause(); @@ -230,6 +242,20 @@ contract UniversalGateway is uniV3Router = ISwapRouterV3(router); } + /// @notice Set the PC20 factory address + /// @param factory PC20Factory address + function setPC20Factory(address factory) external onlyRole(DEFAULT_ADMIN_ROLE) whenNotPaused { + if (factory == address(0)) revert Errors.ZeroAddress(); + pc20Factory = PC20Factory(factory); + } + + /// @notice Set the PC721 factory address + /// @param factory PC721Factory address + function setPC721Factory(address factory) external onlyRole(DEFAULT_ADMIN_ROLE) whenNotPaused { + if (factory == address(0)) revert Errors.ZeroAddress(); + pc721Factory = PC721Factory(factory); + } + /// @notice Set limit thresholds for a batch of tokens (0 disables support for that token) /// @param tokens tokens to set limit thresholds for /// @param thresholds limit thresholds for the tokens @@ -244,20 +270,6 @@ contract UniversalGateway is } } - /// @notice Update limit thresholds for a batch of tokens - /// @param tokens tokens to update limit thresholds for - /// @param thresholds limit thresholds for the tokens - function updateTokenLimitThreshold(address[] calldata tokens, uint256[] calldata thresholds) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - if (tokens.length != thresholds.length) revert Errors.InvalidInput(); - for (uint256 i = 0; i < tokens.length; i++) { - tokenToLimitThreshold[tokens[i]] = thresholds[i]; - emit TokenLimitThresholdUpdated(tokens[i], thresholds[i]); - } - } - /// @notice Update the epoch duration (hard reset schedule) /// @param newDurationSec new epoch duration function updateEpochDuration(uint256 newDurationSec) external onlyRole(DEFAULT_ADMIN_ROLE) { @@ -305,209 +317,215 @@ contract UniversalGateway is } // ========================= - // sendTxWithGas - Fee Abstraction Route + // UG_2: UNIVERSAL TRANSACTION // ========================= /// @inheritdoc IUniversalGateway - function sendTxWithGas( - UniversalPayload calldata payload, - RevertInstructions calldata revertInstruction, - bytes memory signatureData - ) external payable nonReentrant whenNotPaused { - - _sendTxWithGas( - _msgSender(), abi.encode(payload), msg.value, revertInstruction, TX_TYPE.GAS_AND_PAYLOAD, signatureData - ); + function sendUniversalTx(UniversalTxRequest calldata req) external payable nonReentrant whenNotPaused { + uint256 nativeValue = msg.value; + TX_TYPE txType = _fetchTxType(req, nativeValue); + _routeUniversalTx(req, _msgSender(), nativeValue, txType); } /// @inheritdoc IUniversalGateway - function sendTxWithGas( - address tokenIn, - uint256 amountIn, - UniversalPayload calldata payload, - RevertInstructions calldata revertInstruction, - uint256 amountOutMinETH, - uint256 deadline, - bytes memory signatureData - ) external nonReentrant whenNotPaused { - if (tokenIn == address(0)) revert Errors.InvalidInput(); - if (amountIn == 0) revert Errors.InvalidAmount(); - if (amountOutMinETH == 0) revert Errors.InvalidAmount(); - if (deadline != 0 && deadline < block.timestamp) revert Errors.SlippageExceededOrExpired(); - - // Swap token to native ETH - uint256 ethOut = swapToNative(tokenIn, amountIn, amountOutMinETH, deadline); + function sendUniversalTx(UniversalTokenTxRequest calldata reqToken) external payable nonReentrant whenNotPaused { + // Validate token-as-gas parameters + if (reqToken.gasToken == address(0)) revert Errors.InvalidInput(); + if (reqToken.gasAmount == 0) revert Errors.InvalidAmount(); + if (reqToken.amountOutMinETH == 0) revert Errors.InvalidAmount(); + if (reqToken.deadline != 0 && reqToken.deadline < block.timestamp) revert Errors.SlippageExceededOrExpired(); + + // Swap token to native + uint256 nativeValue = swapToNative(reqToken.gasToken, reqToken.gasAmount, reqToken.amountOutMinETH, reqToken.deadline); + + // Build UniversalTxRequest from token request + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: reqToken.recipient, + token: reqToken.token, + amount: reqToken.amount, + payload: reqToken.payload, + revertRecipient: reqToken.revertRecipient, + signatureData: reqToken.signatureData + }); - _sendTxWithGas( - _msgSender(), abi.encode(payload), ethOut, revertInstruction, TX_TYPE.GAS_AND_PAYLOAD, signatureData - ); + TX_TYPE txType = _fetchTxType(req, nativeValue); + _routeUniversalTx(req, _msgSender(), nativeValue, txType); } - /// @notice Internal helper function to deposit for Instant TX. - /// @dev Handles rate-limit checks for Fee Abstraction Tx Route + + // ========================= + // UG_2.1: UNIVERSAL TRANSACTION Internal Helpers + // ========================= + + /// @notice Internal helper function to deposit for TX_TYPE.GAS or TX_TYPE.GAS_AND_PAYLOAD + /// @dev Handles rate-limit checks for Instant Tx Route ( Lower Block Confirmations ) + /// @dev Recipient address(0) indicates the funds are attributed to the caller's UEA on Push Chain. + /// @param _txType TX_TYPE.GAS or TX_TYPE.GAS_AND_PAYLOAD + /// @param _caller Caller address + /// @param _gasAmount Gas amount + /// @param _payload Payload + /// @param _revertRecipient Fund recipient + /// @param _signatureData Signature data function _sendTxWithGas( + TX_TYPE _txType, address _caller, + uint256 _gasAmount, bytes memory _payload, - uint256 _nativeTokenAmount, - RevertInstructions calldata _revertInstruction, - TX_TYPE _txType, + address _revertRecipient, bytes memory _signatureData - ) internal { - if (_revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); - - // performs rate-limit checks and handle deposit - _checkUSDCaps(_nativeTokenAmount); - _checkBlockUSDCap(_nativeTokenAmount); - _handleDeposits(address(0), _nativeTokenAmount); + ) private { - emit UniversalTx({ - sender: _caller, - recipient: address(0), - token: address(0), - amount: _nativeTokenAmount, - payload: _payload, - revertInstruction: _revertInstruction, - txType: _txType, - signatureData: _signatureData - }); - } + if (_gasAmount > 0) { - // ========================= - // sendTxWithFunds - Universal Transaction Route - // ========================= - - /// @inheritdoc IUniversalGateway - function sendFunds( - address recipient, - address bridgeToken, - uint256 bridgeAmount, - RevertInstructions calldata revertInstruction - ) external payable nonReentrant whenNotPaused { - if (recipient == address(0)) revert Errors.InvalidRecipient(); + _checkUSDCaps(_gasAmount); + _checkBlockUSDCap(_gasAmount); + _handleDeposits(address(0), _gasAmount); - if (bridgeToken == address(0)) { - if (msg.value != bridgeAmount) revert Errors.InvalidAmount(); - _consumeRateLimit(address(0), bridgeAmount); - _handleDeposits(address(0), bridgeAmount); - } else { - if (msg.value != 0) revert Errors.InvalidAmount(); - _consumeRateLimit(bridgeToken, bridgeAmount); - _handleDeposits(bridgeToken, bridgeAmount); } - _sendTxWithFunds( - _msgSender(), - recipient, - bridgeToken, - bridgeAmount, - bytes(""), // Empty payload for funds-only bridge - revertInstruction, - TX_TYPE.FUNDS, - bytes("") - ); + _emitUniversalTx( + _caller, address(0), address(0), _gasAmount, _payload, _revertRecipient, _txType, _signatureData); } - /// @inheritdoc IUniversalGateway - function sendTxWithFunds( - address bridgeToken, - uint256 bridgeAmount, - UniversalPayload calldata payload, - RevertInstructions calldata revertInstruction, - bytes memory signatureData - ) external payable nonReentrant whenNotPaused { - if (bridgeAmount == 0) revert Errors.InvalidAmount(); - uint256 gasAmount = msg.value; - if (gasAmount == 0) revert Errors.InvalidAmount(); + /// @notice Internal helper function to deposit for TX_TYPE.FUNDS or TX_TYPE.FUNDS_AND_PAYLOAD + /// @dev Handles rate-limit checks for Universal Tx Route ( Higher Block Confirmations ) + /// @dev Recipient address(0) indicates the funds are attributed to the caller's UEA on Push Chain. + /// @param _req UniversalTxRequest struct + /// @param nativeValue Native value ( msg.value ) + /// @param txType TX_TYPE.FUNDS or TX_TYPE.FUNDS_AND_PAYLOAD + function _sendTxWithFunds(UniversalTxRequest memory _req, uint256 nativeValue, TX_TYPE txType) private { + // Case 1: For TX_TYPE = FUNDS + + if (txType == TX_TYPE.FUNDS) { + address tokenForFunds; + // Case 1.1: Token to bridge is Native Token -> address(0) + if (_req.token == address(0)) { + if (_req.amount != nativeValue) revert Errors.InvalidAmount(); + tokenForFunds = address(0); + } + // Case 1.2: Token to bridge is ERC20 Token -> _req.token + else { + if (nativeValue > 0) revert Errors.InvalidAmount(); + tokenForFunds = _req.token; + } - _sendTxWithGas(_msgSender(), bytes(""), gasAmount, revertInstruction, TX_TYPE.GAS, signatureData); + _consumeRateLimit(tokenForFunds, _req.amount); + _handleDeposits(tokenForFunds, _req.amount); + + _emitUniversalTx( + _msgSender(), + _req.recipient, + tokenForFunds, + _req.amount, + _req.payload, + _req.revertRecipient, + txType, + _req.signatureData + ); + } - // performs rate-limit checks and handle deposit - _consumeRateLimit(bridgeToken, bridgeAmount); - _handleDeposits(bridgeToken, bridgeAmount); + // Case 2: For TX_TYPE = FUNDS_AND_PAYLOAD + // Note: Two possible routes for TX_TYPE.FUNDS_AND_PAYLOAD: + // - Case 2.1: No Batching (nativeValue == 0): user already has UEA with PC token ( gas ) on Push to execute payloads + // -> user already has UEA with native PC tokens on Push Chain. + // -> user can directly move _req.amount for _req.token to Push Chain. + // - Case 2.2: Batching of Gas + Funds_and_Payload (nativeValue > 0): with token == native_token + // -> user refils UEA's gas and also bridges native token. + // -> Split Needed: Native token is split between gasAmount and bridge amount ( nativeValue >= _req.amount ) + // -> _sendTxWithGas is used to send gasAmount + // -> _sendTxWithFunds is used to send bridgeAmount + // - Case 2.3: Batching of Gas + Funds_and_Payload (nativeValue > 0): with token != native_token + // -> user refils UEA's gas and also bridges ERC20 token. + // -> No Split Needed: gasAmount is used via native_token, and bridgeAmount is used via ERC20 token. + // -> _sendTxWithGas is used to send gasAmount + // -> _sendTxWithFunds is used to send bridgeAmount + if (txType == TX_TYPE.FUNDS_AND_PAYLOAD) { + address tokenForFundsAndPayload; + // Case 2.1: No Batching ( nativeValue == 0 ): user already has UEA with PC token ( gas ) on Push to execute payloads + if (nativeValue == 0) { + if (_req.token == address(0)) revert Errors.InvalidAmount(); + + tokenForFundsAndPayload = _req.token; + } + // Case 2.2: Batching of Gas + Funds_and_Payload (nativeValue > 0): with token == native_token + else if (_req.token == address(0)) { + if (nativeValue < _req.amount) revert Errors.InvalidAmount(); + + uint256 gasAmount = nativeValue - _req.amount; + + if (gasAmount > 0) { + _sendTxWithGas( + TX_TYPE.GAS, _msgSender(), gasAmount, bytes(""), _req.revertRecipient, _req.signatureData + ); + } + tokenForFundsAndPayload = address(0); + } + // Case 2.3: Batching of Gas + Funds_and_Payload (nativeValue > 0): with token != native_token + else if (_req.token != address(0)) { + uint256 gasAmount = nativeValue; + // Send Gas to caller's UEA via instant route + _sendTxWithGas( + TX_TYPE.GAS, _msgSender(), gasAmount, bytes(""), _req.revertRecipient, _req.signatureData + ); + + tokenForFundsAndPayload = _req.token; + } - _sendTxWithFunds( - _msgSender(), - address(0), - bridgeToken, - bridgeAmount, - abi.encode(payload), - revertInstruction, - TX_TYPE.FUNDS_AND_PAYLOAD, - signatureData - ); + _consumeRateLimit(tokenForFundsAndPayload, _req.amount); + _handleDeposits(tokenForFundsAndPayload, _req.amount); + // Recipient for FUNDS_AND_PAYLOAD is address(0) -> UEA. + _emitUniversalTx( + _msgSender(), + address(0), + tokenForFundsAndPayload, + _req.amount, + _req.payload, + _req.revertRecipient, + txType, + _req.signatureData + ); + } } - /// @inheritdoc IUniversalGateway - function sendTxWithFunds( - address bridgeToken, - uint256 bridgeAmount, - address gasToken, - uint256 gasAmount, - uint256 amountOutMinETH, - uint256 deadline, - UniversalPayload calldata payload, - RevertInstructions calldata revertInstruction, + /// @notice Internal helper function to emit the UniversalTx event + /// @param sender Sender address + /// @param recipient Recipient address + /// @param token Token address + /// @param amount Amount + /// @param payload Payload + /// @param revertRecipient Fund recipient + /// @param txType TX_TYPE + /// @param signatureData Signature data + function _emitUniversalTx( + address sender, + address recipient, + address token, + uint256 amount, + bytes memory payload, + address revertRecipient, + TX_TYPE txType, bytes memory signatureData - ) external nonReentrant whenNotPaused { - if (bridgeAmount == 0) revert Errors.InvalidAmount(); - if (gasToken == address(0)) revert Errors.InvalidInput(); - if (gasAmount == 0) revert Errors.InvalidAmount(); - - // Swap gasToken to native ETH - uint256 nativeGasAmount = swapToNative(gasToken, gasAmount, amountOutMinETH, deadline); - - _sendTxWithGas(_msgSender(), bytes(""), nativeGasAmount, revertInstruction, TX_TYPE.GAS, signatureData); - - // performs rate-limit checks and handle deposit - _consumeRateLimit(bridgeToken, bridgeAmount); - _handleDeposits(bridgeToken, bridgeAmount); - _sendTxWithFunds( - _msgSender(), - address(0), - bridgeToken, - bridgeAmount, - abi.encode(payload), - revertInstruction, - TX_TYPE.FUNDS_AND_PAYLOAD, - signatureData - ); - } - - /// @notice Internal helper function to deposit for Universal TX. - /// @dev Handles rate-limit checks for Universal Transaction Route - function _sendTxWithFunds( - address _caller, - address _recipient, - address _bridgeToken, - uint256 _bridgeAmount, - bytes memory _payload, - RevertInstructions calldata _revertInstruction, - TX_TYPE _txType, - bytes memory _signatureData - ) internal { - if (_revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); - /// for recipient == address(0), the funds are being moved to UEA of the msg.sender on Push Chain. - if (_recipient == address(0)) { - if (_txType != TX_TYPE.FUNDS_AND_PAYLOAD && _txType != TX_TYPE.GAS_AND_PAYLOAD) { - revert Errors.InvalidTxType(); - } - } - + ) private { emit UniversalTx({ - sender: _caller, - recipient: _recipient, - token: _bridgeToken, - amount: _bridgeAmount, - payload: _payload, - revertInstruction: _revertInstruction, - txType: _txType, - signatureData: _signatureData + sender: sender, + recipient: recipient, + token: token, + amount: amount, + payload: payload, + revertRecipient: revertRecipient, + txType: txType, + signatureData: signatureData }); } + + // ========================= + // UG_3: REVERT HANDLING PATHS + // ========================= + /// @inheritdoc IUniversalGateway function revertUniversalTxToken( - bytes32 txID, + bytes calldata txID, address token, uint256 amount, RevertInstructions calldata revertInstruction @@ -517,20 +535,21 @@ contract UniversalGateway is whenNotPaused onlyRole(VAULT_ROLE) { - if (isExecuted[txID]) revert Errors.PayloadExecuted(); + bytes32 txIDHash = keccak256(txID); + if (isExecuted[txIDHash]) revert Errors.PayloadExecuted(); - if (revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + if (revertInstruction.revertRecipient == address(0)) revert Errors.InvalidRecipient(); if (amount == 0) revert Errors.InvalidAmount(); - isExecuted[txID] = true; - IERC20(token).safeTransfer(revertInstruction.fundRecipient, amount); + isExecuted[txIDHash] = true; + IERC20(token).safeTransfer(revertInstruction.revertRecipient, amount); - emit RevertUniversalTx(txID, revertInstruction.fundRecipient, token, amount, revertInstruction); + emit RevertUniversalTx(txID, revertInstruction.revertRecipient, token, amount, revertInstruction); } - + /// @inheritdoc IUniversalGateway function revertUniversalTx( - bytes32 txID, + bytes calldata txID, uint256 amount, RevertInstructions calldata revertInstruction ) @@ -540,36 +559,38 @@ contract UniversalGateway is whenNotPaused onlyTSS { - if (isExecuted[txID]) revert Errors.PayloadExecuted(); + bytes32 txIDHash = keccak256(txID); + if (isExecuted[txIDHash]) revert Errors.PayloadExecuted(); - if (revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + if (revertInstruction.revertRecipient == address(0)) revert Errors.InvalidRecipient(); if (amount == 0 || msg.value != amount) revert Errors.InvalidAmount(); - isExecuted[txID] = true; - (bool ok,) = payable(revertInstruction.fundRecipient).call{ value: amount }(""); + isExecuted[txIDHash] = true; + (bool ok,) = payable(revertInstruction.revertRecipient).call{ value: amount }(""); if (!ok) revert Errors.WithdrawFailed(); - emit RevertUniversalTx(txID, revertInstruction.fundRecipient, address(0), amount, revertInstruction); + emit RevertUniversalTx(txID, revertInstruction.revertRecipient, address(0), amount, revertInstruction); } // ========================= - // GATEWAY Withdraw and Payload Execution Paths + // UG_4: WITHDRAW AND PAYLOAD EXECUTION PATHS // ========================= /// @inheritdoc IUniversalGateway function withdraw( - bytes32 txID, + bytes calldata txID, address originCaller, address to, uint256 amount ) external payable nonReentrant whenNotPaused onlyTSS { - if (isExecuted[txID]) revert Errors.PayloadExecuted(); + bytes32 txIDHash = keccak256(txID); + if (isExecuted[txIDHash]) revert Errors.PayloadExecuted(); if (to == address(0) || originCaller == address(0)) revert Errors.InvalidInput(); if (amount == 0) revert Errors.InvalidAmount(); if (msg.value != amount) revert Errors.InvalidAmount(); - isExecuted[txID] = true; + isExecuted[txIDHash] = true; (bool ok,) = payable(to).call{ value: amount }(""); if (!ok) revert Errors.WithdrawFailed(); @@ -578,13 +599,14 @@ contract UniversalGateway is /// @inheritdoc IUniversalGateway function withdrawTokens( - bytes32 txID, + bytes calldata txID, address originCaller, address token, address to, uint256 amount ) external nonReentrant whenNotPaused onlyRole(VAULT_ROLE) { - if (isExecuted[txID]) revert Errors.PayloadExecuted(); + bytes32 txIDHash = keccak256(txID); + if (isExecuted[txIDHash]) revert Errors.PayloadExecuted(); if (to == address(0) || originCaller == address(0)) revert Errors.InvalidInput(); if (amount == 0) revert Errors.InvalidAmount(); @@ -592,7 +614,7 @@ contract UniversalGateway is if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); - isExecuted[txID] = true; + isExecuted[txIDHash] = true; IERC20(token).safeTransfer(to, amount); emit WithdrawToken(txID, originCaller, token, to, amount); } @@ -609,35 +631,58 @@ contract UniversalGateway is /// @param amount amount of token to send along /// @param payload calldata to be executed on target function executeUniversalTx( - bytes32 txID, + bytes calldata txID, address originCaller, address token, address target, uint256 amount, bytes calldata payload ) external nonReentrant whenNotPaused onlyRole(VAULT_ROLE) { - if (isExecuted[txID]) revert Errors.PayloadExecuted(); + bytes32 txIDHash = keccak256(txID); + if (isExecuted[txIDHash]) revert Errors.PayloadExecuted(); if (target == address(0) || originCaller == address(0)) revert Errors.InvalidInput(); if (amount == 0) revert Errors.InvalidAmount(); if (token == address(0)) revert Errors.InvalidInput(); // This function is for ERC20 tokens only + + isExecuted[txIDHash] = true; + + // Check for magic marker and handle PC20/PC721 minting + bytes memory strippedPayload = payload; + address actualToken = token; + bool isPCAS = false; - if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); + if (payload.length >= 4) { + bytes4 magic = bytes4(payload[:4]); + if (magic == MAGIC_PCAS) { + (actualToken, strippedPayload) = _handlePCAssetAllocation(amount, payload); + isPCAS = true; + } + } - isExecuted[txID] = true; + // Check the balance after PC20 mint (only for PC20, not PC721) + // For PC20: actualToken is the PC20 address, check if we have enough balance + // For PC721: skip balance check as it's an NFT (already minted in _handlePCAssetAllocation) + if (isPCAS) { + // PC assets are already minted, balance check happens in approval/transfer + } else { + // Regular ERC20 token, check balance + if (IERC20(actualToken).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); + } - _resetApproval(token, target); // reset approval to zero - _safeApprove(token, target, amount); // approve target to spend amount - _executeCall(target, payload, 0); // execute call with required amount - _resetApproval(token, target); // reset approval back to zero + + _resetApproval(actualToken, target); // reset approval to zero + _safeApprove(actualToken, target, amount); // approve target to spend amount + _executeCall(target, strippedPayload, 0); // execute call with stripped payload + _resetApproval(actualToken, target); // reset approval back to zero // Return any remaining tokens to the Vault - uint256 remainingBalance = IERC20(token).balanceOf(address(this)); + uint256 remainingBalance = IERC20(actualToken).balanceOf(address(this)); if (remainingBalance > 0) { - IERC20(token).safeTransfer(VAULT, remainingBalance); + IERC20(actualToken).safeTransfer(VAULT, remainingBalance); } - emit UniversalTxExecuted(txID, originCaller, target, token, amount, payload); + emit UniversalTxExecuted(txID, originCaller, target, token, amount, strippedPayload); } /// @notice Executes a Universal Transaction with native tokens on this chain triggered by TSS after validation on Push Chain. @@ -648,27 +693,28 @@ contract UniversalGateway is /// @param amount amount of native token to send along /// @param payload calldata to be executed on target function executeUniversalTx( - bytes32 txID, + bytes calldata txID, address originCaller, address target, uint256 amount, bytes calldata payload ) external payable nonReentrant whenNotPaused onlyRole(TSS_ROLE) { - if (isExecuted[txID]) revert Errors.PayloadExecuted(); + bytes32 txIDHash = keccak256(txID); + if (isExecuted[txIDHash]) revert Errors.PayloadExecuted(); if (target == address(0) || originCaller == address(0)) revert Errors.InvalidInput(); if (amount == 0) revert Errors.InvalidAmount(); if (msg.value != amount) revert Errors.InvalidAmount(); - isExecuted[txID] = true; - + isExecuted[txIDHash] = true; + _executeCall(target, payload, amount); emit UniversalTxExecuted(txID, originCaller, target, address(0), amount, payload); } // ========================= - // PUBLIC HELPERS + // UG_5: PUBLIC HELPERS // ========================= /// @inheritdoc IUniversalGateway @@ -750,6 +796,22 @@ contract UniversalGateway is usd1e18 = (amountWei * px1e18) / 1e18; } + /// @inheritdoc IUniversalGateway + function currentTokenUsage(address token) public view returns (uint256 used, uint256 remaining) { + uint256 thr = tokenToLimitThreshold[token]; + if (thr == 0) return (0, 0); + + uint256 _epochDuration = epochDurationSec; + if (_epochDuration == 0) return (0, 0); + + uint64 current = uint64(block.timestamp / _epochDuration); + EpochUsage storage e = _usage[token]; + uint256 u = (e.epoch == current) ? uint256(e.used) : 0; + + used = u; + remaining = u >= thr ? 0 : (thr - u); + } + // ========================= // INTERNAL HELPERS // ========================= @@ -762,31 +824,6 @@ contract UniversalGateway is if (usdValue < MIN_CAP_UNIVERSAL_TX_USD) revert Errors.InvalidAmount(); if (usdValue > MAX_CAP_UNIVERSAL_TX_USD) revert Errors.InvalidAmount(); } - - /// @dev Enforce per-block USD budget for GAS routes using two-scalar accounting. - /// - `BLOCK_USD_CAP` is denominated in USD(1e18). When 0, the feature is disabled. - /// - Resets the window when a new block is observed. - /// @param amountWei native amount (in wei) to be accounted against the current block's USD budget - function _checkBlockUSDCap(uint256 amountWei) public { - uint256 cap = BLOCK_USD_CAP; - if (cap == 0) return; - - if (block.number != _lastBlockNumber) { - _lastBlockNumber = block.number; - _consumedUSDinBlock = 0; - } - - uint256 usd1e18 = quoteEthAmountInUsd1e18(amountWei); - - if (usd1e18 > cap) revert Errors.BlockCapLimitExceeded(); - - unchecked { - uint256 newUsed = _consumedUSDinBlock + usd1e18; - if (newUsed > cap) revert Errors.BlockCapLimitExceeded(); - _consumedUSDinBlock = newUsed; - } - } - /// @dev Handle deposits of native ETH or ERC20 tokens /// If token is address(0): Forward native ETH to TSS /// Otherwise: Lock ERC20 in the Vault contract for bridging @@ -804,8 +841,6 @@ contract UniversalGateway is } } - // _handleTokenWithdraw function removed as token withdrawals are now handled by the Vault - /// @dev Safely reset approval to zero before granting any new allowance to target contract. function _resetApproval(address token, address spender) internal { (bool success, bytes memory returnData) = @@ -840,18 +875,42 @@ contract UniversalGateway is /// @dev Unified helper to execute a low-level call to target /// Call can be executed with native value or ERC20 token. /// Reverts with Errors.ExecutionFailed() if the call fails (no bubbling). - function _executeCall(address target, bytes calldata payload, uint256 value) internal returns (bytes memory result) { + function _executeCall(address target, bytes memory payload, uint256 value) internal returns (bytes memory result) { (bool success, bytes memory ret) = target.call{value: value}(payload); if (!success) revert Errors.ExecutionFailed(); return ret; } + /// @dev Enforce per-block USD budget for GAS routes using two-scalar accounting. + /// - `BLOCK_USD_CAP` is denominated in USD(1e18). When 0, the feature is disabled. + /// - Resets the window when a new block is observed. + /// @param amountWei native amount (in wei) to be accounted against the current block's USD budget + function _checkBlockUSDCap(uint256 amountWei) private { + uint256 cap = BLOCK_USD_CAP; + if (cap == 0) return; + + if (block.number != _lastBlockNumber) { + _lastBlockNumber = block.number; + _consumedUSDinBlock = 0; + } + + uint256 usd1e18 = quoteEthAmountInUsd1e18(amountWei); + + if (usd1e18 > cap) revert Errors.BlockCapLimitExceeded(); + + unchecked { + uint256 newUsed = _consumedUSDinBlock + usd1e18; + if (newUsed > cap) revert Errors.BlockCapLimitExceeded(); + _consumedUSDinBlock = newUsed; + } + } + /// @dev Enforce and consume the per-token epoch rate limit. /// For a token, if threshold is 0, it is unsupported. /// epoch.used is reset to 0 when a new epoch starts (no rollover). /// @param token token address to consume rate limit /// @param amount amount of token to consume rate limit - function _consumeRateLimit(address token, uint256 amount) internal { + function _consumeRateLimit(address token, uint256 amount) private { uint256 threshold = tokenToLimitThreshold[token]; if (threshold == 0) revert Errors.NotSupported(); @@ -873,25 +932,6 @@ contract UniversalGateway is } } - /// @notice Returns both the total token amount used and remaining in the current epoch. - /// @param token token address to query (use address(0) for native) - /// @return used amount already consumed in the current epoch (in token's natural units) - /// @return remaining amount still available to send in this epoch (0 if exceeded or unsupported) - function currentTokenUsage(address token) external view returns (uint256 used, uint256 remaining) { - uint256 thr = tokenToLimitThreshold[token]; - if (thr == 0) return (0, 0); - - uint256 _epochDuration = epochDurationSec; - if (_epochDuration == 0) return (0, 0); - - uint64 current = uint64(block.timestamp / _epochDuration); - EpochUsage storage e = _usage[token]; - uint256 u = (e.epoch == current) ? uint256(e.used) : 0; - - used = u; - remaining = u >= thr ? 0 : (thr - u); - } - /// @dev Swap any ERC20 to the chain's native token via a direct Uniswap v3 pool to WETH. /// - If tokenIn == WETH: unwrap to native and return. /// - Else: require a direct tokenIn/WETH v3 pool, swap via exactInputSingle, unwrap, return ETH out. @@ -973,6 +1013,238 @@ contract UniversalGateway is revert Errors.InvalidInput(); } + // ========================= + // UG_6: VALIDATION & ROUTERS for sendUniversalTx() + // ========================= + + /** + * @notice Infers the TX_TYPE for an incoming universal request by inspecting only + * the four decision variables we agreed on: + * - hasPayload := (req.payload.length > 0) + * - hasFunds := (req.amount > 0) + * - fundsIsNative := (req.token == address(0)) + * - hasNativeValue := (nativeValue > 0) // nativeValue = msg.value (native-gas) OR swapped amount (token-gas) + * + * @param req UniversalTxRequest (txType field is ignored here) + * @param nativeValue Effective native value attached to the call path (msg.value or swapped amount) + * @return inferred The inferred TX_TYPE for routing + */ + function _fetchTxType(UniversalTxRequest memory req, uint256 nativeValue) + private + pure + returns (TX_TYPE inferred) + { + bool hasPayload = req.payload.length > 0; + bool hasFunds = req.amount > 0; + bool fundsIsNative = (req.token == address(0)); + bool hasNativeValue = nativeValue > 0; + + // For TX_TYPE.GAS: + // - pure gas top-up (no payload, no funds, nativeValue > 0) + if (!hasPayload && !hasFunds && hasNativeValue) { + return TX_TYPE.GAS; + } + // For TX_TYPE.GAS_AND_PAYLOAD: + // - payload present + // - no funds + // - nativeValue MAY be 0 (payload-only) or > 0 (payload + gas) + if (hasPayload && !hasFunds) { + return TX_TYPE.GAS_AND_PAYLOAD; + } + + // For TX_TYPE.FUNDS: Case 1: Native Funds + if (!hasPayload && hasFunds) { + // Case 1.1: Native Funds Only. + // FUNDS (native) — must come with native value + if (fundsIsNative && hasNativeValue) { + return TX_TYPE.FUNDS; + } + // Case 1.2: ERC-20 Funds Only. + // FUNDS (ERC-20) — must NOT come with native value - Case 1.2 + if (!fundsIsNative && !hasNativeValue) { + return TX_TYPE.FUNDS; + } + revert Errors.InvalidInput(); + } + + // For TX_TYPE.FUNDS_AND_PAYLOAD: Case 2: (Native/ERC20 Funds) + Payload + if (hasPayload && hasFunds) { + // Case 2.1: No batching (ERC-20 funds, user already has UEA gas) + if (!fundsIsNative && !hasNativeValue) { + return TX_TYPE.FUNDS_AND_PAYLOAD; + } + // Case 2.2: Batching: native funds + native gas (later we enforce nativeValue >= amount) + if (fundsIsNative && hasNativeValue) { + return TX_TYPE.FUNDS_AND_PAYLOAD; + } + // Case 2.3: Batching: ERC-20 funds + native gas + if (!fundsIsNative && hasNativeValue) { + return TX_TYPE.FUNDS_AND_PAYLOAD; + } + revert Errors.InvalidInput(); + } + + revert Errors.InvalidInput(); + } + + /// @dev Internal router that dispatches to the appropriate handler based on TX_TYPE + /// @param req UniversalTxRequest struct + /// @param caller Caller address + /// @param nativeValue Native value ( msg.value ) + /// @param _TX_TYPE TX_TYPE + function _routeUniversalTx( + UniversalTxRequest memory req, + address caller, + uint256 nativeValue, + TX_TYPE _TX_TYPE + ) internal { + TX_TYPE txType = _TX_TYPE; + + // Sanity Check : revertRecipient is not address(0) + if (req.revertRecipient == address(0)) { + revert Errors.InvalidRecipient(); + } + + // Route 1: GAS or GAS_AND_PAYLOAD → Instant route + if (txType == TX_TYPE.GAS || txType == TX_TYPE.GAS_AND_PAYLOAD) { + _sendTxWithGas(txType, caller, nativeValue, req.payload, req.revertRecipient, req.signatureData); + } + // Route 2: FUNDS or FUNDS_AND_PAYLOAD → Standard route + else if (txType == TX_TYPE.FUNDS || txType == TX_TYPE.FUNDS_AND_PAYLOAD) { + _sendTxWithFunds(req, nativeValue, txType); + } + // Route 3: Invalid + else { + revert Errors.InvalidTxType(); + } + } + + + /// @notice Handle PC20/PC721 asset allocation based on magic marker + /// @param amount Amount of native tokens (used for PC20 minting amount) + /// @param payload Full payload with magic marker + /// @return pcAssetAddress Address of the created/existing PC20 or PC721 token + /// @return strippedPayload Stripped payload without magic marker + function _handlePCAssetAllocation(uint256 amount, bytes calldata payload) internal returns (address pcAssetAddress, bytes memory strippedPayload) { + // First decode to get magic, version, and kind + (bytes4 magic, uint8 version, uint8 kind) = abi.decode(payload, (bytes4, uint8, uint8)); + + // Verify magic marker and version + if (magic != MAGIC_PCAS || version != META_VERSION) { + revert Errors.InvalidInput(); + } + + if (kind == META_KIND_PC20) { + // PC20: abi.encode(magic, version, kind, token, name, symbol, decimals) + ( + , + , + , + address originToken, + string memory name, + string memory symbol, + uint8 decimals + ) = abi.decode(payload, (bytes4, uint8, uint8, address, string, string, uint8)); + + if (address(pc20Factory) == address(0)) revert Errors.InvalidInput(); + + // Check if PC20 already exists + address pc20Address = pc20Factory.getPC20(originToken); + + // Create only if it doesn't exist + if (pc20Address == address(0)) { + pc20Address = pc20Factory.createPC20(originToken, name, symbol, decimals); + } + + // Mint tokens to target using the amount parameter + IPC20(pc20Address).mint(address(this), amount); + + pcAssetAddress = pc20Address; + + } else if (kind == META_KIND_PC721) { + // PC721: abi.encode(magic, version, kind, token, name, symbol, tokenId, tokenURI) + ( + , + , + , + address originToken, + string memory name, + string memory symbol, + uint256 tokenId, + string memory tokenURI + ) = abi.decode(payload, (bytes4, uint8, uint8, address, string, string, uint256, string)); + + if (address(pc721Factory) == address(0)) revert Errors.InvalidInput(); + + // Check if PC721 already exists + address pc721Address = pc721Factory.getPC721(originToken); + + // Create only if it doesn't exist + if (pc721Address == address(0)) { + pc721Address = pc721Factory.createPC721(originToken, name, symbol); + } + + // Mint NFT to target + IPC721(pc721Address).mint(address(this), tokenId); + + pcAssetAddress = pc721Address; + + } else { + revert Errors.InvalidInput(); + } + + // Calculate the offset where the enriched payload ends + uint256 enrichedLength = _getEnrichedPayloadLength(payload, kind); + + // Return the remaining payload after the magic marker + if (payload.length > enrichedLength) { + strippedPayload = payload[enrichedLength:]; + } else { + strippedPayload = ""; + } + } + + /// @notice Calculate the length of the enriched payload + /// @param payload Full payload with magic marker + /// @param kind Kind of token (PC20 or PC721) + /// @return Length of enriched payload + function _getEnrichedPayloadLength(bytes calldata payload, uint8 kind) internal pure returns (uint256) { + if (kind == META_KIND_PC20) { + // PC20: abi.encode(magic, version, kind, token, name, symbol, decimals) + ( + bytes4 magic, + uint8 version, + uint8 k, + address token, + string memory name, + string memory symbol, + uint8 decimals + ) = abi.decode(payload, (bytes4, uint8, uint8, address, string, string, uint8)); + + // Re-encode to get the exact length + bytes memory enriched = abi.encode(magic, version, k, token, name, symbol, decimals); + return enriched.length; + + } else if (kind == META_KIND_PC721) { + // PC721: abi.encode(magic, version, kind, token, name, symbol, tokenId, tokenURI) + ( + bytes4 magic, + uint8 version, + uint8 k, + address token, + string memory name, + string memory symbol, + uint256 tokenId, + string memory tokenURI + ) = abi.decode(payload, (bytes4, uint8, uint8, address, string, string, uint256, string)); + + // Re-encode to get the exact length + bytes memory enriched = abi.encode(magic, version, k, token, name, symbol, tokenId, tokenURI); + return enriched.length; + } + + revert Errors.InvalidInput(); + } /// @dev Reject plain ETH; we only accept ETH via explicit deposit functions or WETH unwrapping. receive() external payable { diff --git a/contracts/evm-gateway/src/UniversalGatewayPC.sol b/contracts/evm-gateway/src/UniversalGatewayPC.sol index dbe935b..2ca2d1a 100644 --- a/contracts/evm-gateway/src/UniversalGatewayPC.sol +++ b/contracts/evm-gateway/src/UniversalGatewayPC.sol @@ -15,15 +15,17 @@ pragma solidity 0.8.26; */ import { Errors } from "./libraries/Errors.sol"; import { IPRC20 } from "./interfaces/IPRC20.sol"; +import { IPC20 } from "./interfaces/IPC20.sol"; +import { IPC721 } from "./interfaces/IPC721.sol"; import { IVaultPC } from "./interfaces/IVaultPC.sol"; -import { RevertInstructions } from "./libraries/Types.sol"; +import { TX_TYPE, RevertInstructions } from "./libraries/Types.sol"; import { IUniversalCore } from "./interfaces/IUniversalCore.sol"; import { IUniversalGatewayPC } from "./interfaces/IUniversalGatewayPC.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; contract UniversalGatewayPC is Initializable, @@ -32,6 +34,16 @@ contract UniversalGatewayPC is PausableUpgradeable, IUniversalGatewayPC { + /// @notice outboundMode type on Push Chain + enum OutboundMode { + FUNDS, + PAYLOAD, + FUNDS_AND_PAYLOAD + } + + /// @notice outboundMode type on Push Chain + enum AssetType { NONE, PRC20, PC20, PC721 } + /// @notice Pauser role for pausing the contract. bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); @@ -41,6 +53,19 @@ contract UniversalGatewayPC is /// @notice VaultPC on Push Chain (custody vault for fees collected from outbound flows). IVaultPC public VAULT_PC; + // ERC type detection constants + bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7; + bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd; + + // Outbound Tx Nonce + uint256 public outboundTxNonce; + + // Magic Marker + bytes4 constant MAGIC_PCAS = 0x50434153; // "PCAS" + uint8 private constant META_VERSION = 1; + uint8 private constant META_KIND_PC20 = 1; + uint8 private constant META_KIND_PC721 = 2; + /// @notice Initializes the contract. /// @param admin address of the admin. /// @param pauser address of the pauser. @@ -77,72 +102,175 @@ contract UniversalGatewayPC is _unpause(); } - /// @inheritdoc IUniversalGatewayPC - function withdraw( - bytes calldata to, - address token, - uint256 amount, - uint256 gasLimit, - RevertInstructions calldata revertInstruction - ) external whenNotPaused nonReentrant { - _validateCommon(to, token, amount, revertInstruction); - - // Compute fees + collect from caller into the UEM fee sink - (address gasToken, uint256 gasFee, uint256 gasLimitUsed, uint256 protocolFee) = - _calculateGasFeesWithLimit(token, gasLimit); - - _moveFees(msg.sender, gasToken, gasFee); - _burnPRC20(msg.sender, token, amount); - - string memory chainId = IPRC20(token).SOURCE_CHAIN_ID(); - emit UniversalTxWithdraw( - msg.sender, chainId, token, to, amount, gasToken, gasFee, gasLimitUsed, bytes(""), protocolFee, revertInstruction - ); - } + /// TODO: define specs + function sendUniversalTxOutbound( + bytes calldata target, // origin target + address token, // PRC20 / PC20 / PC721, or 0 for payload-only + uint256 amount, // fungible amount (for PRC20 / PC20) + uint256 tokenId, // NFT id (for PC721) + uint256 gasLimit, + bytes calldata payload, // optional payload + string calldata chainNamespace, // chain namespace, e.g. "eip155:1" + RevertInstructions calldata revertInstruction + ) external payable whenNotPaused nonReentrant { + if (target.length == 0) revert Errors.InvalidInput(); + if (revertInstruction.revertRecipient == address(0)) revert Errors.InvalidRecipient(); + if (token == address(0) && (amount != 0 || tokenId != 0)) revert Errors.ZeroAddress(); + if (token == address(0) && payload.length == 0) revert Errors.InvalidTxType(); - /// @inheritdoc IUniversalGatewayPC - function withdrawAndExecute( - bytes calldata target, - address token, - uint256 amount, - bytes calldata payload, - uint256 gasLimit, - RevertInstructions calldata revertInstruction - ) external whenNotPaused nonReentrant { - _validateCommon(target, token, amount, revertInstruction); - - // Compute fees + collect from caller into the UEM fee sink - (address gasToken, uint256 gasFee, uint256 gasLimitUsed, uint256 protocolFee) = - _calculateGasFeesWithLimit(token, gasLimit); - _moveFees(msg.sender, gasToken, gasFee); - - _burnPRC20(msg.sender, token, amount); - - string memory chainId = IPRC20(token).SOURCE_CHAIN_ID(); - emit UniversalTxWithdraw( - msg.sender, chainId, token, target, amount, gasToken, gasFee, gasLimitUsed, payload, protocolFee, revertInstruction - ); - } + // Generate canonical txId + uint256 nonce = outboundTxNonce; + outboundTxNonce = nonce + 1; - // ========= Helpers ========= + bytes32 txId = keccak256( + abi.encode( + bytes32("PUSH.OUTBOUND.TX"), + msg.sender, + token, + amount, + tokenId, + keccak256(payload), + keccak256(bytes(chainNamespace)), + nonce + ) + ); - /// @notice Validates the common parameters. - /// @dev Uses UniversalCore to fetch gasToken, gasFee and protocolFee. - /// @param rawTarget raw destination address on origin chain. - /// @param token PRC20 token address on Push Chain. - /// @param amount amount to withdraw (burn on Push, unlock at origin). - /// @param revertInstruction revert configuration (fundRecipient, revertMsg) for off-chain use. - function _validateCommon( - bytes calldata rawTarget, - address token, - uint256 amount, - RevertInstructions calldata revertInstruction - ) internal pure { - if (rawTarget.length == 0) revert Errors.InvalidInput(); - if (token == address(0)) revert Errors.ZeroAddress(); - if (amount == 0) revert Errors.InvalidAmount(); - if (revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); - } + OutboundMode oMode; + AssetType aType; + + bool hasPayload = payload.length != 0; + bool hasToken = token != address(0); + bool hasFungible = amount != 0; + bool hasNFT = tokenId != 0; + bool hasChainNamespace = bytes(chainNamespace).length != 0; + + if (!hasToken) { + // payload only + if (!hasPayload) revert Errors.InvalidTxType(); + if (hasFungible || hasNFT) revert Errors.InvalidInput(); + if (!hasChainNamespace) revert Errors.InvalidInput(); + + oMode = OutboundMode.PAYLOAD; + aType = AssetType.NONE; + } else { + // token present + if (!hasFungible && !hasNFT) revert Errors.InvalidAmount(); + if (hasFungible && hasNFT) revert Errors.InvalidInput(); + + oMode = hasPayload ? OutboundMode.FUNDS_AND_PAYLOAD : OutboundMode.FUNDS; + + IUniversalCore core = IUniversalCore(UNIVERSAL_CORE); + + if (core.isSupportedToken(token)) { + // PRC20 + if (!hasFungible || hasNFT) revert Errors.InvalidAmount(); + aType = AssetType.PRC20; + } else if (_isERC20(token)) { + // PC20 + if (!hasFungible || hasNFT) revert Errors.InvalidAmount(); + if (!core.isPC20SupportedOnChain(chainNamespace)) revert Errors.InvalidInput(); + if (!hasChainNamespace) revert Errors.InvalidInput(); + aType = AssetType.PC20; + oMode = OutboundMode.FUNDS_AND_PAYLOAD; // carry magic marker in the payload + } else if (_isERC721(token)) { + // PC721 + if (!hasNFT || hasFungible) revert Errors.InvalidAmount(); + if (!core.isPC721SupportedOnChain(chainNamespace)) revert Errors.InvalidInput(); + if (!hasChainNamespace) revert Errors.InvalidInput(); + aType = AssetType.PC721; + oMode = OutboundMode.FUNDS_AND_PAYLOAD; // carry magic marker in the payload + } else { + revert Errors.TokenNotSupported(); + } + } + + (address gasToken, uint256 gasFee, uint256 gasLimitUsed, uint256 protocolFee) = + _calculateGasFeesWithLimit(token, gasLimit, aType); + + if (aType == AssetType.PRC20) { + _moveFees(msg.sender, gasToken, gasFee); + _burnPRC20(msg.sender, token, amount); + + string memory sourceChainId = IPRC20(token).SOURCE_CHAIN_ID(); + emit UniversalTxOutbound( + txId, + msg.sender, + token, + sourceChainId, + target, + amount, + gasToken, + gasFee, + gasLimitUsed, + payload, + protocolFee, + revertInstruction.revertRecipient, + payload.length > 0 ? TX_TYPE.FUNDS_AND_PAYLOAD : TX_TYPE.FUNDS + ); + } else { + // NONE, PC20, PC721 use native PC for fees + _moveFeesNative(gasFee); + + bytes memory finalPayload; + + if (aType == AssetType.PC20) { + _movePC20(msg.sender, token, amount); + + // generate and append magic marker in the payload + IPC20 meta = IPC20(token); + + bytes memory enrichedPayload = abi.encode( + MAGIC_PCAS, // bytes4 + META_VERSION, // uint8 + META_KIND_PC20, // uint8 + token, // address + meta.name(), // string + meta.symbol(), // string + meta.decimals() // uint8 + ); + + finalPayload = abi.encodePacked(enrichedPayload, payload); + + } else if (aType == AssetType.PC721) { + _movePC721(msg.sender, token, tokenId); + + // generate and append magic marker in the payload + IPC721 meta = IPC721(token); + + bytes memory enrichedPayload = abi.encode( + MAGIC_PCAS, // bytes4 + META_VERSION, // uint8 + META_KIND_PC721, // uint8 + token, // address + meta.name(), // string + meta.symbol(), // string + tokenId, // string + meta.tokenURI(tokenId) // string + ); + + finalPayload = abi.encodePacked(enrichedPayload, payload); + } + + emit UniversalTxOutbound( + txId, + msg.sender, + token, + chainNamespace, + target, + amount, + address(0), // native PC as fee currency + gasFee, + gasLimitUsed, + finalPayload, + protocolFee, + revertInstruction.revertRecipient, + payload.length > 0 ? TX_TYPE.FUNDS_AND_PAYLOAD : TX_TYPE.FUNDS + ); + } + } + + + // ========= Helpers ========= /** * @dev Use UniversalCore's withdrawGasFeeWithGasLimit to compute fee (gas coin + amount). @@ -152,21 +280,31 @@ contract UniversalGatewayPC is * @return gasLimitUsed gas limit actually used for the quote. * @return protocolFee the flat protocol fee component (as exposed by PRC20). */ - function _calculateGasFeesWithLimit(address token, uint256 gasLimit) + function _calculateGasFeesWithLimit(address token, uint256 gasLimit, AssetType aType) internal view returns (address gasToken, uint256 gasFee, uint256 gasLimitUsed, uint256 protocolFee) { - if (gasLimit == 0) { - gasLimitUsed = IUniversalCore(UNIVERSAL_CORE).BASE_GAS_LIMIT(); - } else { - gasLimitUsed = gasLimit; - } + IUniversalCore core = IUniversalCore(UNIVERSAL_CORE); - (gasToken, gasFee) = IUniversalCore(UNIVERSAL_CORE).withdrawGasFeeWithGasLimit(token, gasLimitUsed); - if (gasToken == address(0) || gasFee == 0) revert Errors.InvalidData(); + gasLimitUsed = gasLimit == 0 + ? core.BASE_GAS_LIMIT() + : gasLimit; - protocolFee = IPRC20(token).PC_PROTOCOL_FEE(); + if (aType == AssetType.PRC20) { + (gasToken, gasFee) = core.withdrawGasFeeWithGasLimit(token, gasLimitUsed); + if (gasToken == address(0) || gasFee == 0) revert Errors.InvalidData(); + protocolFee = IPRC20(token).PC_PROTOCOL_FEE(); + } else if (aType == AssetType.PC20) { + protocolFee = core.PC20_PROTOCOL_FEES(); + gasFee = protocolFee; // native PC + } else if (aType == AssetType.PC721) { + protocolFee = core.PC721_PROTOCOL_FEES(); + gasFee = protocolFee; // native PC + } else { + protocolFee = core.DEFAULT_PROTOCOL_FEES(); + gasFee = protocolFee; // payload only, native PC + } } /** @@ -181,6 +319,27 @@ contract UniversalGatewayPC is if (!ok) revert Errors.GasFeeTransferFailed(gasToken, from, gasFee); } + /** + * @dev Pull native fee from user into the VaultPC. + */ + function _moveFeesNative(uint256 gasFee) internal { + address _vaultPC = address(VAULT_PC); + if (_vaultPC == address(0)) revert Errors.ZeroAddress(); + if (gasFee == 0) return; + + if (msg.value < gasFee) revert Errors.InvalidAmount(); + + (bool ok, ) = _vaultPC.call{value: gasFee}(""); + if (!ok) revert Errors.GasFeeTransferFailed(address(0), msg.sender, gasFee); + + uint256 refund = msg.value - gasFee; + if (refund > 0) { + (bool refundOk, ) = msg.sender.call{value: refund}(""); + if (!refundOk) revert Errors.RefundFailed(msg.sender, refund); + } + } + + function _burnPRC20(address from, address token, uint256 amount) internal { // Pull PRC20 into this gateway first IPRC20(token).transferFrom(from, address(this), amount); @@ -189,4 +348,40 @@ contract UniversalGatewayPC is bool ok = IPRC20(token).burn(amount); if (!ok) revert Errors.TokenBurnFailed(token, amount); } + + function _movePC20(address from, address token, uint256 amount) internal { + bool ok = IPC20(token).transferFrom(from, address(this), amount); + // TODO: @Zaryab + // if (!ok) revert Errors.TokenTransferFailed(token, amount); + } + + function _movePC721(address from, address token, uint256 tokenId) internal { + IPC721(token).transferFrom(from, address(this), tokenId); + + // TODO: @Zaryab + // if (!ok) revert Errors.NFTTransferFailed(token, tokenId); + } + + // ----- ERC type detection ----- + + function _isERC721(address token) internal view returns (bool) { + (bool ok, bytes memory data) = token.staticcall( + abi.encodeWithSelector(_INTERFACE_ID_ERC165, _INTERFACE_ID_ERC721) + ); + return ok && data.length == 32 && abi.decode(data, (bool)); + } + + function _isERC20(address token) internal view returns (bool) { + if (_isERC721(token)) return false; + + (bool ok1, bytes memory data1) = + token.staticcall(abi.encodeWithSelector(bytes4(keccak256("totalSupply()")))); + if (!ok1 || data1.length != 32) return false; + + (bool ok2, bytes memory data2) = + token.staticcall(abi.encodeWithSelector(bytes4(keccak256("balanceOf(address)")), address(this))); + if (!ok2 || data2.length != 32) return false; + + return true; + } } diff --git a/contracts/evm-gateway/src/UniversalGatewayV0.sol b/contracts/evm-gateway/src/UniversalGatewayV0.sol index b3bbd29..2250fdb 100644 --- a/contracts/evm-gateway/src/UniversalGatewayV0.sol +++ b/contracts/evm-gateway/src/UniversalGatewayV0.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.26; /** - * @title UniversalGateway - * @notice Universal Gateway for EVM chains. + * @title UniversalGatewayV0 + * @notice Universal Gateway for EVM chains [TESTNETs Only] * - Acts as a gateway for all supported external chains to bridge funds and payloads to Push Chain. * - Users of external chains can deposit funds and payloads to Push Chain using the gateway. * @@ -19,23 +19,32 @@ pragma solidity 0.8.26; * - 2. Token Support List: allowlist for ERC20 used as gas inputs on gas tx path. * - Note: Fund management and access control is managed by TSS_ROLE. * - * @dev - USD Cap Checks: - * - TX Types like GAS_TX and GAS_AND_PAYLOAD_TX have require lower block confirmation for execution. - * - Therefore, these transactions have a USD cap checks for gas tx deposits via oracle. - * - Note: Chainlink Oracle is used for ETH/USD price feed. + * @dev - Rate-Limit Checks: + * - Universal Gateway includes rate-limit checks for both Fee Abstraction & Universal Transaction Routes. + * - For Fee Abstraction Route ( Low Block Confirmation Requirement ): + * - Includes _checkUSDCaps: USD cap checks for the deposit amount. Must be within MIN_CAP_UNIVERSAL_TX_USD & MAX_CAP_UNIVERSAL_TX_USD. + * - Includes _checkBlockUSDCap: Block-based USD cap checks. Must be within BLOCK_USD_CAP. + * - For Universal Transaction Route ( Standard Block Confirmation Requirement ): + * - Includes _consumeRateLimit: Consume the per-token epoch rate limit. + * - Every supported token has a per-token epoch limit threshold. + * - New Epoch resets the usage limit threshold of a given token. + * - Includes _checkUSDCaps and _checkBlockUSDCap for _sendTxWithGas function called internally. + * - Note: Check the ./interfaces/IUniversalGateway.sol file for more details on rate-limit checks. + * + * @dev - Chainlink Oracle is used for ETH/USD price feed. */ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Errors} from "./libraries/Errors.sol"; import {IUniversalGatewayV0} from "./interfaces/IUniversalGatewayV0.sol"; -import {RevertInstructions, UniversalPayload, TX_TYPE, EpochUsage} from "./libraries/Types.sol"; +import {RevertInstructions, UniversalPayload, TX_TYPE, EpochUsage, UniversalTxRequest, UniversalTokenTxRequest} from "./libraries/Types.sol"; import {IWETH} from "./interfaces/IWETH.sol"; import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; @@ -70,7 +79,7 @@ contract UniversalGatewayV0 is uint256 public MAX_CAP_UNIVERSAL_TX_USD; // inclusive upper bound = 10USD = 10e18 /// @notice Token whitelist for BRIDGING (assets locked in this contract) - mapping(address => bool) public isSupportedToken; // Deprecated - Use tokenToLimitThreshold instead + mapping(address => bool) public _isSupportedToken; // Deprecated - Use tokenToLimitThreshold instead /// @notice Uniswap V3 factory & router (chain-specific) IUniswapV3Factory public uniV3Factory; @@ -109,6 +118,9 @@ contract UniversalGatewayV0 is mapping(address => EpochUsage) private _usage; // Current-epoch usage per token (address(0) represents native). + /// @notice Map to track if a payload has been executed + mapping(bytes32 => bool) public isExecuted; + uint256[40] private __gap; /** @@ -121,7 +133,7 @@ contract UniversalGatewayV0 is * @param factory UniswapV2 factory * @param router UniswapV2 router */ - function initialize( + function initialize( ///@audit Commented for testnet Size LIMIT address admin, address pauser, address tss, @@ -215,6 +227,12 @@ contract UniversalGatewayV0 is emit CapsUpdated(minCapUsd, maxCapUsd); } + /// @notice Set the per-block USD cap for GAS routes (1e18 = $1). Set to 0 to disable. + /// @audit Commented for testnet Size LIMIT + // function setBlockUsdCap(uint256 cap1e18) external onlyRole(DEFAULT_ADMIN_ROLE) whenNotPaused { + // BLOCK_USD_CAP = cap1e18; + // } + /// @notice Set the default swap deadline window (used when a caller passes deadline = 0) /// @param deadlineSec Number of seconds to add to block.timestamp when defaulting the deadline function setDefaultSwapDeadline(uint256 deadlineSec) external onlyRole(DEFAULT_ADMIN_ROLE) whenNotPaused { @@ -231,17 +249,6 @@ contract UniversalGatewayV0 is uniV3Router = ISwapRouterSepolia(router); } - /// @notice Allows the admin to add support for a given token or remove support for a given token - /// @dev Adding support for given token, indicates the wrapped version of the token is live on Push Chain. - /// @param tokens The tokens to modify the support for - /// @param isSupported The new support status - function modifySupportForToken(address[] calldata tokens, bool[] calldata isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) whenNotPaused { - if (tokens.length != isSupported.length) revert Errors.InvalidInput(); - for (uint256 i = 0; i < tokens.length; i++) { - isSupportedToken[tokens[i]] = isSupported[i]; - } - } - /// @notice Allows the admin to set the fee order for the Uniswap V3 router /// @param a The new fee order /// @param b The new fee order @@ -280,11 +287,6 @@ contract UniversalGatewayV0 is l2SequencerGracePeriodSec = gracePeriodSec; } - /// @notice Set the per-block USD cap for GAS routes (1e18 = $1). Set to 0 to disable. - function setBlockUsdCap(uint256 cap1e18) external onlyRole(DEFAULT_ADMIN_ROLE) whenNotPaused { - BLOCK_USD_CAP = cap1e18; - } - /// @notice Set limit thresholds for a batch of tokens (0 disables support for that token) /// @param tokens tokens to set limit thresholds for /// @param thresholds limit thresholds for the tokens @@ -299,27 +301,14 @@ contract UniversalGatewayV0 is } } - /// @notice Update limit thresholds for a batch of tokens - /// @param tokens tokens to update limit thresholds for - /// @param thresholds limit thresholds for the tokens - function updateTokenLimitThreshold(address[] calldata tokens, uint256[] calldata thresholds) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - if (tokens.length != thresholds.length) revert Errors.InvalidInput(); - for (uint256 i = 0; i < tokens.length; i++) { - tokenToLimitThreshold[tokens[i]] = thresholds[i]; - emit TokenLimitThresholdUpdated(tokens[i], thresholds[i]); - } - } - /// @notice Update the epoch duration (hard reset schedule) /// @param newDurationSec new epoch duration - function updateEpochDuration(uint256 newDurationSec) external onlyRole(DEFAULT_ADMIN_ROLE) { - uint256 old = epochDurationSec; - epochDurationSec = newDurationSec; - emit EpochDurationUpdated(old, newDurationSec); - } + /// @audit Commented for testnet Size LIMIT + // function updateEpochDuration(uint256 newDurationSec) external onlyRole(DEFAULT_ADMIN_ROLE) { + // uint256 old = epochDurationSec; + // epochDurationSec = newDurationSec; + // emit EpochDurationUpdated(old, newDurationSec); + // } // ========================= // DEPOSITS - Fee Abstraction Route @@ -385,215 +374,380 @@ contract UniversalGatewayV0 is emit FundsAdded(msg.sender, _transactionHash, usdAmountStruct); } + + // ========================= + // DEPOSITS - Universal TX Route + // ========================= + /// @inheritdoc IUniversalGatewayV0 - function sendTxWithGas( + // NOTE: This uses the OLD Fee Abstraction Route ( PC MINTED ON Push Chain ) + function sendTxWithFunds( + address bridgeToken, + uint256 bridgeAmount, UniversalPayload calldata payload, - RevertInstructions calldata revertInstruction, + address revertRecipient, bytes memory signatureData ) external payable nonReentrant whenNotPaused { + if (bridgeAmount == 0) revert Errors.InvalidAmount(); + uint256 gasAmount = msg.value; + if (gasAmount == 0) revert Errors.InvalidAmount(); - _sendTxWithGas(_msgSender(), abi.encode(payload), msg.value, revertInstruction, TX_TYPE.GAS_AND_PAYLOAD, signatureData); - } + // Check and initiate Instant TX + // _checkUSDCaps(gasAmount); // TODO: DEPRECATED FOR TESTNET SWAP + _addFunds(bytes32(0), gasAmount); + // Check and initiate Universal TX + _handleDeposits(bridgeToken, bridgeAmount); + _sendTxWithFunds_old( + _msgSender(), + address(0), + bridgeToken, + bridgeAmount, + abi.encode(payload), + revertRecipient, + TX_TYPE.FUNDS_AND_PAYLOAD, + signatureData + ); + } /// @inheritdoc IUniversalGatewayV0 - function sendTxWithGas( - address tokenIn, - uint256 amountIn, - UniversalPayload calldata payload, - RevertInstructions calldata revertInstruction, + function sendTxWithFunds( + address bridgeToken, + uint256 bridgeAmount, + address gasToken, + uint256 gasAmount, uint256 amountOutMinETH, uint256 deadline, + UniversalPayload calldata payload, + address revertRecipient, bytes memory signatureData ) external nonReentrant whenNotPaused { - if (tokenIn == address(0)) revert Errors.InvalidInput(); - if (amountIn == 0) revert Errors.InvalidAmount(); - if (amountOutMinETH == 0) revert Errors.InvalidAmount(); - // Allow deadline == 0 (use contract default); otherwise ensure it's in the future - if (deadline != 0 && deadline < block.timestamp) revert Errors.SlippageExceededOrExpired(); + if (bridgeAmount == 0) revert Errors.InvalidAmount(); + if (gasToken == address(0)) revert Errors.InvalidInput(); + if (gasAmount == 0) revert Errors.InvalidAmount(); - // Swap token to native ETH - uint256 ethOut = swapToNative(tokenIn, amountIn, amountOutMinETH, deadline); + // Swap gasToken to native ETH + uint256 nativeGasAmount = swapToNative(gasToken, gasAmount, amountOutMinETH, deadline); - _sendTxWithGas( + // _checkUSDCaps(nativeGasAmount); // TODO: DEPRECATED FOR TESTNET + _addFunds(bytes32(0), nativeGasAmount); + + _handleDeposits(bridgeToken, bridgeAmount); + _sendTxWithFunds_old( _msgSender(), + address(0), + bridgeToken, + bridgeAmount, abi.encode(payload), - ethOut, - revertInstruction, - TX_TYPE.GAS_AND_PAYLOAD, + revertRecipient, + TX_TYPE.FUNDS_AND_PAYLOAD, signatureData ); + } - /// @dev Internal helper function to deposit for Instant TX. - /// Emits the core TxWithGas event - important for Instant TX Route. - /// @param _caller Sender address - /// @param _payload Payload - /// @param _nativeTokenAmount Amount of native token deposited - /// @param _revertInstruction Revert settings - /// @param _txType Transaction type - function _sendTxWithGas( + /// @notice Internal helper function to deposit for Universal TX. + /// @dev Handles rate-limit checks for Universal Transaction Route + function _sendTxWithFunds_old( address _caller, + address _recipient, + address _bridgeToken, + uint256 _bridgeAmount, bytes memory _payload, - uint256 _nativeTokenAmount, - RevertInstructions calldata _revertInstruction, + address _revertRecipient, TX_TYPE _txType, bytes memory _signatureData ) internal { - if (_revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); - - - - //_checkUSDCaps(_nativeTokenAmount); - _checkBlockUSDCap(_nativeTokenAmount); - _handleNativeDeposit(_nativeTokenAmount); + if (_revertRecipient == address(0)) revert Errors.InvalidRecipient(); + /// for recipient == address(0), the funds are being moved to UEA of the msg.sender on Push Chain. + if (_recipient == address(0)) { + if (_txType != TX_TYPE.FUNDS_AND_PAYLOAD && _txType != TX_TYPE.GAS_AND_PAYLOAD) { + revert Errors.InvalidTxType(); + } + } emit UniversalTx({ sender: _caller, - recipient: address(0), - token: address(0), - amount: _nativeTokenAmount, + recipient: _recipient, + token: _bridgeToken, + amount: _bridgeAmount, payload: _payload, - revertInstruction: _revertInstruction, + revertRecipient: _revertRecipient, txType: _txType, signatureData: _signatureData }); } - // ========================= - // DEPOSITS - Universal TX Route - // ========================= + ///============================== + /// sendUniversalTx() function + ///============================== + function sendUniversalTx(UniversalTxRequest calldata req) external payable nonReentrant whenNotPaused { + uint256 nativeValue = msg.value; + TX_TYPE txType = _fetchTxType(req, nativeValue); + _routeUniversalTx(req, _msgSender(), nativeValue, txType); + } - /// @inheritdoc IUniversalGatewayV0 - function sendFunds( - address recipient, - address bridgeToken, - uint256 bridgeAmount, - RevertInstructions calldata revertInstruction - ) external payable nonReentrant whenNotPaused { - if (recipient == address(0)) revert Errors.InvalidRecipient(); + function sendUniversalTx(UniversalTokenTxRequest calldata reqToken) external payable nonReentrant whenNotPaused { + if (reqToken.gasToken == address(0)) revert Errors.InvalidInput(); + if (reqToken.gasAmount == 0) revert Errors.InvalidAmount(); + if (reqToken.amountOutMinETH == 0) revert Errors.InvalidAmount(); + if (reqToken.deadline != 0 && reqToken.deadline < block.timestamp) revert Errors.SlippageExceededOrExpired(); + + // Swap token to native + uint256 nativeValue = swapToNative(reqToken.gasToken, reqToken.gasAmount, reqToken.amountOutMinETH, reqToken.deadline); + + // Build UniversalTxRequest from token request + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: reqToken.recipient, + token: reqToken.token, + amount: reqToken.amount, + payload: reqToken.payload, + revertRecipient: reqToken.revertRecipient, + signatureData: reqToken.signatureData + }); - if (bridgeToken == address(0)) { - if (msg.value != bridgeAmount) revert Errors.InvalidAmount(); - // _consumeRateLimit(address(0), bridgeAmount); - _handleNativeDeposit(bridgeAmount); - } else { - if (msg.value != 0) revert Errors.InvalidAmount(); - //_consumeRateLimit(bridgeToken, bridgeAmount); - _handleTokenDeposit(bridgeToken, bridgeAmount); + TX_TYPE txType = _fetchTxType(req, nativeValue); + _routeUniversalTx(req, _msgSender(), nativeValue, txType); + } + + /// @notice Internal helper function to deposit for Instant TX. + /// @dev Handles rate-limit checks for Fee Abstraction Tx Route + function _sendTxWithGas( + TX_TYPE _txType, + address _caller, + uint256 _gasAmount, + bytes memory _payload, + address _revertRecipient, + bytes memory _signatureData + ) private { + if (_gasAmount > 0) { + // performs rate-limit checks and handle deposit + //_checkUSDCaps(_gasAmount); + //_checkBlockUSDCap(_gasAmount); + _handleDeposits(address(0), _gasAmount); } - _sendTxWithFunds( + _emitUniversalTx( // recipient as address(0) -> UEA. + _caller, address(0), address(0), _gasAmount, _payload, _revertRecipient, _txType, _signatureData); + } + + + function _sendTxWithFunds(UniversalTxRequest memory _req, uint256 nativeValue, TX_TYPE txType) private { + // Case 1: For TX_TYPE = FUNDS + + if (txType == TX_TYPE.FUNDS) { + address tokenForFunds; + // Case 1.1: Token to bridge is Native Token -> address(0) + if (_req.token == address(0)) { + if (_req.amount != nativeValue) revert Errors.InvalidAmount(); + tokenForFunds = address(0); + } + // Case 1.2: Token to bridge is ERC20 Token -> _req.token + else { + if (nativeValue > 0) revert Errors.InvalidAmount(); + tokenForFunds = _req.token; + } + + //_consumeRateLimit(tokenForFunds, _req.amount); + _handleDeposits(tokenForFunds, _req.amount); + + _emitUniversalTx( _msgSender(), - recipient, - bridgeToken, - bridgeAmount, - bytes(""), // Empty payload for funds-only bridge - revertInstruction, - TX_TYPE.FUNDS, - bytes("") - ); + _req.recipient, + tokenForFunds, + _req.amount, + _req.payload, + _req.revertRecipient, + txType, + _req.signatureData + ); + } + + // Case 2: For TX_TYPE = FUNDS_AND_PAYLOAD + // Note: Two possible routes for TX_TYPE.FUNDS_AND_PAYLOAD: + // - Case 2.1: No Batching (nativeValue == 0): user already has UEA with PC token ( gas ) on Push to execute payloads + // -> user already has UEA with native PC tokens on Push Chain. + // -> user can directly move _req.amount for _req.token to Push Chain. + // - Case 2.2: Batching of Gas + Funds_and_Payload (nativeValue > 0): with token == native_token + // -> user refils UEA's gas and also bridges native token. + // -> Split Needed: Native token is split between gasAmount and bridge amount ( nativeValue >= _req.amount ) + // -> _sendTxWithGas is used to send gasAmount + // -> _sendTxWithFunds is used to send bridgeAmount + // - Case 2.3: Batching of Gas + Funds_and_Payload (nativeValue > 0): with token != native_token + // -> user refils UEA's gas and also bridges ERC20 token. + // -> No Split Needed: gasAmount is used via native_token, and bridgeAmount is used via ERC20 token. + // -> _sendTxWithGas is used to send gasAmount + // -> _sendTxWithFunds is used to send bridgeAmount + if (txType == TX_TYPE.FUNDS_AND_PAYLOAD) { + address tokenForFundsAndPayload; + // Case 2.1: No Batching ( nativeValue == 0 ): user already has UEA with PC token ( gas ) on Push to execute payloads + if (nativeValue == 0) { + if (_req.token == address(0)) revert Errors.InvalidAmount(); + + tokenForFundsAndPayload = _req.token; + } + // Case 2.2: Batching of Gas + Funds_and_Payload (nativeValue > 0): with token == native_token + else if (_req.token == address(0)) { + if (nativeValue < _req.amount) revert Errors.InvalidAmount(); + + uint256 gasAmount = nativeValue - _req.amount; + + if (gasAmount > 0) { + _sendTxWithGas( + TX_TYPE.GAS, _msgSender(), gasAmount, bytes(""), _req.revertRecipient, _req.signatureData + ); + } + tokenForFundsAndPayload = address(0); + } + // Case 2.3: Batching of Gas + Funds_and_Payload (nativeValue > 0): with token != native_token + else if (_req.token != address(0)) { + uint256 gasAmount = nativeValue; + // Send Gas to caller's UEA via instant route + _sendTxWithGas( + TX_TYPE.GAS, _msgSender(), gasAmount, bytes(""), _req.revertRecipient, _req.signatureData + ); + + tokenForFundsAndPayload = _req.token; + } + + //_consumeRateLimit(tokenForFundsAndPayload, _req.amount); + _handleDeposits(tokenForFundsAndPayload, _req.amount); + _emitUniversalTx( + _msgSender(), + _req.recipient, + tokenForFundsAndPayload, + _req.amount, + _req.payload, + _req.revertRecipient, + txType, + _req.signatureData + ); + } } - /// @inheritdoc IUniversalGatewayV0 - function sendTxWithFunds( - address bridgeToken, - uint256 bridgeAmount, + // ========================= + // LEGACY COMPATIBILITY WRAPPERS + // ========================= + // These functions maintain backward compatibility with existing SDKs + // All logic is delegated to the new unified internal functions + + /// @notice Legacy: Send transaction with gas using native token (GAS_AND_PAYLOAD route) + /// @param payload Universal payload for execution + /// @param revertRecipient Fund recipient + /// @param signatureData Signature data for verification + function sendTxWithGas( UniversalPayload calldata payload, - RevertInstructions calldata revertInstruction, + address revertRecipient, bytes memory signatureData ) external payable nonReentrant whenNotPaused { - if (bridgeAmount == 0) revert Errors.InvalidAmount(); - uint256 gasAmount = msg.value; - if (gasAmount == 0) revert Errors.InvalidAmount(); - - // Check and initiate Instant TX - // _checkUSDCaps(gasAmount); // TODO: DEPRECATED FOR TESTNET SWAP - _addFunds(bytes32(0), gasAmount); - - // Check and initiate Universal TX - _handleTokenDeposit(bridgeToken, bridgeAmount); - _sendTxWithFunds( + _sendTxWithGas( + TX_TYPE.GAS_AND_PAYLOAD, _msgSender(), - address(0), - bridgeToken, - bridgeAmount, + msg.value, abi.encode(payload), - revertInstruction, - TX_TYPE.FUNDS_AND_PAYLOAD, + revertRecipient, signatureData ); } - /// @notice NEW Implementation of sendTxWithFunds with new fee abstraction route - ONLY FOR TESTNET - function sendTxWithFunds_new( - address bridgeToken, - uint256 bridgeAmount, + + /// @notice Legacy: Send transaction with gas using ERC20 token (GAS_AND_PAYLOAD route) + /// @param tokenIn Token to swap for gas + /// @param amountIn Amount of tokenIn to swap + /// @param payload Universal payload for execution + /// @param revertRecipient Fund recipient + /// @param amountOutMinETH Minimum ETH to receive from swap + /// @param deadline Swap deadline + /// @param signatureData Signature data for verification + function sendTxWithGas( + address tokenIn, + uint256 amountIn, UniversalPayload calldata payload, - RevertInstructions calldata revertInstruction, + address revertRecipient, + uint256 amountOutMinETH, + uint256 deadline, bytes memory signatureData - ) external payable nonReentrant whenNotPaused { - if (bridgeAmount == 0) revert Errors.InvalidAmount(); - uint256 gasAmount = msg.value; - if (gasAmount == 0) revert Errors.InvalidAmount(); - - _sendTxWithGas(_msgSender(), bytes(""), gasAmount, revertInstruction, TX_TYPE.GAS, signatureData); + ) external nonReentrant whenNotPaused { + if (tokenIn == address(0)) revert Errors.InvalidInput(); + if (amountIn == 0) revert Errors.InvalidAmount(); + if (amountOutMinETH == 0) revert Errors.InvalidAmount(); + if (deadline != 0 && deadline < block.timestamp) revert Errors.SlippageExceededOrExpired(); - // performs rate-limit checks and handle deposit - //_consumeRateLimit(bridgeToken, bridgeAmount); - _handleTokenDeposit(bridgeToken, bridgeAmount); + // Swap token to native ETH + uint256 ethOut = swapToNative(tokenIn, amountIn, amountOutMinETH, deadline); - _sendTxWithFunds( + _sendTxWithGas( + TX_TYPE.GAS_AND_PAYLOAD, _msgSender(), - address(0), - bridgeToken, - bridgeAmount, + ethOut, abi.encode(payload), - revertInstruction, - TX_TYPE.FUNDS_AND_PAYLOAD, + revertRecipient, signatureData ); } + /// @notice Legacy: Send funds only (FUNDS route, no payload) + /// @param recipient Recipient address on Push Chain + /// @param bridgeToken Token to bridge (address(0) for native) + /// @param bridgeAmount Amount to bridge + /// @param revertRecipient Fund recipient + function sendFunds( + address recipient, + address bridgeToken, + uint256 bridgeAmount, + address revertRecipient + ) external payable nonReentrant whenNotPaused { + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: recipient, + token: bridgeToken, + amount: bridgeAmount, + payload: bytes(""), + revertRecipient: revertRecipient, + signatureData: bytes("") + }); - /// @inheritdoc IUniversalGatewayV0 - function sendTxWithFunds( + _routeUniversalTx(req, _msgSender(), msg.value, TX_TYPE.FUNDS); + } + + /// @notice Legacy: Send funds with payload (FUNDS_AND_PAYLOAD route) + /// @param bridgeToken Token to bridge + /// @param bridgeAmount Amount to bridge + /// @param payload Universal payload for execution + /// @param revertRecipient Fund recipient + /// @param signatureData Signature data for verification + function sendTxWithFunds_new( address bridgeToken, uint256 bridgeAmount, - address gasToken, - uint256 gasAmount, - uint256 amountOutMinETH, - uint256 deadline, UniversalPayload calldata payload, - RevertInstructions calldata revertInstruction, + address revertRecipient, bytes memory signatureData - ) external nonReentrant whenNotPaused { - if (bridgeAmount == 0) revert Errors.InvalidAmount(); - if (gasToken == address(0)) revert Errors.InvalidInput(); - if (gasAmount == 0) revert Errors.InvalidAmount(); - - // Swap gasToken to native ETH - uint256 nativeGasAmount = swapToNative(gasToken, gasAmount, amountOutMinETH, deadline); - - // _checkUSDCaps(nativeGasAmount); // TODO: DEPRECATED FOR TESTNET - _addFunds(bytes32(0), nativeGasAmount); + ) external payable nonReentrant whenNotPaused { - _handleTokenDeposit(bridgeToken, bridgeAmount); - _sendTxWithFunds( - _msgSender(), - address(0), - bridgeToken, - bridgeAmount, - abi.encode(payload), - revertInstruction, - TX_TYPE.FUNDS_AND_PAYLOAD, - signatureData - ); + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), + token: bridgeToken, + amount: bridgeAmount, + payload: abi.encode(payload), + revertRecipient: revertRecipient, + signatureData: signatureData + }); + _routeUniversalTx(req, _msgSender(), msg.value, TX_TYPE.FUNDS_AND_PAYLOAD); } - /// @notice NEW Implementation of sendTxWithFunds with new fee abstraction route - ONLY FOR TESTNET + + /// @notice Legacy: Send funds with payload using ERC20 token as gas (FUNDS_AND_PAYLOAD route) + /// @param bridgeToken Token to bridge + /// @param bridgeAmount Amount to bridge + /// @param gasToken Token to swap for gas + /// @param gasAmount Amount of gasToken to swap + /// @param amountOutMinETH Minimum ETH to receive from swap + /// @param deadline Swap deadline + /// @param payload Universal payload for execution + /// @param revertRecipient Fund recipient + /// @param signatureData Signature data for verification function sendTxWithFunds_new( address bridgeToken, uint256 bridgeAmount, @@ -602,119 +756,145 @@ contract UniversalGatewayV0 is uint256 amountOutMinETH, uint256 deadline, UniversalPayload calldata payload, - RevertInstructions calldata revertInstruction, + address revertRecipient, bytes memory signatureData ) external nonReentrant whenNotPaused { if (bridgeAmount == 0) revert Errors.InvalidAmount(); if (gasToken == address(0)) revert Errors.InvalidInput(); if (gasAmount == 0) revert Errors.InvalidAmount(); + if (amountOutMinETH == 0) revert Errors.InvalidAmount(); + if (deadline != 0 && deadline < block.timestamp) revert Errors.SlippageExceededOrExpired(); // Swap gasToken to native ETH uint256 nativeGasAmount = swapToNative(gasToken, gasAmount, amountOutMinETH, deadline); - _sendTxWithGas(_msgSender(), bytes(""), nativeGasAmount, revertInstruction, TX_TYPE.GAS, signatureData); + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), + token: bridgeToken, + amount: bridgeAmount, + payload: abi.encode(payload), + revertRecipient: revertRecipient, + signatureData: signatureData + }); - // performs rate-limit checks and handle deposit - //_consumeRateLimit(bridgeToken, bridgeAmount); - _handleTokenDeposit(bridgeToken, bridgeAmount); - _sendTxWithFunds( - _msgSender(), - address(0), - bridgeToken, - bridgeAmount, - abi.encode(payload), - revertInstruction, - TX_TYPE.FUNDS_AND_PAYLOAD, - signatureData - ); + _routeUniversalTx(req, _msgSender(), nativeGasAmount, TX_TYPE.FUNDS_AND_PAYLOAD); } - /// @notice Internal helper function to deposit for Universal TX. - /// @dev Emits the core TxWithFunds event - important for Universal TX Route. - /// @param _caller Sender address - /// @param _recipient Recipient address - /// @param _bridgeToken Token address to bridge - /// @param _bridgeAmount Amount of token to bridge - /// @param _payload Payload - /// @param _revertInstruction Revert settings - /// @param _txType Transaction type - function _sendTxWithFunds( - address _caller, - address _recipient, - address _bridgeToken, - uint256 _bridgeAmount, - bytes memory _payload, - RevertInstructions calldata _revertInstruction, - TX_TYPE _txType, - bytes memory _signatureData - ) internal { - if (_revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); - /// for recipient == address(0), the funds are being moved to UEA of the msg.sender on Push Chain. - if (_recipient == address(0)){ - if ( - _txType != TX_TYPE.FUNDS_AND_PAYLOAD && - _txType != TX_TYPE.GAS_AND_PAYLOAD - ) { - revert Errors.InvalidTxType(); - } - } - emit UniversalTx({ - sender: _caller, - recipient: _recipient, - token: _bridgeToken, - amount: _bridgeAmount, - payload: _payload, - revertInstruction: _revertInstruction, - txType: _txType, - signatureData: _signatureData - }); - } + ///============================== + /// REVERT UNIVERSAL TX + ///============================== - // ========================= - // WITHDRAW - // ========================= + /// @inheritdoc IUniversalGatewayV0 + function revertUniversalTx( + bytes calldata txID, + uint256 amount, + RevertInstructions calldata revertInstruction + ) + external + payable + nonReentrant + whenNotPaused + onlyTSS + { + bytes32 txIDHash = keccak256(txID); + if (isExecuted[txIDHash]) revert Errors.PayloadExecuted(); + + if (revertInstruction.revertRecipient == address(0)) revert Errors.InvalidRecipient(); + if (amount == 0 || msg.value != amount) revert Errors.InvalidAmount(); + + isExecuted[txIDHash] = true; + (bool ok,) = payable(revertInstruction.revertRecipient).call{ value: amount }(""); + if (!ok) revert Errors.WithdrawFailed(); + + emit RevertUniversalTx(txID, revertInstruction.revertRecipient, address(0), amount, revertInstruction); + } /// @inheritdoc IUniversalGatewayV0 - function withdrawFunds( - address recipient, + function revertUniversalTxToken( + bytes calldata txID, address token, - uint256 amount - ) external nonReentrant whenNotPaused onlyTSS { - if (recipient == address(0)) revert Errors.InvalidRecipient(); + uint256 amount, + RevertInstructions calldata revertInstruction + ) + external + nonReentrant + whenNotPaused + onlyTSS + { + bytes32 txIDHash = keccak256(txID); + if (isExecuted[txIDHash]) revert Errors.PayloadExecuted(); + + if (revertInstruction.revertRecipient == address(0)) revert Errors.InvalidRecipient(); if (amount == 0) revert Errors.InvalidAmount(); + + isExecuted[txIDHash] = true; + IERC20(token).safeTransfer(revertInstruction.revertRecipient, amount); + + emit RevertUniversalTx(txID, revertInstruction.revertRecipient, token, amount, revertInstruction); + } - if (token == address(0)) { - _handleNativeWithdraw(recipient, amount); - } else { - _handleTokenWithdraw(token, recipient, amount); - } - emit WithdrawFunds(recipient, amount, token); - } + // ========================= + // GATEWAY Withdraw and Payload Execution Paths + // ========================= /// @inheritdoc IUniversalGatewayV0 - function revertWithdrawFunds( + function withdraw( + bytes calldata txID, + address originCaller, + address to, + uint256 amount + ) external payable nonReentrant whenNotPaused onlyTSS { + bytes32 txIDHash = keccak256(txID); + if (isExecuted[txIDHash]) revert Errors.PayloadExecuted(); + + if (to == address(0) || originCaller == address(0)) revert Errors.InvalidInput(); + if (amount == 0) revert Errors.InvalidAmount(); + if (msg.value != amount) revert Errors.InvalidAmount(); + + isExecuted[txIDHash] = true; + (bool ok,) = payable(to).call{ value: amount }(""); + if (!ok) revert Errors.WithdrawFailed(); + + emit WithdrawToken(txID, originCaller, address(0), to, amount); + } + //@inheritdocs IUniversalGatewayV0 + function withdrawTokens( + bytes calldata txID, + address originCaller, address token, - uint256 amount, - RevertInstructions calldata revertInstruction + address to, + uint256 amount ) external nonReentrant whenNotPaused onlyTSS { - if (revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + bytes32 txIDHash = keccak256(txID); + if (isExecuted[txIDHash]) revert Errors.PayloadExecuted(); + + if (to == address(0) || originCaller == address(0)) revert Errors.InvalidInput(); if (amount == 0) revert Errors.InvalidAmount(); + if (token == address(0)) revert Errors.InvalidInput(); + + if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); - if (token == address(0)) { - _handleNativeWithdraw(revertInstruction.fundRecipient, amount); - } else { - _handleTokenWithdraw(token, revertInstruction.fundRecipient, amount); - } - - emit WithdrawFunds(revertInstruction.fundRecipient, amount, token); + isExecuted[txIDHash] = true; + IERC20(token).safeTransfer(to, amount); + emit WithdrawToken(txID, originCaller, token, to, amount); } + + // ========================= // PUBLIC HELPERS // ========================= + /// @notice Checks if a token is supported by the gateway. + /// @param token Token address to check + /// @return True if the token is supported, false otherwise + /// @inheritdoc IUniversalGatewayV0 + function isSupportedToken(address token) public view returns (bool) { + return tokenToLimitThreshold[token] != 0; + } + /// @notice Computes the minimum and maximum deposit amounts in native ETH (wei) implied by the USD caps. /// @dev Uses the current ETH/USD price from {getEthUsdPrice}. /// @return minValue Minimum native amount (in wei) allowed by MIN_CAP_UNIVERSAL_TX_USD @@ -830,63 +1010,105 @@ contract UniversalGatewayV0 is if (usdValue > MAX_CAP_UNIVERSAL_TX_USD) revert Errors.InvalidAmount(); } - /// @dev Enforce per-block USD budget for GAS routes using two-scalar accounting. - /// - `BLOCK_USD_CAP` is denominated in USD(1e18). When 0, the feature is disabled. - /// - Resets the window when a new block is observed. - /// @param amountWei native amount (in wei) to be accounted against the current block's USD budget - function _checkBlockUSDCap(uint256 amountWei) public { - uint256 cap = BLOCK_USD_CAP; - if (cap == 0) return; // disabled + /// @dev Minimal private helper to emit the canonical UniversalTx event from a single place. + function _emitUniversalTx( + address sender, + address recipient, + address token, + uint256 amount, + bytes memory payload, + address revertRecipient, + TX_TYPE txType, + bytes memory signatureData + ) private { + emit UniversalTx({ + sender: sender, + recipient: recipient, + token: token, + amount: amount, + payload: payload, + revertRecipient: revertRecipient, + txType: txType, + signatureData: signatureData + }); + } - if (block.number != _lastBlockNumber) { - _lastBlockNumber = block.number; - _consumedUSDinBlock = 0; - } - uint256 usd1e18 = quoteEthAmountInUsd1e18(amountWei); + /// @dev Internal router that dispatches to the appropriate handler based on TX_TYPE + /// @param req The universal transaction request (memory for token-gas, can accept calldata too) + /// @param caller The original caller (msg.sender from the public function) + /// @param nativeValue The effective native value (msg.value for native-gas, swapped amount for token-gas) + function _routeUniversalTx( + UniversalTxRequest memory req, + address caller, + uint256 nativeValue, + TX_TYPE _TX_TYPE + ) internal { + TX_TYPE txType = _TX_TYPE; - if (usd1e18 > cap) revert Errors.BlockCapLimitExceeded(); + // Sanity Check : revertRecipient is not address(0) + if (req.revertRecipient == address(0)) { + revert Errors.InvalidRecipient(); + } - unchecked { - uint256 newUsed = _consumedUSDinBlock + usd1e18; - if (newUsed > cap) revert Errors.BlockCapLimitExceeded(); - _consumedUSDinBlock = newUsed; + // Route 1: GAS or GAS_AND_PAYLOAD → Instant route + if (txType == TX_TYPE.GAS || txType == TX_TYPE.GAS_AND_PAYLOAD) { + _sendTxWithGas(txType, caller, nativeValue, req.payload, req.revertRecipient, req.signatureData); + } + // Route 2: FUNDS or FUNDS_AND_PAYLOAD → Standard route + else if (txType == TX_TYPE.FUNDS || txType == TX_TYPE.FUNDS_AND_PAYLOAD) { + // // Sanity Check : recipient is address(0) // @audit - TBD , for now all recipients allowed for FUNDS + // if (req.recipient != address(0)) { + // revert Errors.InvalidRecipient(); + // } + _sendTxWithFunds(req, nativeValue, txType); + } + // Route 3: Invalid + else { + revert Errors.InvalidTxType(); } } - /// @dev Forward native ETH to TSS; returns amount forwarded (= msg.value or computed after swap). - function _handleNativeDeposit(uint256 amount) internal returns (uint256) { - (bool ok, ) = payable(TSS_ADDRESS).call{value: amount}(""); + /// @dev Enforce per-block USD budget for GAS routes using two-scalar accounting. + /// - `BLOCK_USD_CAP` is denominated in USD(1e18). When 0, the feature is disabled. + /// - Resets the window when a new block is observed. + /// @param amountWei native amount (in wei) to be accounted against the current block's USD budget + /// @audit Commented for testnet Size LIMIT + // function _checkBlockUSDCap(uint256 amountWei) public { + // uint256 cap = BLOCK_USD_CAP; + // if (cap == 0) return; // disabled + + // if (block.number != _lastBlockNumber) { + // _lastBlockNumber = block.number; + // _consumedUSDinBlock = 0; + // } + + // uint256 usd1e18 = quoteEthAmountInUsd1e18(amountWei); + + // if (usd1e18 > cap) revert Errors.BlockCapLimitExceeded(); + + // unchecked { + // uint256 newUsed = _consumedUSDinBlock + usd1e18; + // if (newUsed > cap) revert Errors.BlockCapLimitExceeded(); + // _consumedUSDinBlock = newUsed; + // } + // } + + /// @dev Handle deposits of native ETH or ERC20 tokens + /// If token is address(0): Forward native ETH to TSS + /// Otherwise: Lock ERC20 in the gateway contract for bridging + /// @param token token address (address(0) for native ETH) + /// @param amount amount to deposit + function _handleDeposits(address token, uint256 amount) internal { + if (token == address(0)) { + // Handle native ETH deposit to TSS + (bool ok,) = payable(TSS_ADDRESS).call{ value: amount }(""); if (!ok) revert Errors.DepositFailed(); - return amount; - } - - /// @dev Lock ERC20 in this contract for bridging (must be isSupported). - /// Tokens are stored in gateway contract. - /// @param token Token address to deposit - /// @param amount Amount of token to deposit - function _handleTokenDeposit(address token, uint256 amount) internal { - if (!isSupportedToken[token]) revert Errors.NotSupported(); - IERC20(token).safeTransferFrom(_msgSender(), address(this), amount); - } - - /// @dev Native withdraw by TSS - function _handleNativeWithdraw(address recipient, uint256 amount) internal { - (bool ok, ) = payable(recipient).call{value: amount}(""); - if (!ok) revert Errors.WithdrawFailed(); - } - - /// @dev ERC20 withdraw by TSS (token must be isSupported for bridging) - /// Tokens are moved out of gateway contract. - /// @param token Token address to withdraw - /// @param recipient Recipient address - /// @param amount Amount of token to withdraw - function _handleTokenWithdraw(address token, address recipient, uint256 amount) internal { - // Note: Removing isSupportedToken[token] for now to avoid a rare case scenario - // If a token was supported before and user bridged > but was removed from support list later, funds get stuck. - // if (!isSupportedToken[token]) revert Errors.NotSupported(); - if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); - IERC20(token).safeTransfer(recipient, amount); + } else { + // Handle ERC20 token deposit to gateway + if (tokenToLimitThreshold[token] == 0) revert Errors.NotSupported(); + IERC20(token).safeTransferFrom(_msgSender(), address(this), amount); + } } /// @dev Enforce and consume the per-token epoch rate limit. @@ -894,46 +1116,48 @@ contract UniversalGatewayV0 is /// epoch.used is reset to 0 when a new epoch starts (no rollover). /// @param token token address to consume rate limit /// @param amount amount of token to consume rate limit - function _consumeRateLimit(address token, uint256 amount) internal { - uint256 threshold = tokenToLimitThreshold[token]; - if (threshold == 0) revert Errors.NotSupported(); + /// @audit Commented for testnet Size LIMIT + // function _consumeRateLimit(address token, uint256 amount) internal { + // uint256 threshold = tokenToLimitThreshold[token]; + // if (threshold == 0) revert Errors.NotSupported(); - uint256 _epochDuration = epochDurationSec; - if (_epochDuration == 0) revert Errors.InvalidData(); + // uint256 _epochDuration = epochDurationSec; + // if (_epochDuration == 0) revert Errors.InvalidData(); - uint64 current = uint64(block.timestamp / _epochDuration); - EpochUsage storage e = _usage[token]; + // uint64 current = uint64(block.timestamp / _epochDuration); + // EpochUsage storage e = _usage[token]; - if (e.epoch != current) { - e.epoch = current; - e.used = 0; - } + // if (e.epoch != current) { + // e.epoch = current; + // e.used = 0; + // } - unchecked { - uint256 newUsed = uint256(e.used) + amount; // natural units - if (newUsed > threshold) revert Errors.RateLimitExceeded(); - e.used = uint192(newUsed); - } - } + // unchecked { + // uint256 newUsed = uint256(e.used) + amount; // natural units + // if (newUsed > threshold) revert Errors.RateLimitExceeded(); + // e.used = uint192(newUsed); + // } + // } /// @notice Returns both the total token amount used and remaining in the current epoch. /// @param token token address to query (use address(0) for native) /// @return used amount already consumed in the current epoch (in token's natural units) /// @return remaining amount still available to send in this epoch (0 if exceeded or unsupported) - function currentTokenUsage(address token) external view returns (uint256 used, uint256 remaining) { - uint256 thr = tokenToLimitThreshold[token]; - if (thr == 0) return (0, 0); + /// @audit Commented for testnet Size LIMIT + // function currentTokenUsage(address token) external view returns (uint256 used, uint256 remaining) { + // uint256 thr = tokenToLimitThreshold[token]; + // if (thr == 0) return (0, 0); - uint256 _epochDuration = epochDurationSec; - if (_epochDuration == 0) return (0, 0); + // uint256 _epochDuration = epochDurationSec; + // if (_epochDuration == 0) return (0, 0); - uint64 current = uint64(block.timestamp / _epochDuration); - EpochUsage storage e = _usage[token]; - uint256 u = (e.epoch == current) ? uint256(e.used) : 0; + // uint64 current = uint64(block.timestamp / _epochDuration); + // EpochUsage storage e = _usage[token]; + // uint256 u = (e.epoch == current) ? uint256(e.used) : 0; - used = u; - remaining = u >= thr ? 0 : (thr - u); - } + // used = u; + // remaining = u >= thr ? 0 : (thr - u); + // } /// @dev Swap any ERC20 to the chain's native token via a direct Uniswap v3 pool to WETH. @@ -1034,12 +1258,83 @@ contract UniversalGatewayV0 is revert Errors.InvalidInput(); } + // ========================= + // VALIDATION and Routers for sendUniversalTx() + // ========================= + + /** + * @notice Infers the TX_TYPE for an incoming universal request by inspecting only + * the four decision variables we agreed on: + * - hasPayload := (req.payload.length > 0) + * - hasFunds := (req.amount > 0) + * - fundsIsNative := (req.token == address(0)) + * - hasNativeValue := (nativeValue > 0) // nativeValue = msg.value (native-gas) OR swapped amount (token-gas) + * + * @param req UniversalTxRequest (txType field is ignored here) + * @param nativeValue Effective native value attached to the call path (msg.value or swapped amount) + * @return inferred The inferred TX_TYPE for routing + */ + function _fetchTxType(UniversalTxRequest memory req, uint256 nativeValue) + private + pure + returns (TX_TYPE inferred) + { + bool hasPayload = req.payload.length > 0; + bool hasFunds = req.amount > 0; + bool fundsIsNative = (req.token == address(0)); + bool hasNativeValue = nativeValue > 0; + + // For TX_TYPE.GAS: + // - pure gas top-up (no payload, no funds, nativeValue > 0) + if (!hasPayload && !hasFunds && hasNativeValue) { + return TX_TYPE.GAS; + } + // For TX_TYPE.GAS_AND_PAYLOAD: + // - payload present + // - no funds + // - nativeValue MAY be 0 (payload-only) or > 0 (payload + gas) + if (hasPayload && !hasFunds) { + return TX_TYPE.GAS_AND_PAYLOAD; + } + + // For TX_TYPE.FUNDS: Case 1: Native Funds + if (!hasPayload && hasFunds) { + // Case 1.1: Native Funds Only. + // FUNDS (native) — must come with native value + if (fundsIsNative && hasNativeValue) { + return TX_TYPE.FUNDS; + } + // Case 1.2: ERC-20 Funds Only. + // FUNDS (ERC-20) — must NOT come with native value - Case 1.2 + if (!fundsIsNative && !hasNativeValue) { + return TX_TYPE.FUNDS; + } + revert Errors.InvalidInput(); + } + // For TX_TYPE.FUNDS_AND_PAYLOAD: Case 2: (Native/ERC20 Funds) + Payload + if (hasPayload && hasFunds) { + // Case 2.1: No batching (ERC-20 funds, user already has UEA gas) + if (!fundsIsNative && !hasNativeValue) { + return TX_TYPE.FUNDS_AND_PAYLOAD; + } + // Case 2.2: Batching: native funds + native gas (later we enforce nativeValue >= amount) + if (fundsIsNative && hasNativeValue) { + return TX_TYPE.FUNDS_AND_PAYLOAD; + } + // Case 2.3: Batching: ERC-20 funds + native gas + if (!fundsIsNative && hasNativeValue) { + return TX_TYPE.FUNDS_AND_PAYLOAD; + } + revert Errors.InvalidInput(); + } + + revert Errors.InvalidInput(); + } // ========================= // RECEIVE/FALLBACK // ========================= - /// @dev Reject plain ETH; we only accept ETH via explicit deposit functions or WETH unwrapping. receive() external payable { // Allow WETH unwrapping; block unexpected sends. diff --git a/contracts/evm-gateway/src/Vault.sol b/contracts/evm-gateway/src/Vault.sol index 1c36cfb..7295436 100644 --- a/contracts/evm-gateway/src/Vault.sol +++ b/contracts/evm-gateway/src/Vault.sol @@ -18,10 +18,10 @@ import {IUniversalGateway} from "./interfaces/IUniversalGateway.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; contract Vault is @@ -126,7 +126,7 @@ contract Vault is // WITHDRAW // ========================= /// @inheritdoc IVault - function withdraw(bytes32 txID, address originCaller, address token, address to, uint256 amount) + function withdraw(bytes calldata txID, address originCaller, address token, address to, uint256 amount) external nonReentrant whenNotPaused @@ -143,7 +143,7 @@ contract Vault is } /// @inheritdoc IVault - function withdrawAndExecute(bytes32 txID, address originCaller, address token, address target, uint256 amount, bytes calldata data) + function withdrawAndExecute(bytes calldata txID, address originCaller, address token, address target, uint256 amount, bytes calldata data) external nonReentrant whenNotPaused @@ -164,13 +164,13 @@ contract Vault is } /// @inheritdoc IVault - function revertWithdraw(bytes32 txID, address token, address to, uint256 amount, RevertInstructions calldata revertInstruction) + function revertWithdraw(bytes calldata txID, address token, uint256 amount, RevertInstructions calldata revertInstruction) external nonReentrant whenNotPaused onlyRole(TSS_ROLE) { - if (token == address(0) || to == address(0)) revert Errors.ZeroAddress(); + if (token == address(0)) revert Errors.ZeroAddress(); if (amount == 0) revert Errors.InvalidAmount(); _enforceSupported(token); if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); @@ -178,7 +178,7 @@ contract Vault is IERC20(token).safeTransfer(address(gateway), amount); gateway.revertUniversalTxToken(txID, token, amount, revertInstruction); - emit VaultRevert(token, to, amount, revertInstruction); + emit VaultRevert(token, revertInstruction, amount); } // ========================= diff --git a/contracts/evm-gateway/src/VaultPC.sol b/contracts/evm-gateway/src/VaultPC.sol index a79b4e6..8126ca3 100644 --- a/contracts/evm-gateway/src/VaultPC.sol +++ b/contracts/evm-gateway/src/VaultPC.sol @@ -16,10 +16,10 @@ import {IVaultPC} from "./interfaces/IVaultPC.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; contract VaultPC is diff --git a/contracts/evm-gateway/src/interfaces/IPC20.sol b/contracts/evm-gateway/src/interfaces/IPC20.sol new file mode 100644 index 0000000..1c5c55a --- /dev/null +++ b/contracts/evm-gateway/src/interfaces/IPC20.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/** + * @dev Interface for PRC20 tokens + */ +interface IPC20 { + /** + * @notice ERC-20 metadata + */ + function decimals() external view returns (uint8); + function name() external view returns (string memory); + function symbol() external view returns (string memory); + + /** + * @notice ERC-20 standard functions + */ + function totalSupply() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + function deposit(address to, uint256 amount) external returns (bool); + + function approve(address spender, uint256 amount) external returns (bool); + function transfer(address recipient, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @notice special PC-20 functions + */ + function mint(address to, uint256 amount) external; + function burn(address from, uint256 amount) external; +} diff --git a/contracts/evm-gateway/src/interfaces/IPC721.sol b/contracts/evm-gateway/src/interfaces/IPC721.sol new file mode 100644 index 0000000..9f4c58b --- /dev/null +++ b/contracts/evm-gateway/src/interfaces/IPC721.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/** + * @dev Interface for PC721 (Push-native ERC-721–style NFTs) + */ +interface IPC721 { + /** + * @notice ERC-721 metadata + */ + function name() external view returns (string memory); + function symbol() external view returns (string memory); + + /// @notice Full metadata URI for a given tokenId (ERC721Metadata-style) + function tokenURI(uint256 tokenId) external view returns (string memory); + + /** + * @notice Returns the owner of an NFT + * @param tokenId The NFT ID + */ + function ownerOf(uint256 tokenId) external view returns (address); + + /** + * @notice Returns balance of an address + * @param owner NFT owner address + */ + function balanceOf(address owner) external view returns (uint256); + + /** + * @notice Push-native deposit (mirrors IPC20.deposit) + * This "locks" or "mints" the NFT into the Push Chain account. + */ + function deposit(address to, uint256 tokenId) external returns (bool); + + /** + * @notice Approve another address to transfer the given tokenId + */ + function approve(address to, uint256 tokenId) external; + + /** + * @notice Returns approved account for a token ID + */ + function getApproved(uint256 tokenId) external view returns (address); + + /** + * @notice Set/unset operator approvals + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @notice Check if operator is approved for all tokens of owner + */ + function isApprovedForAll(address owner, address operator) + external + view + returns (bool); + + /** + * @notice Transfer token from one account to another + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /** + * @notice Safe transfer variant + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /** + * @notice Safe transfer with data payload + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external; + + /** + * @notice special PC-20 functions + */ + function mint(address to, uint256 tokenId) external; + function burn(uint256 tokenId) external; +} diff --git a/contracts/evm-gateway/src/interfaces/IUniversalCore.sol b/contracts/evm-gateway/src/interfaces/IUniversalCore.sol index 68b8b97..2aaf294 100644 --- a/contracts/evm-gateway/src/interfaces/IUniversalCore.sol +++ b/contracts/evm-gateway/src/interfaces/IUniversalCore.sol @@ -54,4 +54,43 @@ interface IUniversalCore { * @return gasFee Gas fee */ function withdrawGasFeeWithGasLimit(address _prc20, uint256 gasLimit) external view returns (address gasToken, uint256 gasFee); + + + /** + * @notice Protocol fee applied to PC20 withdrawals and flows that use PC20 as the asset. + * @return fee Protocol fee for PC20 in PC units. + */ + function PC20_PROTOCOL_FEES() external view returns (uint256 fee); + + /** + * @notice Protocol fee applied to PC721 withdrawals and flows that use PC721 as the asset. + * @return fee Protocol fee for PC721 in PC units. + */ + function PC721_PROTOCOL_FEES() external view returns (uint256 fee); + + /** + * @notice Default protocol fee used when the asset type does not match PC20, PC721 or PRC20. + * @return fee Default protocol fee in PC units. + */ + function DEFAULT_PROTOCOL_FEES() external view returns (uint256 fee); + + /** + * @notice Check if PC20 assets are supported for a given external chain namespace. + * @param chainNamespace Chain namespace identifier (for example "eip155:1"). + * @return supported True if PC20 is supported on this chain, false otherwise. + */ + function isPC20SupportedOnChain(string calldata chainNamespace) + external + view + returns (bool supported); + + /** + * @notice Check if PC721 assets are supported for a given external chain namespace. + * @param chainNamespace Chain namespace identifier (for example "eip155:1"). + * @return supported True if PC721 is supported on this chain, false otherwise. + */ + function isPC721SupportedOnChain(string calldata chainNamespace) + external + view + returns (bool supported); } \ No newline at end of file diff --git a/contracts/evm-gateway/src/interfaces/IUniversalGateway.sol b/contracts/evm-gateway/src/interfaces/IUniversalGateway.sol index 9b25744..5baed7b 100644 --- a/contracts/evm-gateway/src/interfaces/IUniversalGateway.sol +++ b/contracts/evm-gateway/src/interfaces/IUniversalGateway.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { RevertInstructions, UniversalPayload, TX_TYPE } from "../libraries/Types.sol"; +import { RevertInstructions, + TX_TYPE, + UniversalTxRequest, + UniversalTokenTxRequest } from "../libraries/Types.sol"; interface IUniversalGateway { // ========================= @@ -22,17 +25,6 @@ interface IUniversalGateway { /// @param token Token address /// @param newThreshold New threshold event TokenLimitThresholdUpdated(address indexed token, uint256 newThreshold); - - /// @notice Checks if a token is supported by the gateway. - /// @param token Token address to check - /// @return True if the token is supported, false otherwise - function isSupportedToken(address token) external view returns (bool); - - /// @notice Computes the minimum and maximum deposit amounts in native ETH (wei) implied by the USD caps. - /// @dev Uses the current ETH/USD price from {getEthUsdPrice}. - /// @return minValue Minimum native amount (in wei) allowed by MIN_CAP_UNIVERSAL_TX_USD - /// @return maxValue Maximum native amount (in wei) allowed by MAX_CAP_UNIVERSAL_TX_USD - function getMinMaxValueForNative() external view returns (uint256 minValue, uint256 maxValue); /// @notice Universal transaction event that originates from external chain. /// @param sender Sender of the tx on external chain @@ -40,7 +32,7 @@ interface IUniversalGateway { /// @param token Token address being sent /// @param amount Amount of token being sent /// @param payload Payload for arbitrary call on Push Chain: for funds-only tx, payload is empty. - /// @param revertInstruction Revert settings configuration + /// @param revertRecipient Fund recipient /// @param txType Transaction type: TX_TYPE enum /// @param signatureData Signature data: for signedVerification, signatureData is the signature of the sender. event UniversalTx( @@ -49,7 +41,7 @@ interface IUniversalGateway { address token, uint256 amount, bytes payload, - RevertInstructions revertInstruction, + address revertRecipient, TX_TYPE txType, bytes signatureData ); @@ -62,7 +54,7 @@ interface IUniversalGateway { /// @param amount Amount of token being sent /// @param data Calldata to be executed on target contract on external chain event UniversalTxExecuted( - bytes32 indexed txID, + bytes indexed txID, address indexed originCaller, address indexed target, address token, @@ -81,7 +73,7 @@ interface IUniversalGateway { /// @param token Token address being sent /// @param to Recipient address on Push Chain /// @param amount Amount of token being sent - event WithdrawToken(bytes32 indexed txID, address indexed originCaller, address indexed token, address to, uint256 amount); + event WithdrawToken(bytes indexed txID, address indexed originCaller, address indexed token, address to, uint256 amount); /// @notice Revert withdraw event: For withdrwals/actions during a revert /// @param txID Unique transaction identifier @@ -89,176 +81,97 @@ interface IUniversalGateway { /// @param token Token address being reverted /// @param amount Amount of token being reverted /// @param revertInstruction Revert settings configuration - event RevertUniversalTx(bytes32 indexed txID, address indexed to, address indexed token, uint256 amount, RevertInstructions revertInstruction); + event RevertUniversalTx(bytes indexed txID, address indexed to, address indexed token, uint256 amount, RevertInstructions revertInstruction); // ========================= - // sendTxWithGas - Fee Abstraction Route + // UG_1: UNIVERSAL TRANSACTION // ========================= - - /// @notice Allows initiating a TX for funding UEAs or quick executions of payloads on Push Chain. - /// @dev Supports 2 TX types: - /// a. GAS. - /// b. GAS_AND_PAYLOAD. - /// @dev TX initiated via fee abstraction route requires lower block confirmations for execution on Push Chain. - /// Thus, the deposit amount is subject to USD cap checks that is strictly enforced with MIN_CAP_UNIVERSAL_TX_USD & MAX_CAP_UNIVERSAL_TX_USD. - /// Gas for this transaction must be paid in the NATIVE token of the source chain. - /// - /// @dev Rate Limit Checks: - /// a. Includes _checkUSDCaps: USD cap checks for the deposit amount. Must be within MIN_CAP_UNIVERSAL_TX_USD & MAX_CAP_UNIVERSAL_TX_USD. - /// b. Includes _checkBlockUSDCap: Block-based USD cap checks. Must be within BLOCK_USD_CAP. - /// - /// @param payload Universal payload to execute on Push Chain - /// @param revertCFG Revert settings - /// @param signatureData Signature data - function sendTxWithGas( - UniversalPayload calldata payload, - RevertInstructions calldata revertCFG, - bytes memory signatureData - ) external payable; - - /// @notice Allows initiating a TX for funding UEAs or quick executions of payloads on Push Chain with any supported Token. - /// @dev Allows users to use any token to fund or execute a payload on Push Chain. - /// The deposited token is swapped to native ETH using Uniswap v3. - /// Supports 2 TX types: - /// a. GAS. - /// b. GAS_AND_PAYLOAD. - /// @dev TX initiated via fee abstraction route requires lower block confirmations for execution on Push Chain. - /// Thus, the deposit amount is subject to USD cap checks that is strictly enforced with MIN_CAP_UNIVERSAL_TX_USD & MAX_CAP_UNIVERSAL_TX_USD. - /// Gas for this transaction can be paid in any token with a valid pool with the native token on AMM. - /// - /// @dev Rate Limit Checks: - /// a. _checkUSDCaps: USD cap checks for the deposit amount. Must be within MIN_CAP_UNIVERSAL_TX_USD & MAX_CAP_UNIVERSAL_TX_USD. - /// b. _checkBlockUSDCap: Block-based USD cap checks. Must be within BLOCK_USD_CAP. - /// - /// @param tokenIn Token address to swap from - /// @param amountIn Amount of token to swap - /// @param payload Universal payload to execute on Push Chain - /// @param revertCFG Revert settings - /// @param amountOutMinETH Minimum ETH expected (slippage protection) - /// @param deadline Swap deadline - /// @param signatureData Signature data - function sendTxWithGas( - address tokenIn, - uint256 amountIn, - UniversalPayload calldata payload, - RevertInstructions calldata revertCFG, - uint256 amountOutMinETH, - uint256 deadline, - bytes memory signatureData - ) external; + /** + * @notice Initiate a Universal Transaction using the chain's native token as gas (if any). + * + * @dev Primary entrypoint for all inbound universal transactions that: + * - Fund a user's UEA on Push Chain with native gas, and/or + * - Bridge funds (native or ERC20) to Push Chain, and/or + * - Execute an arbitrary payload via the user's UEA on Push Chain. + * + * The function accepts a single `UniversalTxRequest` which describes: ( see /libraries/Types.sol for more details) + * + * Based on the UniversalTxRequest, the request is classified into one of four + * supported transaction classes: + * + * 1. TX_TYPE.GAS + * - No payload, no funds, msg.value > 0 + * - Pure gas top-up to the caller's UEA on Push Chain. + * + * 2. TX_TYPE.GAS_AND_PAYLOAD + * - payload present, no funds + * - msg.value MAY be 0 (payload-only, using pre-funded UEA gas) + * or > 0 (payload + fresh gas). + * + * 3. TX_TYPE.FUNDS + * - funds present, no payload: + * a) Native funds: + * - req.token == address(0) + * - msg.value == req.amount + * b) ERC20 funds: + * - req.token != address(0) + * - msg.value == 0 + * + * 4. TX_TYPE.FUNDS_AND_PAYLOAD + * - funds present, payload present: + * a) No batching (ERC20 funds, no native): + * b) Native batching (native funds + native gas): + * c) ERC20 + native gas batching: + * + * Routing & rate-limit behavior: + * -------------------------------- + * - GAS / GAS_AND_PAYLOAD: + * - Routed to the "instant" Fee Abstraction path via `_sendTxWithGas`. + * - Enforces Rate Limit Checks: + * - `_checkUSDCaps` (min/max USD caps per transaction) + * - `_checkBlockUSDCap` (per-block USD budget) + * + * - FUNDS / FUNDS_AND_PAYLOAD: + * - Routed to the Universal Transaction path via `_sendTxWithFunds`. + * - Enforces per-token epoch rate-limits via `_consumeRateLimit(token, amount)` + * + * @param req UniversalTxRequest struct + */ + function sendUniversalTx(UniversalTxRequest calldata req) external payable; + + /** + * @notice Initiate a Universal Transaction using an ERC20 token as gas input. + * + * @dev This overload extends `sendUniversalTx(UniversalTxRequest)` by allowing the + * caller to pay "gas" in any supported ERC20 (`gasToken`) instead of native ETH. + * + * @dev Note that the fundamental flow remains exactly same as sendUniversalTx(UniversalTxRequest) + * + * @param reqToken UniversalTokenTxRequest struct + */ + function sendUniversalTx(UniversalTokenTxRequest calldata reqToken) external payable; // ========================= - // sendTxWithFunds - Universal Transaction Route + // UG_2: REVERT HANDLING PATHS // ========================= - /// @notice Allows initiating a TX for movement of high value funds from source chain to Push Chain. - /// @dev Doesn't support arbitrary execution payload via UEAs. Only allows movement of funds. - /// The tokens moved must be supported by the gateway. - /// Supports only Universal TX type with high value funds, i.e., high block confirmations are required. - /// Supports the TX type - FUNDS. - /// - /// @dev Rate Limit Checks: - /// a. _consumeRateLimit: Consume the per-token epoch rate limit. - /// - Every supported token has a per-token epoch limit threshold. - /// - New Epoch resets the usage limit threshold of a given token. - /// - /// @param recipient Recipient address - /// @param bridgeToken Token address to bridge - /// @param bridgeAmount Amount of token to bridge - /// @param revertCFG Revert settings - function sendFunds( - address recipient, - address bridgeToken, - uint256 bridgeAmount, - RevertInstructions calldata revertCFG - ) external payable; - - /// @notice Allows initiating a TX for movement of funds and payload from source chain to Push Chain. - /// @dev Supports arbitrary execution payload via UEAs. - /// The tokens moved must be supported by the gateway. - /// Supports the TX type - FUNDS_AND_PAYLOAD. - /// Gas for this transaction must be paid in the NATIVE token of the source chain. - /// Note: Recipient for such TXs are always the user's UEA on Push Chain. Hence, no recipient address is needed. - /// - /// @dev Rate Limit Checks: - /// a. _consumeRateLimit: Consume the per-token epoch rate limit. - /// - Every supported token has a per-token epoch limit threshold. - /// - New Epoch resets the usage limit threshold of a given token. - /// b. Includes _checkUSDCaps and _checkBlockUSDCap for _sendTxWithGas function called internally. - /// - /// @param bridgeToken Token address to bridge - /// @param bridgeAmount Amount of token to bridge - /// @param payload Universal payload to execute on Push Chain - /// @param revertCFG Revert settings - /// @param signatureData Signature data - function sendTxWithFunds( - address bridgeToken, - uint256 bridgeAmount, - UniversalPayload calldata payload, - RevertInstructions calldata revertCFG, - bytes memory signatureData - ) external payable; - - /// @notice Allows initiating a TX for movement of funds and payload from source chain to Push Chain. - /// Similar to sendTxWithFunds(), but with a token as gas input. - /// @dev The gas token is swapped to native ETH using Uniswap v3. - /// The tokens moved must be supported by the gateway. - /// Supports the TX type - FUNDS_AND_PAYLOAD. - /// Gas for this transaction can be paid in any token with a valid pool with the native token on AMM. - /// Imposes strict check for USD cap for the deposit amount. - /// @dev The route emits two different events: - /// a. TxWithGas - for gas funding - no payload is moved. - /// allows user to fund their UEA, which will be used for execution of payload. - /// b. TxWithFunds - for funds and payload movement from source chain to Push Chain. - /// - /// Note: Recipient for such TXs are always the user's UEA. Hence, no recipient address is needed. - /// - /// @dev Rate Limit Checks: - /// a. _consumeRateLimit: Consume the per-token epoch rate limit. - /// - Every supported token has a per-token epoch limit threshold. - /// - New Epoch resets the usage limit threshold of a given token. - /// b. Includes _checkUSDCaps and _checkBlockUSDCap for _sendTxWithGas function called internally. - /// - /// @param bridgeToken Token address to bridge - /// @param bridgeAmount Amount of token to bridge - /// @param gasToken Token address to swap from - /// @param gasAmount Amount of token to swap - /// @param amountOutMinETH Minimum ETH expected (slippage protection) - /// @param deadline Swap deadline - /// @param payload Universal payload to execute on Push Chain - /// @param revertCFG Revert settings - /// @param signatureData Signature data - function sendTxWithFunds( - address bridgeToken, - uint256 bridgeAmount, - address gasToken, - uint256 gasAmount, - uint256 amountOutMinETH, - uint256 deadline, - UniversalPayload calldata payload, - RevertInstructions calldata revertCFG, - bytes memory signatureData - ) external; - - /// @notice Withdraw functions (TSS-only) - /// @notice Revert universal transaction with tokens to the recipient specified in revertInstruction /// @param txID unique transaction identifier (for replay protection) /// @param token token address to revert /// @param amount amount of token to revert /// @param revertCFG revert settings - function revertUniversalTxToken(bytes32 txID, address token, uint256 amount, RevertInstructions calldata revertCFG) external; + function revertUniversalTxToken(bytes calldata txID, address token, uint256 amount, RevertInstructions calldata revertCFG) external; /// @notice Revert native tokens to the recipient specified in revertInstruction /// @param txID unique transaction identifier (for replay protection) /// @param amount amount of native token to revert /// @param revertCFG revert settings - function revertUniversalTx(bytes32 txID, uint256 amount, RevertInstructions calldata revertCFG) external payable; + function revertUniversalTx(bytes calldata txID, uint256 amount, RevertInstructions calldata revertCFG) external payable; // ========================= - // Withdraw and Payload Execution Paths + // UG_3: WITHDRAW AND PAYLOAD EXECUTION PATHS // ========================= /// @notice Withdraw native token from the gateway @@ -266,7 +179,7 @@ interface IUniversalGateway { /// @param originCaller original caller/user on source chain /// @param to recipient address /// @param amount amount of native token to withdraw - function withdraw(bytes32 txID, address originCaller, address to, uint256 amount) external payable; + function withdraw(bytes calldata txID, address originCaller, address to, uint256 amount) external payable; /// @notice Withdraw ERC20 token from the gateway /// @param txID unique transaction identifier @@ -274,7 +187,7 @@ interface IUniversalGateway { /// @param token token address (ERC20 token) /// @param to recipient address /// @param amount amount of token to withdraw - function withdrawTokens(bytes32 txID, address originCaller, address token, address to, uint256 amount) external; + function withdrawTokens(bytes calldata txID, address originCaller, address token, address to, uint256 amount) external; /// @notice Executes a Universal Transaction on this chain triggered by Vault after validation on Push Chain. /// @param txID unique transaction identifier @@ -284,7 +197,7 @@ interface IUniversalGateway { /// @param amount amount of token to send along /// @param payload calldata to be executed on target function executeUniversalTx( - bytes32 txID, + bytes calldata txID, address originCaller, address token, address target, @@ -299,10 +212,32 @@ interface IUniversalGateway { /// @param amount amount of native token to send along /// @param payload calldata to be executed on target function executeUniversalTx( - bytes32 txID, + bytes calldata txID, address originCaller, address target, uint256 amount, bytes calldata payload ) external payable; + + + // ========================= + // UG_4: PUBLIC HELPERS + // ========================= + + ///@notice Checks if a token is supported by the gateway. + ///@param token Token address to check + ///@return True if the token is supported, false otherwise + function isSupportedToken(address token) external view returns (bool); + + ///@notice Computes the minimum and maximum deposit amounts in native ETH (wei) implied by the USD caps. + ///@dev Uses the current ETH/USD price from {getEthUsdPrice}. + ///@return minValue Minimum native amount (in wei) allowed by MIN_CAP_UNIVERSAL_TX_USD + ///@return maxValue Maximum native amount (in wei) allowed by MAX_CAP_UNIVERSAL_TX_USD + function getMinMaxValueForNative() external view returns (uint256 minValue, uint256 maxValue); + + ///@notice Returns both the total token amount used and remaining in the current epoch. + ///@param token token address to query (use address(0) for native) + ///@return used amount already consumed in the current epoch (in token's natural units) + ///@return remaining amount still available to send in this epoch (0 if exceeded or unsupported) + function currentTokenUsage(address token) external view returns (uint256 used, uint256 remaining); } diff --git a/contracts/evm-gateway/src/interfaces/IUniversalGatewayPC.sol b/contracts/evm-gateway/src/interfaces/IUniversalGatewayPC.sol index 0021bf6..60ab7c3 100644 --- a/contracts/evm-gateway/src/interfaces/IUniversalGatewayPC.sol +++ b/contracts/evm-gateway/src/interfaces/IUniversalGatewayPC.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {RevertInstructions} from "../libraries/Types.sol"; +import {RevertInstructions, TX_TYPE} from "../libraries/Types.sol"; /** * @title IUniversalGatewayPC @@ -11,21 +11,23 @@ import {RevertInstructions} from "../libraries/Types.sol"; interface IUniversalGatewayPC { // ========= Events ========= - /// @notice Single event covering both flows (funds-only and funds+payload). - /// @param sender EVM sender on Push Chain (burn initiator) on Push Chain - /// @param chainId Origin chain id string, fetched from PRC20 on external chain - /// @param token PRC20 token address being withdrawn (represents origin ERC20/native) on external chain - /// @param target Raw destination address on origin chain (bytes) on external chain - /// @param amount Amount burned on Push Chain - /// @param gasToken PRC20 gas coin used to pay cross-chain execution fees on external chain - /// @param gasFee Amount of gasToken charged on external chain - /// @param gasLimit Gas limit used for fee quote on external chain - /// @param payload Optional payload for arbitrary call on origin chain (empty for funds-only) on external chain - /// @param protocolFee Flat protocol fee portion (as defined by PRC20), included inside gasFee on external chain - event UniversalTxWithdraw( + /// @notice Single event covering all flows (funds-only, payload-only and funds+payload). + /// @param txId Unique TxId for Outbound Universal Tx + /// @param sender EVM sender on Push Chain (burn initiator) on Push Chain + /// @param token PRC20 token address being withdrawn (represents origin ERC20/native) on external chain + /// @param chainNamespace Origin chain id string, fetched from PRC20 on external chain + /// @param target Raw destination address on origin chain (bytes) on external chain + /// @param amount Amount burned on Push Chain + /// @param gasToken PRC20 gas coin used to pay cross-chain execution fees on external chain + /// @param gasFee Amount of gasToken charged on external chain + /// @param gasLimit Gas limit used for fee quote on external chain + /// @param payload Optional payload for arbitrary call on origin chain (empty for funds-only) on external chain + /// @param protocolFee Flat protocol fee portion (as defined by PRC20), included inside gasFee on external chain + event UniversalTxOutbound( + bytes32 indexed txId, address indexed sender, - string indexed chainId, address indexed token, + string chainNamespace, bytes target, uint256 amount, address gasToken, @@ -33,49 +35,38 @@ interface IUniversalGatewayPC { uint256 gasLimit, bytes payload, uint256 protocolFee, - RevertInstructions revertInstruction + address revertRecipient, + TX_TYPE txType ); - /// @notice Emitted when VaultPC address is updated - /// @param oldVaultPC Previous VaultPC address - /// @param newVaultPC New VaultPC address + /// @notice Emitted when VaultPC address is updated + /// @param oldVaultPC Previous VaultPC address + /// @param newVaultPC New VaultPC address event VaultPCUpdated(address indexed oldVaultPC, address indexed newVaultPC); /** - * @notice Withdraw PRC20 back to origin chain (funds only). - * @dev Uses UniversalCore to fetch gasToken, gasFee and protocolFee. - * @param to raw destination address on origin chain. - * @param token PRC20 token address on Push Chain. - * @param amount amount to withdraw (burn on Push, unlock at origin). - * @param gasLimit gas limit to use for fee quote; if 0, uses token's default GAS_LIMIT(). + * @notice Send a universal transaction outbound from Push Chain. + * @dev Supports funds-only, payload-only, and funds+payload flows. + * Handles PRC20, PC20, and PC721 assets. + * @param target raw destination address on origin chain. + * @param token PRC20 / PC20 / PC721 token address, or address(0) for payload-only. + * @param amount fungible amount (for PRC20 / PC20). + * @param tokenId NFT id (for PC721). + * @param gasLimit gas limit to use for fee quote; if 0, uses BASE_GAS_LIMIT. + * @param payload optional payload for arbitrary call execution on origin chain. + * @param chainNamespace chain namespace identifier (e.g., "eip155:1"). * @param revertInstruction revert configuration (fundRecipient, revertMsg) for off-chain use. */ - function withdraw( - bytes calldata to, - address token, - uint256 amount, - uint256 gasLimit, - RevertInstructions calldata revertInstruction - ) external; - - /** - * @notice Withdraw PRC20 and attach an arbitrary payload to be executed on the origin chain. - * @dev Uses UniversalCore to fetch gasToken, gasFee and protocolFee. - * @param target raw destination (contract) address on origin chain. - * @param token PRC20 token address on Push Chain. - * @param amount amount to withdraw (burn on Push, unlock at origin). - * @param payload ABI-encoded calldata to execute on the origin chain. - * @param gasLimit gas limit to use for fee quote; if 0, uses token's default GAS_LIMIT(). - * @param revertInstruction revert configuration (fundRecipient, revertMsg) for off-chain use. - */ - function withdrawAndExecute( + function sendUniversalTxOutbound( bytes calldata target, address token, uint256 amount, - bytes calldata payload, + uint256 tokenId, uint256 gasLimit, + bytes calldata payload, + string calldata chainNamespace, RevertInstructions calldata revertInstruction - ) external; + ) external payable; // ========= View Functions ========= function UNIVERSAL_CORE() external view returns (address); diff --git a/contracts/evm-gateway/src/interfaces/IUniversalGatewayV0.sol b/contracts/evm-gateway/src/interfaces/IUniversalGatewayV0.sol index 9d3abd1..6143426 100644 --- a/contracts/evm-gateway/src/interfaces/IUniversalGatewayV0.sol +++ b/contracts/evm-gateway/src/interfaces/IUniversalGatewayV0.sol @@ -1,83 +1,86 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { RevertInstructions, UniversalPayload, TX_TYPE } from "../libraries/Types.sol"; + +/// @notice Interface for Universal Gateway V0 - EVM TESTNETS Only +import { + RevertInstructions, + UniversalPayload, + TX_TYPE, + UniversalTxRequest, + UniversalTokenTxRequest +} from "../libraries/Types.sol"; interface IUniversalGatewayV0 { // ========================= // EVENTS // ========================= - /// @notice Universal tx deposit (gas funding). Emits for both gas refil and funds+payload movement. + /// @notice Universal tx deposit (gas funding). Emits for both gas refill and funds+payload movement. event UniversalTx( address indexed sender, address indexed recipient, address token, uint256 amount, bytes payload, - RevertInstructions revertInstruction, + address revertRecipient, TX_TYPE txType, bytes signatureData ); - /// @notice Withdraw funds event - event WithdrawFunds(address indexed recipient, uint256 amount, address tokenAddress); /// @notice Caps updated event event CapsUpdated(uint256 minCapUsd, uint256 maxCapUsd); + /// @notice Rate-limit / config events event EpochDurationUpdated(uint256 oldDuration, uint256 newDuration); event TokenLimitThresholdUpdated(address indexed token, uint256 newThreshold); + /// @notice Revert universal transaction event + event RevertUniversalTx( + bytes indexed txID, + address indexed to, + address indexed token, + uint256 amount, + RevertInstructions revertInstruction + ); + + /// @notice Withdraw token event (native token is represented with token = address(0)) + event WithdrawToken( + bytes indexed txID, + address indexed originCaller, + address indexed token, + address to, + uint256 amount + ); + + // ========================= + // PUBLIC HELPERS + // ========================= + + function isSupportedToken(address token) external view returns (bool); + + // ========================= + // sendUniversalTx - Unified Route + // ========================= + + function sendUniversalTx(UniversalTxRequest calldata req) external payable; + + function sendUniversalTx(UniversalTokenTxRequest calldata reqToken) external payable; + // ========================= // sendTxWithGas - Fee Abstraction Route // ========================= - /// @notice Allows initiating a TX for funding UEAs or quick executions of payloads on Push Chain. - /// @dev Supports 2 TX types: - /// a. GAS. - /// b. GAS_AND_PAYLOAD. - /// @dev TX initiated via fee abstraction route requires lower block confirmations for execution on Push Chain. - /// Thus, the deposit amount is subject to USD cap checks that is strictly enforced with MIN_CAP_UNIVERSAL_TX_USD & MAX_CAP_UNIVERSAL_TX_USD. - /// Gas for this transaction must be paid in the NATIVE token of the source chain. - /// - /// @dev Rate Limit Checks: - /// a. Includes _checkUSDCaps: USD cap checks for the deposit amount. Must be within MIN_CAP_UNIVERSAL_TX_USD & MAX_CAP_UNIVERSAL_TX_USD. - /// b. Includes _checkBlockUSDCap: Block-based USD cap checks. Must be within BLOCK_USD_CAP. - /// - /// @param payload Universal payload to execute on Push Chain - /// @param revertCFG Revert settings - /// @param signatureData Signature data function sendTxWithGas( UniversalPayload calldata payload, - RevertInstructions calldata revertCFG, + address revertRecipient, bytes memory signatureData ) external payable; - /// @notice Allows initiating a TX for funding UEAs or quick executions of payloads on Push Chain with any supported Token. - /// @dev Allows users to use any token to fund or execute a payload on Push Chain. - /// The deposited token is swapped to native ETH using Uniswap v3. - /// Supports 2 TX types: - /// a. GAS. - /// b. GAS_AND_PAYLOAD. - /// @dev TX initiated via fee abstraction route requires lower block confirmations for execution on Push Chain. - /// Thus, the deposit amount is subject to USD cap checks that is strictly enforced with MIN_CAP_UNIVERSAL_TX_USD & MAX_CAP_UNIVERSAL_TX_USD. - /// Gas for this transaction can be paid in any token with a valid pool with the native token on AMM. - /// - /// @dev Rate Limit Checks: - /// a. _checkUSDCaps: USD cap checks for the deposit amount. Must be within MIN_CAP_UNIVERSAL_TX_USD & MAX_CAP_UNIVERSAL_TX_USD. - /// b. _checkBlockUSDCap: Block-based USD cap checks. Must be within BLOCK_USD_CAP. - /// - /// @param tokenIn Token address to swap from - /// @param amountIn Amount of token to swap - /// @param payload Universal payload to execute on Push Chain - /// @param revertCFG Revert settings - /// @param amountOutMinETH Minimum ETH expected (slippage protection) - /// @param deadline Swap deadline - /// @param signatureData Signature data function sendTxWithGas( address tokenIn, uint256 amountIn, UniversalPayload calldata payload, - RevertInstructions calldata revertCFG, + address revertRecipient, uint256 amountOutMinETH, uint256 deadline, bytes memory signatureData @@ -87,83 +90,21 @@ interface IUniversalGatewayV0 { // sendTxWithFunds - Universal Transaction Route // ========================= - /// @notice Allows initiating a TX for movement of high value funds from source chain to Push Chain. - /// @dev Doesn't support arbitrary execution payload via UEAs. Only allows movement of funds. - /// The tokens moved must be supported by the gateway. - /// Supports only Universal TX type with high value funds, i.e., high block confirmations are required. - /// Supports the TX type - FUNDS. - /// - /// @dev Rate Limit Checks: - /// a. _consumeRateLimit: Consume the per-token epoch rate limit. - /// - Every supported token has a per-token epoch limit threshold. - /// - New Epoch resets the usage limit threshold of a given token. - /// - /// @param recipient Recipient address - /// @param bridgeToken Token address to bridge - /// @param bridgeAmount Amount of token to bridge - /// @param revertCFG Revert settings function sendFunds( address recipient, address bridgeToken, uint256 bridgeAmount, - RevertInstructions calldata revertCFG + address revertRecipient ) external payable; - /// @notice Allows initiating a TX for movement of funds and payload from source chain to Push Chain. - /// @dev Supports arbitrary execution payload via UEAs. - /// The tokens moved must be supported by the gateway. - /// Supports the TX type - FUNDS_AND_PAYLOAD. - /// Gas for this transaction must be paid in the NATIVE token of the source chain. - /// Note: Recipient for such TXs are always the user's UEA on Push Chain. Hence, no recipient address is needed. - /// - /// @dev Rate Limit Checks: - /// a. _consumeRateLimit: Consume the per-token epoch rate limit. - /// - Every supported token has a per-token epoch limit threshold. - /// - New Epoch resets the usage limit threshold of a given token. - /// b. Includes _checkUSDCaps and _checkBlockUSDCap for _sendTxWithGas function called internally. - /// - /// @param bridgeToken Token address to bridge - /// @param bridgeAmount Amount of token to bridge - /// @param payload Universal payload to execute on Push Chain - /// @param revertCFG Revert settings - /// @param signatureData Signature data function sendTxWithFunds( address bridgeToken, uint256 bridgeAmount, UniversalPayload calldata payload, - RevertInstructions calldata revertCFG, + address revertRecipient, bytes memory signatureData ) external payable; - /// @notice Allows initiating a TX for movement of funds and payload from source chain to Push Chain. - /// Similar to sendTxWithFunds(), but with a token as gas input. - /// @dev The gas token is swapped to native ETH using Uniswap v3. - /// The tokens moved must be supported by the gateway. - /// Supports the TX type - FUNDS_AND_PAYLOAD. - /// Gas for this transaction can be paid in any token with a valid pool with the native token on AMM. - /// Imposes strict check for USD cap for the deposit amount. - /// @dev The route emits two different events: - /// a. TxWithGas - for gas funding - no payload is moved. - /// allows user to fund their UEA, which will be used for execution of payload. - /// b. TxWithFunds - for funds and payload movement from source chain to Push Chain. - /// - /// Note: Recipient for such TXs are always the user's UEA. Hence, no recipient address is needed. - /// - /// @dev Rate Limit Checks: - /// a. _consumeRateLimit: Consume the per-token epoch rate limit. - /// - Every supported token has a per-token epoch limit threshold. - /// - New Epoch resets the usage limit threshold of a given token. - /// b. Includes _checkUSDCaps and _checkBlockUSDCap for _sendTxWithGas function called internally. - /// - /// @param bridgeToken Token address to bridge - /// @param bridgeAmount Amount of token to bridge - /// @param gasToken Token address to swap from - /// @param gasAmount Amount of token to swap - /// @param amountOutMinETH Minimum ETH expected (slippage protection) - /// @param deadline Swap deadline - /// @param payload Universal payload to execute on Push Chain - /// @param revertCFG Revert settings - /// @param signatureData Signature data function sendTxWithFunds( address bridgeToken, uint256 bridgeAmount, @@ -172,22 +113,42 @@ interface IUniversalGatewayV0 { uint256 amountOutMinETH, uint256 deadline, UniversalPayload calldata payload, - RevertInstructions calldata revertCFG, + address revertRecipient, + bytes memory signatureData + ) external; + + function sendTxWithFunds_new( + address bridgeToken, + uint256 bridgeAmount, + UniversalPayload calldata payload, + address revertRecipient, + bytes memory signatureData + ) external payable; + + function sendTxWithFunds_new( + address bridgeToken, + uint256 bridgeAmount, + address gasToken, + uint256 gasAmount, + uint256 amountOutMinETH, + uint256 deadline, + UniversalPayload calldata payload, + address revertRecipient, bytes memory signatureData ) external; - /// @notice Withdraw functions (TSS-only) + // ========================= + // REVERT & WITHDRAW + // ========================= + + function revertUniversalTx(bytes calldata txID, uint256 amount, RevertInstructions calldata revertCFG) + external + payable; + + function revertUniversalTxToken(bytes calldata txID, address token, uint256 amount, RevertInstructions calldata revertCFG) + external; - /// @notice TSS-only withdraw (unlock) to an external recipient on Push Chain. - /// @param recipient destination address - /// @param token address(0) for native; ERC20 otherwise - /// @param amount amount to withdraw - function withdrawFunds(address recipient, address token, uint256 amount) external; + function withdraw(bytes calldata txID, address originCaller, address to, uint256 amount) external payable; - /// @notice Refund (revert) path controlled by TSS (e.g., failed universal/bridge). - /// @dev Sends funds to revertCFG.fundRecipient using same rules as withdraw. - /// @param token address(0) for native; ERC20 otherwise - /// @param amount amount to refund - /// @param revertCFG (fundRecipient, revertMsg) - function revertWithdrawFunds(address token, uint256 amount, RevertInstructions calldata revertCFG) external; + function withdrawTokens(bytes calldata txID, address originCaller, address token, address to, uint256 amount) external; } diff --git a/contracts/evm-gateway/src/interfaces/IVault.sol b/contracts/evm-gateway/src/interfaces/IVault.sol index a0cf246..64b5a30 100644 --- a/contracts/evm-gateway/src/interfaces/IVault.sol +++ b/contracts/evm-gateway/src/interfaces/IVault.sol @@ -34,14 +34,13 @@ interface IVault { /// @param token Token address /// @param to Recipient address /// @param amount Amount of token - event VaultWithdraw(bytes32 indexed txID, address indexed originCaller, address indexed token, address to, uint256 amount); + event VaultWithdraw(bytes indexed txID, address indexed originCaller, address indexed token, address to, uint256 amount); /// @notice Vault revert event /// @param token Token address - /// @param to Recipient address + /// @param revertInstruction Recurrent instruction /// @param amount Amount of token - /// @param revertInstruction Revert instructions configuration - event VaultRevert(address indexed token, address indexed to, uint256 amount, RevertInstructions revertInstruction); + event VaultRevert(address indexed token, RevertInstructions indexed revertInstruction, uint256 amount); // ========================= // WITHDRAW @@ -55,7 +54,7 @@ interface IVault { * @param to recipient address on external chain * @param amount amount of token to transfer on external chain */ - function withdraw(bytes32 txID, address originCaller, address token, address to, uint256 amount) external; + function withdraw(bytes calldata txID, address originCaller, address token, address to, uint256 amount) external; /** * @notice TSS-only withdraw and execute transaction via gateway on external chains @@ -67,16 +66,16 @@ interface IVault { * @param amount token amount to transfer and use in execution on external chain * @param data calldata for the target execution on external chain */ - function withdrawAndExecute(bytes32 txID, address originCaller, address token, address target, uint256 amount, bytes calldata data) external; + function withdrawAndExecute(bytes calldata txID, address originCaller, address token, address target, uint256 amount, bytes calldata data) external; /** * @notice TSS-only refund path (e.g., failed outbound flow) to a designated recipient on external chains * @dev Moves token to gateway contract and then transfers to recipient or executes the payload. - * @param txID unique transaction identifier (for replay protection) - * @param token ERC20 token to refund (must be supported) on external chain - * @param to recipient of the refund on external chain - * @param amount amount to refund on external chain + * @param txID unique transaction identifier (for replay protection) + * @param token ERC20 token to refund (must be supported) on external chain + * @param amount amount to refund on external chain + * @param revertInstruction revert instruction containing revertRecipient and revertMsg */ - function revertWithdraw(bytes32 txID, address token, address to, uint256 amount, RevertInstructions calldata revertInstruction) external; + function revertWithdraw(bytes calldata txID, address token, uint256 amount, RevertInstructions calldata revertInstruction) external; } diff --git a/contracts/evm-gateway/src/libraries/Errors.sol b/contracts/evm-gateway/src/libraries/Errors.sol index 69b4aa4..987766b 100644 --- a/contracts/evm-gateway/src/libraries/Errors.sol +++ b/contracts/evm-gateway/src/libraries/Errors.sol @@ -24,6 +24,8 @@ library Errors { error RateLimitExceeded(); error BlockCapLimitExceeded(); error SlippageExceededOrExpired(); + error TokenNotSupported(); error TokenBurnFailed(address token, uint256 amount); error GasFeeTransferFailed(address token, address from, uint256 amount); + error RefundFailed(address recipient, uint256 amount); } diff --git a/contracts/evm-gateway/src/libraries/Types.sol b/contracts/evm-gateway/src/libraries/Types.sol index e69de02..ce914a2 100644 --- a/contracts/evm-gateway/src/libraries/Types.sol +++ b/contracts/evm-gateway/src/libraries/Types.sol @@ -19,9 +19,9 @@ enum TX_TYPE { struct RevertInstructions { /// where funds go in revert / refund cases - address fundRecipient; + address revertRecipient; /// arbitrary message for relayers/UEA - bytes revertContext; + bytes revertMsg; } /// @notice Packed per-token usage for the current epoch only (no on-chain history kept). @@ -48,3 +48,27 @@ struct UniversalPayload { uint256 deadline; // Timestamp after which this payload is invalid VerificationType vType; // Type of verification (signedVerification or universalTxVerification) } + +/// @notice Universal transaction request for native token as GAS +struct UniversalTxRequest { + address recipient; // address(0) => credit to UEA on Push + address token; // address(0) => native path (gas-only) + uint256 amount; // native amount or ERC20 amount + bytes payload; // call data / memo = UNIVERSAL PAYLOAD + address revertRecipient; // address to receive funds in case of revert + bytes signatureData; // signature data for further verification +} + +/// @notice Universal transaction request for ERC20 token as GAS +struct UniversalTokenTxRequest { + address recipient; // address(0) => credit to UEA on Push + address token; // address(0) => native path (gas-only) + uint256 amount; // native amount or ERC20 amount + address gasToken; // token used for paying GAS + uint256 gasAmount; // amount of the token to be used as GAS. + bytes payload; // call data / memo = UNIVERSAL PAYLOAD + address revertRecipient; // address to receive funds in case of revert + bytes signatureData; // signature data for further verification + uint256 amountOutMinETH; // minimum amount of ETH to receive + uint256 deadline; // timestamp after which this request is invalid +} \ No newline at end of file diff --git a/contracts/evm-gateway/test/BaseTest.t.sol b/contracts/evm-gateway/test/BaseTest.t.sol index d480228..094d0e2 100644 --- a/contracts/evm-gateway/test/BaseTest.t.sol +++ b/contracts/evm-gateway/test/BaseTest.t.sol @@ -10,7 +10,13 @@ import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/trans import { UniversalGateway } from "../src/UniversalGateway.sol"; import { IUniversalGateway } from "../src/interfaces/IUniversalGateway.sol"; -import { TX_TYPE, RevertInstructions, UniversalPayload, VerificationType } from "../src/libraries/Types.sol"; +import { + TX_TYPE, + RevertInstructions, + UniversalPayload, + VerificationType, + UniversalTxRequest +} from "../src/libraries/Types.sol"; import { Errors } from "../src/libraries/Errors.sol"; import { MockERC20 } from "./mocks/MockERC20.sol"; import { MockWETH } from "./mocks/MockWETH.sol"; @@ -330,8 +336,8 @@ abstract contract BaseTest is Test { h = keccak256(abi.encode(p)); } - function revertCfg(address fundRecipient_) internal pure returns (RevertInstructions memory) { - return RevertInstructions({ fundRecipient: fundRecipient_, revertContext: bytes("") }); + function revertCfg(address revertRecipient_) internal pure returns (RevertInstructions memory) { + return RevertInstructions({ revertRecipient: revertRecipient_, revertMsg: bytes("") }); } /// @notice Build a default payload for testing (commonly used across test files) @@ -353,7 +359,84 @@ abstract contract BaseTest is Test { /// @notice Build default revert instructions for testing (commonly used across test files) /// @dev Returns revert instructions with a default recipient function buildDefaultRevertInstructions() internal pure returns (RevertInstructions memory) { - return RevertInstructions({ fundRecipient: address(0x456), revertContext: bytes("") }); + return RevertInstructions({ revertRecipient: address(0x456), revertMsg: bytes("") }); + } + + // ========================= + // UNIVERSAL TX REQUEST BUILDERS + // ========================= + + /// @notice Build a UniversalTxRequest for GAS_AND_PAYLOAD transactions + /// @dev Creates a request that routes to TX_TYPE.GAS_AND_PAYLOAD + /// Requirements: recipient=address(0), token=address(0), amount=0, non-empty payload, msg.value>0 + /// @return UniversalTxRequest struct configured for GAS_AND_PAYLOAD route + function _buildGasTxRequest() internal view virtual returns (UniversalTxRequest memory) { + UniversalPayload memory payload = buildDefaultPayload(); + return UniversalTxRequest({ + recipient: address(0), // GAS routes always use address(0) for UEA credit + token: address(0), // Native token + amount: 0, // No funds (amount = 0) for GAS/GAS_AND_PAYLOAD routes + payload: abi.encode(payload), // Non-empty payload routes to GAS_AND_PAYLOAD + revertRecipient: address(0x456), + signatureData: bytes("") + }); + } + + /// @notice Build a UniversalTxRequest for FUNDS transactions + /// @param token Token address (address(0) for native, or ERC20 token address) + /// @param amount Amount of tokens to send + /// @return UniversalTxRequest struct configured for FUNDS route + function _buildFundsTxRequest(address token, uint256 amount) + internal + pure + virtual + returns (UniversalTxRequest memory) + { + return _buildFundsTxRequest(token, amount, address(0x456)); + } + + /// @notice Build a UniversalTxRequest for FUNDS transactions with custom fund recipient + /// @param token Token address (address(0) for native, or ERC20 token address) + /// @param amount Amount of tokens to send + /// @param revertRecipient Address to receive funds in case of revert + /// @return UniversalTxRequest struct configured for FUNDS route + function _buildFundsTxRequest(address token, uint256 amount, address revertRecipient) + internal + pure + virtual + returns (UniversalTxRequest memory) + { + return UniversalTxRequest({ + recipient: address(0), // FUNDS requires recipient == address(0) for UEA credit + token: token, + amount: amount, + payload: bytes(""), // Empty payload for FUNDS route + revertRecipient: revertRecipient, + signatureData: bytes("") + }); + } + + /// @notice Build a UniversalTxRequest for FUNDS_AND_PAYLOAD transactions + /// @dev Creates a request that routes to TX_TYPE.FUNDS_AND_PAYLOAD + /// Requirements: recipient=address(0), non-zero amount, non-empty payload + /// @param token Token address (address(0) for native, or ERC20 token address) + /// @param amount Amount of tokens to send + /// @param payload UniversalPayload to encode in the request + /// @return UniversalTxRequest struct configured for FUNDS_AND_PAYLOAD route + function _buildFundsAndPayloadTxRequest(address token, uint256 amount, UniversalPayload memory payload) + internal + pure + virtual + returns (UniversalTxRequest memory) + { + return UniversalTxRequest({ + recipient: address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) for UEA credit + token: token, + amount: amount, + payload: abi.encode(payload), // Non-empty payload required for FUNDS_AND_PAYLOAD + revertRecipient: address(0x456), + signatureData: bytes("") + }); } // ========================= @@ -470,7 +553,7 @@ abstract contract BaseTest is Test { vType: VerificationType(0) }); - RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: to, revertContext: bytes("") }); + RevertInstructions memory revertCfg_ = RevertInstructions({ revertRecipient: to, revertMsg: bytes("") }); return (payload, revertCfg_); } diff --git a/contracts/evm-gateway/test/coverage/src/UniversalGateway.sol.gcov.html b/contracts/evm-gateway/test/coverage/src/UniversalGateway.sol.gcov.html index 07dc325..71188e4 100644 --- a/contracts/evm-gateway/test/coverage/src/UniversalGateway.sol.gcov.html +++ b/contracts/evm-gateway/test/coverage/src/UniversalGateway.sol.gcov.html @@ -392,7 +392,7 @@ 321 : : RevertSettings calldata _revertCFG, 322 : : TX_TYPE _txType 323 : : ) internal { - 324 [ + ]: 31 : if (_revertCFG.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + 324 [ + ]: 31 : if (_revertCFG.revertRecipient == address(0)) revert Errors.InvalidRecipient(); 325 : : 326 : 30 : emit TxWithGas({ 327 : : sender: _caller, @@ -533,7 +533,7 @@ 462 : : RevertSettings calldata _revertCFG, 463 : : TX_TYPE _txType 464 : : ) internal { - 465 [ # ]: 25 : if (_revertCFG.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + 465 [ # ]: 25 : if (_revertCFG.revertRecipient == address(0)) revert Errors.InvalidRecipient(); 466 : : /// for recipient == address(0), the funds are being moved to UEA of the msg.sender on Push Chain. 467 [ + ]: 25 : if (_recipient == address(0)){ 468 : : if ( @@ -583,16 +583,16 @@ 512 : : uint256 amount, 513 : : RevertSettings calldata revertCFG 514 : : ) external nonReentrant whenNotPaused onlyTSS { - 515 [ + ]: 7 : if (revertCFG.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + 515 [ + ]: 7 : if (revertCFG.revertRecipient == address(0)) revert Errors.InvalidRecipient(); 516 [ + ]: 6 : if (amount == 0) revert Errors.InvalidAmount(); 517 : : 518 [ + + ]: 5 : if (token == address(0)) { - 519 : 3 : _handleNativeWithdraw(revertCFG.fundRecipient, amount); + 519 : 3 : _handleNativeWithdraw(revertCFG.revertRecipient, amount); 520 : : } else { - 521 : 2 : _handleTokenWithdraw(token, revertCFG.fundRecipient, amount); + 521 : 2 : _handleTokenWithdraw(token, revertCFG.revertRecipient, amount); 522 : : } 523 : : - 524 : 5 : emit WithdrawFunds(revertCFG.fundRecipient, amount, token); + 524 : 5 : emit WithdrawFunds(revertCFG.revertRecipient, amount, token); 525 : : } 526 : : 527 : : // ========================= diff --git a/contracts/evm-gateway/test/coverage/src/UniversalGatewayV0.sol.gcov.html b/contracts/evm-gateway/test/coverage/src/UniversalGatewayV0.sol.gcov.html index 05b5442..2019eea 100644 --- a/contracts/evm-gateway/test/coverage/src/UniversalGatewayV0.sol.gcov.html +++ b/contracts/evm-gateway/test/coverage/src/UniversalGatewayV0.sol.gcov.html @@ -469,7 +469,7 @@ 398 : : RevertSettings calldata _revertCFG, 399 : : TX_TYPE _txType 400 : : ) internal { - 401 [ # ]: 0 : if (_revertCFG.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + 401 [ # ]: 0 : if (_revertCFG.revertRecipient == address(0)) revert Errors.InvalidRecipient(); 402 : : 403 : 0 : emit TxWithGas({ 404 : : sender: _caller, @@ -600,7 +600,7 @@ 529 : : TX_TYPE _txType, 530 : : bytes memory _signatureData 531 : : ) internal { - 532 [ # ]: 0 : if (_revertCFG.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + 532 [ # ]: 0 : if (_revertCFG.revertRecipient == address(0)) revert Errors.InvalidRecipient(); 533 : : /// for recipient == address(0), the funds are being moved to UEA of the msg.sender on Push Chain. 534 [ # ]: 0 : if (_recipient == address(0)){ 535 : : if ( @@ -651,16 +651,16 @@ 580 : : uint256 amount, 581 : : RevertSettings calldata revertCFG 582 : : ) external nonReentrant whenNotPaused onlyTSS { - 583 [ # ]: 0 : if (revertCFG.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + 583 [ # ]: 0 : if (revertCFG.revertRecipient == address(0)) revert Errors.InvalidRecipient(); 584 [ # ]: 0 : if (amount == 0) revert Errors.InvalidAmount(); 585 : : 586 [ # # ]: 0 : if (token == address(0)) { - 587 : 0 : _handleNativeWithdraw(revertCFG.fundRecipient, amount); + 587 : 0 : _handleNativeWithdraw(revertCFG.revertRecipient, amount); 588 : : } else { - 589 : 0 : _handleTokenWithdraw(token, revertCFG.fundRecipient, amount); + 589 : 0 : _handleTokenWithdraw(token, revertCFG.revertRecipient, amount); 590 : : } 591 : : - 592 : 0 : emit WithdrawFunds(revertCFG.fundRecipient, amount, token); + 592 : 0 : emit WithdrawFunds(revertCFG.revertRecipient, amount, token); 593 : : } 594 : : 595 : : // ========================= diff --git a/contracts/evm-gateway/test/coverage/test/BaseTest.t.sol.gcov.html b/contracts/evm-gateway/test/coverage/test/BaseTest.t.sol.gcov.html index 1930ddd..8d08957 100644 --- a/contracts/evm-gateway/test/coverage/test/BaseTest.t.sol.gcov.html +++ b/contracts/evm-gateway/test/coverage/test/BaseTest.t.sol.gcov.html @@ -395,9 +395,9 @@ 324 : 14 : h = keccak256(abi.encode(p)); 325 : : } 326 : : - 327 : 29 : function revertCfg(address fundRecipient_) internal pure returns (RevertSettings memory) { + 327 : 29 : function revertCfg(address revertRecipient_) internal pure returns (RevertSettings memory) { 328 : 29 : return RevertSettings({ - 329 : : fundRecipient: fundRecipient_, + 329 : : revertRecipient: revertRecipient_, 330 : : revertMsg: "" 331 : : }); 332 : : } @@ -513,7 +513,7 @@ 442 : : }); 443 : : 444 : 4 : RevertSettings memory revertCfg_ = RevertSettings({ - 445 : : fundRecipient: to, + 445 : : revertRecipient: to, 446 : : revertMsg: "" 447 : : }); 448 : : diff --git a/contracts/evm-gateway/test/gateway/4_GatewayTSSFunctions.t.sol b/contracts/evm-gateway/test/gateway/10_withdrawTokens.t.sol similarity index 75% rename from contracts/evm-gateway/test/gateway/4_GatewayTSSFunctions.t.sol rename to contracts/evm-gateway/test/gateway/10_withdrawTokens.t.sol index a4f246d..d947c9a 100644 --- a/contracts/evm-gateway/test/gateway/4_GatewayTSSFunctions.t.sol +++ b/contracts/evm-gateway/test/gateway/10_withdrawTokens.t.sol @@ -9,8 +9,8 @@ import { RevertInstructions, UniversalPayload, TX_TYPE } from "../../src/librari import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { MockERC20 } from "../mocks/MockERC20.sol"; -/// @notice Test suite for missing TSS functions and sendTxWithFunds 4-parameter version -/// @dev Tests revertNative, revertTokens, onlyTSS modifier, and sendTxWithFunds overload +/// @notice Test suite for TSS withdrawal functions (revertUniversalTx, revertUniversalTxToken, withdraw) +/// @dev Tests revertNative, revertTokens, onlyTSS modifier, and withdrawal functionality contract GatewayTSSFunctionsTest is BaseTest { // ========================= // SETUP @@ -25,11 +25,11 @@ contract GatewayTSSFunctionsTest is BaseTest { address[] memory tokens = new address[](2); tokens[0] = address(usdc); tokens[1] = address(tokenA); - + uint256[] memory thresholds = new uint256[](2); - thresholds[0] = 1000000e6; // 1M USDC + thresholds[0] = 1000000e6; // 1M USDC thresholds[1] = 1000000e18; // 1M tokenA - + vm.prank(admin); gateway.setTokenLimitThresholds(tokens, thresholds); @@ -44,7 +44,7 @@ contract GatewayTSSFunctionsTest is BaseTest { function testOnlyTSS_NonTSSShouldRevert() public { // Non-TSS user should not be able to call TSS functions - bytes32 txID = bytes32(uint256(1)); + bytes memory txID = abi.encodePacked(bytes32(uint256(1))); vm.prank(user1); vm.expectRevert(abi.encodeWithSelector(Errors.WithdrawFailed.selector)); gateway.revertUniversalTx(txID, 1 ether, RevertInstructions(user1, "")); @@ -52,12 +52,12 @@ contract GatewayTSSFunctionsTest is BaseTest { function testOnlyTSS_TSSShouldSucceed() public { // TSS should be able to call TSS functions - bytes32 txID = bytes32(uint256(2)); + bytes memory txID = abi.encodePacked(bytes32(uint256(2))); uint256 initialBalance = user1.balance; vm.deal(tss, 1 ether); vm.prank(tss); - gateway.revertUniversalTx{value: 1 ether}(txID, 1 ether, RevertInstructions(user1, "")); + gateway.revertUniversalTx{ value: 1 ether }(txID, 1 ether, RevertInstructions(user1, "")); assertEq(user1.balance, initialBalance + 1 ether); } @@ -67,7 +67,7 @@ contract GatewayTSSFunctionsTest is BaseTest { // ========================= function testWithdrawFunds_NativeETH_Success() public { - bytes32 txID = bytes32(uint256(3)); + bytes memory txID = abi.encodePacked(bytes32(uint256(3))); uint256 withdrawAmount = 2 ether; uint256 initialRecipientBalance = user1.balance; @@ -77,21 +77,23 @@ contract GatewayTSSFunctionsTest is BaseTest { vm.deal(tss, withdrawAmount); vm.prank(tss); - gateway.revertUniversalTx{value: withdrawAmount}(txID, withdrawAmount, RevertInstructions(user1, "")); + gateway.revertUniversalTx{ value: withdrawAmount }(txID, withdrawAmount, RevertInstructions(user1, "")); // Check balances assertEq(user1.balance, initialRecipientBalance + withdrawAmount); } function testWithdrawFunds_ERC20Token_Success() public { - bytes32 txID = bytes32(uint256(4)); + bytes memory txID = abi.encodePacked(bytes32(uint256(4))); uint256 withdrawAmount = 100e6; // 100 USDC uint256 initialGatewayBalance = usdc.balanceOf(address(gateway)); uint256 initialRecipientBalance = usdc.balanceOf(user1); // Expect RevertUniversalTx event vm.expectEmit(true, true, true, true); - emit IUniversalGateway.RevertUniversalTx(txID, user1, address(usdc), withdrawAmount, RevertInstructions(user1, "")); + emit IUniversalGateway.RevertUniversalTx( + txID, user1, address(usdc), withdrawAmount, RevertInstructions(user1, "") + ); // revertUniversalTxToken requires VAULT_ROLE (test contract has this role) gateway.revertUniversalTxToken(txID, address(usdc), withdrawAmount, RevertInstructions(user1, "")); @@ -102,32 +104,32 @@ contract GatewayTSSFunctionsTest is BaseTest { } function testWithdrawFunds_InvalidRecipient_Revert() public { - bytes32 txID = bytes32(uint256(5)); + bytes memory txID = abi.encodePacked(bytes32(uint256(5))); vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidRecipient.selector)); gateway.revertUniversalTx(txID, 1 ether, RevertInstructions(address(0), "")); } function testWithdrawFunds_InvalidAmount_Revert() public { - bytes32 txID = bytes32(uint256(6)); + bytes memory txID = abi.encodePacked(bytes32(uint256(6))); vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); gateway.revertUniversalTx(txID, 0, RevertInstructions(user1, "")); } function testWithdrawFunds_InsufficientBalance_Revert() public { - bytes32 txID = bytes32(uint256(7)); + bytes memory txID = abi.encodePacked(bytes32(uint256(7))); uint256 amount = 1 ether; uint256 wrongValue = 0.5 ether; vm.deal(tss, wrongValue); vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); - gateway.revertUniversalTx{value: wrongValue}(txID, amount, RevertInstructions(user1, "")); + gateway.revertUniversalTx{ value: wrongValue }(txID, amount, RevertInstructions(user1, "")); } function testWithdrawFunds_ERC20InsufficientBalance_Revert() public { - bytes32 txID = bytes32(uint256(8)); + bytes memory txID = abi.encodePacked(bytes32(uint256(8))); uint256 excessiveAmount = usdc.balanceOf(address(gateway)) + 1; vm.prank(tss); @@ -140,7 +142,7 @@ contract GatewayTSSFunctionsTest is BaseTest { // ========================= function testRevertWithdrawFunds_NativeETH_Success() public { - bytes32 txID = bytes32(uint256(9)); + bytes memory txID = abi.encodePacked(bytes32(uint256(9))); uint256 withdrawAmount = 1.5 ether; uint256 initialRecipientBalance = user1.balance; @@ -152,14 +154,14 @@ contract GatewayTSSFunctionsTest is BaseTest { vm.deal(tss, withdrawAmount); vm.prank(tss); - gateway.revertUniversalTx{value: withdrawAmount}(txID, withdrawAmount, revertCfg); + gateway.revertUniversalTx{ value: withdrawAmount }(txID, withdrawAmount, revertCfg); // Check balances assertEq(user1.balance, initialRecipientBalance + withdrawAmount); } function testRevertWithdrawFunds_ERC20Token_Success() public { - bytes32 txID = bytes32(uint256(10)); + bytes memory txID = abi.encodePacked(bytes32(uint256(10))); uint256 withdrawAmount = 200e6; // 200 USDC uint256 initialGatewayBalance = usdc.balanceOf(address(gateway)); uint256 initialRecipientBalance = usdc.balanceOf(user1); @@ -179,7 +181,7 @@ contract GatewayTSSFunctionsTest is BaseTest { } function testRevertWithdrawFunds_InvalidRecipient_Revert() public { - bytes32 txID = bytes32(uint256(11)); + bytes memory txID = abi.encodePacked(bytes32(uint256(11))); RevertInstructions memory revertCfg = revertCfg(address(0)); // Invalid user1 vm.prank(tss); @@ -188,7 +190,7 @@ contract GatewayTSSFunctionsTest is BaseTest { } function testRevertWithdrawFunds_InvalidAmount_Revert() public { - bytes32 txID = bytes32(uint256(12)); + bytes memory txID = abi.encodePacked(bytes32(uint256(12))); RevertInstructions memory revertCfg = revertCfg(user1); vm.prank(tss); @@ -197,7 +199,7 @@ contract GatewayTSSFunctionsTest is BaseTest { } function testRevertWithdrawFunds_InsufficientBalance_Revert() public { - bytes32 txID = bytes32(uint256(13)); + bytes memory txID = abi.encodePacked(bytes32(uint256(13))); uint256 amount = 1 ether; uint256 wrongValue = 0.8 ether; RevertInstructions memory revertCfg = revertCfg(user1); @@ -205,7 +207,7 @@ contract GatewayTSSFunctionsTest is BaseTest { vm.deal(tss, wrongValue); vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); - gateway.revertUniversalTx{value: wrongValue}(txID, amount, revertCfg); + gateway.revertUniversalTx{ value: wrongValue }(txID, amount, revertCfg); } // ========================= @@ -213,7 +215,7 @@ contract GatewayTSSFunctionsTest is BaseTest { // ========================= function testWithdrawFunds_WhenPaused_Revert() public { - bytes32 txID = bytes32(uint256(14)); + bytes memory txID = abi.encodePacked(bytes32(uint256(14))); // Pause the contract vm.prank(admin); gateway.pause(); @@ -224,7 +226,7 @@ contract GatewayTSSFunctionsTest is BaseTest { } function testRevertWithdrawFunds_WhenPaused_Revert() public { - bytes32 txID = bytes32(uint256(15)); + bytes memory txID = abi.encodePacked(bytes32(uint256(15))); // Pause the contract vm.prank(admin); gateway.pause(); @@ -237,13 +239,13 @@ contract GatewayTSSFunctionsTest is BaseTest { } function testWithdrawFunds_ReentrancyProtection() public { - bytes32 txID = bytes32(uint256(16)); + bytes memory txID = abi.encodePacked(bytes32(uint256(16))); // This test ensures the nonReentrant modifier is working // We can't easily test reentrancy without a malicious contract, // but the modifier is there and will be covered by the test execution vm.deal(tss, 1 ether); vm.prank(tss); - gateway.revertUniversalTx{value: 1 ether}(txID, 1 ether, RevertInstructions(user1, "")); + gateway.revertUniversalTx{ value: 1 ether }(txID, 1 ether, RevertInstructions(user1, "")); // If we get here without reverting, the reentrancy protection is working assertTrue(true); @@ -257,19 +259,25 @@ contract GatewayTSSFunctionsTest is BaseTest { // Withdraw USDC (requires VAULT_ROLE) uint256 initialUsdcBalance = usdc.balanceOf(user1); - gateway.revertUniversalTxToken(bytes32(uint256(17)), address(usdc), usdcAmount, RevertInstructions(user1, "")); + gateway.revertUniversalTxToken( + abi.encodePacked(bytes32(uint256(17))), address(usdc), usdcAmount, RevertInstructions(user1, "") + ); assertEq(usdc.balanceOf(user1), initialUsdcBalance + usdcAmount); // Withdraw TokenA (requires VAULT_ROLE) uint256 initialTokenABalance = tokenA.balanceOf(user1); - gateway.revertUniversalTxToken(bytes32(uint256(18)), address(tokenA), tokenAAmount, RevertInstructions(user1, "")); + gateway.revertUniversalTxToken( + abi.encodePacked(bytes32(uint256(18))), address(tokenA), tokenAAmount, RevertInstructions(user1, "") + ); assertEq(tokenA.balanceOf(user1), initialTokenABalance + tokenAAmount); // Withdraw ETH (requires TSS_ROLE) uint256 initialEthBalance = user1.balance; vm.deal(tss, ethAmount); vm.prank(tss); - gateway.revertUniversalTx{value: ethAmount}(bytes32(uint256(19)), ethAmount, RevertInstructions(user1, "")); + gateway.revertUniversalTx{ value: ethAmount }( + abi.encodePacked(bytes32(uint256(19))), ethAmount, RevertInstructions(user1, "") + ); assertEq(user1.balance, initialEthBalance + ethAmount); } @@ -282,14 +290,14 @@ contract GatewayTSSFunctionsTest is BaseTest { // Revert USDC (requires VAULT_ROLE) uint256 initialUsdcBalance = usdc.balanceOf(user1); - gateway.revertUniversalTxToken(bytes32(uint256(20)), address(usdc), usdcAmount, revertCfg); + gateway.revertUniversalTxToken(abi.encodePacked(bytes32(uint256(20))), address(usdc), usdcAmount, revertCfg); assertEq(usdc.balanceOf(user1), initialUsdcBalance + usdcAmount); // Revert ETH (requires TSS_ROLE) uint256 initialEthBalance = user1.balance; vm.deal(tss, ethAmount); vm.prank(tss); - gateway.revertUniversalTx{value: ethAmount}(bytes32(uint256(21)), ethAmount, revertCfg); + gateway.revertUniversalTx{ value: ethAmount }(abi.encodePacked(bytes32(uint256(21))), ethAmount, revertCfg); assertEq(user1.balance, initialEthBalance + ethAmount); } @@ -298,28 +306,28 @@ contract GatewayTSSFunctionsTest is BaseTest { // ========================= function testRevertUniversalTx_ReplayProtection_Native() public { - bytes32 txID = bytes32(uint256(22)); + bytes memory txID = abi.encodePacked(bytes32(uint256(22))); uint256 amount = 1 ether; - + // First call should succeed vm.deal(tss, amount); vm.prank(tss); - gateway.revertUniversalTx{value: amount}(txID, amount, RevertInstructions(user1, "")); - + gateway.revertUniversalTx{ value: amount }(txID, amount, RevertInstructions(user1, "")); + // Second call with same txID should revert vm.deal(tss, amount); vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.PayloadExecuted.selector)); - gateway.revertUniversalTx{value: amount}(txID, amount, RevertInstructions(user1, "")); + gateway.revertUniversalTx{ value: amount }(txID, amount, RevertInstructions(user1, "")); } function testRevertUniversalTxToken_ReplayProtection() public { - bytes32 txID = bytes32(uint256(23)); + bytes memory txID = abi.encodePacked(bytes32(uint256(23))); uint256 amount = 100e6; - + // First call should succeed gateway.revertUniversalTxToken(txID, address(usdc), amount, RevertInstructions(user1, "")); - + // Second call with same txID should revert vm.expectRevert(abi.encodeWithSelector(Errors.PayloadExecuted.selector)); gateway.revertUniversalTxToken(txID, address(usdc), amount, RevertInstructions(user1, "")); @@ -330,128 +338,128 @@ contract GatewayTSSFunctionsTest is BaseTest { // ========================= function testWithdraw_Native_Success() public { - bytes32 txID = bytes32(uint256(24)); + bytes memory txID = abi.encodePacked(bytes32(uint256(24))); uint256 amount = 5 ether; address originCaller = user2; - + uint256 initialBalance = user1.balance; - + vm.expectEmit(true, true, true, true); emit IUniversalGateway.WithdrawToken(txID, originCaller, address(0), user1, amount); - + vm.deal(tss, amount); vm.prank(tss); - gateway.withdraw{value: amount}(txID, originCaller, user1, amount); - + gateway.withdraw{ value: amount }(txID, originCaller, user1, amount); + assertEq(user1.balance, initialBalance + amount); } function testWithdraw_Native_OnlyTSS() public { - bytes32 txID = bytes32(uint256(25)); + bytes memory txID = abi.encodePacked(bytes32(uint256(25))); uint256 amount = 1 ether; - + vm.deal(user1, amount); vm.prank(user1); vm.expectRevert(abi.encodeWithSelector(Errors.WithdrawFailed.selector)); - gateway.withdraw{value: amount}(txID, user1, user2, amount); + gateway.withdraw{ value: amount }(txID, user1, user2, amount); } function testWithdraw_Native_ReplayProtection() public { - bytes32 txID = bytes32(uint256(26)); + bytes memory txID = abi.encodePacked(bytes32(uint256(26))); uint256 amount = 2 ether; - + // First call should succeed vm.deal(tss, amount); vm.prank(tss); - gateway.withdraw{value: amount}(txID, user1, user2, amount); - + gateway.withdraw{ value: amount }(txID, user1, user2, amount); + // Second call with same txID should revert vm.deal(tss, amount); vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.PayloadExecuted.selector)); - gateway.withdraw{value: amount}(txID, user1, user2, amount); + gateway.withdraw{ value: amount }(txID, user1, user2, amount); } function testWithdraw_Native_ZeroRecipient_Reverts() public { - bytes32 txID = bytes32(uint256(27)); + bytes memory txID = abi.encodePacked(bytes32(uint256(27))); uint256 amount = 1 ether; - + vm.deal(tss, amount); vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); - gateway.withdraw{value: amount}(txID, user1, address(0), amount); + gateway.withdraw{ value: amount }(txID, user1, address(0), amount); } function testWithdraw_Native_ZeroOriginCaller_Reverts() public { - bytes32 txID = bytes32(uint256(28)); + bytes memory txID = abi.encodePacked(bytes32(uint256(28))); uint256 amount = 1 ether; - + vm.deal(tss, amount); vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); - gateway.withdraw{value: amount}(txID, address(0), user1, amount); + gateway.withdraw{ value: amount }(txID, address(0), user1, amount); } function testWithdraw_Native_ZeroAmount_Reverts() public { - bytes32 txID = bytes32(uint256(29)); - + bytes memory txID = abi.encodePacked(bytes32(uint256(29))); + vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); gateway.withdraw(txID, user1, user2, 0); } function testWithdraw_Native_AmountMismatch_Reverts() public { - bytes32 txID = bytes32(uint256(30)); + bytes memory txID = abi.encodePacked(bytes32(uint256(30))); uint256 amount = 1 ether; uint256 wrongValue = 0.5 ether; - + vm.deal(tss, wrongValue); vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); - gateway.withdraw{value: wrongValue}(txID, user1, user2, amount); + gateway.withdraw{ value: wrongValue }(txID, user1, user2, amount); } function testWithdraw_Native_WhenPaused_Reverts() public { - bytes32 txID = bytes32(uint256(31)); + bytes memory txID = abi.encodePacked(bytes32(uint256(31))); uint256 amount = 1 ether; - + vm.prank(admin); gateway.pause(); - + vm.deal(tss, amount); vm.prank(tss); vm.expectRevert(); - gateway.withdraw{value: amount}(txID, user1, user2, amount); + gateway.withdraw{ value: amount }(txID, user1, user2, amount); } function testWithdraw_Native_EmitsCorrectEvent() public { - bytes32 txID = bytes32(uint256(32)); + bytes memory txID = abi.encodePacked(bytes32(uint256(32))); uint256 amount = 3 ether; address originCaller = user2; address recipient = user1; - + vm.expectEmit(true, true, true, true); emit IUniversalGateway.WithdrawToken(txID, originCaller, address(0), recipient, amount); - + vm.deal(tss, amount); vm.prank(tss); - gateway.withdraw{value: amount}(txID, originCaller, recipient, amount); + gateway.withdraw{ value: amount }(txID, originCaller, recipient, amount); } function testWithdraw_Native_MultipleSequentialWithdrawals() public { uint256 amount1 = 1 ether; uint256 amount2 = 2 ether; - + uint256 initialBalance = user1.balance; - + vm.deal(tss, amount1); vm.prank(tss); - gateway.withdraw{value: amount1}(bytes32(uint256(33)), user2, user1, amount1); - + gateway.withdraw{ value: amount1 }(abi.encodePacked(bytes32(uint256(33))), user2, user1, amount1); + vm.deal(tss, amount2); vm.prank(tss); - gateway.withdraw{value: amount2}(bytes32(uint256(34)), user2, user1, amount2); - + gateway.withdraw{ value: amount2 }(abi.encodePacked(bytes32(uint256(34))), user2, user1, amount2); + assertEq(user1.balance, initialBalance + amount1 + amount2); } } diff --git a/contracts/evm-gateway/test/gateway/7_GatewayExecuteUniversalTx.t.sol b/contracts/evm-gateway/test/gateway/11_executeUniversalTx.t.sol similarity index 53% rename from contracts/evm-gateway/test/gateway/7_GatewayExecuteUniversalTx.t.sol rename to contracts/evm-gateway/test/gateway/11_executeUniversalTx.t.sol index 243c041..c5cf44e 100644 --- a/contracts/evm-gateway/test/gateway/7_GatewayExecuteUniversalTx.t.sol +++ b/contracts/evm-gateway/test/gateway/11_executeUniversalTx.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.26; import { Test } from "forge-std/Test.sol"; import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { IUniversalGateway } from "../../src/interfaces/IUniversalGateway.sol"; import { Errors } from "../../src/libraries/Errors.sol"; import { MockERC20 } from "../mocks/MockERC20.sol"; import { MockTarget } from "../mocks/MockTarget.sol"; @@ -24,8 +25,8 @@ contract GatewayExecuteUniversalTxTest is Test { address public user = address(0x4); address public targetAddress = address(0x5); - bytes32 public constant TX_ID = keccak256("test-tx-id"); - bytes32 public constant DUPLICATE_TX_ID = keccak256("duplicate-tx-id"); + bytes public constant TX_ID = abi.encodePacked(keccak256("test-tx-id")); + bytes public constant DUPLICATE_TX_ID = abi.encodePacked(keccak256("duplicate-tx-id")); address public constant ORIGIN_CALLER = address(0x6); address public constant ZERO_ADDRESS = address(0); uint256 public constant AMOUNT = 1000e18; @@ -33,8 +34,8 @@ contract GatewayExecuteUniversalTxTest is Test { bytes public constant EMPTY_PAYLOAD = ""; event UniversalTxExecuted( - bytes32 indexed txID, - address indexed originCaller, + bytes indexed txID, + address indexed ueaAddress, address indexed target, address token, uint256 amount, @@ -62,6 +63,11 @@ contract GatewayExecuteUniversalTxTest is Test { address(0x123) // weth (dummy address for testing) ); + // Grant TSS_ROLE to test contract for native token executeUniversalTx tests + vm.startPrank(admin); + gateway.grantRole(gateway.TSS_ROLE(), address(this)); + vm.stopPrank(); + // Set up token support address[] memory tokens = new address[](3); uint256[] memory thresholds = new uint256[](3); @@ -84,7 +90,7 @@ contract GatewayExecuteUniversalTxTest is Test { // executeUniversalTx Tests // ========================= - function testExecuteUniversalTx_NotTSS_Reverts() public { + function testExecuteUniversalTx_NotVault_Reverts() public { vm.expectRevert(); vm.prank(user); gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); @@ -95,7 +101,7 @@ contract GatewayExecuteUniversalTxTest is Test { gateway.pause(); vm.expectRevert(); - vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + // ERC20 token executeUniversalTx requires VAULT_ROLE (test contract has this role) gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); } @@ -105,7 +111,7 @@ contract GatewayExecuteUniversalTxTest is Test { gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); } // Input Validation Tests @@ -121,7 +127,7 @@ contract GatewayExecuteUniversalTxTest is Test { gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); } - function testExecuteUniversalTx_ZeroOriginCaller_Reverts() public { + function testExecuteUniversalTx_ZeroueaAddress_Reverts() public { // Transfer tokens from Vault (test contract) to Gateway token.transfer(address(gateway), AMOUNT); @@ -151,7 +157,7 @@ contract GatewayExecuteUniversalTxTest is Test { gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, EMPTY_PAYLOAD); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); // Verify the target received the call assertEq(target.lastCaller(), address(gateway)); } @@ -160,42 +166,36 @@ contract GatewayExecuteUniversalTxTest is Test { function testExecuteUniversalTx_NativePath_Success() public { uint256 initialBalance = address(target).balance; - vm.deal(tss, AMOUNT); + vm.deal(address(this), AMOUNT); vm.expectEmit(true, true, true, false); emit UniversalTxExecuted(TX_ID, ORIGIN_CALLER, address(target), ZERO_ADDRESS, AMOUNT, PAYLOAD); - vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE - gateway.executeUniversalTx{ value: AMOUNT }( - TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD - ); + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); assertEq(address(target).balance, initialBalance + AMOUNT); } function testExecuteUniversalTx_NativePath_WrongValue_Reverts() public { - vm.deal(tss, AMOUNT - 1); - vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + vm.deal(address(this), AMOUNT - 1); + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); - gateway.executeUniversalTx{ value: AMOUNT - 1 }( - TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD - ); + gateway.executeUniversalTx{ value: AMOUNT - 1 }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); - vm.deal(tss, AMOUNT + 1); - vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + vm.deal(address(this), AMOUNT + 1); + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); - gateway.executeUniversalTx{ value: AMOUNT + 1 }( - TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD - ); + gateway.executeUniversalTx{ value: AMOUNT + 1 }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); } function testExecuteUniversalTx_NativePath_NonPayableTarget_Reverts() public { - vm.deal(tss, AMOUNT); + vm.deal(address(this), AMOUNT); bytes memory nonPayablePayload = abi.encodeWithSignature("receiveFundsNonPayable()"); - vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); gateway.executeUniversalTx{ value: AMOUNT }( TX_ID, ORIGIN_CALLER, address(revertingTarget), AMOUNT, nonPayablePayload @@ -203,38 +203,54 @@ contract GatewayExecuteUniversalTxTest is Test { } function testExecuteUniversalTx_NativePath_TargetReverts_Reverts() public { - vm.deal(tss, AMOUNT); + vm.deal(address(this), AMOUNT); - vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); - gateway.executeUniversalTx{ value: AMOUNT }( - TX_ID, ORIGIN_CALLER, address(revertingTarget), AMOUNT, PAYLOAD - ); + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(revertingTarget), AMOUNT, PAYLOAD); - assertFalse(gateway.isExecuted(TX_ID)); + assertFalse(gateway.isExecuted(keccak256(TX_ID))); } function testExecuteUniversalTx_NativePath_GasExhaustion_Reverts() public { - vm.deal(tss, AMOUNT); + vm.deal(address(this), AMOUNT); bytes memory gasHeavyPayload = abi.encodeWithSignature("receiveFundsGasHeavy()"); - vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); gateway.executeUniversalTx{ value: AMOUNT, gas: 3000000 }( TX_ID, ORIGIN_CALLER, address(revertingTarget), AMOUNT, gasHeavyPayload ); - assertFalse(gateway.isExecuted(TX_ID)); + assertFalse(gateway.isExecuted(keccak256(TX_ID))); } // ERC-20 Path Tests function testExecuteUniversalTx_ERC20Path_WithValue_Reverts() public { - vm.deal(tss, 1); + // ERC20 path should not accept msg.value + // Transfer tokens to gateway first + token.transfer(address(gateway), AMOUNT); - vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE - vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); - gateway.executeUniversalTx{ value: 1 }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); + vm.deal(address(this), 1); + // ERC20 executeUniversalTx is non-payable, so calling with value will fail + // Use low-level call to bypass Solidity's compile-time check + // Use the ERC20 overload signature: executeUniversalTx(bytes32,address,address,address,uint256,bytes) + (bool success,) = address(gateway).call{ value: 1 }( + abi.encodeWithSignature( + "executeUniversalTx(bytes32,address,address,address,uint256,bytes)", + TX_ID, + ORIGIN_CALLER, + address(token), + address(target), + AMOUNT, + PAYLOAD + ) + ); + // Non-payable function called with value should fail + assertFalse(success); + // Verify txID was not executed + assertFalse(gateway.isExecuted(keccak256(TX_ID))); } function testExecuteUniversalTx_ERC20Path_InsufficientBalance_Reverts() public { @@ -261,7 +277,7 @@ contract GatewayExecuteUniversalTxTest is Test { // Execute as Vault (this contract has VAULT_ROLE) gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, tokenPayload); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); assertEq(token.balanceOf(address(target)), initialBalance + AMOUNT); } @@ -272,19 +288,19 @@ contract GatewayExecuteUniversalTxTest is Test { vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(revertingTarget), AMOUNT, PAYLOAD); - assertFalse(gateway.isExecuted(TX_ID)); + assertFalse(gateway.isExecuted(keccak256(TX_ID))); } function testExecuteUniversalTx_ERC20Path_SafeApproveFails_Reverts() public { approvalToken.setApprovalBehavior(MockTokenApprovalVariants.ApprovalBehavior.RETURN_FALSE); - + // Transfer tokens from Vault (test contract) to Gateway approvalToken.transfer(address(gateway), AMOUNT); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidData.selector)); gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, PAYLOAD); - assertFalse(gateway.isExecuted(TX_ID)); + assertFalse(gateway.isExecuted(keccak256(TX_ID))); } function testExecuteUniversalTx_ERC20Path_ResetApprovalFails_Reverts() public { @@ -304,7 +320,7 @@ contract GatewayExecuteUniversalTxTest is Test { gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, tokenPayload); // Transaction should not be executed - assertFalse(gateway.isExecuted(TX_ID)); + assertFalse(gateway.isExecuted(keccak256(TX_ID))); } // Reentrancy Tests @@ -320,14 +336,14 @@ contract GatewayExecuteUniversalTxTest is Test { } function testExecuteUniversalTx_StateUpdatedCorrectly() public { - assertFalse(gateway.isExecuted(TX_ID)); + assertFalse(gateway.isExecuted(keccak256(TX_ID))); // Transfer tokens from Vault (test contract) to Gateway token.transfer(address(gateway), AMOUNT); gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); } // ========================= @@ -376,7 +392,7 @@ contract GatewayExecuteUniversalTxTest is Test { gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, tokenPayload); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); assertEq(approvalToken.balanceOf(address(target)), AMOUNT); } @@ -396,7 +412,7 @@ contract GatewayExecuteUniversalTxTest is Test { gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, tokenPayload); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); assertEq(approvalToken.balanceOf(address(target)), AMOUNT); } @@ -417,7 +433,7 @@ contract GatewayExecuteUniversalTxTest is Test { vm.expectRevert(abi.encodeWithSelector(Errors.InvalidData.selector)); gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, tokenPayload); - assertFalse(gateway.isExecuted(TX_ID)); + assertFalse(gateway.isExecuted(keccak256(TX_ID))); } // ========================= @@ -474,7 +490,7 @@ contract GatewayExecuteUniversalTxTest is Test { // So we're just testing that the direct approve would fail without the reset // Transaction should not be executed - assertFalse(gateway.isExecuted(TX_ID)); + assertFalse(gateway.isExecuted(keccak256(TX_ID))); } function testSafeApprove_NoReturnData_Success() public { @@ -492,7 +508,7 @@ contract GatewayExecuteUniversalTxTest is Test { gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, tokenPayload); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); assertEq(approvalToken.balanceOf(address(target)), AMOUNT); } @@ -554,29 +570,27 @@ contract GatewayExecuteUniversalTxTest is Test { assertEq(target.lastCaller(), address(gateway)); assertEq(target.lastAmount(), 0); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); } function testExecuteCall_SuccessWithValue() public { uint256 initialBalance = address(target).balance; - vm.deal(tss, AMOUNT); + vm.deal(address(this), AMOUNT); - vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE - gateway.executeUniversalTx{ value: AMOUNT }( - TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD - ); + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); assertEq(address(target).balance, initialBalance + AMOUNT); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); } function testExecuteCall_NonPayableTargetWithValue_Reverts() public { - vm.deal(tss, AMOUNT); + vm.deal(address(this), AMOUNT); bytes memory nonPayablePayload = abi.encodeWithSignature("receiveFundsNonPayable()"); - vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); gateway.executeUniversalTx{ value: AMOUNT }( TX_ID, ORIGIN_CALLER, address(revertingTarget), AMOUNT, nonPayablePayload @@ -611,13 +625,13 @@ contract GatewayExecuteUniversalTxTest is Test { gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(token), AMOUNT, approvePayload); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); assertEq(token.allowance(address(gateway), address(0x7)), 500e18); } function testExecuteUniversalTx_MultipleExecutionsDifferentTxIDs() public { - bytes32 txId1 = keccak256("tx1"); - bytes32 txId2 = keccak256("tx2"); + bytes memory txId1 = abi.encodePacked(keccak256("tx1")); + bytes memory txId2 = abi.encodePacked(keccak256("tx2")); // Transfer tokens from Vault (test contract) to Gateway for first execution token.transfer(address(gateway), AMOUNT); @@ -629,18 +643,367 @@ contract GatewayExecuteUniversalTxTest is Test { gateway.executeUniversalTx(txId2, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); - assertTrue(gateway.isExecuted(txId1)); - assertTrue(gateway.isExecuted(txId2)); + assertTrue(gateway.isExecuted(keccak256(txId1))); + assertTrue(gateway.isExecuted(keccak256(txId2))); } function testIsExecutedMapping() public { - assertFalse(gateway.isExecuted(TX_ID)); + assertFalse(gateway.isExecuted(keccak256(TX_ID))); // Transfer tokens from Vault (test contract) to Gateway token.transfer(address(gateway), AMOUNT); gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); - assertTrue(gateway.isExecuted(TX_ID)); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); + } + + // ========================= + // ENHANCED NATIVE TOKEN EXECUTION TESTS + // ========================= + + function testExecuteUniversalTx_NativePath_OnlyVault_Reverts() public { + vm.deal(user, AMOUNT); + + // Non-TSS user should not be able to execute native token transactions + vm.prank(user); + vm.expectRevert(); + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); + + // TSS (test contract) should be able to execute + vm.deal(address(this), AMOUNT); + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); + } + + function testExecuteUniversalTx_NativePath_EventEmission_Complete() public { + vm.deal(address(this), AMOUNT); + + // Expect complete event emission with all parameters + vm.expectEmit(true, true, true, true); + emit UniversalTxExecuted(TX_ID, ORIGIN_CALLER, address(target), ZERO_ADDRESS, AMOUNT, PAYLOAD); + + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); + } + + function testExecuteUniversalTx_NativePath_EmptyPayload_Succeeds() public { + vm.deal(address(this), AMOUNT); + uint256 initialBalance = address(target).balance; + + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, EMPTY_PAYLOAD); + + assertTrue(gateway.isExecuted(keccak256(TX_ID))); + assertEq(address(target).balance, initialBalance + AMOUNT); + } + + function testExecuteUniversalTx_NativePath_ZeroueaAddress_Reverts() public { + vm.deal(address(this), AMOUNT); + + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ZERO_ADDRESS, address(target), AMOUNT, PAYLOAD); + } + + function testExecuteUniversalTx_NativePath_ZeroTarget_Reverts() public { + vm.deal(address(this), AMOUNT); + + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, ZERO_ADDRESS, AMOUNT, PAYLOAD); + } + + function testExecuteUniversalTx_NativePath_ZeroAmount_Reverts() public { + vm.deal(address(this), 1); + + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); + gateway.executeUniversalTx{ value: 1 }(TX_ID, ORIGIN_CALLER, address(target), 0, PAYLOAD); + } + + function testExecuteUniversalTx_NativePath_DuplicateTxID_Reverts() public { + vm.deal(address(this), AMOUNT * 2); + + // First execution succeeds + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); + + // Second execution with same txID reverts + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + vm.expectRevert(abi.encodeWithSelector(Errors.PayloadExecuted.selector)); + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); + } + + function testExecuteUniversalTx_NativePath_ComplexPayload_Succeeds() public { + vm.deal(address(this), AMOUNT); + // Use a payload that works with native ETH (receiveFunds or any payable function) + bytes memory complexPayload = abi.encodeWithSignature("receiveFunds()"); + + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, complexPayload); + + assertTrue(gateway.isExecuted(keccak256(TX_ID))); + assertEq(target.lastCaller(), address(gateway)); + assertEq(target.lastAmount(), AMOUNT); + } + + function testExecuteUniversalTx_NativePath_WhenPaused_Reverts() public { + vm.deal(address(this), AMOUNT); + + vm.prank(admin); + gateway.pause(); + + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + vm.expectRevert(); + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); + } + + function testExecuteUniversalTx_NativePath_StateNotUpdatedOnRevert() public { + vm.deal(address(this), AMOUNT * 2); + + // Try to execute with reverting target + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(revertingTarget), AMOUNT, PAYLOAD); + + // State should not be updated + assertFalse(gateway.isExecuted(keccak256(TX_ID))); + + // Should be able to retry with valid target + // Native token executeUniversalTx requires TSS_ROLE (test contract has this role) + gateway.executeUniversalTx{ value: AMOUNT }(TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); + } + + // ========================= + // WITHDRAW FUNDS (ERC20) TESTS + // ========================= + + function testWithdrawFunds_Success() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-1")); + address recipient = address(0x999); + uint256 withdrawAmount = 500e18; + + // Transfer tokens to gateway + token.transfer(address(gateway), withdrawAmount); + + uint256 initialRecipientBalance = token.balanceOf(recipient); + uint256 initialGatewayBalance = token.balanceOf(address(gateway)); + + // Expect WithdrawToken event + vm.expectEmit(true, true, true, true); + emit IUniversalGateway.WithdrawToken(txID, ORIGIN_CALLER, address(token), recipient, withdrawAmount); + + // Execute as Vault (this contract has VAULT_ROLE) + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), recipient, withdrawAmount); + + assertTrue(gateway.isExecuted(keccak256(txID))); + assertEq(token.balanceOf(recipient), initialRecipientBalance + withdrawAmount); + assertEq(token.balanceOf(address(gateway)), initialGatewayBalance - withdrawAmount); + } + + function testWithdrawFunds_OnlyVault_Reverts() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-2")); + token.transfer(address(gateway), AMOUNT); + + // Non-Vault user should not be able to withdraw funds + vm.prank(user); + vm.expectRevert(); + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), address(target), AMOUNT); + + // Vault should be able to withdraw + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), address(target), AMOUNT); + assertTrue(gateway.isExecuted(keccak256(txID))); + } + + function testWithdrawFunds_WhenPaused_Reverts() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-3")); + token.transfer(address(gateway), AMOUNT); + + vm.prank(admin); + gateway.pause(); + + vm.expectRevert(); + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), address(target), AMOUNT); + } + + function testWithdrawFunds_DuplicateTxID_Reverts() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-4")); + token.transfer(address(gateway), AMOUNT * 2); + + // First withdrawal succeeds + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), address(target), AMOUNT); + + // Second withdrawal with same txID reverts + vm.expectRevert(abi.encodeWithSelector(Errors.PayloadExecuted.selector)); + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), address(target), AMOUNT); + } + + function testWithdrawFunds_ZeroueaAddress_Reverts() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-5")); + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); + gateway.withdrawTokens(txID, ZERO_ADDRESS, address(token), address(target), AMOUNT); + } + + function testWithdrawFunds_ZeroRecipient_Reverts() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-6")); + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), ZERO_ADDRESS, AMOUNT); + } + + function testWithdrawFunds_ZeroAmount_Reverts() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-7")); + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), address(target), 0); + } + + function testWithdrawFunds_ZeroToken_Reverts() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-8")); + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); + gateway.withdrawTokens(txID, ORIGIN_CALLER, ZERO_ADDRESS, address(target), AMOUNT); + } + + function testWithdrawFunds_InsufficientBalance_Reverts() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-9")); + uint256 largeAmount = AMOUNT * 2; + + // Transfer only a small amount + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), address(target), largeAmount); + } + + function testWithdrawFunds_MultipleTokens_Success() public { + bytes memory txID1 = abi.encodePacked(keccak256("withdraw-funds-10")); + bytes memory txID2 = abi.encodePacked(keccak256("withdraw-funds-11")); + + // Transfer both tokens + token.transfer(address(gateway), AMOUNT); + usdtToken.transfer(address(gateway), 1000e6); + + // Withdraw tokenA + gateway.withdrawTokens(txID1, ORIGIN_CALLER, address(token), address(target), AMOUNT); + assertTrue(gateway.isExecuted(keccak256(txID1))); + + // Withdraw USDT + gateway.withdrawTokens(txID2, ORIGIN_CALLER, address(usdtToken), address(target), 1000e6); + assertTrue(gateway.isExecuted(keccak256(txID2))); + } + + function testWithdrawFunds_EventEmission_Complete() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-12")); + address recipient = address(0x888); + token.transfer(address(gateway), AMOUNT); + + // Expect complete event emission + vm.expectEmit(true, true, true, true); + emit IUniversalGateway.WithdrawToken(txID, ORIGIN_CALLER, address(token), recipient, AMOUNT); + + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), recipient, AMOUNT); + } + + function testWithdrawFunds_PartialWithdrawal_Success() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-13")); + uint256 totalAmount = AMOUNT * 2; + uint256 withdrawAmount = AMOUNT; + + token.transfer(address(gateway), totalAmount); + + uint256 initialGatewayBalance = token.balanceOf(address(gateway)); + uint256 initialRecipientBalance = token.balanceOf(address(target)); + + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), address(target), withdrawAmount); + + assertEq(token.balanceOf(address(gateway)), initialGatewayBalance - withdrawAmount); + assertEq(token.balanceOf(address(target)), initialRecipientBalance + withdrawAmount); + assertTrue(gateway.isExecuted(keccak256(txID))); + } + + function testWithdrawFunds_StateNotUpdatedOnRevert() public { + bytes memory txID = abi.encodePacked(keccak256("withdraw-funds-14")); + uint256 largeAmount = AMOUNT * 2; + + // Transfer only a small amount + token.transfer(address(gateway), AMOUNT); + + // Try to withdraw more than available + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), address(target), largeAmount); + + // State should not be updated + assertFalse(gateway.isExecuted(keccak256(txID))); + + // Should be able to withdraw the available amount + gateway.withdrawTokens(txID, ORIGIN_CALLER, address(token), address(target), AMOUNT); + assertTrue(gateway.isExecuted(keccak256(txID))); + } + + // ========================= + // ENHANCED ERC20 EXECUTION TESTS + // ========================= + + function testExecuteUniversalTx_ERC20Path_RemainingTokensReturnedToVault() public { + bytes memory txID = abi.encodePacked(keccak256("remaining-tokens")); + uint256 extraAmount = 100e18; + uint256 totalAmount = AMOUNT + extraAmount; + + // Transfer more tokens than needed to gateway + token.transfer(address(gateway), totalAmount); + + // Get vault balance after transfer (before execution) + uint256 vaultBalanceAfterTransfer = token.balanceOf(address(this)); + + // Verify gateway has the tokens + assertEq(token.balanceOf(address(gateway)), totalAmount); + + // Use a payload that transfers tokens (receiveToken function) + bytes memory tokenPayload = abi.encodeWithSignature("receiveToken(address,uint256)", address(token), AMOUNT); + + gateway.executeUniversalTx(txID, ORIGIN_CALLER, address(token), address(target), AMOUNT, tokenPayload); + + // Remaining tokens should be returned to vault + // Gateway should have 0 balance after execution (target received AMOUNT, vault received extraAmount) + assertEq(token.balanceOf(address(gateway)), 0); + // Vault should have received the extra tokens back (balance increased by extraAmount) + assertEq(token.balanceOf(address(this)), vaultBalanceAfterTransfer + extraAmount); + // Target should have received AMOUNT + assertEq(token.balanceOf(address(target)), AMOUNT); + } + + function testExecuteUniversalTx_ERC20Path_NoRemainingTokens() public { + bytes memory txID = abi.encodePacked(keccak256("no-remaining")); + uint256 vaultBalanceBefore = token.balanceOf(address(this)); + + // Transfer exact amount needed + token.transfer(address(gateway), AMOUNT); + + gateway.executeUniversalTx(txID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + + // No tokens should be returned (gateway balance is 0) + assertEq(token.balanceOf(address(gateway)), 0); + assertEq(token.balanceOf(address(this)), vaultBalanceBefore); + } + + function testExecuteUniversalTx_ERC20Path_OnlyVault_Reverts() public { + token.transfer(address(gateway), AMOUNT); + + // Non-Vault user should not be able to execute + vm.prank(user); + vm.expectRevert(); + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + + // Vault should be able to execute + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + assertTrue(gateway.isExecuted(keccak256(TX_ID))); } } diff --git a/contracts/evm-gateway/test/gateway/5_GatewayBlockRateLimit.t.sol b/contracts/evm-gateway/test/gateway/12_rateLimit_BlockBased.t.sol similarity index 71% rename from contracts/evm-gateway/test/gateway/5_GatewayBlockRateLimit.t.sol rename to contracts/evm-gateway/test/gateway/12_rateLimit_BlockBased.t.sol index ad0fc89..ae27c00 100644 --- a/contracts/evm-gateway/test/gateway/5_GatewayBlockRateLimit.t.sol +++ b/contracts/evm-gateway/test/gateway/12_rateLimit_BlockBased.t.sol @@ -5,7 +5,7 @@ import "../BaseTest.t.sol"; import { IUniversalGateway } from "../../src/interfaces/IUniversalGateway.sol"; import { UniversalGateway } from "../../src/UniversalGateway.sol"; import { Errors } from "../../src/libraries/Errors.sol"; -import { UniversalPayload, RevertInstructions, TX_TYPE } from "../../src/libraries/Types.sol"; +import { UniversalPayload, RevertInstructions, TX_TYPE, UniversalTxRequest } from "../../src/libraries/Types.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { MockERC20 } from "../mocks/MockERC20.sol"; import { MockAggregatorV3 } from "../mocks/MockAggregatorV3.sol"; @@ -97,13 +97,11 @@ contract GatewayBlockRateLimitTest is BaseTest { assertEq(gateway.BLOCK_USD_CAP(), 0, "Block USD cap not disabled"); // Test that with cap disabled, multiple transactions can go through - uint256 totalAmount = ETH_FOR_5_USD * 5; // 5x the cap if it was enabled + // 5x the cap if it was enabled (ETH_FOR_5_USD * 5) for (uint256 i = 0; i < 5; i++) { vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); } // If we got here without reverting, the test passed @@ -126,7 +124,7 @@ contract GatewayBlockRateLimitTest is BaseTest { // Send tx that should fail due to per-tx cap, not block cap vm.prank(user1); vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithGas{ value: ethAmount }(buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ethAmount }(_buildGasTxRequest()); } function testSingleCallExceedsBlockCap() public { @@ -141,9 +139,7 @@ contract GatewayBlockRateLimitTest is BaseTest { // Send tx worth $12 (exceeds block cap) vm.prank(user1); vm.expectRevert(Errors.BlockCapLimitExceeded.selector); - gateway.sendTxWithGas{ value: ETH_FOR_12_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_12_USD }(_buildGasTxRequest()); } function testExactlyEqualToBlockCap() public { @@ -153,9 +149,7 @@ contract GatewayBlockRateLimitTest is BaseTest { // Send tx worth exactly $10 vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_10_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_10_USD }(_buildGasTxRequest()); } // =========================== @@ -172,30 +166,23 @@ contract GatewayBlockRateLimitTest is BaseTest { // Send first tx: $2 vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_2_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_2_USD }(_buildGasTxRequest()); // Verify we're still in the same block assertEq(block.number, startingBlockNumber, "Block number changed unexpectedly"); // Send second tx: $3 vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_2_USD + ETH_FOR_2_USD / 2 }( // $3 - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ETH_FOR_2_USD + ETH_FOR_2_USD / 2 }(_buildGasTxRequest()); // $3 // Send third tx: $5 vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); // Try to send a fourth tx that would exceed the cap - should revert vm.prank(user1); vm.expectRevert(Errors.BlockCapLimitExceeded.selector); - gateway.sendTxWithGas{ value: ETH_FOR_2_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_2_USD }(_buildGasTxRequest()); // If we got here without reverting earlier and the final transaction reverted, // it proves that the accumulation worked correctly @@ -211,8 +198,7 @@ contract GatewayBlockRateLimitTest is BaseTest { // Send first tx: $6 vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD + ETH_FOR_2_USD / 2 }( // $6 - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD + ETH_FOR_2_USD / 2 }(_buildGasTxRequest()); // $6 // Verify we're still in the same block assertEq(block.number, startingBlockNumber, "Block number changed unexpectedly"); @@ -220,20 +206,16 @@ contract GatewayBlockRateLimitTest is BaseTest { // Send second tx: $5 (should revert as total would be $11) vm.prank(user1); vm.expectRevert(Errors.BlockCapLimitExceeded.selector); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); // We should be able to send a smaller tx that fits within the remaining cap vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_2_USD * 2 }( // $4 - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ETH_FOR_2_USD * 2 }(_buildGasTxRequest()); // $4 // But trying to send even $1 more should fail vm.prank(user1); vm.expectRevert(Errors.BlockCapLimitExceeded.selector); - gateway.sendTxWithGas{ value: ETH_FOR_2_USD / 2 }( // $1 - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ETH_FOR_2_USD / 2 }(_buildGasTxRequest()); // $1 } function testCrossSenderGlobalBudget() public { @@ -246,8 +228,7 @@ contract GatewayBlockRateLimitTest is BaseTest { // First user sends tx: $6 vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD + ETH_FOR_2_USD / 2 }( // $6 - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD + ETH_FOR_2_USD / 2 }(_buildGasTxRequest()); // $6 // Verify we're still in the same block assertEq(block.number, startingBlockNumber, "Block number changed unexpectedly"); @@ -255,9 +236,7 @@ contract GatewayBlockRateLimitTest is BaseTest { // Second user sends tx: $5 (should revert as total would be $11) vm.prank(user2); vm.expectRevert(Errors.BlockCapLimitExceeded.selector); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); } // =========================== @@ -271,18 +250,14 @@ contract GatewayBlockRateLimitTest is BaseTest { // Consume full cap in block N vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_10_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_10_USD }(_buildGasTxRequest()); // Move to next block vm.roll(block.number + 1); // Should be able to send $10 again in the new block vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_10_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_10_USD }(_buildGasTxRequest()); // If we got here without reverting, the test passed } @@ -294,45 +269,36 @@ contract GatewayBlockRateLimitTest is BaseTest { // Use $5 in block N vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( // $5 - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); // $5 // We should be able to send another $2 in the same block vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_2_USD }( // $2 - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ETH_FOR_2_USD }(_buildGasTxRequest()); // $2 // And another $2 vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_2_USD }( // $2 - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ETH_FOR_2_USD }(_buildGasTxRequest()); // $2 // But trying to send even $2 more should fail as we've used $9 of $10 vm.prank(user1); vm.expectRevert(Errors.BlockCapLimitExceeded.selector); - gateway.sendTxWithGas{ value: ETH_FOR_2_USD }( // $2 - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ETH_FOR_2_USD }(_buildGasTxRequest()); // $2 // Move to next block vm.roll(block.number + 1); // Should be able to send $5 in the new block vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); // And another $5 vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); // But trying to send even $1 more should fail vm.prank(user1); vm.expectRevert(Errors.BlockCapLimitExceeded.selector); - gateway.sendTxWithGas{ value: ETH_FOR_2_USD / 2 }( // $1 - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ETH_FOR_2_USD / 2 }(_buildGasTxRequest()); // $1 } // =========================== @@ -358,9 +324,11 @@ contract GatewayBlockRateLimitTest is BaseTest { tokenA.approve(address(gateway), 1 ether); // Send tx with native gas worth $5 and token bridge + UniversalPayload memory payload = buildDefaultPayload(); + vm.prank(user1); - gateway.sendTxWithFunds{ value: ETH_FOR_5_USD }( - address(tokenA), 1 ether, buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }( + _buildFundsAndPayloadTxRequest(address(tokenA), 1 ether, payload) ); // Send another tx with native gas worth $6 - should fail due to block cap @@ -369,8 +337,8 @@ contract GatewayBlockRateLimitTest is BaseTest { vm.prank(user1); vm.expectRevert(Errors.BlockCapLimitExceeded.selector); - gateway.sendTxWithFunds{ value: ETH_FOR_5_USD + ETH_FOR_2_USD / 2 }( // $6 - address(tokenA), 1 ether, buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD + ETH_FOR_2_USD / 2 }( // $6 + _buildFundsAndPayloadTxRequest(address(tokenA), 1 ether, payload)); } // =========================== @@ -390,9 +358,7 @@ contract GatewayBlockRateLimitTest is BaseTest { // Try to send tx - should revert due to TSS rejecting ETH vm.prank(user1); vm.expectRevert(Errors.DepositFailed.selector); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); // Restore normal TSS vm.prank(admin); @@ -400,22 +366,16 @@ contract GatewayBlockRateLimitTest is BaseTest { // Send same tx again - should succeed because usage wasn't recorded due to revert vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); // Send another tx to test that block cap is working normally vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); // Third tx should fail as we've now reached the cap vm.prank(user1); vm.expectRevert(Errors.BlockCapLimitExceeded.selector); - gateway.sendTxWithGas{ value: ETH_FOR_2_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_2_USD }(_buildGasTxRequest()); } // =========================== @@ -433,7 +393,7 @@ contract GatewayBlockRateLimitTest is BaseTest { // Just under should pass vm.prank(user1); - gateway.sendTxWithGas{ value: justUnder }(buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: justUnder }(_buildGasTxRequest()); // Reset for next test vm.roll(block.number + 1); @@ -442,7 +402,7 @@ contract GatewayBlockRateLimitTest is BaseTest { // Note: The per-tx cap check happens before the block cap check vm.prank(user1); vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithGas{ value: justOver }(buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("")); + gateway.sendUniversalTx{ value: justOver }(_buildGasTxRequest()); } // Note: sendFunds which is a non-gas route is unaffected by the block cap. @@ -454,21 +414,16 @@ contract GatewayBlockRateLimitTest is BaseTest { // Consume full block cap with gas route vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_10_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_10_USD }(_buildGasTxRequest()); // Funds-only route should still work vm.prank(user1); tokenA.approve(address(gateway), 1 ether); + address revertRecipient = user2; + vm.prank(user1); - gateway.sendFunds( - user2, // recipient - address(tokenA), - 1 ether, - buildDefaultRevertInstructions() - ); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 1 ether, revertRecipient)); // If we got here without reverting, the test passed } @@ -484,9 +439,7 @@ contract GatewayBlockRateLimitTest is BaseTest { // Use $5 of the cap vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); // Pause the contract vm.prank(admin); @@ -495,9 +448,7 @@ contract GatewayBlockRateLimitTest is BaseTest { // Try to send tx while paused - should revert due to pause vm.prank(user1); vm.expectRevert("EnforcedPause()"); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); // Unpause vm.prank(admin); @@ -505,24 +456,23 @@ contract GatewayBlockRateLimitTest is BaseTest { // Should be able to use remaining $5 of cap vm.prank(user1); - gateway.sendTxWithGas{ value: ETH_FOR_5_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_5_USD }(_buildGasTxRequest()); // Additional tx should fail due to cap vm.prank(user1); vm.expectRevert(Errors.BlockCapLimitExceeded.selector); - gateway.sendTxWithGas{ value: ETH_FOR_2_USD }( - buildDefaultPayload(), buildDefaultRevertInstructions(), bytes("") - ); + gateway.sendUniversalTx{ value: ETH_FOR_2_USD }(_buildGasTxRequest()); } // =========================== // HELPER FUNCTIONS // =========================== - // Helper functions moved to BaseTest.t.sol for reusability - // Use buildDefaultPayload() and buildDefaultRevertInstructions() from BaseTest + // Helper functions for building UniversalTxRequest are available in BaseTest.t.sol: + // - _buildGasTxRequest() - for GAS_AND_PAYLOAD transactions + // - _buildFundsTxRequest(address token, uint256 amount) - for FUNDS transactions + // - _buildFundsTxRequest(address token, uint256 amount, RevertInstructions) - with custom revert instructions + // - _buildFundsAndPayloadTxRequest(address token, uint256 amount, UniversalPayload) - for FUNDS_AND_PAYLOAD function _getEthAmountFromUsd(uint256 usdAmount1e18) internal view returns (uint256) { // USD(1e18) / ETH_price(1e18) * 1e18 = ETH(wei) diff --git a/contracts/evm-gateway/test/gateway/6_GatewayGlobalRateLimit.t.sol b/contracts/evm-gateway/test/gateway/13_rateLimit_EpochBased.t.sol similarity index 83% rename from contracts/evm-gateway/test/gateway/6_GatewayGlobalRateLimit.t.sol rename to contracts/evm-gateway/test/gateway/13_rateLimit_EpochBased.t.sol index 4db30b8..8330ba5 100644 --- a/contracts/evm-gateway/test/gateway/6_GatewayGlobalRateLimit.t.sol +++ b/contracts/evm-gateway/test/gateway/13_rateLimit_EpochBased.t.sol @@ -5,7 +5,13 @@ import { Test, console2 } from "forge-std/Test.sol"; import { BaseTest } from "../BaseTest.t.sol"; import { Errors } from "../../src/libraries/Errors.sol"; import { IUniversalGateway } from "../../src/interfaces/IUniversalGateway.sol"; -import { RevertInstructions, UniversalPayload, TX_TYPE, VerificationType } from "../../src/libraries/Types.sol"; +import { + RevertInstructions, + UniversalPayload, + TX_TYPE, + VerificationType, + UniversalTxRequest +} from "../../src/libraries/Types.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { UniversalGateway } from "../../src/UniversalGateway.sol"; import { MockERC20 } from "../mocks/MockERC20.sol"; @@ -53,16 +59,15 @@ contract GatewayGlobalRateLimitTest is BaseTest { } // Helper functions moved to BaseTest.t.sol for reusability - // Use buildDefaultPayload() and buildDefaultRevertInstructions() from BaseTest - - function _buildDefaultRevertInstructions() internal view returns (RevertInstructions memory) { - return RevertInstructions({ fundRecipient: user1, revertContext: bytes("") }); - } + // Use buildDefaultPayload() from BaseTest function _getCurrentEpoch() internal view returns (uint256) { return block.timestamp / gateway.epochDurationSec(); } + // Helper function _buildFundsTxRequest is available in BaseTest.t.sol + // Use the overload with custom fund recipient: _buildFundsTxRequest(token, amount, user1) + // ========================================== // 1. INITIALIZATION AND CONFIGURATION TESTS // ========================================== @@ -123,7 +128,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { ); } - function testUpdateTokenLimitThreshold() public { + function testSetTokenLimitThresholdsAllowsUpdating() public { // First set initial thresholds address[] memory tokens = new address[](1); uint256[] memory thresholds = new uint256[](1); @@ -142,7 +147,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { emit TokenLimitThresholdUpdated(address(tokenA), newThreshold); vm.prank(admin); - gateway.updateTokenLimitThreshold(tokens, thresholds); + gateway.setTokenLimitThresholds(tokens, thresholds); // Verify threshold was updated assertEq(gateway.tokenToLimitThreshold(address(tokenA)), newThreshold, "TokenA threshold not updated correctly"); @@ -175,19 +180,6 @@ contract GatewayGlobalRateLimitTest is BaseTest { gateway.setTokenLimitThresholds(tokens, thresholds); } - function testUpdateTokenLimitThresholdArrayMismatch() public { - address[] memory tokens = new address[](2); - uint256[] memory thresholds = new uint256[](1); // Mismatch - - tokens[0] = address(tokenA); - tokens[1] = address(tokenB); - thresholds[0] = TOKEN_A_THRESHOLD; - - vm.prank(admin); - vm.expectRevert(Errors.InvalidInput.selector); - gateway.updateTokenLimitThreshold(tokens, thresholds); - } - function testOnlyAdminCanSetThresholds() public { address[] memory tokens = new address[](1); uint256[] memory thresholds = new uint256[](1); @@ -205,23 +197,6 @@ contract GatewayGlobalRateLimitTest is BaseTest { gateway.setTokenLimitThresholds(tokens, thresholds); } - function testOnlyAdminCanUpdateThresholds() public { - address[] memory tokens = new address[](1); - uint256[] memory thresholds = new uint256[](1); - - tokens[0] = address(tokenA); - thresholds[0] = TOKEN_A_THRESHOLD; - - // Non-admin should not be able to update thresholds - vm.prank(user1); - vm.expectRevert(); - gateway.updateTokenLimitThreshold(tokens, thresholds); - - // Admin should be able to update thresholds - vm.prank(admin); - gateway.updateTokenLimitThreshold(tokens, thresholds); - } - function testOnlyAdminCanUpdateEpochDuration() public { uint256 newDuration = 12 hours; @@ -246,15 +221,8 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Payload not needed for this test - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - vm.expectRevert(Errors.NotSupported.selector); - gateway.sendFunds( - recipient, - address(tokenA), // Unsupported token - 10 ether, - revertInstructions - ); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 10 ether, user1)); vm.stopPrank(); } @@ -278,10 +246,8 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Payload not needed for this test - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - vm.expectRevert(Errors.InvalidData.selector); - gateway.sendFunds(recipient, address(tokenA), 10 ether, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 10 ether, user1)); vm.stopPrank(); } @@ -299,15 +265,8 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - vm.expectRevert(Errors.RateLimitExceeded.selector); - gateway.sendFunds( - recipient, - address(tokenA), - TOKEN_A_THRESHOLD + 1, // Exceeds threshold - revertInstructions - ); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), TOKEN_A_THRESHOLD + 1, user1)); vm.stopPrank(); } @@ -324,14 +283,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - - gateway.sendFunds( - recipient, - address(tokenA), - TOKEN_A_THRESHOLD, // Exactly the threshold - revertInstructions - ); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), TOKEN_A_THRESHOLD, user1)); vm.stopPrank(); @@ -353,11 +305,9 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - uint256 sendAmount = TOKEN_A_THRESHOLD / 2; - gateway.sendFunds(recipient, address(tokenA), sendAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), sendAmount, user1)); vm.stopPrank(); @@ -379,13 +329,11 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - uint256 firstAmount = TOKEN_A_THRESHOLD / 3; uint256 secondAmount = TOKEN_A_THRESHOLD / 3; uint256 thirdAmount = TOKEN_A_THRESHOLD / 3; - gateway.sendFunds(recipient, address(tokenA), firstAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), firstAmount, user1)); // Verify first usage (uint256 usedAfterFirst, uint256 remainingAfterFirst) = gateway.currentTokenUsage(address(tokenA)); @@ -393,7 +341,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { assertEq(remainingAfterFirst, TOKEN_A_THRESHOLD - firstAmount, "Remaining amount after first tx incorrect"); // Second transaction - gateway.sendFunds(recipient, address(tokenA), secondAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), secondAmount, user1)); // Verify second usage (uint256 usedAfterSecond, uint256 remainingAfterSecond) = gateway.currentTokenUsage(address(tokenA)); @@ -405,7 +353,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { ); // Third transaction - gateway.sendFunds(recipient, address(tokenA), thirdAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), thirdAmount, user1)); // Verify third usage (uint256 usedAfterThird, uint256 remainingAfterThird) = gateway.currentTokenUsage(address(tokenA)); @@ -419,7 +367,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { uint256 fourthAmount = TOKEN_A_THRESHOLD - firstAmount - secondAmount - thirdAmount + 1; // Just over the limit vm.expectRevert(Errors.RateLimitExceeded.selector); - gateway.sendFunds(recipient, address(tokenA), fourthAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), fourthAmount, user1)); vm.stopPrank(); } @@ -438,17 +386,9 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Send native token vm.startPrank(user1); - // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - // Send half the threshold uint256 firstAmount = NATIVE_THRESHOLD / 2; - gateway.sendFunds{ value: firstAmount }( - recipient, - address(0), // Native token - firstAmount, - revertInstructions - ); + gateway.sendUniversalTx{ value: firstAmount }(_buildFundsTxRequest(address(0), firstAmount, user1)); // Verify usage (uint256 used, uint256 remaining) = gateway.currentTokenUsage(address(0)); @@ -460,12 +400,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Expect revert with RateLimitExceeded vm.expectRevert(Errors.RateLimitExceeded.selector); - gateway.sendFunds{ value: secondAmount }( - recipient, - address(0), // Native token - secondAmount, - revertInstructions - ); + gateway.sendUniversalTx{ value: secondAmount }(_buildFundsTxRequest(address(0), secondAmount, user1)); vm.stopPrank(); } @@ -490,17 +425,9 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Try to send funds with unsupported native token vm.startPrank(user1); - // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - // Expect revert with NotSupported vm.expectRevert(Errors.NotSupported.selector); - gateway.sendFunds{ value: 1 ether }( - recipient, - address(0), // Native token - 1 ether, - revertInstructions - ); + gateway.sendUniversalTx{ value: 1 ether }(_buildFundsTxRequest(address(0), 1 ether, user1)); vm.stopPrank(); } @@ -525,11 +452,8 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); - // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - // First epoch - use full threshold - gateway.sendFunds(recipient, address(tokenA), TOKEN_A_THRESHOLD, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), TOKEN_A_THRESHOLD, user1)); // Verify first epoch usage (uint256 usedFirstEpoch, uint256 remainingFirstEpoch) = gateway.currentTokenUsage(address(tokenA)); @@ -578,7 +502,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { assertEq(remainingFarFuture, TOKEN_A_THRESHOLD, "Far future epoch remaining should be full threshold"); // Send funds in far future epoch - gateway.sendFunds(recipient, address(tokenA), TOKEN_A_THRESHOLD, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), TOKEN_A_THRESHOLD, user1)); // Verify usage in far future epoch (uint256 usedAfterSend, uint256 remainingAfterSend) = gateway.currentTokenUsage(address(tokenA)); @@ -601,12 +525,10 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - - uint256 firstEpochAmount = TOKEN_A_THRESHOLD * 3 / 4; // Use 75% of the threshold + uint256 firstEpochAmount = (TOKEN_A_THRESHOLD * 3) / 4; // Use 75% of the threshold // Send in first epoch - gateway.sendFunds(recipient, address(tokenA), firstEpochAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), firstEpochAmount, user1)); // Verify first epoch usage (uint256 usedFirstEpoch, uint256 remainingFirstEpoch) = gateway.currentTokenUsage(address(tokenA)); @@ -632,7 +554,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { uint256 secondEpochAmount = TOKEN_A_THRESHOLD; // Use 100% of threshold in new epoch // Send in second epoch - gateway.sendFunds(recipient, address(tokenA), secondEpochAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), secondEpochAmount, user1)); // Verify second epoch usage after transaction (uint256 usedSecondEpoch, uint256 remainingSecondEpoch) = gateway.currentTokenUsage(address(tokenA)); @@ -644,7 +566,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Expect revert with RateLimitExceeded vm.expectRevert(Errors.RateLimitExceeded.selector); - gateway.sendFunds(recipient, address(tokenA), excessAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), excessAmount, user1)); vm.stopPrank(); } @@ -663,12 +585,9 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Send funds to consume part of the threshold vm.startPrank(user1); - // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - uint256 firstEpochAmount = TOKEN_A_THRESHOLD / 2; // Use 50% of the threshold - gateway.sendFunds(recipient, address(tokenA), firstEpochAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), firstEpochAmount, user1)); // Verify first epoch usage (uint256 usedFirstEpoch, uint256 remainingFirstEpoch) = gateway.currentTokenUsage(address(tokenA)); @@ -685,7 +604,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { uint256 secondEpochFirstAmount = TOKEN_A_THRESHOLD / 2; // Use 50% of threshold in new epoch - gateway.sendFunds(recipient, address(tokenA), secondEpochFirstAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), secondEpochFirstAmount, user1)); // Verify second epoch usage after first transaction (uint256 usedSecondEpochFirst, uint256 remainingSecondEpochFirst) = gateway.currentTokenUsage(address(tokenA)); @@ -698,7 +617,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { uint256 secondEpochSecondAmount = TOKEN_A_THRESHOLD / 2; // Use remaining 50% - gateway.sendFunds(recipient, address(tokenA), secondEpochSecondAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), secondEpochSecondAmount, user1)); (uint256 usedSecondEpochSecond, uint256 remainingSecondEpochSecond) = gateway.currentTokenUsage(address(tokenA)); assertEq( @@ -725,12 +644,9 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Send funds to consume part of the threshold vm.startPrank(user1); - // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - // First epoch uint256 firstEpochAmount = TOKEN_A_THRESHOLD; - gateway.sendFunds(recipient, address(tokenA), firstEpochAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), firstEpochAmount, user1)); // Verify first epoch usage (uint256 usedFirstEpoch, uint256 remainingFirstEpoch) = gateway.currentTokenUsage(address(tokenA)); @@ -747,7 +663,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Second epoch uint256 secondEpochAmount = TOKEN_A_THRESHOLD / 2; - gateway.sendFunds(recipient, address(tokenA), secondEpochAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), secondEpochAmount, user1)); // Verify second epoch usage (uint256 usedSecondEpoch, uint256 remainingSecondEpoch) = gateway.currentTokenUsage(address(tokenA)); @@ -764,7 +680,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Third epoch uint256 thirdEpochAmount = TOKEN_A_THRESHOLD / 4; - gateway.sendFunds(recipient, address(tokenA), thirdEpochAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), thirdEpochAmount, user1)); // Verify third epoch usage (uint256 usedThirdEpoch, uint256 remainingThirdEpoch) = gateway.currentTokenUsage(address(tokenA)); @@ -785,7 +701,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // After multiple skipped epochs, should still be able to use full threshold uint256 finalEpochAmount = TOKEN_A_THRESHOLD; - gateway.sendFunds(recipient, address(tokenA), finalEpochAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), finalEpochAmount, user1)); // Verify final epoch usage (uint256 usedFinalEpoch, uint256 remainingFinalEpoch) = gateway.currentTokenUsage(address(tokenA)); @@ -809,15 +725,12 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Send funds in first epoch vm.startPrank(user1); - // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - // Record current epoch uint256 firstEpoch = _getCurrentEpoch(); // First epoch transaction uint256 firstEpochAmount = TOKEN_A_THRESHOLD / 2; - gateway.sendFunds(recipient, address(tokenA), firstEpochAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), firstEpochAmount, user1)); // Verify first epoch usage (uint256 usedFirstEpoch, uint256 remainingFirstEpoch) = gateway.currentTokenUsage(address(tokenA)); @@ -838,7 +751,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Second epoch transaction uint256 secondEpochAmount = TOKEN_A_THRESHOLD / 3; - gateway.sendFunds(recipient, address(tokenA), secondEpochAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), secondEpochAmount, user1)); // Verify second epoch usage after transaction (uint256 usedSecondEpoch, uint256 remainingSecondEpoch) = gateway.currentTokenUsage(address(tokenA)); @@ -866,18 +779,10 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Send funds with native token vm.startPrank(user1); - // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - uint256 sendAmount = NATIVE_THRESHOLD / 2; // Send funds - gateway.sendFunds{ value: sendAmount }( - recipient, - address(0), // Native token - sendAmount, - revertInstructions - ); + gateway.sendUniversalTx{ value: sendAmount }(_buildFundsTxRequest(address(0), sendAmount, user1)); // Verify usage was recorded correctly (uint256 used, uint256 remaining) = gateway.currentTokenUsage(address(0)); @@ -906,23 +811,22 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions and payload - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; UniversalPayload memory payload = buildDefaultPayload(); // Try to send funds with unsupported bridge token (tokenB) - // The error is InvalidInput() because tokenB is not supported in the gateway - vm.expectRevert(Errors.InvalidInput.selector); - gateway.sendTxWithFunds( - address(tokenB), // Unsupported bridge token - 1 ether, - address(tokenA), // Gas token - 1 ether, - 0.01 ether, // amountOutMinETH - block.timestamp + 3600, // deadline - payload, - revertInstructions, - bytes("") - ); + // The error is NotSupported() because tokenB is not supported in the gateway (no threshold set) + bytes memory encodedPayload = abi.encode(payload); + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + token: address(tokenB), // Unsupported bridge token + amount: 1 ether, + payload: encodedPayload, + revertRecipient: revertRecipient, + signatureData: bytes("") + }); + vm.expectRevert(Errors.NotSupported.selector); + gateway.sendUniversalTx{ value: 0 }(req); vm.stopPrank(); } @@ -942,17 +846,17 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; - uint256 firstAmount = TOKEN_A_THRESHOLD * 3 / 4; // Use 75% of the threshold + uint256 firstAmount = (TOKEN_A_THRESHOLD * 3) / 4; // Use 75% of the threshold // First transaction - gateway.sendFunds(recipient, address(tokenA), firstAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), firstAmount, user1)); uint256 secondAmount = TOKEN_A_THRESHOLD / 2; vm.expectRevert(Errors.RateLimitExceeded.selector); - gateway.sendFunds(recipient, address(tokenA), secondAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), secondAmount, user1)); vm.stopPrank(); } @@ -963,17 +867,9 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Try to send funds with an unsupported token vm.startPrank(user1); - // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - // Expect revert with NotSupported vm.expectRevert(Errors.NotSupported.selector); - gateway.sendFunds( - recipient, - address(tokenA), // Unsupported token - 10 ether, - revertInstructions - ); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 10 ether, user1)); vm.stopPrank(); } @@ -997,12 +893,12 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; - uint256 firstAmount = TOKEN_A_THRESHOLD * 3 / 4; // Use 75% of the threshold + uint256 firstAmount = (TOKEN_A_THRESHOLD * 3) / 4; // Use 75% of the threshold // First transaction - gateway.sendFunds(recipient, address(tokenA), firstAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), firstAmount, user1)); (uint256 usedBefore, uint256 remainingBefore) = gateway.currentTokenUsage(address(tokenA)); assertEq(usedBefore, firstAmount, "Used amount before update incorrect"); @@ -1014,7 +910,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { thresholds[0] = newThreshold; vm.prank(admin); - gateway.updateTokenLimitThreshold(tokens, thresholds); + gateway.setTokenLimitThresholds(tokens, thresholds); (uint256 usedAfterUpdate, uint256 remainingAfterUpdate) = gateway.currentTokenUsage(address(tokenA)); assertEq(usedAfterUpdate, firstAmount, "Used amount should not change after threshold increase"); @@ -1024,7 +920,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { uint256 secondAmount = TOKEN_A_THRESHOLD; - gateway.sendFunds(recipient, address(tokenA), secondAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), secondAmount, user1)); // Verify usage after second transaction (uint256 usedAfter, uint256 remainingAfter) = gateway.currentTokenUsage(address(tokenA)); @@ -1087,7 +983,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Admin should be able to update threshold while paused vm.prank(admin); - gateway.updateTokenLimitThreshold(tokens, thresholds); + gateway.setTokenLimitThresholds(tokens, thresholds); // Verify threshold was updated assertEq( @@ -1097,7 +993,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Non-admin should still not be able to update threshold vm.prank(user1); vm.expectRevert(); - gateway.updateTokenLimitThreshold(tokens, thresholds); + gateway.setTokenLimitThresholds(tokens, thresholds); // Unpause the contract vm.prank(admin); @@ -1149,11 +1045,11 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; // Expect revert with EnforcedPause vm.expectRevert("EnforcedPause()"); - gateway.sendFunds(recipient, address(tokenA), 1 ether, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 1 ether, user1)); vm.stopPrank(); @@ -1164,7 +1060,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Now sending funds should work vm.startPrank(user1); - gateway.sendFunds(recipient, address(tokenA), 1 ether, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 1 ether, user1)); vm.stopPrank(); } @@ -1188,10 +1084,10 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; // Send exactly 1 wei (the threshold) - gateway.sendFunds(recipient, address(tokenA), 1, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 1, user1)); // Verify usage was recorded correctly (uint256 used, uint256 remaining) = gateway.currentTokenUsage(address(tokenA)); @@ -1200,7 +1096,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Try to send 1 more wei, should revert vm.expectRevert(Errors.RateLimitExceeded.selector); - gateway.sendFunds(recipient, address(tokenA), 1, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 1, user1)); vm.stopPrank(); } @@ -1220,10 +1116,10 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; // Send exactly the threshold amount - gateway.sendFunds(recipient, address(tokenA), TOKEN_A_THRESHOLD, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), TOKEN_A_THRESHOLD, user1)); // Verify usage was recorded correctly (uint256 used, uint256 remaining) = gateway.currentTokenUsage(address(tokenA)); @@ -1232,7 +1128,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Try to send 1 more wei, should revert vm.expectRevert(Errors.RateLimitExceeded.selector); - gateway.sendFunds(recipient, address(tokenA), 1, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 1, user1)); vm.stopPrank(); } @@ -1256,11 +1152,11 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; // Expect revert with InvalidData vm.expectRevert(Errors.InvalidData.selector); - gateway.sendFunds(recipient, address(tokenA), 1 ether, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 1 ether, user1)); // Set epoch duration back to a valid value vm.stopPrank(); @@ -1270,7 +1166,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Now sending funds should work vm.startPrank(user1); - gateway.sendFunds(recipient, address(tokenA), 1 ether, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 1 ether, user1)); vm.stopPrank(); } @@ -1290,12 +1186,12 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; uint256 firstAmount = TOKEN_A_THRESHOLD / 2; // First transaction - gateway.sendFunds(recipient, address(tokenA), firstAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), firstAmount, user1)); // Verify usage (uint256 usedBefore, uint256 remainingBefore) = gateway.currentTokenUsage(address(tokenA)); @@ -1308,7 +1204,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { thresholds[0] = 0; vm.prank(admin); - gateway.updateTokenLimitThreshold(tokens, thresholds); + gateway.setTokenLimitThresholds(tokens, thresholds); // Verify token is now unsupported (uint256 usedAfter, uint256 remainingAfter) = gateway.currentTokenUsage(address(tokenA)); @@ -1320,7 +1216,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Expect revert with NotSupported vm.expectRevert(Errors.NotSupported.selector); - gateway.sendFunds(recipient, address(tokenA), 1 ether, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), 1 ether, user1)); vm.stopPrank(); } @@ -1344,20 +1240,17 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Send funds for each token vm.startPrank(user1); - // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); - // Send tokenA (50% of threshold) uint256 tokenAAmount = TOKEN_A_THRESHOLD / 2; - gateway.sendFunds(recipient, address(tokenA), tokenAAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), tokenAAmount, user1)); // Send tokenB (75% of threshold) - uint256 tokenBAmount = TOKEN_B_THRESHOLD * 3 / 4; - gateway.sendFunds(recipient, address(tokenB), tokenBAmount, revertInstructions); + uint256 tokenBAmount = (TOKEN_B_THRESHOLD * 3) / 4; + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenB), tokenBAmount, user1)); // Send native token (90% of threshold) - uint256 nativeAmount = NATIVE_THRESHOLD * 9 / 10; - gateway.sendFunds{ value: nativeAmount }(recipient, address(0), nativeAmount, revertInstructions); + uint256 nativeAmount = (NATIVE_THRESHOLD * 9) / 10; + gateway.sendUniversalTx{ value: nativeAmount }(_buildFundsTxRequest(address(0), nativeAmount, user1)); // Verify usage for each token (uint256 usedA, uint256 remainingA) = gateway.currentTokenUsage(address(tokenA)); @@ -1375,20 +1268,19 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Try to exceed threshold for tokenA vm.expectRevert(Errors.RateLimitExceeded.selector); - gateway.sendFunds( - recipient, - address(tokenA), - TOKEN_A_THRESHOLD - tokenAAmount + 1, // Just over the remaining limit - revertInstructions + gateway.sendUniversalTx{ value: 0 }( + _buildFundsTxRequest(address(tokenA), TOKEN_A_THRESHOLD - tokenAAmount + 1, user1) ); // But we should still be able to send more of tokenB and native // Send more tokenB (up to threshold) - gateway.sendFunds(recipient, address(tokenB), TOKEN_B_THRESHOLD - tokenBAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }( + _buildFundsTxRequest(address(tokenB), TOKEN_B_THRESHOLD - tokenBAmount, user1) + ); // Send more native (up to threshold) - gateway.sendFunds{ value: NATIVE_THRESHOLD - nativeAmount }( - recipient, address(0), NATIVE_THRESHOLD - nativeAmount, revertInstructions + gateway.sendUniversalTx{ value: NATIVE_THRESHOLD - nativeAmount }( + _buildFundsTxRequest(address(0), NATIVE_THRESHOLD - nativeAmount, user1) ); // Verify final usage for each token @@ -1444,12 +1336,12 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; uint256 firstAmount = TOKEN_A_THRESHOLD / 2; // Use 50% of the threshold // First transaction - gateway.sendFunds(recipient, address(tokenA), firstAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), firstAmount, user1)); // Verify usage (uint256 usedBefore, uint256 remainingBefore) = gateway.currentTokenUsage(address(tokenA)); @@ -1462,7 +1354,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { thresholds[0] = TOKEN_A_THRESHOLD / 4; // 25% of original vm.prank(admin); - gateway.updateTokenLimitThreshold(tokens, thresholds); + gateway.setTokenLimitThresholds(tokens, thresholds); // Continue sending funds vm.startPrank(user1); @@ -1472,7 +1364,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Second transaction should revert vm.expectRevert(Errors.RateLimitExceeded.selector); - gateway.sendFunds(recipient, address(tokenA), secondAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), secondAmount, user1)); // Verify usage after update - should be unchanged (uint256 usedAfter, uint256 remainingAfter) = gateway.currentTokenUsage(address(tokenA)); @@ -1497,12 +1389,12 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; uint256 firstAmount = TOKEN_A_THRESHOLD / 2; // Use 50% of the threshold // First transaction - gateway.sendFunds(recipient, address(tokenA), firstAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), firstAmount, user1)); // Store the current epoch uint256 currentEpoch = _getCurrentEpoch(); @@ -1529,7 +1421,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { uint256 secondAmount = TOKEN_A_THRESHOLD / 4; // Another 25% // Second transaction - gateway.sendFunds(recipient, address(tokenA), secondAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), secondAmount, user1)); // Verify usage after update (uint256 usedAfter, uint256 remainingAfter) = gateway.currentTokenUsage(address(tokenA)); @@ -1548,7 +1440,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { uint256 thirdAmount = TOKEN_A_THRESHOLD / 4; // Final 25% // Third transaction - gateway.sendFunds(recipient, address(tokenA), thirdAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), thirdAmount, user1)); // Warp time to the next epoch with the new duration vm.warp(block.timestamp + oldDuration); // Now we've warped by 2x the old duration = 1x new duration @@ -1560,7 +1452,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { uint256 fourthAmount = TOKEN_A_THRESHOLD; // Fourth transaction in new epoch - gateway.sendFunds(recipient, address(tokenA), fourthAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), fourthAmount, user1)); vm.stopPrank(); } @@ -1585,12 +1477,12 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; uint256 firstAmount = TOKEN_A_THRESHOLD / 3; // Use 1/3 of the threshold // First transaction - gateway.sendFunds(recipient, address(tokenA), firstAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), firstAmount, user1)); // Verify state after first transaction (uint256 usedAfterFirst, uint256 remainingAfterFirst) = gateway.currentTokenUsage(address(tokenA)); @@ -1600,7 +1492,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Second transaction uint256 secondAmount = TOKEN_A_THRESHOLD / 3; // Use another 1/3 - gateway.sendFunds(recipient, address(tokenA), secondAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), secondAmount, user1)); // Verify state after second transaction (uint256 usedAfterSecond, uint256 remainingAfterSecond) = gateway.currentTokenUsage(address(tokenA)); @@ -1652,7 +1544,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Call the function that should emit the event vm.prank(admin); - gateway.updateTokenLimitThreshold(tokens, thresholds); + gateway.setTokenLimitThresholds(tokens, thresholds); } function testEpochDurationUpdatedEvent() public { @@ -1717,12 +1609,12 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; uint256 sendAmount = TOKEN_A_THRESHOLD / 2; // Send funds - gateway.sendFunds(recipient, address(tokenA), sendAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), sendAmount, user1)); vm.stopPrank(); @@ -1752,12 +1644,12 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.startPrank(user1); // Create revert instructions - RevertInstructions memory revertInstructions = _buildDefaultRevertInstructions(); + address revertRecipient = user1; uint256 sendAmount = TOKEN_A_THRESHOLD / 2; // Send funds - gateway.sendFunds(recipient, address(tokenA), sendAmount, revertInstructions); + gateway.sendUniversalTx{ value: 0 }(_buildFundsTxRequest(address(tokenA), sendAmount, user1)); vm.stopPrank(); diff --git a/contracts/evm-gateway/test/gateway/14_gatewayPC.t.sol b/contracts/evm-gateway/test/gateway/14_gatewayPC.t.sol new file mode 100644 index 0000000..0bb75b4 --- /dev/null +++ b/contracts/evm-gateway/test/gateway/14_gatewayPC.t.sol @@ -0,0 +1,2146 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { Test } from "forge-std/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { ProxyAdmin } from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import { UniversalGatewayPC } from "../../src/UniversalGatewayPC.sol"; +import { IUniversalGatewayPC } from "../../src/interfaces/IUniversalGatewayPC.sol"; +import { TX_TYPE, RevertInstructions } from "../../src/libraries/Types.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { MockPRC20 } from "../mocks/MockPRC20.sol"; +import { MockPC20 } from "../mocks/MockPC20.sol"; +import { MockPC721 } from "../mocks/MockPC721.sol"; +import { MockUniversalCoreReal } from "../mocks/MockUniversalCoreReal.sol"; +import { MockReentrantContract } from "../mocks/MockReentrantContract.sol"; + +/** + * @title UniversalGatewayPCTest + * @notice Comprehensive test suite for UniversalGatewayPC contract + * @dev Tests initialization, admin functions, and user withdrawal flows + */ +contract UniversalGatewayPCTest is Test { + // ========================= + // ACTORS + // ========================= + address public admin; + address public pauser; + address public user1; + address public user2; + address public attacker; + address public uem; + address public vaultPC; + + // ========================= + // CONTRACTS + // ========================= + UniversalGatewayPC public gateway; + TransparentUpgradeableProxy public gatewayProxy; + ProxyAdmin public proxyAdmin; + + // ========================= + // MOCKS + // ========================= + MockUniversalCoreReal public universalCore; + MockPRC20 public prc20Token; + MockPRC20 public gasToken;// PC20 / PC721 mocks + MockPC20 public pc20Token; + MockPC721 public pc721Token; + + // ========================= + // TEST CONSTANTS + // ========================= + uint256 public constant LARGE_AMOUNT = 1000000 * 1e18; + uint256 public constant DEFAULT_GAS_LIMIT = 500_000; // Matches UniversalCore.BASE_GAS_LIMIT + uint256 public constant DEFAULT_PROTOCOL_FEE = 0.01 ether; + uint256 public constant DEFAULT_GAS_PRICE = 20 gwei; + string public constant SOURCE_CHAIN_ID = "1"; // Ethereum mainnet + string public constant SOURCE_TOKEN_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC + string public constant ETH_CHAIN_NAMESPACE = "eip155:1"; // ETHEREUM + string public constant UNSUPPORTED_CHAIN_NAMESPACE = "eip155:8453"; // UNSUPPORTED + + // ========================= + // SETUP + // ========================= + function setUp() public { + _createActors(); + _deployMocks(); + _deployGateway(); + _initializeGateway(); + _setupTokens(); + } + + // ========================= + // HELPER FUNCTIONS + // ========================= + function calculateExpectedGasFee(uint256 gasLimit) internal view returns (uint256) { + return DEFAULT_GAS_PRICE * gasLimit + DEFAULT_PROTOCOL_FEE; + } + + function buildRevertInstructions(address recipient) internal pure returns (RevertInstructions memory) { + return RevertInstructions({ + revertRecipient: recipient, + revertMsg: bytes("") + }); + } + + function testInitializeSuccess() public { + // Deploy new gateway for testing initialization + UniversalGatewayPC newImplementation = new UniversalGatewayPC(); + ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); + + bytes memory initData = abi.encodeWithSelector( + UniversalGatewayPC.initialize.selector, admin, pauser, address(universalCore), vaultPC + ); + + TransparentUpgradeableProxy newProxy = + new TransparentUpgradeableProxy(address(newImplementation), address(newProxyAdmin), initData); + + UniversalGatewayPC newGateway = UniversalGatewayPC(address(newProxy)); + + // Verify initialization + assertEq(newGateway.UNIVERSAL_CORE(), address(universalCore)); + assertEq(address(newGateway.VAULT_PC()), vaultPC); + assertTrue(newGateway.hasRole(newGateway.DEFAULT_ADMIN_ROLE(), admin)); + assertTrue(newGateway.hasRole(newGateway.PAUSER_ROLE(), pauser)); + } + + function testInitializeRevertZeroAdmin() public { + UniversalGatewayPC newImplementation = new UniversalGatewayPC(); + ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); + + bytes memory initData = abi.encodeWithSelector( + UniversalGatewayPC.initialize.selector, + address(0), // zero admin + pauser, + address(universalCore), + vaultPC + ); + + vm.expectRevert(); // Proxy wraps the error + new TransparentUpgradeableProxy(address(newImplementation), address(newProxyAdmin), initData); + } + + function testInitializeRevertZeroPauser() public { + UniversalGatewayPC newImplementation = new UniversalGatewayPC(); + ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); + + bytes memory initData = abi.encodeWithSelector( + UniversalGatewayPC.initialize.selector, + admin, + address(0), // zero pauser + address(universalCore), + vaultPC + ); + + vm.expectRevert(); // Proxy wraps the error + new TransparentUpgradeableProxy(address(newImplementation), address(newProxyAdmin), initData); + } + + function testInitializeRevertZeroUniversalCore() public { + UniversalGatewayPC newImplementation = new UniversalGatewayPC(); + ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); + + bytes memory initData = abi.encodeWithSelector( + UniversalGatewayPC.initialize.selector, + admin, + pauser, + address(0), // zero universal core + vaultPC + ); + + vm.expectRevert(); // Proxy wraps the error + new TransparentUpgradeableProxy(address(newImplementation), address(newProxyAdmin), initData); + } + + function testInitializeRevertZeroVaultPC() public { + UniversalGatewayPC newImplementation = new UniversalGatewayPC(); + ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); + + bytes memory initData = abi.encodeWithSelector( + UniversalGatewayPC.initialize.selector, + admin, + pauser, + address(universalCore), + address(0) // zero vaultPC + ); + + vm.expectRevert(); // Proxy wraps the error + new TransparentUpgradeableProxy(address(newImplementation), address(newProxyAdmin), initData); + } + + function testInitializeRevertDoubleInit() public { + UniversalGatewayPC newImplementation = new UniversalGatewayPC(); + ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); + + bytes memory initData = abi.encodeWithSelector( + UniversalGatewayPC.initialize.selector, admin, pauser, address(universalCore), vaultPC + ); + + TransparentUpgradeableProxy newProxy = + new TransparentUpgradeableProxy(address(newImplementation), address(newProxyAdmin), initData); + + UniversalGatewayPC newGateway = UniversalGatewayPC(address(newProxy)); + + // Try to initialize again + vm.expectRevert(); + newGateway.initialize(admin, pauser, address(universalCore), vaultPC); + } + + // ========================= + // ADMIN FUNCTION TESTS + // ========================= + + function testSetVaultPCSuccess() public { + address newVaultPC = address(0x999); + + // Admin sets new VaultPC + vm.prank(admin); + vm.expectEmit(true, true, false, false); + emit IUniversalGatewayPC.VaultPCUpdated(vaultPC, newVaultPC); + gateway.setVaultPC(newVaultPC); + + // Verify state changes + assertEq(address(gateway.VAULT_PC()), newVaultPC); + } + + function testSetVaultPCRevertNonAdmin() public { + address newVaultPC = address(0x999); + + vm.prank(attacker); + vm.expectRevert(); + gateway.setVaultPC(newVaultPC); + } + + function testSetVaultPCRevertZeroAddress() public { + vm.prank(admin); + vm.expectRevert(Errors.ZeroAddress.selector); + gateway.setVaultPC(address(0)); + } + + function testSetVaultPCRevertWhenPaused() public { + // Pause the gateway first + vm.prank(pauser); + gateway.pause(); + + address newVaultPC = address(0x999); + + // Attempt to set VaultPC while paused should revert + vm.prank(admin); + vm.expectRevert(); + gateway.setVaultPC(newVaultPC); + } + + function testPauseSuccess() public { + assertFalse(gateway.paused()); + + vm.prank(pauser); + gateway.pause(); + + assertTrue(gateway.paused()); + } + + function testPauseRevertNonPauser() public { + vm.prank(attacker); + vm.expectRevert(); + gateway.pause(); + } + + function testPauseRevertAlreadyPaused() public { + // Pause the contract + vm.prank(pauser); + gateway.pause(); + + // Try to pause again + vm.prank(pauser); + vm.expectRevert(); + gateway.pause(); + } + + function testUnpauseSuccess() public { + // Pause the contract first + vm.prank(pauser); + gateway.pause(); + assertTrue(gateway.paused()); + + // Pauser unpauses the contract + vm.prank(pauser); + gateway.unpause(); + + // Verify contract is unpaused + assertFalse(gateway.paused()); + } + + function testUnpauseRevertNonPauser() public { + // Pause the contract first + vm.prank(pauser); + gateway.pause(); + + // Non-pauser tries to unpause + vm.prank(attacker); + vm.expectRevert(); + gateway.unpause(); + } + + function testUnpauseRevertNotPaused() public { + // Contract is not paused initially + assertFalse(gateway.paused()); + + // Try to unpause + vm.prank(pauser); + vm.expectRevert(); + gateway.unpause(); + } + + function testAdminFunctionsWorkWhenPaused() public { + // Pause the contract + vm.prank(pauser); + gateway.pause(); + + // Verify that the contract is paused + assertTrue(gateway.paused()); + + // Unpause should still work + vm.prank(pauser); + gateway.unpause(); + + assertFalse(gateway.paused()); + } + + // ========================= + // WITHDRAW FUNCTION TESTS + // ========================= + + function testWithdrawSuccessWithCustomGasLimit() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = 150_000; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + // Ensure user has enough balance + uint256 userBalance = prc20Token.balanceOf(user1); + if (userBalance < amount) { + prc20Token.mint(user1, amount); + vm.prank(user1); + prc20Token.approve(address(gateway), amount); + } + + uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); + uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); + uint256 initialPrc20Balance = prc20Token.balanceOf(user1); + + vm.prank(user1); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + + // Verify token balances + assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); + assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); + } + + function testWithdrawEventEmission() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = 150_000; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.recordLogs(); + vm.prank(user1); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertTrue(logs.length >= 1, "At least one event should be emitted"); + } + + function testWithdrawEventEmissionWithTxType() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = 150_000; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); + + // Expect the UniversalTxOutbound event with TX_TYPE.FUNDS + vm.expectEmit(false, true, true, true); + emit IUniversalGatewayPC.UniversalTxOutbound( + bytes32(0), // txId (will be computed) + user1, // sender + address(prc20Token), // token + SOURCE_CHAIN_ID, // chainNamespace + to, // target + amount, // amount + address(gasToken), // gasToken + expectedGasFee, // gasFee + gasLimit, // gasLimit + bytes(""), // payload (empty for withdraw) + DEFAULT_PROTOCOL_FEE, // protocolFee + revertRecipient, // revertRecipient + TX_TYPE.FUNDS // txType + ); + + vm.prank(user1); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + } + + function testWithdrawSuccessWithDefaultGasLimit() public { + uint256 amount = 1000 * 1e6; // 1000 USDC (6 decimals) + uint256 gasLimit = 0; // Use default gas limit + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + uint256 expectedGasFee = calculateExpectedGasFee(DEFAULT_GAS_LIMIT); + uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); + uint256 initialPrc20Balance = prc20Token.balanceOf(user1); + + vm.prank(user1); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + + // Verify token balances + assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); + assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); + } + + function testWithdrawRevertEmptyTarget() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = bytes(""); // Empty target + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert(Errors.InvalidInput.selector); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + } + + function testWithdrawRevertZeroToken() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert(Errors.ZeroAddress.selector); + gateway.sendUniversalTxOutbound(to, address(0), amount, 0, gasLimit, "", "", revertCfg); + } + + function testWithdrawRevertZeroAmount() public { + uint256 amount = 0; // Zero amount + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert(Errors.InvalidAmount.selector); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + } + + function testWithdrawRevertInvalidRecipient() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = address(0); // Zero recipient + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert(Errors.InvalidRecipient.selector); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + } + + function testWithdrawRevertWhenPaused() public { + vm.prank(pauser); + gateway.pause(); + + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert(); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + } + + function testWithdrawRevertInsufficientGasTokenBalance() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + // Calculate required gas fee + uint256 requiredGasFee = calculateExpectedGasFee(gasLimit); + + // Set user1's gas token balance to less than required + uint256 currentBalance = gasToken.balanceOf(user1); + vm.prank(user1); + gasToken.transfer(address(0xdead), currentBalance); + + // Give user1 insufficient gas tokens (less than required fee) + if (requiredGasFee > 1) { + gasToken.mint(user1, requiredGasFee - 1); + vm.prank(user1); + gasToken.approve(address(gateway), type(uint256).max); + } + + vm.prank(user1); + vm.expectRevert(); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + } + + function testWithdrawRevertInsufficientGasTokenAllowance() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + // Remove gas token allowance + vm.prank(user1); + gasToken.approve(address(gateway), 0); + + vm.prank(user1); + vm.expectRevert("MockPRC20: insufficient allowance"); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + } + + function testWithdrawRevertInsufficientPrc20Balance() public { + uint256 amount = LARGE_AMOUNT + 1; // More than user has + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert("MockPRC20: insufficient balance"); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + } + + // ========================= + // WITHDRAW AND EXECUTE FUNCTION TESTS + // ========================= + + function testWithdrawAndExecuteSuccessWithCustomGasLimit() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = 200_000; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); + uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); + uint256 initialPrc20Balance = prc20Token.balanceOf(user1); + + vm.prank(user1); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + + // Verify token balances + assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); + assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); + } + + function testWithdrawAndExecuteEventEmission() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = 200_000; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.recordLogs(); + vm.prank(user1); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertTrue(logs.length >= 1, "At least one event should be emitted"); + } + + function testWithdrawAndExecuteEventEmissionWithTxType() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = 200_000; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); + + // Expect the UniversalTxOutbound event with TX_TYPE.FUNDS_AND_PAYLOAD + vm.expectEmit(false, true, true, true); + emit IUniversalGatewayPC.UniversalTxOutbound( + bytes32(0), // txId (will be computed) + user1, // sender + address(prc20Token), // token + SOURCE_CHAIN_ID, // chainNamespace + target, // target + amount, // amount + address(gasToken), // gasToken + expectedGasFee, // gasFee + gasLimit, // gasLimit + payload, // payload (non-empty for withdrawAndExecute) + DEFAULT_PROTOCOL_FEE, // protocolFee + revertRecipient, // revertRecipient + TX_TYPE.FUNDS_AND_PAYLOAD // txType + ); + + vm.prank(user1); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + } + + function testWithdrawAndExecuteSuccessWithDefaultGasLimit() public { + uint256 amount = 1000 * 1e6; // 1000 USDC (6 decimals) + uint256 gasLimit = 0; // Use default gas limit + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + uint256 expectedGasFee = calculateExpectedGasFee(DEFAULT_GAS_LIMIT); + uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); + uint256 initialPrc20Balance = prc20Token.balanceOf(user1); + + vm.prank(user1); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + + // Verify token balances + assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); + assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); + } + + function testWithdrawAndExecuteSuccessWithEmptyPayload() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = bytes(""); // Empty payload + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); + uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); + uint256 initialPrc20Balance = prc20Token.balanceOf(user1); + + vm.prank(user1); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + + // Verify token balances + assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); + assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); + } + + function testWithdrawAndExecuteSuccessWithComplexPayload() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + + // Complex payload with multiple parameters + bytes memory payload = abi.encodeWithSignature( + "complexFunction(address,uint256,bytes32,string)", + user2, + 1000, + keccak256("test"), + "complex string parameter" + ); + + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); + uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); + uint256 initialPrc20Balance = prc20Token.balanceOf(user1); + + vm.prank(user1); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + + // Verify token balances + assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); + assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); + } + + function testWithdrawAndExecuteRevertEmptyTarget() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = bytes(""); // Empty target + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert(Errors.InvalidInput.selector); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + } + + function testWithdrawAndExecuteRevertZeroToken() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert(Errors.ZeroAddress.selector); + gateway.sendUniversalTxOutbound(target, address(0), amount, 0, gasLimit, payload, "", revertCfg); + } + + function testWithdrawAndExecuteRevertZeroAmount() public { + uint256 amount = 0; // Zero amount + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert(Errors.InvalidAmount.selector); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + } + + function testWithdrawAndExecuteRevertInvalidRecipient() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = address(0); // Zero recipient + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert(Errors.InvalidRecipient.selector); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + } + + function testWithdrawAndExecuteRevertWhenPaused() public { + vm.prank(pauser); + gateway.pause(); + + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert(); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + } + + function testWithdrawAndExecuteRevertInsufficientGasTokenBalance() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + // Calculate required gas fee + uint256 requiredGasFee = calculateExpectedGasFee(gasLimit); + + // Set user1's gas token balance to less than required + uint256 currentBalance = gasToken.balanceOf(user1); + vm.prank(user1); + gasToken.transfer(address(0xdead), currentBalance); + + // Give user1 insufficient gas tokens (less than required fee) + if (requiredGasFee > 1) { + gasToken.mint(user1, requiredGasFee - 1); + vm.prank(user1); + gasToken.approve(address(gateway), type(uint256).max); + } + + vm.prank(user1); + vm.expectRevert(); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + } + + function testWithdrawAndExecuteRevertInsufficientGasTokenAllowance() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + // Remove gas token allowance + vm.prank(user1); + gasToken.approve(address(gateway), 0); + + vm.prank(user1); + vm.expectRevert("MockPRC20: insufficient allowance"); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + } + + function testWithdrawAndExecuteRevertInsufficientPrc20Balance() public { + uint256 amount = LARGE_AMOUNT + 1; // More than user has + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + vm.expectRevert("MockPRC20: insufficient balance"); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, payload, "", revertCfg); + } + + function testWithdrawAndExecuteDifferentPayloadSizes() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + uint256 initialBalance = prc20Token.balanceOf(user1); + + // Test with small payload + bytes memory smallPayload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + + vm.prank(user1); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, smallPayload, "", revertCfg); + + // Reset balances for next test + prc20Token.mint(user1, amount); + vm.prank(user1); + prc20Token.approve(address(gateway), amount); + + // Test with large payload + bytes memory largePayload = abi.encodeWithSignature( + "largeFunction(address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)", + user2, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ); + + vm.prank(user1); + gateway.sendUniversalTxOutbound(target, address(prc20Token), amount, 0, gasLimit, largePayload, "", revertCfg); + + // Both should succeed - verify final balance + uint256 finalBalance = prc20Token.balanceOf(user1); + assertEq(finalBalance, initialBalance - amount); + } + + // ========================= + // EDGE CASES & ADDITIONAL TESTS + // ========================= + + function testReentrancyProtection() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + // Create a contract that calls the gateway + MockReentrantContract reentrantContract = + new MockReentrantContract(address(gateway), address(prc20Token), address(gasToken)); + + // Fund the contract + prc20Token.mint(address(reentrantContract), amount); + gasToken.mint(address(reentrantContract), LARGE_AMOUNT); + + vm.prank(address(reentrantContract)); + prc20Token.approve(address(gateway), amount); + vm.prank(address(reentrantContract)); + gasToken.approve(address(gateway), type(uint256).max); + + // Call should succeed (reentrancy protection is for preventing recursive calls during execution) + vm.prank(address(reentrantContract)); + reentrantContract.attemptReentrancy(to, amount, gasLimit, revertRecipient); + + // Verify the withdrawal succeeded + assertEq(prc20Token.balanceOf(address(reentrantContract)), 0); + } + + function testReentrancyProtectionWithExecute() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + // Create a contract that calls the gateway + MockReentrantContract reentrantContract = + new MockReentrantContract(address(gateway), address(prc20Token), address(gasToken)); + + // Fund the contract + prc20Token.mint(address(reentrantContract), amount); + gasToken.mint(address(reentrantContract), LARGE_AMOUNT); + + vm.prank(address(reentrantContract)); + prc20Token.approve(address(gateway), amount); + vm.prank(address(reentrantContract)); + gasToken.approve(address(gateway), type(uint256).max); + + // Call should succeed (reentrancy protection is for preventing recursive calls during execution) + vm.prank(address(reentrantContract)); + reentrantContract.attemptReentrancyWithExecute(target, amount, payload, gasLimit, revertCfg); + + // Verify the withdrawal succeeded + assertEq(prc20Token.balanceOf(address(reentrantContract)), 0); + } + + function testMaxGasLimit() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = 1_000_000; // Large gas limit + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); + uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); + + // Ensure user has enough gas tokens for the fee + uint256 userGasBalance = gasToken.balanceOf(user1); + if (userGasBalance < expectedGasFee) { + gasToken.mint(user1, expectedGasFee - userGasBalance + 1 ether); + vm.prank(user1); + gasToken.approve(address(gateway), type(uint256).max); + } + + vm.prank(user1); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + + // Verify withdrawal with max gas limit succeeded + assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); + } + + function testGasFeeCalculationAccuracy() public { + uint256 amount = 1000 * 1e6; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + // Test specific gas limits individually + _testGasFeeForLimit(amount, to, revertRecipient, 50_000); + _testGasFeeForLimit(amount, to, revertRecipient, 100_000); + _testGasFeeForLimit(amount, to, revertRecipient, 200_000); + _testGasFeeForLimit(amount, to, revertRecipient, 500_000); + _testGasFeeForLimit(amount, to, revertRecipient, 1_000_000); + } + + function _testGasFeeForLimit(uint256 amount, bytes memory to, address revertRecipient, uint256 gasLimit) internal { + uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); + uint256 balanceBefore = gasToken.balanceOf(vaultPC); + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + vm.prank(user1); + gateway.sendUniversalTxOutbound(to, address(prc20Token), amount, 0, gasLimit, "", "", revertCfg); + + uint256 balanceAfter = gasToken.balanceOf(vaultPC); + assertEq(balanceAfter - balanceBefore, expectedGasFee); + + // Reset for next iteration + prc20Token.mint(user1, amount); + vm.prank(user1); + prc20Token.approve(address(gateway), amount); + } + + function testSetVaultPCToZeroReverts() public { + // Attempt to set VaultPC to zero should revert + vm.prank(admin); + vm.expectRevert(Errors.ZeroAddress.selector); + gateway.setVaultPC(address(0)); + } + + function testInvalidFeeQuoteZeroGasToken() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + // Create token with unconfigured chain ID (no gas token set for this chain) + string memory unconfiguredChainId = "999"; // Chain ID not configured in universalCore + MockPRC20 invalidToken = new MockPRC20( + "Invalid Token", + "INV", + 6, + unconfiguredChainId, + MockPRC20.TokenType.ERC20, + DEFAULT_PROTOCOL_FEE, + address(universalCore), + SOURCE_TOKEN_ADDRESS + ); + + // Mark token as supported + vm.prank(admin); + universalCore.setSupportedToken(address(invalidToken), true); + + // Setup token for user1 + invalidToken.mint(user1, amount); + vm.prank(user1); + invalidToken.approve(address(gateway), amount); + + // Withdrawal should fail with "MockUniversalCore: zero gas token" error + vm.prank(user1); + vm.expectRevert("MockUniversalCore: zero gas token"); + gateway.sendUniversalTxOutbound(to, address(invalidToken), amount, 0, gasLimit, "", "", revertCfg); + } + + function _createInvalidToken() internal returns (MockPRC20) { + return new MockPRC20( + "Invalid Token", + "INV", + 6, + SOURCE_CHAIN_ID, + MockPRC20.TokenType.ERC20, + DEFAULT_PROTOCOL_FEE, + address(universalCore), + SOURCE_TOKEN_ADDRESS + ); + } + + function _createInvalidCoreWithZeroGasToken() internal returns (MockUniversalCoreReal) { + MockUniversalCoreReal invalidCore = new MockUniversalCoreReal(uem); + vm.prank(uem); + invalidCore.setGasPrice(SOURCE_CHAIN_ID, DEFAULT_GAS_PRICE); + vm.prank(uem); + invalidCore.setGasTokenPRC20(SOURCE_CHAIN_ID, address(0)); // Zero gas token + return invalidCore; + } + + function testInvalidFeeQuoteZeroGasFee() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + // Create token with a chain ID that has gas token but no gas price configured + string memory chainWithTokenNoPrice = "777"; + + // Configure this chain in universalCore with gas token but NO gas price + vm.prank(uem); + universalCore.setGasTokenPRC20(chainWithTokenNoPrice, address(gasToken)); + // Intentionally NOT setting gas price for this chain + + MockPRC20 invalidToken = new MockPRC20( + "Invalid Token", + "INV", + 6, + chainWithTokenNoPrice, + MockPRC20.TokenType.ERC20, + DEFAULT_PROTOCOL_FEE, + address(universalCore), + SOURCE_TOKEN_ADDRESS + ); + + // Mark token as supported + vm.prank(admin); + universalCore.setSupportedToken(address(invalidToken), true); + + // Setup token for user1 + invalidToken.mint(user1, amount); + vm.prank(user1); + invalidToken.approve(address(gateway), amount); + + // Withdrawal should fail with "MockUniversalCore: zero gas price" error + vm.prank(user1); + vm.expectRevert("MockUniversalCore: zero gas price"); + gateway.sendUniversalTxOutbound(to, address(invalidToken), amount, 0, gasLimit, "", "", revertCfg); + } + + function _createInvalidCoreWithZeroGasPrice() internal returns (MockUniversalCoreReal) { + MockUniversalCoreReal invalidCore = new MockUniversalCoreReal(uem); + vm.prank(uem); + invalidCore.setGasPrice(SOURCE_CHAIN_ID, 0); // Zero gas price + vm.prank(uem); + invalidCore.setGasTokenPRC20(SOURCE_CHAIN_ID, address(gasToken)); + return invalidCore; + } + + function testTokenBurnFailure() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory to = abi.encodePacked(user2); + address revertRecipient = user2; + RevertInstructions memory revertCfg = buildRevertInstructions(revertRecipient); + + // Create failing token + MockPRC20 failingToken = _createFailingToken(); + + // Mark token as supported + vm.prank(admin); + universalCore.setSupportedToken(address(failingToken), true); + + // Setup token for user1 + failingToken.mint(user1, amount); + vm.prank(user1); + failingToken.approve(address(gateway), amount); + + // Mock the burn function to fail by setting balance to 0 + failingToken.setBalance(user1, 0); + + // Withdrawal should fail with transfer failure + vm.prank(user1); + vm.expectRevert("MockPRC20: insufficient balance"); + gateway.sendUniversalTxOutbound(to, address(failingToken), amount, 0, gasLimit, "", "", revertCfg); + } + + // ========================= + // PC20 OUTBOUND TESTS + // ========================= + + function testPC20_FundsOnly_Success() public { + uint256 amount = 1000 ether; + bytes memory target = abi.encodePacked(user2); + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + uint256 beforeVaultBalance = vaultPC.balance; + uint256 beforeUserBalance = pc20Token.balanceOf(user1); + + // PC20 outbound uses native PC for protocol fee + vm.prank(user1); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc20Token), + amount, + 0, + 0, // gasLimit, ignored for PC20 in current fee model + "", + ETH_CHAIN_NAMESPACE, + revertCfg + ); + + // Vault receives the native fee + assertEq(vaultPC.balance, beforeVaultBalance + DEFAULT_PROTOCOL_FEE); + + // PC20 moved from user to gateway + assertEq(pc20Token.balanceOf(user1), beforeUserBalance - amount); + assertEq(pc20Token.balanceOf(address(gateway)), amount); + } + + function testPC20_FundsAndPayload_Success() public { + uint256 amount = 500 ether; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("someFunction(uint256)", 42); + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + uint256 beforeVaultBalance = vaultPC.balance; + uint256 beforeUserBalance = pc20Token.balanceOf(user1); + + vm.prank(user1); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc20Token), + amount, + 0, + 123_456, // gasLimit hint, still flat fee now + payload, + ETH_CHAIN_NAMESPACE, + revertCfg + ); + + assertEq(vaultPC.balance, beforeVaultBalance + DEFAULT_PROTOCOL_FEE); + assertEq(pc20Token.balanceOf(user1), beforeUserBalance - amount); + assertEq(pc20Token.balanceOf(address(gateway)), amount); + } + + function testPC20_Revert_EmptyChainNamespace() public { + uint256 amount = 1000 ether; + bytes memory target = abi.encodePacked(user2); + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + vm.prank(user1); + vm.expectRevert(Errors.InvalidInput.selector); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc20Token), + amount, + 0, + 0, + "", + "", // empty chainNamespace + revertCfg + ); + } + + function testPC20_Revert_UnsupportedChainNamespace() public { + uint256 amount = 1000 ether; + bytes memory target = abi.encodePacked(user2); + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + // Disable PC20 support on ETH_CHAIN_NAMESPACE + vm.prank(admin); + universalCore.setPC20SupportOnChain(ETH_CHAIN_NAMESPACE, false); + + vm.prank(user1); + vm.expectRevert(Errors.InvalidInput.selector); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc20Token), + amount, + 0, + 0, + "", + ETH_CHAIN_NAMESPACE, + revertCfg + ); + } + + function testPC20_Revert_InsufficientNativeFee() public { + uint256 amount = 1000 ether; + bytes memory target = abi.encodePacked(user2); + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + // Send less native value than required protocol fee + vm.prank(user1); + vm.expectRevert(Errors.InvalidAmount.selector); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE - 1}( + target, + address(pc20Token), + amount, + 0, + 0, + "", + ETH_CHAIN_NAMESPACE, + revertCfg + ); + } + // ========================= + // PC721 OUTBOUND TESTS + // ========================= + + function testPC721_FundsOnly_Success() public { + uint256 tokenId = 1; + bytes memory target = abi.encodePacked(user2); + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + uint256 beforeVaultBalance = vaultPC.balance; + address beforeOwner = pc721Token.ownerOf(tokenId); + + assertEq(beforeOwner, user1); + + vm.prank(user1); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc721Token), + 0, + tokenId, + 0, + "", + ETH_CHAIN_NAMESPACE, + revertCfg + ); + + assertEq(vaultPC.balance, beforeVaultBalance + DEFAULT_PROTOCOL_FEE); + assertEq(pc721Token.ownerOf(tokenId), address(gateway)); + } + + function testPC721_FundsAndPayload_Success() public { + uint256 tokenId = 2; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("doSomething(address,uint256)", user2, tokenId); + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + uint256 beforeVaultBalance = vaultPC.balance; + address beforeOwner = pc721Token.ownerOf(tokenId); + assertEq(beforeOwner, user1); + + vm.prank(user1); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc721Token), + 0, + tokenId, + 250_000, + payload, + ETH_CHAIN_NAMESPACE, + revertCfg + ); + + assertEq(vaultPC.balance, beforeVaultBalance + DEFAULT_PROTOCOL_FEE); + assertEq(pc721Token.ownerOf(tokenId), address(gateway)); + } + + function testPC721_Revert_EmptyChainNamespace() public { + uint256 tokenId = 3; + bytes memory target = abi.encodePacked(user2); + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + // ensure user2 owns tokenId 3 in setup + assertEq(pc721Token.ownerOf(3), user2); + + vm.prank(user2); + vm.expectRevert(Errors.InvalidInput.selector); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc721Token), + 0, + tokenId, + 0, + "", + "", + revertCfg + ); + } + + function testPC721_Revert_UnsupportedChainNamespace() public { + uint256 tokenId = 1; + bytes memory target = abi.encodePacked(user2); + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + vm.prank(admin); + universalCore.setPC721SupportOnChain(ETH_CHAIN_NAMESPACE, false); + + vm.prank(user1); + vm.expectRevert(Errors.InvalidInput.selector); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc721Token), + 0, + tokenId, + 0, + "", + ETH_CHAIN_NAMESPACE, + revertCfg + ); + } + + function testPC721_Revert_InsufficientNativeFee() public { + uint256 tokenId = 2; + bytes memory target = abi.encodePacked(user2); + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + vm.prank(user1); + vm.expectRevert(Errors.InvalidAmount.selector); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE - 1}( + target, + address(pc721Token), + 0, + tokenId, + 0, + "", + ETH_CHAIN_NAMESPACE, + revertCfg + ); + } + + function test_UniversalTxOutbound_TxIdComputedCorrectly_PRC20() public { + uint256 amount = 1000 * 1e6; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = ""; // keep simple + string memory chainNamespace = ""; // PRC20 path ignores this for routing + + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + // 1) Read nonce BEFORE call (this is what the contract hashes in) + uint256 nonceBefore = gateway.outboundTxNonce(); + + // 2) Execute and record logs + vm.recordLogs(); + vm.prank(user1); + gateway.sendUniversalTxOutbound( + target, + address(prc20Token), + amount, + 0, // tokenId + gasLimit, + payload, + chainNamespace, + revertCfg + ); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + // 3) Find the first log from gateway and take topics[1] as txId + bytes32 actualTxId; + bool found; + + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].emitter == address(gateway)) { + // topic[0] = event signature, topic[1] = first indexed arg (txId) + require(logs[i].topics.length > 1, "no indexed txId in event"); + actualTxId = logs[i].topics[1]; + found = true; + break; + } + } + + assertTrue(found, "UniversalTxOutbound event not found"); + + // 4) Compute expectedTxId with EXACT same formula as the contract + bytes32 expectedTxId = keccak256( + abi.encode( + bytes32("PUSH.OUTBOUND.TX"), + user1, // msg.sender + address(prc20Token), // token + amount, // amount + uint256(0), // tokenId + keccak256(payload), // keccak(payload) + keccak256(bytes(chainNamespace)), // keccak(bytes(chainNamespace)) + nonceBefore // nonce before increment + ) + ); + + assertEq(actualTxId, expectedTxId, "txId mismatch"); + } + + function test_UniversalTxOutbound_TxIdComputedCorrectly_PC20() public { + // Enable PC20 support on this chain namespace + vm.prank(admin); + universalCore.setPC20SupportOnChain(ETH_CHAIN_NAMESPACE, true); + + // Deploy and fund PC20 for user1 + MockPC20 pc20 = new MockPC20("PC20 Test Token", "PC20T"); + pc20.mint(user1, 1_000 ether); + + vm.prank(user1); + pc20.approve(address(gateway), type(uint256).max); + + uint256 amount = 100 ether; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("pc20Function(address,uint256)", user2, amount); + + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + // 1) Capture nonce BEFORE call (this is what the contract hashes in) + uint256 nonceBefore = gateway.outboundTxNonce(); + + // 2) Execute and record logs + vm.recordLogs(); + vm.prank(user1); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc20), + amount, + 0, // tokenId (not used for PC20) + gasLimit, + payload, + ETH_CHAIN_NAMESPACE, + revertCfg + ); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + // 3) Find the UniversalTxOutbound event and extract txId from topics[1] + bytes32 eventSig = keccak256( + "UniversalTxOutbound(bytes32,address,address,string,bytes,uint256,address,uint256,uint256,bytes,uint256,address,uint8)" + ); + + bytes32 actualTxId; + bool found; + + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == eventSig) { + actualTxId = logs[i].topics[1]; + found = true; + break; + } + } + + assertTrue(found, "UniversalTxOutbound event not found for PC20"); + + // 4) Compute expectedTxId with EXACT same formula as the contract + bytes32 expectedTxId = keccak256( + abi.encode( + bytes32("PUSH.OUTBOUND.TX"), + user1, // msg.sender + address(pc20), // token + amount, // amount + uint256(0), // tokenId + keccak256(payload), // keccak(payload) + keccak256(bytes(ETH_CHAIN_NAMESPACE)), // keccak(bytes(chainNamespace)) + nonceBefore // nonce before increment + ) + ); + + assertEq(actualTxId, expectedTxId, "txId mismatch for PC20"); + } + + function test_UniversalTxOutbound_TxIdComputedCorrectly_PC721() public { + // Enable PC721 support on this chain namespace + vm.prank(admin); + universalCore.setPC721SupportOnChain(ETH_CHAIN_NAMESPACE, true); + + // Deploy and mint PC721 for user1 + MockPC721 pc721 = new MockPC721("PC721 Test Token", "PC721T"); + uint256 tokenId = 1; + pc721.mint(user1, tokenId); + + vm.prank(user1); + pc721.approve(address(gateway), tokenId); + + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("pc721Function(address,uint256)", user2, tokenId); + + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + // 1) Capture nonce BEFORE call (this is what the contract hashes in) + uint256 nonceBefore = gateway.outboundTxNonce(); + + // 2) Execute and record logs + vm.recordLogs(); + vm.prank(user1); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc721), + 0, // amount (not used for PC721) + tokenId, + gasLimit, + payload, + ETH_CHAIN_NAMESPACE, + revertCfg + ); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + // 3) Find the UniversalTxOutbound event and extract txId from topics[1] + bytes32 eventSig = keccak256( + "UniversalTxOutbound(bytes32,address,address,string,bytes,uint256,address,uint256,uint256,bytes,uint256,address,uint8)" + ); + + bytes32 actualTxId; + bool found; + + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == eventSig) { + actualTxId = logs[i].topics[1]; + found = true; + break; + } + } + + assertTrue(found, "UniversalTxOutbound event not found for PC721"); + + // 4) Compute expectedTxId with EXACT same formula as the contract + bytes32 expectedTxId = keccak256( + abi.encode( + bytes32("PUSH.OUTBOUND.TX"), + user1, // msg.sender + address(pc721), // token + uint256(0), // amount + tokenId, // tokenId + keccak256(payload), // keccak(payload) + keccak256(bytes(ETH_CHAIN_NAMESPACE)), // keccak(bytes(chainNamespace)) + nonceBefore // nonce before increment + ) + ); + + assertEq(actualTxId, expectedTxId, "txId mismatch for PC721"); + } + + function test_UniversalTxOutbound_TxIdComputedCorrectly_PayloadOnly() public { + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory payload = abi.encodeWithSignature("executeAction(address,string)", user2, "test"); + + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + // 1) Capture nonce BEFORE call (this is what the contract hashes in) + uint256 nonceBefore = gateway.outboundTxNonce(); + + // 2) Execute and record logs + vm.recordLogs(); + vm.prank(user1); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(0), // token = address(0) for payload-only + 0, // amount + 0, // tokenId + gasLimit, + payload, + ETH_CHAIN_NAMESPACE, + revertCfg + ); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + // 3) Find the UniversalTxOutbound event and extract txId from topics[1] + bytes32 eventSig = keccak256( + "UniversalTxOutbound(bytes32,address,address,string,bytes,uint256,address,uint256,uint256,bytes,uint256,address,uint8)" + ); + + bytes32 actualTxId; + bool found; + + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == eventSig) { + actualTxId = logs[i].topics[1]; + found = true; + break; + } + } + + assertTrue(found, "UniversalTxOutbound event not found for PayloadOnly"); + + // 4) Compute expectedTxId with EXACT same formula as the contract + bytes32 expectedTxId = keccak256( + abi.encode( + bytes32("PUSH.OUTBOUND.TX"), + user1, // msg.sender + address(0), // token (address(0) for payload-only) + uint256(0), // amount + uint256(0), // tokenId + keccak256(payload), // keccak(payload) + keccak256(bytes(ETH_CHAIN_NAMESPACE)), // keccak(bytes(chainNamespace)) + nonceBefore // nonce before increment + ) + ); + + assertEq(actualTxId, expectedTxId, "txId mismatch for PayloadOnly"); + } + + // ========================= + // MAGIC MARKER TESTS + // ========================= + + function test_PC20_MagicMarkerInPayload() public { + // Enable PC20 support on this chain namespace + vm.prank(admin); + universalCore.setPC20SupportOnChain(ETH_CHAIN_NAMESPACE, true); + + // Deploy and fund PC20 for user1 + MockPC20 pc20 = new MockPC20("PC20 Test Token", "PC20T"); + pc20.mint(user1, 1_000 ether); + + vm.prank(user1); + pc20.approve(address(gateway), type(uint256).max); + + uint256 amount = 100 ether; + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory originalPayload = abi.encodeWithSignature("pc20Function(address,uint256)", user2, amount); + + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + // Execute and record logs + vm.recordLogs(); + vm.prank(user1); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc20), + amount, + 0, + gasLimit, + originalPayload, + ETH_CHAIN_NAMESPACE, + revertCfg + ); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + // Find the UniversalTxOutbound event + bytes32 eventSig = keccak256( + "UniversalTxOutbound(bytes32,address,address,string,bytes,uint256,address,uint256,uint256,bytes,uint256,address,uint8)" + ); + + bytes memory emittedPayload; + bool found; + + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == eventSig) { + // Decode the event data to extract the payload + ( + , // skip chainNamespace + , // skip target + , // skip amount + , // skip gasToken + , // skip gasFee + , // skip gasLimit + emittedPayload, + , // skip protocolFee + , // skip revertRecipient + // skip txType + ) = abi.decode(logs[i].data, (string, bytes, uint256, address, uint256, uint256, bytes, uint256, address, TX_TYPE)); + found = true; + break; + } + } + + assertTrue(found, "UniversalTxOutbound event not found"); + + // Verify magic marker is present in the payload + // Magic marker constants + bytes4 MAGIC_PCAS = 0x50434153; // "PCAS" + uint8 META_VERSION = 1; + uint8 META_KIND_PC20 = 1; + + // Construct expected enriched payload + bytes memory expectedEnrichedPayload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC20, + address(pc20), + pc20.name(), + pc20.symbol(), + pc20.decimals() + ); + + bytes memory expectedFinalPayload = abi.encodePacked(expectedEnrichedPayload, originalPayload); + + assertEq(emittedPayload, expectedFinalPayload, "Payload with magic marker mismatch for PC20"); + + // Verify the magic marker is at the beginning + bytes4 extractedMagic; + assembly { + extractedMagic := mload(add(emittedPayload, 32)) + } + assertEq(extractedMagic, MAGIC_PCAS, "Magic marker PCAS not found at payload start"); + } + + function test_PC721_MagicMarkerInPayload() public { + // Enable PC721 support on this chain namespace + vm.prank(admin); + universalCore.setPC721SupportOnChain(ETH_CHAIN_NAMESPACE, true); + + // Deploy and mint PC721 for user1 + MockPC721 pc721 = new MockPC721("PC721 Test Token", "PC721T"); + uint256 tokenId = 1; + pc721.mint(user1, tokenId); + + vm.prank(user1); + pc721.approve(address(gateway), tokenId); + + uint256 gasLimit = DEFAULT_GAS_LIMIT; + bytes memory target = abi.encodePacked(user2); + bytes memory originalPayload = abi.encodeWithSignature("pc721Function(address,uint256)", user2, tokenId); + + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + // Execute and record logs + vm.recordLogs(); + vm.prank(user1); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc721), + 0, + tokenId, + gasLimit, + originalPayload, + ETH_CHAIN_NAMESPACE, + revertCfg + ); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + // Find the UniversalTxOutbound event + bytes32 eventSig = keccak256( + "UniversalTxOutbound(bytes32,address,address,string,bytes,uint256,address,uint256,uint256,bytes,uint256,address,uint8)" + ); + + bytes memory emittedPayload; + bool found; + + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == eventSig) { + // Decode the event data to extract the payload + ( + , // skip chainNamespace + , // skip target + , // skip amount + , // skip gasToken + , // skip gasFee + , // skip gasLimit + emittedPayload, + , // skip protocolFee + , // skip revertRecipient + // skip txType + ) = abi.decode(logs[i].data, (string, bytes, uint256, address, uint256, uint256, bytes, uint256, address, TX_TYPE)); + found = true; + break; + } + } + + assertTrue(found, "UniversalTxOutbound event not found"); + + // Verify magic marker is present in the payload + // Magic marker constants + bytes4 MAGIC_PCAS = 0x50434153; // "PCAS" + uint8 META_VERSION = 1; + uint8 META_KIND_PC721 = 2; + + // Construct expected enriched payload + bytes memory expectedEnrichedPayload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC721, + address(pc721), + pc721.name(), + pc721.symbol(), + tokenId, // tokenId + pc721.tokenURI(tokenId) // tokenURI for the specific NFT + ); + + bytes memory expectedFinalPayload = abi.encodePacked(expectedEnrichedPayload, originalPayload); + + assertEq(emittedPayload, expectedFinalPayload, "Payload with magic marker mismatch for PC721"); + + // Verify the magic marker is at the beginning + bytes4 extractedMagic; + assembly { + extractedMagic := mload(add(emittedPayload, 32)) + } + assertEq(extractedMagic, MAGIC_PCAS, "Magic marker PCAS not found at payload start"); + } + + function test_PC20_MagicMarkerWithEmptyPayload() public { + // Enable PC20 support on this chain namespace + vm.prank(admin); + universalCore.setPC20SupportOnChain(ETH_CHAIN_NAMESPACE, true); + + // Deploy and fund PC20 for user1 + MockPC20 pc20 = new MockPC20("PC20 Test Token", "PC20T"); + pc20.mint(user1, 1_000 ether); + + vm.prank(user1); + pc20.approve(address(gateway), type(uint256).max); + + uint256 amount = 100 ether; + bytes memory target = abi.encodePacked(user2); + bytes memory emptyPayload = ""; + + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + // Execute and record logs + vm.recordLogs(); + vm.prank(user1); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc20), + amount, + 0, + 0, + emptyPayload, + ETH_CHAIN_NAMESPACE, + revertCfg + ); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + // Find the UniversalTxOutbound event + bytes32 eventSig = keccak256( + "UniversalTxOutbound(bytes32,address,address,string,bytes,uint256,address,uint256,uint256,bytes,uint256,address,uint8)" + ); + + bytes memory emittedPayload; + bool found; + + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == eventSig) { + ( + , // skip chainNamespace + , // skip target + , // skip amount + , // skip gasToken + , // skip gasFee + , // skip gasLimit + emittedPayload, + , // skip protocolFee + , // skip revertRecipient + // skip txType + ) = abi.decode(logs[i].data, (string, bytes, uint256, address, uint256, uint256, bytes, uint256, address, TX_TYPE)); + found = true; + break; + } + } + + assertTrue(found, "UniversalTxOutbound event not found"); + + // Even with empty original payload, magic marker should be present + bytes4 MAGIC_PCAS = 0x50434153; + uint8 META_VERSION = 1; + uint8 META_KIND_PC20 = 1; + + bytes memory expectedEnrichedPayload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC20, + address(pc20), + pc20.name(), + pc20.symbol(), + pc20.decimals() + ); + + // With empty original payload, final payload should just be the enriched payload + bytes memory expectedFinalPayload = abi.encodePacked(expectedEnrichedPayload, emptyPayload); + + assertEq(emittedPayload, expectedFinalPayload, "Magic marker should be present even with empty payload"); + assertTrue(emittedPayload.length > 0, "Payload should not be empty"); + } + + function test_PC721_MagicMarkerWithEmptyPayload() public { + // Enable PC721 support on this chain namespace + vm.prank(admin); + universalCore.setPC721SupportOnChain(ETH_CHAIN_NAMESPACE, true); + + // Deploy and mint PC721 for user1 + MockPC721 pc721 = new MockPC721("PC721 Test Token", "PC721T"); + uint256 tokenId = 1; + pc721.mint(user1, tokenId); + + vm.prank(user1); + pc721.approve(address(gateway), tokenId); + + bytes memory target = abi.encodePacked(user2); + bytes memory emptyPayload = ""; + + RevertInstructions memory revertCfg = buildRevertInstructions(user2); + + // Execute and record logs + vm.recordLogs(); + vm.prank(user1); + gateway.sendUniversalTxOutbound{value: DEFAULT_PROTOCOL_FEE}( + target, + address(pc721), + 0, + tokenId, + 0, + emptyPayload, + ETH_CHAIN_NAMESPACE, + revertCfg + ); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + // Find the UniversalTxOutbound event + bytes32 eventSig = keccak256( + "UniversalTxOutbound(bytes32,address,address,string,bytes,uint256,address,uint256,uint256,bytes,uint256,address,uint8)" + ); + + bytes memory emittedPayload; + bool found; + + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == eventSig) { + ( + , // skip chainNamespace + , // skip target + , // skip amount + , // skip gasToken + , // skip gasFee + , // skip gasLimit + emittedPayload, + , // skip protocolFee + , // skip revertRecipient + // skip txType + ) = abi.decode(logs[i].data, (string, bytes, uint256, address, uint256, uint256, bytes, uint256, address, TX_TYPE)); + found = true; + break; + } + } + + assertTrue(found, "UniversalTxOutbound event not found"); + + // Even with empty original payload, magic marker should be present + bytes4 MAGIC_PCAS = 0x50434153; + uint8 META_VERSION = 1; + uint8 META_KIND_PC721 = 2; + + bytes memory expectedEnrichedPayload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC721, + address(pc721), + pc721.name(), + pc721.symbol(), + tokenId, // tokenId + pc721.tokenURI(tokenId) // tokenURI for the specific NFT + ); + + bytes memory expectedFinalPayload = abi.encodePacked(expectedEnrichedPayload, emptyPayload); + + assertEq(emittedPayload, expectedFinalPayload, "Magic marker should be present even with empty payload"); + assertTrue(emittedPayload.length > 0, "Payload should not be empty"); + } + + + + + function _createFailingToken() internal returns (MockPRC20) { + return new MockPRC20( + "Failing Token", + "FAIL", + 6, + SOURCE_CHAIN_ID, + MockPRC20.TokenType.ERC20, + DEFAULT_PROTOCOL_FEE, + address(universalCore), + SOURCE_TOKEN_ADDRESS + ); + } + + // ========================= + // INTERNAL FUNCTIONS + // ========================= + + function _createActors() internal { + admin = address(0x1); + pauser = address(0x2); + user1 = address(0x3); + user2 = address(0x4); + attacker = address(0x5); + uem = address(0x6); + vaultPC = address(0x7); + + vm.label(admin, "admin"); + vm.label(pauser, "pauser"); + vm.label(user1, "user1"); + vm.label(user2, "user2"); + vm.label(attacker, "attacker"); + vm.label(uem, "uem"); + vm.label(vaultPC, "vaultPC"); + + vm.deal(admin, 100 ether); + vm.deal(pauser, 100 ether); + vm.deal(user1, 1000 ether); + vm.deal(user2, 1000 ether); + vm.deal(attacker, 1000 ether); + } + + function _deployMocks() internal { + // Deploy UniversalCore mock + universalCore = new MockUniversalCoreReal(uem); + + // Grant admin role to admin address + universalCore.grantRole(universalCore.DEFAULT_ADMIN_ROLE(), admin); + + // Deploy gas token (PC native token) + gasToken = new MockPRC20( + "Push Chain Native", + "PC", + 18, + SOURCE_CHAIN_ID, + MockPRC20.TokenType.PC, + DEFAULT_PROTOCOL_FEE, + address(universalCore), + "" + ); + + // Deploy PRC20 token (wrapped USDC) + prc20Token = new MockPRC20( + "USDC on Push Chain", + "USDC", + 6, + SOURCE_CHAIN_ID, + MockPRC20.TokenType.ERC20, + DEFAULT_PROTOCOL_FEE, + address(universalCore), + SOURCE_TOKEN_ADDRESS + ); + + // Configure UniversalCore with gas settings + vm.prank(uem); + universalCore.setGasPrice(SOURCE_CHAIN_ID, DEFAULT_GAS_PRICE); + vm.prank(uem); + universalCore.setGasTokenPRC20(SOURCE_CHAIN_ID, address(gasToken)); + + // Configure protocol fees for PC20 / PC721 / default + vm.prank(admin); + universalCore.setProtocolFees( + DEFAULT_PROTOCOL_FEE, // PC20 + DEFAULT_PROTOCOL_FEE, // PC721 + DEFAULT_PROTOCOL_FEE // default + ); + + // Mark PC20 / PC721 as supported on this chainNamespace + vm.prank(admin); + universalCore.setPC20SupportOnChain(ETH_CHAIN_NAMESPACE, true); + vm.prank(admin); + universalCore.setPC721SupportOnChain(ETH_CHAIN_NAMESPACE, true); + + // Mark PRC20 token as supported + vm.prank(admin); + universalCore.setSupportedToken(address(prc20Token), true); + + // Deploy PC20 and PC721 mocks + pc20Token = new MockPC20("PC20 Test Token", "PC20T"); + pc721Token = new MockPC721("PC721 Test Token", "PC721T"); + + vm.label(address(universalCore), "UniversalCore"); + vm.label(address(prc20Token), "PRC20Token"); + vm.label(address(gasToken), "GasToken"); + vm.label(address(pc20Token), "PC20Token"); + vm.label(address(pc721Token), "PC721Token"); + } + + function _deployGateway() internal { + // Deploy implementation + UniversalGatewayPC implementation = new UniversalGatewayPC(); + + // Deploy proxy admin + proxyAdmin = new ProxyAdmin(admin); + + // Deploy transparent upgradeable proxy + bytes memory initData = abi.encodeWithSelector( + UniversalGatewayPC.initialize.selector, admin, pauser, address(universalCore), vaultPC + ); + + gatewayProxy = new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); + + // Cast proxy to gateway interface + gateway = UniversalGatewayPC(address(gatewayProxy)); + + vm.label(address(gateway), "UniversalGatewayPC"); + vm.label(address(gatewayProxy), "GatewayProxy"); + vm.label(address(proxyAdmin), "ProxyAdmin"); + } + + function _initializeGateway() internal view { + // Gateway is already initialized via proxy constructor + // Verify initialization + assertEq(gateway.UNIVERSAL_CORE(), address(universalCore)); + assertEq(address(gateway.VAULT_PC()), vaultPC); + assertTrue(gateway.hasRole(gateway.DEFAULT_ADMIN_ROLE(), admin)); + assertTrue(gateway.hasRole(gateway.PAUSER_ROLE(), pauser)); + } + + function _setupTokens() internal { + // Mint tokens to users + prc20Token.mint(user1, LARGE_AMOUNT); + prc20Token.mint(user2, LARGE_AMOUNT); + gasToken.mint(user1, LARGE_AMOUNT); + gasToken.mint(user2, LARGE_AMOUNT); + + // Approve gateway to spend tokens + vm.prank(user1); + prc20Token.approve(address(gateway), type(uint256).max); + vm.prank(user1); + gasToken.approve(address(gateway), type(uint256).max); + + vm.prank(user2); + prc20Token.approve(address(gateway), type(uint256).max); + vm.prank(user2); + gasToken.approve(address(gateway), type(uint256).max); + + // Mint PC20 and PC721 to users for tests + pc20Token.mint(user1, LARGE_AMOUNT); + pc20Token.mint(user2, LARGE_AMOUNT); + + // Approve gateway for PC20 + vm.prank(user1); + pc20Token.approve(address(gateway), type(uint256).max); + vm.prank(user2); + pc20Token.approve(address(gateway), type(uint256).max); + + // Mint NFTs to users + pc721Token.mint(user1, 1); + pc721Token.mint(user1, 2); + pc721Token.mint(user2, 3); + + // Approve gateway for NFTs + vm.prank(user1); + pc721Token.setApprovalForAll(address(gateway), true); + vm.prank(user2); + pc721Token.setApprovalForAll(address(gateway), true); + } +} diff --git a/contracts/evm-gateway/test/gateway/15_executeUniversalTxPCAS.t.sol b/contracts/evm-gateway/test/gateway/15_executeUniversalTxPCAS.t.sol new file mode 100644 index 0000000..d3c9e66 --- /dev/null +++ b/contracts/evm-gateway/test/gateway/15_executeUniversalTxPCAS.t.sol @@ -0,0 +1,490 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BaseTest } from "../BaseTest.t.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { PC20Factory } from "../../src/PC20Factory.sol"; +import { PC721Factory } from "../../src/PC721Factory.sol"; +import { IPC20 } from "../../src/interfaces/IPC20.sol"; +import { IPC721 } from "../../src/interfaces/IPC721.sol"; +import { UniversalTxRequest } from "../../src/libraries/Types.sol"; +import { MockERC20 } from "../mocks/MockERC20.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; + +/** + * @title Execute Universal Tx PCAS Test Suite + * @notice Comprehensive tests for MAGIC_PCAS functionality in executeUniversalTx + * @dev Tests PC20 and PC721 minting with magic markers for ERC20 token transactions only + * PCAS (Push Chain Asset System) only applies to token bridging, not native ETH transactions + */ +contract ExecuteUniversalTxPCASTest is BaseTest { + // Magic Marker Constants + bytes4 private constant MAGIC_PCAS = 0x50434153; // "PCAS" + uint8 private constant META_VERSION = 1; + uint8 private constant META_KIND_PC20 = 1; + uint8 private constant META_KIND_PC721 = 2; + + // Test constants + address private constant ORIGIN_TOKEN_1 = address(0x1111111111111111111111111111111111111111); + address private constant ORIGIN_TOKEN_2 = address(0x2222222222222222222222222222222222222222); + address private constant TARGET_CONTRACT = address(0x9876543210987654321098765432109876543210); + string private constant TOKEN_NAME = "Test PC Token"; + string private constant TOKEN_SYMBOL = "TPC"; + uint8 private constant TOKEN_DECIMALS = 18; + uint256 private constant MINT_AMOUNT = 1000e18; + uint256 private constant TOKEN_ID = 42; + string private constant TOKEN_URI = "https://example.com/token/42"; + + PC20Factory public pc20Factory; + PC721Factory public pc721Factory; + + function setUp() public override { + super.setUp(); + + // Deploy and set PC20/PC721 factories + pc20Factory = new PC20Factory(address(gateway)); + pc721Factory = new PC721Factory(address(gateway)); + + vm.prank(admin); + gateway.setPC20Factory(address(pc20Factory)); + + vm.prank(admin); + gateway.setPC721Factory(address(pc721Factory)); + } + + // ========================= + // PC20 TESTS - TOKEN TRANSACTIONS (VAULT_ROLE) + // ========================= + + function test_executeUniversalTx_PC20_TokenTx_CreatesAndMints() public { + // Create PC20 payload with magic marker - using the exact format expected by _handlePCAssetAllocation + // The function uses both byte extraction and abi.decode, so we need to match the expected format + bytes memory pc20Payload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC20, + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + bytes memory txID = abi.encodePacked("test_pc20_token_tx"); + + // Create mock token (no minting - PCAS will handle PC20 minting) + MockERC20 mockToken = new MockERC20("Mock", "MOCK", 18, 0); + + // Execute transaction using 6-parameter version (token transactions, VAULT_ROLE) + // Test contract has VAULT_ROLE from BaseTest setup + gateway.executeUniversalTx( + txID, + user1, + address(mockToken), + TARGET_CONTRACT, + MINT_AMOUNT, + pc20Payload + ); + + // Verify PC20 was created and minted + address pc20Address = pc20Factory.getPC20(ORIGIN_TOKEN_1); + assertTrue(pc20Address != address(0), "PC20 should be created"); + + IPC20 pc20 = IPC20(pc20Address); + assertEq(pc20.name(), TOKEN_NAME, "PC20 name should match"); + assertEq(pc20.symbol(), TOKEN_SYMBOL, "PC20 symbol should match"); + assertEq(pc20.decimals(), TOKEN_DECIMALS, "PC20 decimals should match"); + + // PC20 tokens are transferred to VAULT after execution + address vault = gateway.VAULT(); + assertEq(pc20.balanceOf(vault), MINT_AMOUNT, "PC20 should be transferred to VAULT"); + } + + function test_executeUniversalTx_PC20_TokenTx_ExistingToken() public { + // First create the PC20 token + test_executeUniversalTx_PC20_TokenTx_CreatesAndMints(); + + // Create another payload for the same origin token + bytes memory pc20Payload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC20, + ORIGIN_TOKEN_1, // Same origin token + "New Name", // Different name (should be ignored) + "NEW", // Different symbol (should be ignored) + TOKEN_DECIMALS + ); + + bytes memory txID = abi.encodePacked("test_pc20_existing"); + + // Create mock token (no minting - PCAS will handle PC20 minting) + MockERC20 mockToken = new MockERC20("Mock", "MOCK", 18, 0); + + uint256 additionalAmount = 500e18; + + // Execute transaction - should mint to existing PC20 + gateway.executeUniversalTx( + txID, + user1, + address(mockToken), + TARGET_CONTRACT, + additionalAmount, + pc20Payload + ); + + // Verify PC20 balance increased in VAULT + address pc20Address = pc20Factory.getPC20(ORIGIN_TOKEN_1); + IPC20 pc20 = IPC20(pc20Address); + address vault = gateway.VAULT(); + assertEq(pc20.balanceOf(vault), MINT_AMOUNT + additionalAmount, "PC20 balance should increase in VAULT"); + + // Verify original metadata unchanged + assertEq(pc20.name(), TOKEN_NAME, "PC20 name should remain original"); + assertEq(pc20.symbol(), TOKEN_SYMBOL, "PC20 symbol should remain original"); + } + + + // ========================= + // PC721 TESTS - TOKEN TRANSACTIONS (VAULT_ROLE) + // ========================= + + function test_executeUniversalTx_PC721_TokenTx_CreatesAndMints() public { + // Create PC721 payload with magic marker + bytes memory pc721Payload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC721, + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_ID, + TOKEN_URI + ); + + bytes memory txID = abi.encodePacked("test_pc721_token_tx"); + + // Create mock token (no minting - PCAS will handle PC721 minting) + MockERC20 mockToken = new MockERC20("Mock", "MOCK", 18, 0); + + // Execute transaction using 6-parameter version (VAULT_ROLE) + gateway.executeUniversalTx( + txID, + user1, + address(mockToken), + TARGET_CONTRACT, + MINT_AMOUNT, + pc721Payload + ); + + // Verify PC721 was created and minted + address pc721Address = pc721Factory.getPC721(ORIGIN_TOKEN_1); + assertTrue(pc721Address != address(0), "PC721 should be created"); + + IPC721 pc721 = IPC721(pc721Address); + assertEq(pc721.name(), TOKEN_NAME, "PC721 name should match"); + assertEq(pc721.symbol(), TOKEN_SYMBOL, "PC721 symbol should match"); + + // PC721 tokens are transferred to VAULT after execution + address vault = gateway.VAULT(); + assertEq(pc721.ownerOf(TOKEN_ID), vault, "PC721 should be transferred to VAULT"); + } + + + // ========================= + // ERROR CASES + // ========================= + + function test_executeUniversalTx_NoPC20Factory_Revert() public { + bytes memory pc20Payload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC20, + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + bytes memory txID = abi.encodePacked("test_no_factory"); + MockERC20 mockToken = new MockERC20("Mock", "MOCK", 18, 0); + mockToken.mint(address(gateway), MINT_AMOUNT); + + // Should revert with ZeroAddress when trying to set factory to address(0) + vm.prank(admin); + vm.expectRevert(Errors.ZeroAddress.selector); + gateway.setPC20Factory(address(0)); + } + + function test_executeUniversalTx_NoPC721Factory_Revert() public { + bytes memory pc721Payload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC721, + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_ID, + TOKEN_URI + ); + + bytes memory txID = abi.encodePacked("test_no_pc721_factory"); + MockERC20 mockToken = new MockERC20("Mock", "MOCK", 18, 0); + mockToken.mint(address(gateway), MINT_AMOUNT); + + // Should revert with ZeroAddress when trying to set factory to address(0) + vm.prank(admin); + vm.expectRevert(Errors.ZeroAddress.selector); + gateway.setPC721Factory(address(0)); + } + + function test_executeUniversalTx_InvalidMagicMarker_NoProcessing() public { + // Create payload with invalid magic marker + bytes memory invalidPayload = abi.encode( + bytes4(0x12345678), // Invalid magic + META_VERSION, + META_KIND_PC20, + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + bytes memory txID = abi.encodePacked("test_invalid_magic"); + MockERC20 mockToken = new MockERC20("Mock", "MOCK", 18, 0); + mockToken.mint(address(gateway), MINT_AMOUNT); + + // Execute transaction - should succeed but not create PC20 + gateway.executeUniversalTx( + txID, + user1, + address(mockToken), + TARGET_CONTRACT, + MINT_AMOUNT, + invalidPayload + ); + + // Verify no PC20 was created + address pc20Address = pc20Factory.getPC20(ORIGIN_TOKEN_1); + assertEq(pc20Address, address(0), "No PC20 should be created with invalid magic"); + } + + function test_executeUniversalTx_InvalidVersion_Revert() public { + // Create payload with invalid version + bytes memory invalidPayload = abi.encode( + MAGIC_PCAS, + uint8(99), // Invalid version + META_KIND_PC20, + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + bytes memory txID = abi.encodePacked("test_invalid_version"); + MockERC20 mockToken = new MockERC20("Mock", "MOCK", 18, 0); + mockToken.mint(address(gateway), MINT_AMOUNT); + + // Should revert with InvalidInput + vm.expectRevert(Errors.InvalidInput.selector); + gateway.executeUniversalTx( + txID, + user1, + address(mockToken), + TARGET_CONTRACT, + MINT_AMOUNT, + invalidPayload + ); + } + + function test_executeUniversalTx_InvalidKind_Revert() public { + // Create payload with invalid kind + bytes memory invalidPayload = abi.encode( + MAGIC_PCAS, + META_VERSION, + uint8(99), // Invalid kind + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + bytes memory txID = abi.encodePacked("test_invalid_kind"); + MockERC20 mockToken = new MockERC20("Mock", "MOCK", 18, 0); + mockToken.mint(address(gateway), MINT_AMOUNT); + + // Should revert with InvalidInput + vm.expectRevert(Errors.InvalidInput.selector); + gateway.executeUniversalTx( + txID, + user1, + address(mockToken), + TARGET_CONTRACT, + MINT_AMOUNT, + invalidPayload + ); + } + + function test_executeUniversalTx_ShortPayload_NoProcessing() public { + // Create payload shorter than 4 bytes (no magic marker processing) + bytes memory shortPayload = abi.encodePacked(uint16(0x1234)); + + bytes memory txID = abi.encodePacked("test_short_payload"); + MockERC20 mockToken = new MockERC20("Mock", "MOCK", 18, 0); + mockToken.mint(address(gateway), MINT_AMOUNT); + + // Execute transaction - should succeed but not process magic marker + gateway.executeUniversalTx( + txID, + user1, + address(mockToken), + TARGET_CONTRACT, + MINT_AMOUNT, + shortPayload + ); + + // Verify no PC20 was created + address pc20Address = pc20Factory.getPC20(ORIGIN_TOKEN_1); + assertEq(pc20Address, address(0), "No PC20 should be created with short payload"); + } + + // ========================= + // INTEGRATION TESTS + // ========================= + + function test_executeUniversalTx_MultiplePC20Tokens() public { + // Test creating multiple different PC20 tokens + address[] memory originTokens = new address[](3); + originTokens[0] = address(0x1111); + originTokens[1] = address(0x2222); + originTokens[2] = address(0x3333); + + MockERC20 mockToken = new MockERC20("Mock Token", "MOCK", 18, 0); + address vault = gateway.VAULT(); + + for (uint i = 0; i < originTokens.length; i++) { + bytes memory pc20Payload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC20, + originTokens[i], + string(abi.encodePacked(TOKEN_NAME, " ", vm.toString(i))), + string(abi.encodePacked(TOKEN_SYMBOL, vm.toString(i))), + TOKEN_DECIMALS + ); + + bytes memory txID = abi.encodePacked("test_tx_multi_", vm.toString(i)); + + gateway.executeUniversalTx( + txID, + user1, + address(mockToken), + TARGET_CONTRACT, + MINT_AMOUNT, + pc20Payload + ); + + // Verify each PC20 was created and transferred to VAULT + address pc20Address = pc20Factory.getPC20(originTokens[i]); + assertTrue(pc20Address != address(0), string(abi.encodePacked("PC20 ", vm.toString(i), " should be created"))); + + IPC20 pc20 = IPC20(pc20Address); + assertEq(pc20.balanceOf(vault), MINT_AMOUNT, string(abi.encodePacked("PC20 ", vm.toString(i), " should be minted to VAULT"))); + } + } + + function test_executeUniversalTx_PC20WithAdditionalPayload() public { + // Create PC20 payload with additional data after the magic marker data + bytes memory pc20Data = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC20, + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + // Append additional payload data + bytes memory additionalData = abi.encodePacked("additional_call_data"); + bytes memory combinedPayload = abi.encodePacked(pc20Data, additionalData); + + bytes memory txID = abi.encodePacked("test_pc20_with_payload"); + MockERC20 mockToken = new MockERC20("Mock", "MOCK", 18, 0); + + // Execute transaction + gateway.executeUniversalTx( + txID, + user1, + address(mockToken), + TARGET_CONTRACT, + MINT_AMOUNT, + combinedPayload + ); + + // Verify PC20 was created and minted to VAULT + address pc20Address = pc20Factory.getPC20(ORIGIN_TOKEN_1); + assertTrue(pc20Address != address(0), "PC20 should be created"); + + IPC20 pc20 = IPC20(pc20Address); + address vault = gateway.VAULT(); + assertEq(pc20.balanceOf(vault), MINT_AMOUNT, "PC20 should be minted to VAULT"); + } + + // ========================= + // ACCESS CONTROL TESTS + // ========================= + + function test_executeUniversalTx_TokenTx_RequiresVaultRole() public { + bytes memory pc20Payload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC20, + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + bytes memory txID = abi.encodePacked("test_vault_role"); + MockERC20 mockToken = new MockERC20("Mock", "MOCK", 18, 0); + mockToken.mint(address(gateway), MINT_AMOUNT); + + // Should revert when called by non-VAULT_ROLE address + vm.expectRevert(); + vm.prank(user1); + gateway.executeUniversalTx( + txID, + user1, + address(mockToken), + TARGET_CONTRACT, + MINT_AMOUNT, + pc20Payload + ); + } + + function test_executeUniversalTx_NativeTx_RequiresTSSRole() public { + bytes memory pc20Payload = abi.encode( + MAGIC_PCAS, + META_VERSION, + META_KIND_PC20, + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + bytes memory txID = abi.encodePacked("test_tss_role"); + uint256 ethAmount = 1e18; + + // Should revert when called by non-TSS_ROLE address + vm.deal(user1, ethAmount); + vm.expectRevert(); + vm.prank(user1); + gateway.executeUniversalTx{value: ethAmount}( + txID, + user1, + TARGET_CONTRACT, + ethAmount, + pc20Payload + ); + } +} diff --git a/contracts/evm-gateway/test/gateway/1_GatewayAdminSetters.t.sol b/contracts/evm-gateway/test/gateway/1_adminActions.t.sol similarity index 72% rename from contracts/evm-gateway/test/gateway/1_GatewayAdminSetters.t.sol rename to contracts/evm-gateway/test/gateway/1_adminActions.t.sol index 4606d2b..778bbb9 100644 --- a/contracts/evm-gateway/test/gateway/1_GatewayAdminSetters.t.sol +++ b/contracts/evm-gateway/test/gateway/1_adminActions.t.sol @@ -492,9 +492,196 @@ contract GatewayAdminSettersTest is BaseTest { gateway.setL2SequencerGracePeriod(300); // TSS operations should be blocked - bytes32 txID = bytes32(uint256(1)); + bytes memory txID = abi.encodePacked(uint256(1)); vm.prank(tss); vm.expectRevert(); gateway.revertUniversalTx(txID, 1, RevertInstructions(user2, "")); } + + // ========================= + // VAULT UPDATE TESTS + // ========================= + + function testUpdateVault() public { + address newVault = address(0x999); + address oldVault = gateway.VAULT(); + + // Must be paused to update vault + vm.prank(admin); + gateway.pause(); + + // Expect VaultUpdated event + vm.expectEmit(true, true, true, true); + emit IUniversalGateway.VaultUpdated(oldVault, newVault); + + vm.prank(admin); + gateway.updateVault(newVault); + + assertEq(gateway.VAULT(), newVault); + assertTrue(gateway.hasRole(gateway.VAULT_ROLE(), newVault)); + assertFalse(gateway.hasRole(gateway.VAULT_ROLE(), oldVault)); + } + + function testUpdateVaultOnlyAdmin() public { + address newVault = address(0x999); + + // Pause first + vm.prank(admin); + gateway.pause(); + + // Non-admin should not be able to update vault + vm.prank(user1); + vm.expectRevert(); + gateway.updateVault(newVault); + + // Admin should be able to update vault + vm.prank(admin); + gateway.updateVault(newVault); + assertEq(gateway.VAULT(), newVault); + } + + function testUpdateVaultRequiresPaused() public { + address newVault = address(0x999); + + // Should revert when not paused (whenPaused modifier checks for paused state) + vm.prank(admin); + vm.expectRevert("ExpectedPause()"); + gateway.updateVault(newVault); + + // Should work when paused + vm.prank(admin); + gateway.pause(); + + vm.prank(admin); + gateway.updateVault(newVault); + assertEq(gateway.VAULT(), newVault); + } + + function testUpdateVaultZeroAddressReverts() public { + // Pause first + vm.prank(admin); + gateway.pause(); + + vm.prank(admin); + vm.expectRevert(Errors.ZeroAddress.selector); + gateway.updateVault(address(0)); + } + + function testUpdateVaultRoleTransfer() public { + address newVault1 = address(0x888); + address newVault2 = address(0x999); + address oldVault = gateway.VAULT(); + + // Pause first + vm.prank(admin); + gateway.pause(); + + // First update + vm.prank(admin); + gateway.updateVault(newVault1); + assertTrue(gateway.hasRole(gateway.VAULT_ROLE(), newVault1)); + assertFalse(gateway.hasRole(gateway.VAULT_ROLE(), oldVault)); + + // Second update - should transfer role from newVault1 to newVault2 + vm.prank(admin); + gateway.updateVault(newVault2); + assertTrue(gateway.hasRole(gateway.VAULT_ROLE(), newVault2)); + assertFalse(gateway.hasRole(gateway.VAULT_ROLE(), newVault1)); + assertFalse(gateway.hasRole(gateway.VAULT_ROLE(), oldVault)); + } + + // ========================= + // EPOCH DURATION TESTS + // ========================= + + function testUpdateEpochDuration() public { + uint256 newDuration = 12 hours; + uint256 oldDuration = gateway.epochDurationSec(); + + // Expect EpochDurationUpdated event + vm.expectEmit(true, true, true, true); + emit IUniversalGateway.EpochDurationUpdated(oldDuration, newDuration); + + vm.prank(admin); + gateway.updateEpochDuration(newDuration); + + assertEq(gateway.epochDurationSec(), newDuration); + } + + function testUpdateEpochDurationOnlyAdmin() public { + uint256 newDuration = 12 hours; + + // Non-admin should not be able to update epoch duration + vm.prank(user1); + vm.expectRevert(); + gateway.updateEpochDuration(newDuration); + + // Admin should be able to update epoch duration + vm.prank(admin); + gateway.updateEpochDuration(newDuration); + assertEq(gateway.epochDurationSec(), newDuration); + } + + function testUpdateEpochDurationCanBeCalledWhenPaused() public { + uint256 newDuration = 12 hours; + + // Pause the contract + vm.prank(admin); + gateway.pause(); + + // Should still be able to update epoch duration when paused + vm.prank(admin); + gateway.updateEpochDuration(newDuration); + assertEq(gateway.epochDurationSec(), newDuration); + } + + function testUpdateEpochDurationZeroDuration() public { + // Zero duration is allowed (though may not be practical) + vm.prank(admin); + gateway.updateEpochDuration(0); + assertEq(gateway.epochDurationSec(), 0); + } + + function testUpdateEpochDurationMultipleUpdates() public { + uint256 duration1 = 6 hours; + uint256 duration2 = 12 hours; + uint256 duration3 = 24 hours; + + vm.prank(admin); + gateway.updateEpochDuration(duration1); + assertEq(gateway.epochDurationSec(), duration1); + + vm.prank(admin); + gateway.updateEpochDuration(duration2); + assertEq(gateway.epochDurationSec(), duration2); + + vm.prank(admin); + gateway.updateEpochDuration(duration3); + assertEq(gateway.epochDurationSec(), duration3); + } + + // ========================= + // ENHANCED TESTS FOR EXISTING FUNCTIONS + // ========================= + + function testSetL2SequencerGracePeriodZeroAllowed() public { + // Zero grace period is allowed (disables grace period check) + vm.prank(admin); + gateway.setL2SequencerGracePeriod(0); + assertEq(gateway.l2SequencerGracePeriodSec(), 0); + } + + function testSetL2SequencerGracePeriodMultipleUpdates() public { + vm.prank(admin); + gateway.setL2SequencerGracePeriod(300); + assertEq(gateway.l2SequencerGracePeriodSec(), 300); + + vm.prank(admin); + gateway.setL2SequencerGracePeriod(600); + assertEq(gateway.l2SequencerGracePeriodSec(), 600); + + vm.prank(admin); + gateway.setL2SequencerGracePeriod(0); + assertEq(gateway.l2SequencerGracePeriodSec(), 0); + } } diff --git a/contracts/evm-gateway/test/gateway/2_GatewayDepositNative.t.sol b/contracts/evm-gateway/test/gateway/2_GatewayDepositNative.t.sol deleted file mode 100644 index 96d9992..0000000 --- a/contracts/evm-gateway/test/gateway/2_GatewayDepositNative.t.sol +++ /dev/null @@ -1,672 +0,0 @@ -pragma solidity 0.8.26; - -import { Test, console2 } from "forge-std/Test.sol"; -import { BaseTest } from "../BaseTest.t.sol"; -import { Errors } from "../../src/libraries/Errors.sol"; -import { TX_TYPE, RevertInstructions, UniversalPayload, VerificationType } from "../../src/libraries/Types.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IUniversalGateway } from "../../src/interfaces/IUniversalGateway.sol"; -import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; - -/// @notice Test suite for NATIVE ETH deposit functions in UniversalGateway -/// @dev Covers only the 2 functions that use PURE native ETH: -/// 1. sendTxWithGas(payload, revertCFG) - Native ETH gas funding -/// 2. sendFunds(recipient, address(0), bridgeAmount, revertCFG) - Native ETH bridging -/// @dev Note: sendTxWithFunds uses native ETH for gas but requires ERC20 for bridging, so it's not pure native -contract GatewayDepositNativeTest is BaseTest { - // ========================= - // SETUP - // ========================= - function setUp() public override { - super.setUp(); - // No additional setup needed for pure native ETH tests - } - - // ========================= - // HAPPY PATH TESTS - Native ETH Functions - // ========================= - - /// @notice Test sendTxWithGas (native ETH) with valid parameters - function testSendTxWithGas_NativeETH_HappyPath() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - - // Calculate valid ETH amount (within USD caps) - // ETH price is $2000, so for $5 (middle of $1-$10 range): 5e18 / 2000e18 = 0.0025 ETH - uint256 validEthAmount = 25e14; // 0.0025 ETH = $5 - - // Fund user1 with ETH - vm.deal(user1, validEthAmount); - - // Record initial balances - uint256 initialTSSBalance = tss.balance; - uint256 initialUserBalance = user1.balance; - - // Expect UniversalTx event emission - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user1, - address(0), - address(0), - validEthAmount, - abi.encode(payload), - revertCfg_, - TX_TYPE.GAS_AND_PAYLOAD, - bytes("") - ); - - // Execute the transaction - vm.prank(user1); - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - - // Verify TSS received the ETH - assertEq(tss.balance, initialTSSBalance + validEthAmount, "TSS should receive ETH"); - assertEq(user1.balance, initialUserBalance - validEthAmount, "User should pay ETH"); - } - - /// @notice Test sendFunds (native ETH + funds) with valid parameters - function testSendFunds_NativeETH_HappyPath() public { - // Setup: Use native ETH as bridge token (address(0)) and create revert config - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 bridgeAmount = 1e18; // 1 ETH - - // Fund user1 with ETH for the transaction - vm.deal(user1, bridgeAmount); - - // Record initial balances - uint256 initialTSSBalance = tss.balance; - uint256 initialUserBalance = user1.balance; - uint256 initialGatewayBalance = address(gateway).balance; - - // Expect UniversalTx event emission - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user1, - recipient, - address(0), // address(0) for native ETH bridging - bridgeAmount, - bytes(""), // Empty payload for funds-only bridge - revertCfg_, - TX_TYPE.FUNDS, - bytes("") - ); - - // Execute the transaction - native ETH bridging - vm.prank(user1); - gateway.sendFunds{ value: bridgeAmount }( - recipient, - address(0), // address(0) for native ETH bridging - bridgeAmount, - revertCfg_ - ); - - // Verify TSS received the ETH - assertEq(tss.balance, initialTSSBalance + bridgeAmount, "TSS should receive ETH"); - assertEq(user1.balance, initialUserBalance - bridgeAmount, "User should pay ETH"); - assertEq(address(gateway).balance, initialGatewayBalance, "Gateway should not hold ETH (sent to TSS)"); - } - - /// @notice Test all native functions with minimum valid amounts - function testAllNativeFunctions_MinimumAmounts_Success() public { - // Calculate minimum ETH amount for USD caps - // ETH price is $2000, so for $1.01 (just above $1 min): 1.01e18 / 2000e18 = 0.000505 ETH - uint256 minEthAmount = 505e12; // 0.000505 ETH = $1.01 - - // Setup payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - - // Fund user1 with minimum ETH - vm.deal(user1, minEthAmount * 3); // Enough for all 3 functions - - // tokenA is already minted and approved in BaseTest setup - - // Test 1: sendTxWithGas with minimum amount - uint256 initialTSSBalance = tss.balance; - - // Expect UniversalTx event emission - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user1, - address(0), - address(0), - minEthAmount, - abi.encode(payload), - revertCfg_, - TX_TYPE.GAS_AND_PAYLOAD, - bytes("") - ); - - vm.prank(user1); - gateway.sendTxWithGas{ value: minEthAmount }(payload, revertCfg_, bytes("")); - assertEq(tss.balance, initialTSSBalance + minEthAmount, "TSS should receive min ETH for sendTxWithGas"); - - // Test 2: sendFunds with minimum amounts (native ETH bridging) - initialTSSBalance = tss.balance; - - // Expect UniversalTx event emission - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user1, - recipient, - address(0), // address(0) for native ETH bridging - minEthAmount, // bridge amount = ETH amount for native bridging - bytes(""), // Empty payload for funds-only bridge - revertCfg_, - TX_TYPE.FUNDS, - bytes("") - ); - - vm.prank(user1); - gateway.sendFunds{ value: minEthAmount }( - recipient, - address(0), // address(0) for native ETH bridging - minEthAmount, // bridge amount = ETH amount for native bridging - revertCfg_ - ); - assertEq(tss.balance, initialTSSBalance + minEthAmount, "TSS should receive min ETH for sendFunds"); - } - - /// @notice Test all native functions with maximum valid amounts - function testAllNativeFunctions_MaximumAmounts_Success() public { - // Calculate maximum ETH amount for USD caps - // ETH price is $2000, so for $9.99 (just below $10 max): 9.99e18 / 2000e18 = 0.004995 ETH - uint256 maxEthAmount = 4995e12; // 0.004995 ETH = $9.99 - // Setup payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - - // Fund user1 with maximum ETH - vm.deal(user1, maxEthAmount * 3); // Enough for all 3 functions - - // tokenA is already minted and approved in BaseTest setup - - // Test 1: sendTxWithGas with maximum amount - uint256 initialTSSBalance = tss.balance; - - // Expect UniversalTx event emission - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user1, - address(0), - address(0), - maxEthAmount, - abi.encode(payload), - revertCfg_, - TX_TYPE.GAS_AND_PAYLOAD, - bytes("") - ); - - vm.prank(user1); - gateway.sendTxWithGas{ value: maxEthAmount }(payload, revertCfg_, bytes("")); - assertEq(tss.balance, initialTSSBalance + maxEthAmount, "TSS should receive max ETH for sendTxWithGas"); - // Test 2: sendFunds with maximum amounts (native ETH bridging) - initialTSSBalance = tss.balance; - - // Expect UniversalTx event emission - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user1, - recipient, - address(0), // address(0) for native ETH bridging - maxEthAmount, // bridge amount = ETH amount for native bridging - bytes(""), // Empty payload for funds-only bridge - revertCfg_, - TX_TYPE.FUNDS, - bytes("") - ); - - vm.prank(user1); - gateway.sendFunds{ value: maxEthAmount }( - recipient, - address(0), // address(0) for native ETH bridging - maxEthAmount, // bridge amount = ETH amount for native bridging - revertCfg_ - ); - assertEq(tss.balance, initialTSSBalance + maxEthAmount, "TSS should receive max ETH for sendFunds"); - } - - // ========================= - // USD CAP VALIDATION TESTS - // ========================= - - /// @notice Test sendTxWithGas (native) below minimum USD cap - function testSendTxWithGas_NativeETH_BelowMinCap_Reverts() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - - // Calculate ETH amount below minimum USD cap - // ETH price is $2000, so for $0.99 (below $1 min): 0.99e18 / 2000e18 = 0.000495 ETH - uint256 belowMinEthAmount = 495e12; // 0.000495 ETH = $0.99 - - // Fund user1 with ETH - vm.deal(user1, belowMinEthAmount); - - // Execute the transaction and expect it to revert - vm.prank(user1); - vm.expectRevert(Errors.InvalidAmount.selector); // Should revert due to USD cap check - gateway.sendTxWithGas{ value: belowMinEthAmount }(payload, revertCfg_, bytes("")); - } - - /// @notice Test sendTxWithGas (native) above maximum USD cap - function testSendTxWithGas_NativeETH_AboveMaxCap_Reverts() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - - // Calculate ETH amount above maximum USD cap - // ETH price is $2000, so for $10.01 (above $10 max): 10.01e18 / 2000e18 = 0.005005 ETH - uint256 aboveMaxEthAmount = 5005e12; // 0.005005 ETH = $10.01 - - // Fund user1 with ETH - vm.deal(user1, aboveMaxEthAmount); - - // Execute the transaction and expect it to revert - vm.prank(user1); - vm.expectRevert(Errors.InvalidAmount.selector); // Should revert due to USD cap check - gateway.sendTxWithGas{ value: aboveMaxEthAmount }(payload, revertCfg_, bytes("")); - } - - /// @notice Test sendTxWithGas (native) with zero amount - function testSendTxWithGas_NativeETH_ZeroAmount_Reverts() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - - // Fund user1 with some ETH (but we'll send 0) - vm.deal(user1, 1e18); - - // Execute the transaction with zero value and expect it to revert - vm.prank(user1); - vm.expectRevert(Errors.InvalidAmount.selector); // Should revert due to zero amount - gateway.sendTxWithGas{ value: 0 }(payload, revertCfg_, bytes("")); - } - - // ========================= - // TOKEN SUPPORT VALIDATION TESTS - // ========================= - - /// @notice Test sendFunds with zero bridge amount - function testSendFunds_ZeroBridgeAmount_Reverts() public { - // Setup: Create revert config - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 ethAmount = 1e18; // 1 ETH - - // Fund user1 with ETH - vm.deal(user1, ethAmount); - - // Execute the transaction with zero bridge amount and expect it to revert - vm.prank(user1); - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendFunds{ value: ethAmount }( - recipient, - address(0), // address(0) for native ETH bridging - 0, // Zero bridge amount - revertCfg_ - ); - } - - // ========================= - // ACCESS CONTROL & PAUSE TESTS - // ========================= - - /// @notice Test sendTxWithGas (native) when contract is paused - function testSendTxWithGas_NativeETH_WhenPaused_Reverts() public { - // Pause the contract - vm.prank(admin); - gateway.pause(); - - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 validEthAmount = 25e14; // 0.0025 ETH = $5 - vm.deal(user1, validEthAmount); - - // Execute the transaction and expect it to revert due to pause - vm.prank(user1); - vm.expectRevert(); // Should revert due to whenNotPaused modifier - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - } - - /// @notice Test sendFunds when contract is paused - function testSendFunds_WhenPaused_Reverts() public { - // Pause the contract - vm.prank(admin); - gateway.pause(); - - // Setup: Create revert config - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 bridgeAmount = 1e18; // 1 ETH - vm.deal(user1, bridgeAmount); - - // Execute the transaction and expect it to revert due to pause - vm.prank(user1); - vm.expectRevert(); // Should revert due to whenNotPaused modifier - gateway.sendFunds{ value: bridgeAmount }( - recipient, - address(0), // address(0) for native ETH bridging - bridgeAmount, - revertCfg_ - ); - } - - /// @notice Test sendTxWithGas (native) reentrancy protection via nonReentrant modifier - function testSendTxWithGas_NativeETH_ReentrancyProtection_Success() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 validEthAmount = 25e14; // 0.0025 ETH = $5 - vm.deal(user1, validEthAmount); - - // Test that the function works normally (reentrancy protection is built into the modifier) - vm.prank(user1); - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - - // Verify the transaction succeeded and TSS received the ETH - assertTrue(true, "Reentrancy protection is handled by nonReentrant modifier"); - } - - /// @notice Test sendFunds reentrancy protection via nonReentrant modifier - function testSendFunds_ReentrancyProtection_Success() public { - // Setup: Create revert config - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 bridgeAmount = 1e18; // 1 ETH - vm.deal(user1, bridgeAmount); - - // Test that the function works normally (reentrancy protection is built into the modifier) - vm.prank(user1); - gateway.sendFunds{ value: bridgeAmount }( - recipient, - address(0), // address(0) for native ETH bridging - bridgeAmount, - revertCfg_ - ); - - // Verify the transaction succeeded and TSS received the ETH - assertTrue(true, "Reentrancy protection is handled by nonReentrant modifier"); - } - - // ========================= - // ORACLE INTEGRATION TESTS - // ========================= - - /// @notice Test sendTxWithGas (native) with oracle failures - function testSendTxWithGas_NativeETH_OracleFailures_Reverts() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 validEthAmount = 25e14; // 0.0025 ETH = $5 - vm.deal(user1, validEthAmount); - - // Test 1: Stale oracle data - vm.warp(block.timestamp + 3601); // Move time beyond stale period (3600s default) - vm.prank(user1); - vm.expectRevert(Errors.InvalidData.selector); - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - - // Reset time - vm.warp(block.timestamp - 3601); - - // Test 2: Zero price from oracle - ethUsdFeedMock.setAnswer(0, block.timestamp); // price = 0 - vm.prank(user1); - vm.expectRevert(Errors.InvalidData.selector); - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - - // Test 3: Negative price from oracle - ethUsdFeedMock.setAnswer(-1000, block.timestamp); // price = -1000 - vm.prank(user1); - vm.expectRevert(Errors.InvalidData.selector); - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - - // Test 4: Sequencer down (if L2 sequencer feed is configured) - if (address(sequencerMock) != address(0)) { - sequencerMock.setStatus(true, block.timestamp); // status = 1 (DOWN) - vm.prank(user1); - vm.expectRevert(Errors.InvalidData.selector); - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - } - } - - // ========================= - // PAYLOAD VALIDATION TESTS - // ========================= - - // ========================= - // PARAMETER VALIDATION TESTS - // ========================= - - /// @notice Test sendFunds with zero recipient - function testSendFunds_ZeroRecipient_Reverts() public { - // Setup: Create revert config - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 bridgeAmount = 1e18; // 1 ETH - uint256 ethAmount = 1e18; // 1 ETH - vm.deal(user1, ethAmount); - - // Execute the transaction with zero recipient and expect it to revert - vm.prank(user1); - vm.expectRevert(Errors.InvalidRecipient.selector); - gateway.sendFunds{ value: ethAmount }( - address(0), // Zero recipient - address(0), // address(0) for native ETH bridging - bridgeAmount, - revertCfg_ - ); - } - - // ========================= - // TSS TRANSFER VERIFICATION TESTS - // ========================= - - /// @notice Test that TSS actually receives ETH from sendTxWithGas - function testSendTxWithGas_TSSReceivesETH_Success() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 validEthAmount = 25e14; // 0.0025 ETH = $5 - - // Fund user1 with ETH - vm.deal(user1, validEthAmount); - - // Record initial TSS balance - uint256 initialTSSBalance = tss.balance; - - // Execute the transaction - vm.prank(user1); - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - - // Verify TSS received the exact amount - assertEq(tss.balance, initialTSSBalance + validEthAmount, "TSS should receive exact ETH amount"); - - // Verify gateway contract has no ETH (it forwards to TSS) - assertEq(address(gateway).balance, 0, "Gateway should not hold ETH after forwarding to TSS"); - } - - /// @notice Test that TSS actually receives ETH from sendFunds - function testSendFunds_TSSReceivesETH_Success() public { - // Setup: Create revert config - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 bridgeAmount = 1e18; // 1 ETH - - // Fund user1 with ETH - vm.deal(user1, bridgeAmount); - - // Record initial TSS balance - uint256 initialTSSBalance = tss.balance; - - // Execute the transaction - vm.prank(user1); - gateway.sendFunds{ value: bridgeAmount }( - recipient, - address(0), // address(0) for native ETH bridging - bridgeAmount, - revertCfg_ - ); - - // Verify TSS received the exact amount - assertEq(tss.balance, initialTSSBalance + bridgeAmount, "TSS should receive exact ETH amount"); - - // Verify gateway contract has no ETH (it forwards to TSS) - assertEq(address(gateway).balance, 0, "Gateway should not hold ETH after forwarding to TSS"); - } - - // ========================= - // COMPREHENSIVE EDGE CASES - // ========================= - - /// @notice Test boundary values around USD caps - function testUSD_CapBoundaryValues_Success() public { - // Get current ETH price and calculate exact boundary amounts - (uint256 minEthAmount, uint256 maxEthAmount) = gateway.getMinMaxValueForNative(); - - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - - // Test minimum amount (should pass) - vm.deal(user1, minEthAmount); - vm.prank(user1); - gateway.sendTxWithGas{ value: minEthAmount }(payload, revertCfg_, bytes("")); - - // Test maximum amount (should pass) - vm.deal(user1, maxEthAmount); - vm.prank(user1); - gateway.sendTxWithGas{ value: maxEthAmount }(payload, revertCfg_, bytes("")); - - // Test just below minimum (should fail) - uint256 belowMin = minEthAmount - 1; - vm.deal(user1, belowMin); - vm.prank(user1); - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithGas{ value: belowMin }(payload, revertCfg_, bytes("")); - - // Test just above maximum (should fail) - uint256 aboveMax = maxEthAmount + 1; - vm.deal(user1, aboveMax); - vm.prank(user1); - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithGas{ value: aboveMax }(payload, revertCfg_, bytes("")); - } - - /// @notice Test with different ETH prices to verify USD cap calculations - function testDifferentETH_Prices_Success() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - - // Test with very high ETH price ($10,000) - ethUsdFeedMock.setAnswer(10000e8, block.timestamp); // $10,000 ETH - - // With $10,000 ETH, $1 = 0.0001 ETH, $10 = 0.001 ETH - uint256 minAmount = 1e14; // 0.0001 ETH = $1 - uint256 maxAmount = 1e15; // 0.001 ETH = $10 - - vm.deal(user1, maxAmount); - vm.prank(user1); - gateway.sendTxWithGas{ value: maxAmount }(payload, revertCfg_, bytes("")); - - // Test with very low ETH price ($100) - ethUsdFeedMock.setAnswer(100e8, block.timestamp); // $100 ETH - - // With $100 ETH, $1 = 0.01 ETH, $10 = 0.1 ETH - minAmount = 1e16; // 0.01 ETH = $1 - maxAmount = 1e17; // 0.1 ETH = $10 - - vm.deal(user1, maxAmount); - vm.prank(user1); - gateway.sendTxWithGas{ value: maxAmount }(payload, revertCfg_, bytes("")); - } - - /// @notice Test multiple deposits in sequence - function testMultipleDeposits_Sequence_Success() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 validEthAmount = 25e14; // 0.0025 ETH = $5 - - // Fund user1 with enough ETH for multiple deposits - vm.deal(user1, validEthAmount * 5); - - uint256 initialTSSBalance = tss.balance; - - // Make 5 deposits in sequence - for (uint256 i = 0; i < 5; i++) { - vm.prank(user1); - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - } - - // Verify TSS received all deposits - assertEq(tss.balance, initialTSSBalance + (validEthAmount * 5), "TSS should receive all sequential deposits"); - } - - /// @notice Test deposits from different users - function testDeposits_DifferentUsers_Success() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload,) = buildValuePayload(recipient, abi.encodeWithSignature("receive()"), 0); - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 validEthAmount = 25e14; // 0.0025 ETH = $5 - - // Fund multiple users - vm.deal(user1, validEthAmount); - vm.deal(user2, validEthAmount); - vm.deal(user3, validEthAmount); - - uint256 initialTSSBalance = tss.balance; - - // Each user makes a deposit - vm.prank(user1); - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - - vm.prank(user2); - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - - vm.prank(user3); - gateway.sendTxWithGas{ value: validEthAmount }(payload, revertCfg_, bytes("")); - - // Verify TSS received all deposits - assertEq(tss.balance, initialTSSBalance + (validEthAmount * 3), "TSS should receive deposits from all users"); - } - - /// @notice Test sendFunds with mismatched msg.value and bridgeAmount - function testSendFunds_MismatchedAmounts_Reverts() public { - // Setup: Create revert config - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 bridgeAmount = 1e18; // 1 ETH - uint256 msgValue = 2e18; // 2 ETH (different from bridgeAmount) - - vm.deal(user1, msgValue); - - // For native ETH bridging, msg.value must equal bridgeAmount - vm.prank(user1); - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendFunds{ value: msgValue }( - recipient, - address(0), // address(0) for native ETH bridging - bridgeAmount, // Different from msg.value - revertCfg_ - ); - } - - /// @notice Test sendFunds with non-zero msg.value when using ERC20 token - function testSendFunds_NonZeroMsgValueWithERC20_Reverts() public { - // Setup: Create revert config - RevertInstructions memory revertCfg_ = revertCfg(recipient); - uint256 bridgeAmount = 1e18; // 1 ETH worth of tokens - uint256 msgValue = 1e18; // 1 ETH (should be 0 for ERC20) - - vm.deal(user1, msgValue); - - // For ERC20 bridging, msg.value must be 0 - vm.prank(user1); - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendFunds{ value: msgValue }( - recipient, - address(weth), // ERC20 token - bridgeAmount, - revertCfg_ - ); - } -} diff --git a/contracts/evm-gateway/test/gateway/2_sendUniversalTx.t.sol b/contracts/evm-gateway/test/gateway/2_sendUniversalTx.t.sol new file mode 100644 index 0000000..9b0884d --- /dev/null +++ b/contracts/evm-gateway/test/gateway/2_sendUniversalTx.t.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BaseTest } from "../BaseTest.t.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { TX_TYPE, RevertInstructions, UniversalPayload, UniversalTxRequest } from "../../src/libraries/Types.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/** + * @title GatewaySendUniversalTx Test Suite + * @notice Tests for the sendUniversalTx() router function on UniversalGateway + * @dev Focus: Router logic and correct delegation to internal functions + * Does NOT test deep branch logic of _sendTxWithGas or _sendTxWithFunds + * Those are tested in separate dedicated test files + */ +contract GatewaySendUniversalTxTest is BaseTest { + // UniversalGateway instance (overrides BaseTest's gateway) + UniversalGateway public gatewayTemp; + + // ========================= + // EVENTS + // ========================= + event UniversalTx( + address indexed sender, + address indexed recipient, + address token, + uint256 amount, + bytes payload, + address revertRecipient, + TX_TYPE txType, + bytes signatureData + ); + + // ========================= + // SETUP + // ========================= + function setUp() public override { + super.setUp(); + + // Deploy UniversalGateway instead of UniversalGateway + _deployGatewayTemp(); + + // Wire oracle to the new gateway instance + vm.prank(admin); + gatewayTemp.setEthUsdFeed(address(ethUsdFeedMock)); + + // Setup token support on gatewayTemp (native + all mock ERC20s) + address[] memory tokens = new address[](4); + uint256[] memory thresholds = new uint256[](4); + tokens[0] = address(0); // Native token + tokens[1] = address(tokenA); // Mock ERC20 tokenA + tokens[2] = address(usdc); // Mock ERC20 usdc + tokens[3] = address(weth); // Mock WETH + thresholds[0] = 1000000 ether; // Large threshold for native + thresholds[1] = 1000000 ether; // Large threshold for tokenA + thresholds[2] = 1000000e6; // Large threshold for usdc (6 decimals) + thresholds[3] = 1000000 ether; // Large threshold for weth + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + // Re-approve tokens to gatewayTemp (BaseTest approved to old gateway) + address[] memory users = new address[](5); + users[0] = user1; + users[1] = user2; + users[2] = user3; + users[3] = user4; + users[4] = attacker; + + for (uint256 i = 0; i < users.length; i++) { + vm.prank(users[i]); + tokenA.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + usdc.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + weth.approve(address(gatewayTemp), type(uint256).max); + } + } + + /// @notice Deploy UniversalGateway (overrides BaseTest's UniversalGateway deployment) + function _deployGatewayTemp() internal { + // Deploy implementation + UniversalGateway implementation = new UniversalGateway(); + + // Deploy transparent upgradeable proxy + bytes memory initData = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, + tss, + address(this), // vault address (same as BaseTest) + MIN_CAP_USD, + MAX_CAP_USD, + uniV3Factory, + uniV3Router, + address(weth) + ); + + TransparentUpgradeableProxy tempProxy = + new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); + + // Cast proxy to UniversalGateway + gatewayTemp = UniversalGateway(payable(address(tempProxy))); + + vm.label(address(gatewayTemp), "UniversalGateway"); + } + + /// @notice Helper to build UniversalTxRequest structs + function buildUniversalTxRequest(address recipient_, address token, uint256 amount, bytes memory payload) + internal + pure + returns (UniversalTxRequest memory) + { + return UniversalTxRequest({ + recipient: recipient_, + token: token, + amount: amount, + payload: payload, + revertRecipient: address(0x456), + signatureData: bytes("") + }); + } + + // ========================= + // HAPPY PATH TESTS - GAS ROUTE + // ========================= + + /// @notice Test sendUniversalTx with TX_TYPE.GAS routes correctly to _sendTxWithGas + /// @dev Verifies: + /// - Function accepts valid GAS request + /// - Routes to instant route (_sendTxWithGas) + /// - Emits correct UniversalTx event + /// - Native ETH forwarded to TSS + function test_SendUniversalTx_GAS_HappyPath() public { + // Arrange + uint256 gasAmount = 0.001 ether; // Within USD caps at $2000/ETH: $2 + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // recipient (will be address(0) for gas route) + address(0), // token (native) + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") // empty payload for GAS type + ); + + uint256 tssBalanceBefore = tss.balance; + + // Act & Assert + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + sender: user1, + recipient: address(0), // Gas always credits UEA (address(0)) + token: address(0), // Native token + amount: gasAmount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + txType: TX_TYPE.GAS, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + // Assert: TSS received the native ETH + assertEq(tss.balance, tssBalanceBefore + gasAmount, "TSS should receive gas amount"); + } + + /// @notice Test sendUniversalTx with TX_TYPE.GAS_AND_PAYLOAD routes correctly + /// @dev Verifies: + /// - Function accepts valid GAS_AND_PAYLOAD request + /// - Routes to instant route (_sendTxWithGas) + /// - Emits correct UniversalTx event with payload + /// - Native ETH forwarded to TSS + function test_SendUniversalTx_GAS_AND_PAYLOAD_HappyPath() public { + // Arrange + uint256 gasAmount = 0.002 ether; // Within USD caps at $2000/ETH: $4 + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // recipient (will be address(0) for gas route) + address(0), // token (native) + 0, // amount must be 0 for GAS_AND_PAYLOAD route (matrix requires !hasFunds) + encodedPayload // non-empty payload required + ); + + uint256 tssBalanceBefore = tss.balance; + + // Act & Assert + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + sender: user1, + recipient: address(0), // Gas always credits UEA (address(0)) + token: address(0), // Native token + amount: gasAmount, + payload: encodedPayload, + revertRecipient: req.revertRecipient, + txType: TX_TYPE.GAS_AND_PAYLOAD, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + // Assert: TSS received the native ETH + assertEq(tss.balance, tssBalanceBefore + gasAmount, "TSS should receive gas amount"); + } + + // ========================= + // HAPPY PATH TESTS - FUNDS ROUTE + // ========================= + + /// @notice Test sendUniversalTx with TX_TYPE.FUNDS (native) routes correctly + /// @dev Verifies: + /// - Function accepts valid FUNDS request with native token + /// - Routes to standard route (_sendTxWithFunds) + /// - Emits correct UniversalTx event + /// - Native ETH forwarded to TSS + function test_SendUniversalTx_FUNDS_Native_HappyPath() public { + // Arrange + uint256 fundsAmount = 100 ether; // Large amount (no USD caps on FUNDS route) + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(0), // native token + fundsAmount, + bytes("") // empty payload for FUNDS type + ); + + uint256 tssBalanceBefore = tss.balance; + + // Act & Assert + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + sender: user1, + recipient: address(0), // FUNDS credits caller's UEA + token: address(0), // Native token + amount: fundsAmount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + txType: TX_TYPE.FUNDS, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + + // Assert: TSS received the native ETH + assertEq(tss.balance, tssBalanceBefore + fundsAmount, "TSS should receive funds amount"); + } + + /// @notice Test sendUniversalTx with TX_TYPE.FUNDS (ERC20) routes correctly + /// @dev Verifies: + /// - Function accepts valid FUNDS request with ERC20 + /// - Routes to standard route (_sendTxWithFunds) + /// - Emits correct UniversalTx event + /// - ERC20 transferred to VAULT + function test_SendUniversalTx_FUNDS_ERC20_HappyPath() public { + // Arrange: tokenA already enabled in setUp() + uint256 fundsAmount = 1000 ether; // Large amount + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(tokenA), // ERC20 token + fundsAmount, + bytes("") // empty payload for FUNDS type + ); + + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + + // Act + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); // No native value for ERC20 + + // Assert: VAULT received the ERC20 + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + fundsAmount, "VAULT should receive ERC20"); + } + + /// @notice Test sendUniversalTx with TX_TYPE.FUNDS_AND_PAYLOAD (no batching) routes correctly + /// @dev Verifies: + /// - Function accepts valid FUNDS_AND_PAYLOAD request (ERC20, no gas batching) + /// - Routes to standard route (_sendTxWithFunds) + /// - Emits correct UniversalTx event with payload + /// - ERC20 transferred to VAULT + function test_SendUniversalTx_FUNDS_AND_PAYLOAD_NoBatching_HappyPath() public { + // Arrange: tokenA already enabled in setUp() + uint256 fundsAmount = 500 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), // ERC20 token + fundsAmount, + encodedPayload // non-empty payload required + ); + + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + + // Act + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); // No native value (no batching) + + // Assert: VAULT received the ERC20 + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + fundsAmount, "VAULT should receive ERC20"); + } +} diff --git a/contracts/evm-gateway/test/gateway/3_GatewayDepositNonNative.t.sol b/contracts/evm-gateway/test/gateway/3_GatewayDepositNonNative.t.sol deleted file mode 100644 index c972e82..0000000 --- a/contracts/evm-gateway/test/gateway/3_GatewayDepositNonNative.t.sol +++ /dev/null @@ -1,1501 +0,0 @@ -pragma solidity 0.8.26; - -import { Test, console2 } from "forge-std/Test.sol"; -import { BaseTest } from "../BaseTest.t.sol"; -import { Errors } from "../../src/libraries/Errors.sol"; -import { TX_TYPE, RevertInstructions, UniversalPayload, VerificationType } from "../../src/libraries/Types.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IUniversalGateway } from "../../src/interfaces/IUniversalGateway.sol"; -import { UniversalGateway } from "../../src/UniversalGateway.sol"; -import { IWETH } from "../../src/interfaces/IWETH.sol"; -import { MockERC20 } from "../mocks/MockERC20.sol"; -import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; - -// USDT interface for non-standard transfer and approve functions - -interface TetherToken { - function transfer(address to, uint256 amount) external; - - function approve(address spender, uint256 amount) external; - - function balanceOf(address account) external view returns (uint256); -} - -/// @notice Test suite for ERC20 deposit functions in UniversalGateway -/// @dev Covers all functions that use ERC20 tokens (not native ETH): -/// 1. sendTxWithGas(tokenIn, amountIn, payload, revertCFG, amountOutMinETH, deadline) - ERC20 gas funding -/// 2. sendFunds(recipient, bridgeToken, bridgeAmount, revertCFG) - ERC20 bridging (when bridgeToken != address(0)) -/// 3. sendTxWithFunds(bridgeToken, bridgeAmount, gasToken, gasAmount, amountOutMinETH, deadline, payload, revertCFG) - ERC20 gas + ERC20 bridging -/// @dev Note: Uses mainnet fork for Uniswap integration testing -contract GatewayDepositNonNativeTest is BaseTest { - // ========================= - // MAINNET CONTRACTS - // ========================= - // Mainnet WETH address - address constant MAINNET_WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - - // Mainnet USDC address - address constant MAINNET_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - - // Mainnet USDT address - address constant MAINNET_USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; - - // Mainnet DAI address - address constant MAINNET_DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; - - // Mainnet Uniswap V3 Router - address constant MAINNET_UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; - - // Mainnet Uniswap V3 Factory - address constant MAINNET_UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; - - // Mainnet Chainlink ETH/USD Feed - address constant MAINNET_ETH_USD_FEED = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; - - // Real mainnet token contracts - IERC20 mainnetWETH; - IERC20 mainnetUSDC; - TetherToken mainnetUSDT; - IERC20 mainnetDAI; - - // ========================= - // SETUP - // ========================= - function setUp() public override { - // Use mainnet fork for Uniswap integration - vm.createSelectFork("https://eth-mainnet.public.blastapi.io"); - super.setUp(); - - // Redeploy gateway with mainnet WETH address - _redeployGatewayWithMainnetWETH(); - - // Override gateway configuration to use mainnet contracts - vm.prank(admin); - gateway.setRouters(MAINNET_UNISWAP_V3_FACTORY, MAINNET_UNISWAP_V3_ROUTER); - - // Initialize real mainnet token contracts - mainnetWETH = IERC20(MAINNET_WETH); - mainnetUSDC = IERC20(MAINNET_USDC); - mainnetUSDT = TetherToken(MAINNET_USDT); - mainnetDAI = IERC20(MAINNET_DAI); - - // Enable mainnet ERC20 token support for testing - address[] memory tokens = new address[](5); - bool[] memory supported = new bool[](5); - tokens[0] = MAINNET_WETH; - tokens[1] = MAINNET_USDC; - tokens[2] = MAINNET_USDT; - tokens[3] = MAINNET_DAI; - supported[0] = true; - supported[1] = true; - supported[2] = true; - supported[3] = true; - supported[4] = true; - - vm.prank(admin); - // Set threshold to a large value to enable support (0 means unsupported) - uint256[] memory thresholds = new uint256[](5); - for (uint256 i = 0; i < 5; i++) { - thresholds[i] = supported[i] ? 1000000 ether : 0; - } - gateway.setTokenLimitThresholds(tokens, thresholds); - } - - // ========================= - // HELPER FUNCTIONS - // ========================= - - /// @notice Fund user with real mainnet tokens by impersonating a whale - function fundUserWithMainnetTokens(address user, address token, uint256 amount) internal override { - // Find a whale address that has the token - address whale; - if (token == MAINNET_WETH) { - whale = 0x28C6c06298d514Db089934071355E5743bf21d60; // Binance 14 - } else if (token == MAINNET_USDC) { - whale = 0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341; // SKY - } else if (token == MAINNET_USDT) { - whale = 0xF977814e90dA44bFA03b6295A0616a897441aceC; // Binance 20 - } else if (token == MAINNET_DAI) { - whale = 0x28C6c06298d514Db089934071355E5743bf21d60; // Binance 14 - } else { - revert("Unknown token"); - } - - // Check if whale has enough tokens - uint256 whaleBalance = IERC20(token).balanceOf(whale); - require(whaleBalance >= amount, "Whale doesn't have enough tokens"); - - // Impersonate whale and transfer tokens - - vm.startPrank(whale); - - // Handle USDT specially since it has non-standard transfer function - if (token == MAINNET_USDT) { - // USDT transfer returns void, not bool - TetherToken(token).transfer(user, amount); - } else { - IERC20(token).transfer(user, amount); - } - - vm.stopPrank(); - - // Verify transfer was successful (user should have at least the amount we sent) - assertGe(IERC20(token).balanceOf(user), amount, "User should receive tokens"); - } - - /// @notice Redeploy gateway with mainnet WETH address - function _redeployGatewayWithMainnetWETH() internal { - // Deploy new implementation - UniversalGateway newImplementation = new UniversalGateway(); - - // Create initialization data with mainnet WETH - bytes memory initData = abi.encodeWithSelector( - UniversalGateway.initialize.selector, - admin, // admin - tss, // tss - address(this), // vault address - MIN_CAP_USD, - MAX_CAP_USD, - uniV3Factory, - uniV3Router, - MAINNET_WETH // Use mainnet WETH instead of mock - ); - - // Deploy new proxy - gatewayProxy = new TransparentUpgradeableProxy(address(newImplementation), address(proxyAdmin), initData); - - // Update gateway reference - gateway = UniversalGateway(payable(address(gatewayProxy))); - - // Label for debugging - vm.label(address(gateway), "UniversalGateway-MainnetWETH"); - vm.label(address(gatewayProxy), "GatewayProxy-MainnetWETH"); - - // Re-initialize gateway settings - vm.prank(admin); - gateway.setEthUsdFeed(MAINNET_ETH_USD_FEED); - - // Set the correct fee order for Uniswap V3 - vm.prank(admin); - gateway.setV3FeeOrder(500, 3000, 10000); - } - - // ========================= - // HAPPY PATH TESTS - ERC20 Functions - // ========================= - - /// @notice Test sendTxWithGas (ERC20) with valid parameters - function testSendTxWithGas_ERC20_HappyPath() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - // Use WETH for gas funding (fast path - no swap needed) - // Using a smaller amount to stay within USD caps - uint256 wethAmount = 0.002e18; // 0.002 WETH (well within USD caps) - uint256 amountOutMinETH = 0.0001e18; // Allow 95% slippage for testing - uint256 deadline = block.timestamp + 3600; // 1 hour deadline - - // Fund user with mainnet WETH and approve gateway - fundUserWithMainnetTokens(user1, MAINNET_WETH, wethAmount); - vm.prank(user1); - mainnetWETH.approve(address(gateway), wethAmount); - - // Record initial balances - uint256 initialTSSBalance = tss.balance; - uint256 initialUserWETHBalance = mainnetWETH.balanceOf(user1); - uint256 initialGatewayWETHBalance = mainnetWETH.balanceOf(address(gateway)); - - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user1, - address(0), - address(0), - wethAmount, - abi.encode(payload), - revertCfg_, - TX_TYPE.GAS_AND_PAYLOAD, - bytes("") - ); - - // Execute the transaction - vm.prank(user1); - gateway.sendTxWithGas(MAINNET_WETH, wethAmount, payload, revertCfg_, amountOutMinETH, deadline, bytes("")); - - // Verify TSS received the ETH (WETH was unwrapped to ETH) - assertEq(tss.balance, initialTSSBalance + wethAmount, "TSS should receive ETH from WETH unwrapping"); - - // Verify user's WETH balance decreased - assertEq(mainnetWETH.balanceOf(user1), initialUserWETHBalance - wethAmount, "User should pay WETH"); - - // Verify gateway's WETH balance is unchanged (WETH was unwrapped) - assertEq( - mainnetWETH.balanceOf(address(gateway)), - initialGatewayWETHBalance, - "Gateway should not hold WETH (unwrapped to ETH)" - ); - } - - /// @notice Test sendFunds (ERC20) with valid parameters - function testSendFunds_ERC20_HappyPath() public { - // Setup: Create revert config - RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertContext: bytes("") }); - - // Use USDC for bridging - uint256 bridgeAmount = 1000e6; // 1000 USDC (6 decimals) - - // Fund user with mainnet USDC and approve gateway - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount); - vm.prank(user1); - mainnetUSDC.approve(address(gateway), bridgeAmount); - - // Record initial balances - uint256 initialUserTokenBalance = mainnetUSDC.balanceOf(user1); - uint256 initialGatewayTokenBalance = mainnetUSDC.balanceOf(address(gateway)); - - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user1, recipient, MAINNET_USDC, bridgeAmount, bytes(""), revertCfg_, TX_TYPE.FUNDS, bytes("") - ); - - // Execute the transaction - ERC20 bridging (msg.value must be 0) - vm.prank(user1); - gateway.sendFunds{ value: 0 }( - recipient, - MAINNET_USDC, // ERC20 token for bridging - bridgeAmount, - revertCfg_ - ); - - // Verify user's token balance decreased - assertEq(mainnetUSDC.balanceOf(user1), initialUserTokenBalance - bridgeAmount, "User should pay ERC20 tokens"); - - // Verify VAULT's token balance increased (tokens are now transferred to VAULT) - assertEq( - mainnetUSDC.balanceOf(gateway.VAULT()), - bridgeAmount, - "VAULT should receive ERC20 tokens" - ); - } - - /// @notice Test sendTxWithFunds (ERC20 gas + ERC20 bridging) with valid parameters - function testSendTxWithFunds_ERC20_HappyPath() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - // Use WETH for gas funding and USDC for bridging - (, uint256 maxEthAmount) = gateway.getMinMaxValueForNative(); - uint256 gasAmount = (maxEthAmount * 8) / 10; // 80% of max ETH amount for gas - uint256 bridgeAmount = 1000e6; // 1000 USDC for bridging (6 decimals) - uint256 amountOutMinETH = gasAmount; // same as weth amount - uint256 deadline = block.timestamp + 3600; // 1 hour deadline - - // Fund user with mainnet tokens - fundUserWithMainnetTokens(user1, MAINNET_WETH, gasAmount); - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount); - - // Approve gateway for both tokens - vm.startPrank(user1); - mainnetWETH.approve(address(gateway), gasAmount); - mainnetUSDC.approve(address(gateway), bridgeAmount); - vm.stopPrank(); - - // Record initial balances - uint256 initialTSSBalance = tss.balance; - uint256 initialUserWETHBalance = mainnetWETH.balanceOf(user1); - uint256 initialUserTokenBalance = mainnetUSDC.balanceOf(user1); - uint256 initialGatewayTokenBalance = mainnetUSDC.balanceOf(address(gateway)); - - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user1, address(0), address(0), gasAmount, bytes(""), revertCfg_, TX_TYPE.GAS, bytes("") - ); - - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user1, - address(0), - MAINNET_USDC, - bridgeAmount, - abi.encode(payload), - revertCfg_, - TX_TYPE.FUNDS_AND_PAYLOAD, - bytes("") - ); - - // Execute the transaction - vm.prank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, // Bridge token - bridgeAmount, - MAINNET_WETH, // Gas token - gasAmount, - amountOutMinETH, - deadline, - payload, - revertCfg_, - bytes("") - ); - - // Verify TSS received the ETH (from WETH unwrapping) - assertEq(tss.balance, initialTSSBalance + gasAmount, "TSS should receive ETH from WETH unwrapping"); - - // Verify user's WETH balance decreased - assertEq(mainnetWETH.balanceOf(user1), initialUserWETHBalance - gasAmount, "User should pay WETH for gas"); - - // Verify user's token balance decreased - assertEq( - mainnetUSDC.balanceOf(user1), initialUserTokenBalance - bridgeAmount, "User should pay USDC for bridging" - ); - - // Verify gateway's token balance increased - assertEq( - mainnetUSDC.balanceOf(gateway.VAULT()), - bridgeAmount, - "VAULT should receive USDC for bridging" - ); - } - - /// @notice Test all ERC20 functions with minimum valid amounts - function testAllERC20Functions_MinimumAmounts_Success() public { - // Test sendFunds with minimum amount (no Uniswap dependency) - RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertContext: bytes("") }); - - uint256 minAmount = 1; // Minimum amount - - // Test sendFunds with minimum amount - fundUserWithMainnetTokens(user2, MAINNET_USDC, minAmount); - vm.prank(user2); - mainnetUSDC.approve(address(gateway), minAmount); - - uint256 initialGatewayBalance = IERC20(MAINNET_USDC).balanceOf(address(gateway)); - - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user2, recipient, MAINNET_USDC, minAmount, bytes(""), revertCfg_, TX_TYPE.FUNDS, bytes("") - ); - - vm.prank(user2); - gateway.sendFunds{ value: 0 }(recipient, MAINNET_USDC, minAmount, revertCfg_); - - // Test passes if no revert occurs - assertEq( - IERC20(MAINNET_USDC).balanceOf(gateway.VAULT()), - minAmount, - "VAULT should receive USDC" - ); - } - - /// @notice Test all ERC20 functions with maximum valid amounts - function testAllERC20Functions_MaximumAmounts_Success() public { - // Test sendFunds with maximum amount (no Uniswap dependency) - RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertContext: bytes("") }); - - // Test sendFunds with maximum amount - uint256 maxTokenAmount = 1000000e6; // Large token amount - fundUserWithMainnetTokens(user2, MAINNET_USDC, maxTokenAmount); - vm.prank(user2); - mainnetUSDC.approve(address(gateway), maxTokenAmount); - - uint256 initialGatewayBalance = IERC20(MAINNET_USDC).balanceOf(address(gateway)); - - vm.expectEmit(true, true, true, true); - emit IUniversalGateway.UniversalTx( - user2, recipient, MAINNET_USDC, maxTokenAmount, bytes(""), revertCfg_, TX_TYPE.FUNDS, bytes("") - ); - - vm.prank(user2); - gateway.sendFunds{ value: 0 }(recipient, MAINNET_USDC, maxTokenAmount, revertCfg_); - } - - // ========================= - // PARAMETER VALIDATION TESTS - // ========================= - - function testUSDCapValidation() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - uint256 bridgeAmount = 1000e6; // 1000 USDC (6 decimals) - (uint256 minEth, uint256 maxEth) = gateway.getMinMaxValueForNative(); - uint256 aboveMaxEth = maxEth + 1000000; // Above max USD cap - - vm.deal(user1, aboveMaxEth); // Give user enough ETH for the transaction - - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount); - vm.startPrank(user1); - mainnetUSDC.approve(address(gateway), bridgeAmount); - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithFunds{ value: aboveMaxEth }( - MAINNET_USDC, // ERC20 token for bridging - bridgeAmount, - payload, - revertCfg_, - bytes("") - ); - - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithFunds{ value: minEth - 1 }( - MAINNET_USDC, // ERC20 token for bridging - bridgeAmount, - payload, - revertCfg_, - bytes("") - ); - - uint256 amountOutMinETH = 1; // Place holder - this logic won't be reached - uint256 deadline = block.timestamp + 3600; - uint256 amountIn = 1e5; // Below min cap - - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithGas( - MAINNET_USDC, // ERC20 token for bridging - amountIn, - payload, - revertCfg_, - amountOutMinETH, - deadline, - bytes("") - ); - amountIn = 11e6; // Above max cap - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithGas( - MAINNET_USDC, // ERC20 token for bridging - amountIn, - payload, - revertCfg_, - amountOutMinETH, - deadline, - bytes("") - ); - - vm.stopPrank(); - } - - /// @notice Test sendTxWithGas (ERC20) with zero token address - function testSendTxWithGas_ERC20_WrongValues_Reverts() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - uint256 amountIn = 1e18; - uint256 amountOutMinETH = 1e18; - uint256 deadline = block.timestamp + 3600; - - // Execute the transaction with zero token address and expect it to revert - vm.startPrank(user1); - vm.expectRevert(Errors.InvalidInput.selector); - gateway.sendTxWithGas( - address(0), // Zero token address - amountIn, - payload, - revertCfg_, - amountOutMinETH, - deadline, - bytes("") - ); - - // Execute the transaction with zero amount and expect it to revert - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithGas( - MAINNET_WETH, - 0, // Zero amount - payload, - revertCfg_, - amountOutMinETH, - deadline, - bytes("") - ); - - // Execute the transaction with zero amountOutMinETH and expect it to revert - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithGas( - MAINNET_WETH, - amountIn, - payload, - revertCfg_, - 0, // Zero amountOutMinETH - deadline, - bytes("") - ); - - // Execute the transaction with expired deadline and expect it to revert - uint256 expiredDeadline = block.timestamp - 1; // Expired deadline - vm.expectRevert(Errors.SlippageExceededOrExpired.selector); - gateway.sendTxWithGas(MAINNET_WETH, amountIn, payload, revertCfg_, amountOutMinETH, expiredDeadline, bytes("")); - - // Execute the transaction with non-zero msg.value and expect it to revert - uint256 bridgeAmount = 1000e6; // 1000 USDC (6 decimals) - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendFunds{ value: 1e18 }( // Non-zero msg.value - recipient, - MAINNET_USDC, // ERC20 token for bridging - bridgeAmount, - revertCfg_ - ); - - // Execute the transaction with zero bridge amount and expect it to revert - uint256 gasAmount = 1e18; - - // Execute the transaction with zero gas token address and expect it to revert - vm.expectRevert(Errors.InvalidInput.selector); - gateway.sendTxWithFunds( - MAINNET_USDC, // Bridge token - bridgeAmount, - address(0), // Zero gas token address - gasAmount, - amountOutMinETH, - deadline, - payload, - revertCfg_, - bytes("") - ); - - // Execute the transaction with zero gas amount and expect it to revert - // Fund user with WETH for gas token - fundUserWithMainnetTokens(user1, MAINNET_WETH, gasAmount); - vm.prank(user1); - IERC20(MAINNET_WETH).approve(address(gateway), gasAmount); - - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithFunds( - MAINNET_USDC, // Bridge token - bridgeAmount, - MAINNET_WETH, // Gas token - 0, // Zero gas amount - amountOutMinETH, - deadline, - payload, - revertCfg_, - bytes("") - ); - } - - /// @notice Test sendFunds (ERC20) with zero bridge amount - function testSendFunds_ERC20_ZeroBridgeAmount_Reverts() public { - // Setup: Create revert config - RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertContext: bytes("") }); - - // Note: ERC20 sendFunds doesn't explicitly check for zero amount - // The _handleTokenDeposit function just calls safeTransferFrom with zero amount - // which might not revert. This test verifies the current behavior. - - // Execute the transaction with zero bridge amount - vm.prank(user1); - gateway.sendFunds{ value: 0 }( - recipient, - MAINNET_USDC, // ERC20 token for bridging - 0, // Zero bridge amount - revertCfg_ - ); - - // Verify the transaction succeeded (current behavior) - // This test documents that zero amounts are allowed for ERC20 sendFunds - } - - // ========================= - // TOKEN SUPPORT VALIDATION TESTS - // ========================= - - /// @notice Test all ERC20 functions with unsupported tokens - function testERC20Functions_UnsupportedTokens_Reverts() public { - // Setup: Create payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - uint256 amount = 1000e6; // 1000 USDC (6 decimals) - uint256 deadline = block.timestamp + 3600; - - // Create an unsupported token (not in the supported list) - MockERC20 unsupportedToken = new MockERC20("Unsupported", "UNS", 18, 0); - unsupportedToken.mint(user1, amount); - vm.startPrank(user1); - unsupportedToken.approve(address(gateway), amount); - - // Test sendTxWithGas with unsupported token - vm.expectRevert(Errors.InvalidInput.selector); // Any revert is acceptable for unsupported token - gateway.sendTxWithGas(address(unsupportedToken), amount, payload, revertCfg_, 0.5e18, deadline, bytes("")); - - // Test sendFunds with unsupported token - vm.expectRevert(Errors.NotSupported.selector); // Any revert is acceptable for unsupported token - gateway.sendFunds{ value: 0 }(recipient, address(unsupportedToken), amount, revertCfg_); - - // Test sendTxWithFunds with unsupported bridge token - fundUserWithMainnetTokens(user1, MAINNET_WETH, amount); - (, uint256 maxEthAmount) = gateway.getMinMaxValueForNative(); - uint256 amountOutMinETH = (maxEthAmount * 8) / 10; - mainnetWETH.approve(address(gateway), amountOutMinETH); - vm.expectRevert(Errors.NotSupported.selector); // Any revert is acceptable for unsupported token - gateway.sendTxWithFunds( - address(unsupportedToken), // Unsupported bridge token - amount, - address(MAINNET_WETH), // Supported gas token - (maxEthAmount * 8) / 10, - amountOutMinETH, - deadline, - payload, - revertCfg_, - bytes("") - ); - - // Test sendTxWithFunds with unsupported gas token - vm.expectRevert(Errors.InvalidInput.selector); // Any revert is acceptable for unsupported token - gateway.sendTxWithFunds( - MAINNET_USDC, // Supported bridge token - amount, - address(unsupportedToken), // Unsupported gas token - amount, - 0.5e18, - deadline, - payload, - revertCfg_, - bytes("") - ); - } - - // ========================= - // ACCESS CONTROL & PAUSE TESTS - // ========================= - - /// @notice Test all ERC20 functions when contract is paused - function testERC20Functions_WhenPaused_Reverts() public { - // Setup: Create payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - uint256 amount = 1000e6; // 1000 USDC (6 decimals) - uint256 deadline = block.timestamp + 3600; - - // Fund user with mainnet tokens - fundUserWithMainnetTokens(user1, MAINNET_WETH, amount); - fundUserWithMainnetTokens(user1, MAINNET_USDC, amount); - vm.startPrank(user1); - mainnetWETH.approve(address(gateway), amount); - mainnetUSDC.approve(address(gateway), amount); - vm.stopPrank(); - - // Pause the contract - vm.prank(admin); - gateway.pause(); - - vm.startPrank(user1); - // Test sendTxWithGas when paused - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); // Any revert is acceptable for paused state - gateway.sendTxWithGas(MAINNET_WETH, amount, payload, revertCfg_, 0.5e18, deadline, bytes("")); - - // Test sendFunds when paused - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); // Any revert is acceptable for paused state - gateway.sendFunds{ value: 0 }(recipient, MAINNET_USDC, amount, revertCfg_); - - // Test sendTxWithFunds when paused - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); // Any revert is acceptable for paused state - gateway.sendTxWithFunds( - MAINNET_USDC, amount, MAINNET_WETH, amount, 0.5e18, deadline, payload, revertCfg_, bytes("") - ); - vm.stopPrank(); - } - - // ========================= - // USD CAP VALIDATION TESTS - // ========================= - - /// @notice Test sendTxWithFunds (ERC20) with gas amount below minimum USD cap - function testSendTxWithFunds_ERC20_GasAmountBelowMinCap_Reverts() public { - // Setup: Create payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - uint256 bridgeAmount = 1000e6; // 1000 USDC (6 decimals) - uint256 gasAmount = 0.001e6; // Very small amount (below min cap) - 6 decimals - uint256 deadline = block.timestamp + 3600; - - // Mint tokens to user1 - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount + gasAmount); - vm.startPrank(user1); - mainnetUSDC.approve(address(gateway), bridgeAmount + gasAmount); - - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithFunds( - MAINNET_USDC, bridgeAmount + gasAmount, MAINNET_USDC, gasAmount, 1, deadline, payload, revertCfg_, bytes("") - ); - vm.stopPrank(); - } - - /// @notice Test sendTxWithFunds (ERC20) with gas amount above maximum USD cap - function testSendTxWithFunds_ERC20_GasAmountAboveMaxCap_Reverts() public { - // Setup: Create payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - uint256 bridgeAmount = 1000e6; // 1000 USDC (6 decimals) - uint256 gasAmount = 1000000e6; // Very large amount (above max cap) - 6 decimals - uint256 deadline = block.timestamp + 3600; - - // Mint tokens to user1 - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount + gasAmount); - vm.startPrank(user1); - mainnetUSDC.approve(address(gateway), bridgeAmount + gasAmount); - - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.sendTxWithFunds( - MAINNET_USDC, bridgeAmount, MAINNET_USDC, gasAmount, 1, deadline, payload, revertCfg_, bytes("") - ); - vm.stopPrank(); - } - - // ========================= - // TOKEN TRANSFER VERIFICATION TESTS - // ========================= - - /// @notice Test that ERC20 bridge tokens are actually transferred to gateway - function testSendFunds_ERC20_TokenTransferToGateway_Success() public { - // Setup: Create revert config - RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertContext: bytes("") }); - - uint256 tokenAmount = 1000e6; // 1000 USDC (6 decimals) - - // Record initial balances - uint256 initialUserBalance = mainnetUSDC.balanceOf(user1); - uint256 initialGatewayBalance = mainnetUSDC.balanceOf(address(gateway)); - - // Fund user with mainnet tokens - fundUserWithMainnetTokens(user1, MAINNET_USDC, tokenAmount); - vm.prank(user1); - mainnetUSDC.approve(address(gateway), tokenAmount); - - // Execute sendFunds - vm.prank(user1); - gateway.sendFunds{ value: 0 }(recipient, MAINNET_USDC, tokenAmount, revertCfg_); - - // Verify token transfer - assertEq( - mainnetUSDC.balanceOf(user1), - initialUserBalance, - "User balance should remain unchanged (tokens transferred to gateway)" - ); - - assertEq( - mainnetUSDC.balanceOf(gateway.VAULT()), - tokenAmount, - "VAULT should receive the tokens" - ); - } - - /// @notice Test that ERC20 gas tokens are actually transferred to gateway - function testSendTxWithFunds_ERC20_GasTokenTransferToGateway_Success() public { - // Setup: Create payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - uint256 bridgeAmount = 1000e6; // 1000 USDC (6 decimals) - uint256 gasAmount = 9e6; // 9 USDC (6 decimals) - uint256 deadline = block.timestamp + 3600; - - // Mint tokens to user1 - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount + gasAmount); - - // Record initial balances - uint256 initialUserBalance = mainnetUSDC.balanceOf(user1); - uint256 initialGatewayBalance = mainnetUSDC.balanceOf(address(gateway)); - uint256 initialTSSEthBalance = tss.balance; - - vm.prank(user1); - mainnetUSDC.approve(address(gateway), bridgeAmount + gasAmount); - - (uint256 ethPrice, uint8 decimals) = gateway.getEthUsdPrice(); - uint256 gasAmountInETH = (gasAmount * 10 ** (18 - 6) * 1e18) / ethPrice; - // Execute sendTxWithFunds - vm.startPrank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, // Bridge token - bridgeAmount, - MAINNET_USDC, // Gas token - gasAmount, - (gasAmountInETH * 9) / 10, - deadline, - payload, - revertCfg_, - bytes("") - ); - vm.stopPrank(); - // Verify gas token transfer - assertEq( - mainnetUSDC.balanceOf(user1), - initialUserBalance - bridgeAmount - gasAmount, - "User balance should remain unchanged (tokens transferred to gateway)" - ); - - assertEq( - mainnetUSDC.balanceOf(gateway.VAULT()), - bridgeAmount, - "VAULT should receive the bridge tokens" - ); - - assertApproxEqAbs( - tss.balance, initialTSSEthBalance + gasAmountInETH, 1e15, "TSS should receive ETH from gas tokens" - ); - } - - // ========================= - // COMPREHENSIVE EDGE CASES - // ========================= - - /// @notice Test comprehensive edge cases for ERC20 functions - function testERC20Functions_EdgeCases_Success() public { - // Setup: Create payload and revert config - RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertContext: bytes("") }); - - uint256 amount = 1000e6; // 1000 USDC (6 decimals) - - // Test 1: Different ERC20 tokens (USDC only to avoid support issues) - fundUserWithMainnetTokens(user1, MAINNET_USDC, amount); - vm.prank(user1); - mainnetUSDC.approve(address(gateway), amount); - - vm.prank(user1); - gateway.sendFunds{ value: 0 }( - recipient, - MAINNET_USDC, // Use USDC which is supported - amount, - revertCfg_ - ); - // Test 2: Different amounts with USDT (skip Uniswap-dependent tests) - uint256 amount2 = 500e6; // 500 USDT (6 decimals) - fundUserWithMainnetTokens(user2, MAINNET_USDT, amount2); - vm.prank(user2); - // USDT approve returns void, not bool - TetherToken(MAINNET_USDT).approve(address(gateway), amount2); - - vm.prank(user2); - gateway.sendFunds{ value: 0 }(recipient, MAINNET_USDT, amount2, revertCfg_); - - // Test 3: Different deadlines with DAI (1 hour, 1 day, 1 week) - uint256 daiAmount = 1000e18; // 1000 DAI (18 decimals) - fundUserWithMainnetTokens(user3, MAINNET_DAI, daiAmount); - vm.prank(user3); - mainnetDAI.approve(address(gateway), daiAmount); - - // Test with 1 day deadline - vm.prank(user3); - gateway.sendFunds{ value: 0 }(recipient, MAINNET_DAI, daiAmount, revertCfg_); - - // Test 4: Multiple ERC20 deposits in sequence - for (uint256 i = 0; i < 3; i++) { - fundUserWithMainnetTokens(user4, MAINNET_USDC, amount); - vm.prank(user4); - mainnetUSDC.approve(address(gateway), amount); - vm.prank(user4); - gateway.sendFunds{ value: 0 }(recipient, MAINNET_USDC, amount, revertCfg_); - } - - // Test 5: Different users making deposits - address[] memory users = new address[](3); - users[0] = user1; - users[1] = user2; - users[2] = user3; - - for (uint256 i = 0; i < users.length; i++) { - fundUserWithMainnetTokens(users[i], MAINNET_USDC, amount); - vm.prank(users[i]); - mainnetUSDC.approve(address(gateway), amount); - - vm.prank(users[i]); - gateway.sendFunds{ value: 0 }(recipient, MAINNET_USDC, amount, revertCfg_); - } - - // All tests should succeed, demonstrating robust edge case handling - } - - /// @notice Test ERC20 deposits with insufficient token balance - function testERC20Deposits_InsufficientBalance_Reverts() public { - // Setup: Create payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - uint256 tokenAmount = 1000e6; // 1000 USDC (6 decimals) - uint256 deadline = block.timestamp + 3600; - - // Don't fund user1 with tokens (insufficient balance) - vm.prank(user1); - mainnetUSDC.approve(address(gateway), tokenAmount); - - // Execute sendTxWithGas with insufficient balance (should fail) - vm.prank(user1); - vm.expectRevert(); - gateway.sendTxWithGas( - MAINNET_USDC, - tokenAmount, - payload, - revertCfg_, - 0.5e18, // 50% slippage tolerance - deadline, - bytes("") - ); - } - - /// @notice Test ERC20 deposits with insufficient token allowance - function testERC20Deposits_InsufficientAllowance_Reverts() public { - // Setup: Create payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - uint256 tokenAmount = 1000e6; // 1000 USDC (6 decimals) - uint256 deadline = block.timestamp + 3600; - - // Fund user1 with tokens but don't approve (insufficient allowance) - fundUserWithMainnetTokens(user1, MAINNET_USDC, tokenAmount); - - // Execute sendTxWithGas with insufficient allowance (should fail) - vm.prank(user1); - vm.expectRevert(); - gateway.sendTxWithGas( - MAINNET_USDC, - tokenAmount, - payload, - revertCfg_, - 0.5e18, // 50% slippage tolerance - deadline, - bytes("") - ); - } - - // ========================= - // UNISWAP POOL VALIDATION TESTS - // ========================= - - /// @notice Test with non-existent Uniswap pool - function testSendTxWithGas_ERC20_NonExistentPool_Reverts() public { - // Setup: Create payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg_) = - buildERC20Payload(recipient, abi.encodeWithSignature("receive()"), 0); - - // Use a token that doesn't have a WETH pair on Uniswap - // Create a mock token with no liquidity - MockERC20 fakeToken = new MockERC20("Fake", "FAKE", 18, 0); - fakeToken.mint(user1, 1e18); - - vm.prank(user1); - fakeToken.approve(address(gateway), 1e18); - - // This should revert because there's no WETH/FAKE pool - vm.prank(user1); - vm.expectRevert(Errors.InvalidInput.selector); // Any revert is acceptable for non-existent pool - gateway.sendTxWithGas(address(fakeToken), 1e18, payload, revertCfg_, 0.01e18, block.timestamp + 3600, bytes("")); - } - - // ========================= - // HELPER FUNCTIONS - // ========================= - - /// @notice Helper function to create valid ERC20 payload - function buildERC20Payload(address to, bytes memory data, uint256 value) - internal - pure - override - returns (UniversalPayload memory, RevertInstructions memory) - { - UniversalPayload memory payload = UniversalPayload({ - to: to, - value: value, - data: data, - gasLimit: 0, - maxFeePerGas: 0, - maxPriorityFeePerGas: 0, - nonce: 0, - deadline: 0, - vType: VerificationType.signedVerification - }); - - RevertInstructions memory revertCfg = RevertInstructions({ fundRecipient: to, revertContext: bytes("") }); - - return (payload, revertCfg); - } - - /// @notice Helper function to calculate minimum ETH output for slippage protection - function calculateMinETHOutput(uint256 tokenAmount, uint256 slippageBps) internal pure returns (uint256) { - // Calculate minimum amount out based on slippage tolerance - // slippageBps is in basis points (e.g., 100 = 1%) - uint256 slippageAmount = (tokenAmount * slippageBps) / 10000; - return tokenAmount - slippageAmount; - } - - /// @notice Helper function to setup ERC20 token with balance and allowance - function setupERC20Token(address token, address user, uint256 amount) internal { - // Mint tokens to user and approve gateway - MockERC20(token).mint(user, amount); - vm.prank(user); - MockERC20(token).approve(address(gateway), amount); - } - - // ========================= - // TRANSACTION TYPE VALIDATIONS TESTS - // ========================= - - function testTransactionTypeValidations_AddressZeroWithInvalidTxType_Reverts() public { - // Test with address(0) recipient and invalid transaction type - (UniversalPayload memory payload, RevertInstructions memory revertCfg) = - buildERC20Payload(address(0), abi.encodeWithSignature("receive()"), 0); - - // Fund user with tokens - use amount within USD caps - uint256 amount = 9e6; // 9 USDC = $9, well above min cap - fundUserWithMainnetTokens(user1, MAINNET_USDC, amount); - vm.prank(user1); - mainnetUSDC.approve(address(gateway), amount); - (, uint256 maxEth) = gateway.getMinMaxValueForNative(); - uint256 gasAmount = (maxEth * 8) / 10; - - // This should revert because address(0) with FUNDS type is invalid - vm.expectRevert(abi.encodeWithSelector(Errors.InvalidRecipient.selector)); - vm.prank(user1); - gateway.sendTxWithFunds{ value: gasAmount }(MAINNET_USDC, amount, payload, revertCfg, bytes("")); - - vm.expectRevert(abi.encodeWithSelector(Errors.InvalidRecipient.selector)); - vm.prank(user1); - gateway.sendFunds{ value: 0 }(address(0), MAINNET_USDC, amount, revertCfg); - } - - function testTransactionTypeValidations_AddressZeroWithValidTxType_Success() public { - // Test with address(0) recipient and valid transaction type - (UniversalPayload memory payload, RevertInstructions memory revertCfg) = - buildERC20Payload(address(1), abi.encodeWithSignature("receive()"), 0); - - // Fund user with tokens - use amount within USD caps - uint256 amount = 9e6; // 9 USDC = $9, well above min cap - fundUserWithMainnetTokens(user1, MAINNET_USDC, amount); - vm.prank(user1); - mainnetUSDC.approve(address(gateway), amount); - (, uint256 maxEth) = gateway.getMinMaxValueForNative(); - uint256 gasAmount = (maxEth * 8) / 10; - - // This should not revert because FUNDS_AND_PAYLOAD is valid for address(0) - vm.prank(user1); - gateway.sendTxWithFunds{ value: gasAmount }(MAINNET_USDC, amount, payload, revertCfg, bytes("")); - } - - function testTransactionTypeValidations_NonZeroAddressWithAnyTxType_Success() public { - // Test with non-zero recipient and any transaction type - (UniversalPayload memory payload, RevertInstructions memory revertCfg) = - buildERC20Payload(user1, abi.encodeWithSignature("receive()"), 0); - - // Fund user with tokens - use amount within USD caps - uint256 amount = 9e6; // 9 USDC = $9, well above min cap - fundUserWithMainnetTokens(user1, MAINNET_USDC, amount); - vm.prank(user1); - mainnetUSDC.approve(address(gateway), amount); - - // This should not revert for any transaction type with non-zero recipient - (, uint256 maxEth) = gateway.getMinMaxValueForNative(); - uint256 gasAmount = (maxEth * 8) / 10; - vm.prank(user1); - gateway.sendTxWithFunds{ value: gasAmount }(MAINNET_USDC, amount, payload, revertCfg, bytes("")); - } - - // ========================= - // SWAP TO NATIVE WITH VARIOUS TOKENS TESTS - // ========================= - - function testSwapToNative_VariousTokens_Success() public { - // Test with USDT to verify swapToNative functionality - address token = MAINNET_USDT; - uint256 gasAmount = 8e6; // 8 USDT (6 decimals) - - // Fund user with tokens - use amount that's within USD caps - fundUserWithMainnetTokens(user1, token, gasAmount); - - // Create a new gateway proxy to avoid any state issues from previous tests - _redeployGatewayWithMainnetWETH(); - - // Override gateway configuration to use mainnet contracts - vm.prank(admin); - gateway.setRouters(MAINNET_UNISWAP_V3_FACTORY, MAINNET_UNISWAP_V3_ROUTER); - - // Enable mainnet ERC20 token support for testing - address[] memory tokens = new address[](5); - bool[] memory supported = new bool[](5); - tokens[0] = MAINNET_WETH; - tokens[1] = MAINNET_USDC; - tokens[2] = MAINNET_USDT; - tokens[3] = MAINNET_DAI; - supported[0] = true; - supported[1] = true; - supported[2] = true; - supported[3] = true; - supported[4] = true; - - vm.prank(admin); - // Set threshold to a large value to enable support (0 means unsupported) - uint256[] memory thresholds = new uint256[](5); - for (uint256 i = 0; i < 5; i++) { - thresholds[i] = supported[i] ? 1000000 ether : 0; - } - gateway.setTokenLimitThresholds(tokens, thresholds); - - // Set up Chainlink oracle - vm.prank(admin); - gateway.setEthUsdFeed(MAINNET_ETH_USD_FEED); - - // Set the correct fee order for Uniswap V3 - vm.prank(admin); - gateway.setV3FeeOrder(500, 3000, 10000); - - // Test swapToNative (this is an internal function, so we test it indirectly) - // by calling sendTxWithGas which uses swapToNative internally - (UniversalPayload memory payload, RevertInstructions memory revertCfg) = - buildERC20Payload(user1, abi.encodeWithSignature("receive()"), 0); - vm.prank(user1); - // USDT approve returns void, not bool - TetherToken(token).approve(address(gateway), gasAmount); - - // Record initial balances - uint256 initialTSSBalance = tss.balance; - uint256 initialUserBalance = TetherToken(token).balanceOf(user1); - - // This should not revert for supported tokens - vm.prank(user1); - gateway.sendTxWithGas(token, gasAmount, payload, revertCfg, 1, block.timestamp + 3600, bytes("")); - - // Verify the user's token balance decreased - assertEq( - TetherToken(token).balanceOf(user1), initialUserBalance - gasAmount, "User token balance should decrease" - ); - - // Verify TSS received ETH (approximate check) - assertGt(tss.balance, initialTSSBalance, "TSS should receive ETH from token swap"); - } - - function testSwapToNative_UnsupportedToken_Reverts() public { - // Test with unsupported token - //Toggle the support for USDC - MockERC20 newNonSupportedToken = new MockERC20("NonSupported", "NS", 18, 0); - newNonSupportedToken.mint(user1, 1e18); - vm.prank(user1); - newNonSupportedToken.approve(address(gateway), 1e18); - - (UniversalPayload memory payload, RevertInstructions memory revertCfg) = - buildERC20Payload(user1, abi.encodeWithSignature("receive()"), 0); - - // Get the actual USD caps from the contract and use the max amount - (, uint256 maxEth) = gateway.getMinMaxValueForNative(); - uint256 amount = (maxEth * 8) / 10; - - vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); - vm.prank(user1); - gateway.sendTxWithGas( - address(newNonSupportedToken), amount, payload, revertCfg, 1, block.timestamp + 3600, bytes("") - ); - } - - // ========================= - // 4-PARAMETER sendTxWithFunds TESTS - // ========================= - - function testSendTxWithFunds_4Params_ERC20_HappyPath() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg) = - buildERC20Payload(user1, abi.encodeWithSignature("receive()"), 0); - - // Fund user with tokens - use amount within USD caps - uint256 bridgeAmount = 9e6; // 9 USDC = $9, well above min cap - uint256 gasAmount = 8e6; // 8 USDC for gas - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount + gasAmount); - - // Approve gateway to spend tokens - vm.prank(user1); - IERC20(MAINNET_USDC).approve(address(gateway), bridgeAmount + gasAmount); - - // This should not revert for supported tokens - vm.prank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, - bridgeAmount, - MAINNET_USDC, - gasAmount, - 1, - block.timestamp + 3600, - payload, - revertCfg, - bytes("") - ); - } - - function testSendTxWithFunds_4Params_ERC20_ErrorConditions() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg) = - buildERC20Payload(user1, abi.encodeWithSignature("receive()"), 0); - - uint256 bridgeAmount = 9e6; - uint256 gasAmount = 8e6; - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount + gasAmount); - - vm.prank(user1); - IERC20(MAINNET_USDC).approve(address(gateway), bridgeAmount + gasAmount); - // This should succeed (no revert expected) - vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); - vm.prank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, 0, MAINNET_USDC, gasAmount, 1, block.timestamp + 3600, payload, revertCfg, bytes("") - ); - - // Test 2: Zero gas amount - vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); - vm.prank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, bridgeAmount, MAINNET_USDC, 0, 1, block.timestamp + 3600, payload, revertCfg, bytes("") - ); - - // Test 3: Invalid gas token (address(0)) - vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); - vm.prank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, bridgeAmount, address(0), gasAmount, 1, block.timestamp + 3600, payload, revertCfg, bytes("") - ); - - // Test 4: Expired deadline - vm.expectRevert(abi.encodeWithSelector(Errors.SlippageExceededOrExpired.selector)); - vm.prank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, bridgeAmount, MAINNET_USDC, gasAmount, 1, block.timestamp - 1, payload, revertCfg, bytes("") - ); - } - - function testSendTxWithFunds_4Params_ERC20_USDCapValidations() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg) = - buildERC20Payload(user1, abi.encodeWithSignature("receive()"), 0); - - // Get USD caps - (uint256 minEth, uint256 maxEth) = gateway.getMinMaxValueForNative(); - - // Test 1: Gas amount below minimum cap - uint256 bridgeAmount = 9e6; - uint256 gasAmount = 1; // Very small amount, definitely below minimum - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount + gasAmount); - vm.prank(user1); - IERC20(MAINNET_USDC).approve(address(gateway), bridgeAmount + gasAmount); - vm.expectRevert("Too little received"); - vm.prank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, - bridgeAmount, - MAINNET_USDC, - gasAmount, - 1, - block.timestamp + 3600, - payload, - revertCfg, - bytes("") - ); - - // Test 2: Gas amount above maximum cap - gasAmount = 100000e6; // 100K USDC, definitely above maximum cap - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount + gasAmount); - vm.prank(user1); - IERC20(MAINNET_USDC).approve(address(gateway), bridgeAmount + gasAmount); - vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); - vm.prank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, - bridgeAmount, - MAINNET_USDC, - gasAmount, - 1, - block.timestamp + 3600, - payload, - revertCfg, - bytes("") - ); - } - - function testSendTxWithFunds_4Params_ERC20_UnsupportedTokens() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg) = - buildERC20Payload(user1, abi.encodeWithSignature("receive()"), 0); - - // Test with unsupported gas token - address unsupportedToken = address(0x1234567890123456789012345678901234567890); - uint256 bridgeAmount = 9e6; - uint256 gasAmount = 8e6; - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount); - - vm.prank(user1); - IERC20(MAINNET_USDC).approve(address(gateway), bridgeAmount); - vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); - vm.prank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, - bridgeAmount, - unsupportedToken, - gasAmount, - 1, - block.timestamp + 3600, - payload, - revertCfg, - bytes("") - ); - } - - function testSendTxWithFunds_4Params_ERC20_WhenPaused() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg) = - buildERC20Payload(user1, abi.encodeWithSignature("receive()"), 0); - - uint256 bridgeAmount = 9e6; - uint256 gasAmount = 8e6; - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount + gasAmount); - - // Pause the gateway - vm.prank(admin); - gateway.pause(); - - vm.prank(user1); - IERC20(MAINNET_USDC).approve(address(gateway), bridgeAmount + gasAmount); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, - bridgeAmount, - MAINNET_USDC, - gasAmount, - 1, - block.timestamp + 3600, - payload, - revertCfg, - bytes("") - ); - } - - function testSendTxWithFunds_4Params_ERC20_EdgeCases() public { - // Setup: Create a valid payload and revert config - (UniversalPayload memory payload, RevertInstructions memory revertCfg) = - buildERC20Payload(user1, abi.encodeWithSignature("receive()"), 0); - - uint256 bridgeAmount = 9e6; - uint256 gasAmount = 8e6; - fundUserWithMainnetTokens(user1, MAINNET_USDC, bridgeAmount + gasAmount); - - // Test with deadline = 0 (should use contract default) - vm.prank(user1); - IERC20(MAINNET_USDC).approve(address(gateway), bridgeAmount + gasAmount); - vm.prank(user1); - gateway.sendTxWithFunds( - MAINNET_USDC, bridgeAmount, MAINNET_USDC, gasAmount, 1, 0, payload, revertCfg, bytes("") - ); - } - - // ========================= - // GATEWAY INITIALIZATION TESTS - // ========================= - - function testGatewayInitialization_AllBranches() public { - // Test 1: Zero address validation - admin - UniversalGateway newGateway = new UniversalGateway(); - vm.expectRevert(abi.encodeWithSelector(Errors.ZeroAddress.selector)); - newGateway.initialize( - address(0), // Zero admin - tss, - address(this), // vault address - 100e18, // minCapUsd - 10000e18, // maxCapUsd - address(0x123), // factory - address(0x456), // router - MAINNET_WETH - ); - - // Test 2: Zero address validation - tss - vm.expectRevert(abi.encodeWithSelector(Errors.ZeroAddress.selector)); - newGateway.initialize( - admin, - address(0), // Zero tss - address(this), // vault address - 100e18, - 10000e18, - address(0x123), - address(0x456), - MAINNET_WETH - ); - - // Test 3: Zero address validation - vault - vm.expectRevert(abi.encodeWithSelector(Errors.ZeroAddress.selector)); - newGateway.initialize( - admin, - tss, - address(0), // Zero vault - 100e18, - 10000e18, - address(0x123), - address(0x456), - MAINNET_WETH - ); - - // Test 4: Zero address validation - WETH - vm.expectRevert(abi.encodeWithSelector(Errors.ZeroAddress.selector)); - newGateway.initialize( - admin, - tss, - address(this), // vault address - 100e18, - 10000e18, - address(0x123), - address(0x456), - address(0) // Zero WETH - ); - - // Test 5: Successful initialization with Uniswap addresses - newGateway.initialize( - admin, - tss, - address(this), // vault address - 100e18, - 10000e18, - address(0x123), // Non-zero factory - address(0x456), // Non-zero router - MAINNET_WETH - ); - - // Verify Uniswap addresses are set - assertEq(address(newGateway.uniV3Factory()), address(0x123)); - assertEq(address(newGateway.uniV3Router()), address(0x456)); - assertEq(newGateway.WETH(), MAINNET_WETH); - assertEq(newGateway.TSS_ADDRESS(), tss); - assertEq(newGateway.MIN_CAP_UNIVERSAL_TX_USD(), 100e18); - assertEq(newGateway.MAX_CAP_UNIVERSAL_TX_USD(), 10000e18); - assertEq(newGateway.defaultSwapDeadlineSec(), 10 minutes); - assertEq(newGateway.chainlinkStalePeriod(), 1 hours); - - // Test 6: Successful initialization with zero Uniswap addresses - UniversalGateway newGateway2 = new UniversalGateway(); - newGateway2.initialize( - admin, - tss, - address(this), // vault address - 50e18, - 5000e18, - address(0), // Zero factory - address(0), // Zero router - MAINNET_WETH - ); - - // Verify Uniswap addresses are NOT set (should be address(0)) - assertEq(address(newGateway2.uniV3Factory()), address(0)); - assertEq(address(newGateway2.uniV3Router()), address(0)); - assertEq(newGateway2.WETH(), MAINNET_WETH); - assertEq(newGateway2.TSS_ADDRESS(), tss); - assertEq(newGateway2.MIN_CAP_UNIVERSAL_TX_USD(), 50e18); - assertEq(newGateway2.MAX_CAP_UNIVERSAL_TX_USD(), 5000e18); - assertEq(newGateway2.defaultSwapDeadlineSec(), 10 minutes); - assertEq(newGateway2.chainlinkStalePeriod(), 1 hours); - - // Test 7: Verify roles are set correctly - assertTrue(newGateway.hasRole(newGateway.DEFAULT_ADMIN_ROLE(), admin)); - assertTrue(newGateway.hasRole(newGateway.TSS_ROLE(), tss)); - - // Test 8: Verify initial state - assertFalse(newGateway.paused()); - assertEq(newGateway.v3FeeOrder(0), 500); - assertEq(newGateway.v3FeeOrder(1), 3000); - assertEq(newGateway.v3FeeOrder(2), 10000); - } -} diff --git a/contracts/evm-gateway/test/gateway/3_sendUniversalTx_token.t.sol b/contracts/evm-gateway/test/gateway/3_sendUniversalTx_token.t.sol new file mode 100644 index 0000000..46c40cc --- /dev/null +++ b/contracts/evm-gateway/test/gateway/3_sendUniversalTx_token.t.sol @@ -0,0 +1,794 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BaseTest } from "../BaseTest.t.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { + TX_TYPE, + RevertInstructions, + UniversalPayload, + UniversalTxRequest, + UniversalTokenTxRequest +} from "../../src/libraries/Types.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import { IWETH } from "../../src/interfaces/IWETH.sol"; +import { MockUniswapV3Factory } from "../mocks/MockUniswapV3.sol"; +import { MockUniswapV3Router } from "../mocks/MockUniswapV3.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import { MockERC20 } from "../mocks/MockERC20.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/** + * @title GatewaySendUniversalTxTokenGas Test Suite + * @notice Comprehensive tests for sendUniversalTx(UniversalTokenTxRequest) - token-as-gas entrypoint + * @dev Tests unique aspects of the token-as-gas function: + * - Parameter validation (gasToken, gasAmount, amountOutMinETH, deadline) + * - swapToNative integration (WETH fast-path and error cases) + * - TX_TYPE inference when nativeValue comes from swap + * - msg.value semantics + * - Error paths (no pool, slippage, deadline, paused state) + * + * @dev Note: This test suite focuses on the unique behavior of the token-as-gas entrypoint. + * Internal routing logic (_sendTxWithGas, _sendTxWithFunds, _fetchTxType) is already + * covered by existing tests. Here we test integration and surface-level semantics. + */ +contract GatewaySendUniversalTxTokenGasTest is BaseTest { + // UniversalGateway instance + UniversalGateway public gatewayTemp; + + // Uniswap mocks + MockUniswapV3Factory public mockFactory; + MockUniswapV3Router public mockRouter; + address public mockPool; + + // ========================= + // EVENTS + // ========================= + // Event definition matches IUniversalGateway.sol + event UniversalTx( + address indexed sender, + address indexed recipient, + address token, + uint256 amount, + bytes payload, + address revertRecipient, + TX_TYPE txType, + bytes signatureData + ); + + // ========================= + // SETUP + // ========================= + function setUp() public override { + super.setUp(); + + // Deploy Uniswap mocks + mockFactory = new MockUniswapV3Factory(); + mockRouter = new MockUniswapV3Router(address(weth)); + mockPool = address(0x1234); // Dummy pool address + + // Setup pool for tokenA/WETH with fee tier 500 (first tier gateway checks) + vm.prank(mockFactory.owner()); + mockFactory.setPool(address(tokenA), address(weth), 500, mockPool); + + // Setup swap rate: 1 tokenA = 0.001 ETH (1e15) + mockRouter.setSwapRate(address(tokenA), 1e15); + + // Deploy UniversalGateway with mocks + _deployGatewayTemp(); + + // Update gateway with mock Uniswap addresses + vm.prank(admin); + gatewayTemp.setRouters(address(mockFactory), address(mockRouter)); + + // Explicitly set fee order to ensure it's initialized (default should be [500, 3000, 10000]) + vm.prank(admin); + gatewayTemp.setV3FeeOrder(500, 3000, 10000); + + // Wire oracle to the new gateway instance + vm.prank(admin); + gatewayTemp.setEthUsdFeed(address(ethUsdFeedMock)); + + // Setup token support on gatewayTemp (native + all mock ERC20s) + address[] memory tokens = new address[](4); + uint256[] memory thresholds = new uint256[](4); + tokens[0] = address(0); // Native token + tokens[1] = address(tokenA); // Mock ERC20 tokenA + tokens[2] = address(usdc); // Mock ERC20 usdc + tokens[3] = address(weth); // Mock WETH + thresholds[0] = 1000000 ether; // Large threshold for native + thresholds[1] = 1000000 ether; // Large threshold for tokenA + thresholds[2] = 1000000e6; // Large threshold for usdc (6 decimals) + thresholds[3] = 1000000 ether; // Large threshold for weth + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + // Re-approve tokens to gatewayTemp + address[] memory users = new address[](5); + users[0] = user1; + users[1] = user2; + users[2] = user3; + users[3] = user4; + users[4] = attacker; + + for (uint256 i = 0; i < users.length; i++) { + vm.prank(users[i]); + tokenA.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + usdc.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + weth.approve(address(gatewayTemp), type(uint256).max); + } + + // Fund mock router with ETH (it will convert to WETH as needed) + vm.deal(address(mockRouter), 1000 ether); + + // Fund WETH contract with ETH so it can transfer ETH on withdraw + vm.deal(address(weth), 1000 ether); + } + + /// @notice Deploy UniversalGateway + function _deployGatewayTemp() internal { + UniversalGateway implementation = new UniversalGateway(); + + bytes memory initData = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, + tss, + address(this), + MIN_CAP_USD, + MAX_CAP_USD, + address(mockFactory), // Use mock factory + address(mockRouter), // Use mock router + address(weth) + ); + + gatewayProxy = new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); + gatewayTemp = UniversalGateway(payable(address(gatewayProxy))); + + vm.label(address(gatewayTemp), "UniversalGateway"); + } + + // ========================= + // HELPER FUNCTIONS + // ========================= + + /// @notice Build a UniversalTokenTxRequest for testing + function _buildTokenGasRequest( + address recipient, + address token, + uint256 amount, + address gasToken, + uint256 gasAmount, + bytes memory payload, + uint256 amountOutMinETH, + uint256 deadline + ) internal pure returns (UniversalTokenTxRequest memory) { + return UniversalTokenTxRequest({ + recipient: recipient, + token: token, + amount: amount, + gasToken: gasToken, + gasAmount: gasAmount, + payload: payload, + revertRecipient: address(0x456), + signatureData: bytes(""), + amountOutMinETH: amountOutMinETH, + deadline: deadline + }); + } + + /// @notice Build a minimal valid UniversalTokenTxRequest for GAS route + function _buildMinimalTokenGasRequest(address gasToken, uint256 gasAmount, uint256 amountOutMinETH) + internal + view + returns (UniversalTokenTxRequest memory) + { + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), // recipient + address(0), // token + 0, // amount + gasToken, + gasAmount, + bytes(""), // empty payload + amountOutMinETH, + block.timestamp + 1 hours // deadline + ); + // Set revertRecipient to non-zero for GAS routes (required by _routeUniversalTx) + req.revertRecipient = address(0x456); + return req; + } + + // ========================= + // PARAMETER VALIDATION TESTS + // ========================= + + /// @notice Test revert when gasToken is zero address + function test_TokenGas_RevertOn_ZeroGasToken() public { + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(address(0), 1 ether, 0.001 ether); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + /// @notice Test revert when gasAmount is zero + function test_TokenGas_RevertOn_ZeroGasAmount() public { + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(address(tokenA), 0, 0.001 ether); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + /// @notice Test revert when amountOutMinETH is zero + function test_TokenGas_RevertOn_ZeroAmountOutMinETH() public { + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(address(tokenA), 1 ether, 0); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + /// @notice Test revert when deadline is in the past + function test_TokenGas_RevertOn_ExpiredDeadline() public { + // Use smaller amount to stay within USD caps + uint256 gasAmount = 1 ether; // 1 tokenA = 0.001 ETH = $2, within caps + uint256 expectedETH = (gasAmount * 1e15) / 1e18; // = 0.001 ETH + uint256 amountOutMinETH = expectedETH - 1; + + // Set deadline to past timestamp (must be non-zero to trigger the check) + // First advance time to ensure deadline is definitely in the past + vm.warp(block.timestamp + 1000); + uint256 pastDeadline = block.timestamp - 100; // Definitely in the past + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), + 0, + address(tokenA), + gasAmount, + bytes(""), + amountOutMinETH, + pastDeadline // Past deadline + ); + req.revertRecipient = address(0x456); + + vm.expectRevert(Errors.SlippageExceededOrExpired.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + /// @notice Test revert when contract is paused + function test_TokenGas_RevertOn_Paused() public { + vm.prank(admin); + gatewayTemp.pause(); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(address(tokenA), 1 ether, 0.001 ether); + + vm.expectRevert(); + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + // ========================= + // SWAP INTEGRATION TESTS + // ========================= + + /// @notice Test revert when Uniswap router/factory are not configured + /// @dev swapToNative checks if uniV3Router or uniV3Factory are zero and reverts + function test_TokenGas_RevertOn_UniswapNotConfigured() public { + // Create a new gateway without Uniswap configured + UniversalGateway implementation2 = new UniversalGateway(); + bytes memory initData2 = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, + tss, + address(this), + MIN_CAP_USD, + MAX_CAP_USD, + address(0), // No factory + address(0), // No router + address(weth) + ); + TransparentUpgradeableProxy proxy2 = + new TransparentUpgradeableProxy(address(implementation2), address(proxyAdmin), initData2); + UniversalGateway gatewayNoUniswap = UniversalGateway(payable(address(proxy2))); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(address(tokenA), 1 ether, 0.001 ether); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayNoUniswap.sendUniversalTx(req); + } + + /// @notice Test WETH fast-path when Uniswap is configured + /// @dev When gasToken == WETH, swapToNative uses fast-path: pull WETH, unwrap to native + function test_TokenGas_WETHFastPath_Success() public { + // Arrange: User has WETH + // Use smaller amount to stay within USD caps: 0.001 ETH = $2 + uint256 gasAmount = 0.001 ether; // 0.001 ETH = $2, within caps + uint256 amountOutMinETH = (gasAmount * 99) / 100; // Allow 1% slippage + + vm.prank(user1); + weth.deposit{ value: gasAmount }(); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), address(0), 0, address(weth), gasAmount, bytes(""), amountOutMinETH, block.timestamp + 1 hours + ); + + uint256 tssBalanceBefore = tss.balance; + uint256 userWethBalanceBefore = weth.balanceOf(user1); + + // Act: Expect UniversalTx event with TX_TYPE.GAS + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx( + user1, + address(0), + address(0), + gasAmount, // nativeValue from unwrap + bytes(""), + req.revertRecipient, + TX_TYPE.GAS, + bytes("") + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + + // Assert: WETH was unwrapped and sent to TSS + assertEq(tss.balance, tssBalanceBefore + gasAmount, "TSS should receive unwrapped ETH"); + assertEq(weth.balanceOf(user1), userWethBalanceBefore - gasAmount, "User WETH should be consumed"); + } + + /// @notice Test revert when pool is not found for non-WETH token + /// @dev _findV3PoolWithNative reverts with InvalidInput when no pool exists + function test_TokenGas_RevertOn_NoPoolFound() public { + // Arrange: Use a token without a pool + MockERC20 tokenWithoutPool = new MockERC20("NoPool", "NOP", 18, 0); + vm.prank(user1); + tokenWithoutPool.mint(user1, 1000 ether); + vm.prank(user1); + tokenWithoutPool.approve(address(gatewayTemp), type(uint256).max); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), + 0, + address(tokenWithoutPool), + 1 ether, + bytes(""), + 0.001 ether, + block.timestamp + 1 hours + ); + + // Act & Assert: Should revert when pool is not found + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + // ========================= + // TX_TYPE INFERENCE TESTS + // ========================= + + /// @notice Test that TX_TYPE.GAS is correctly inferred when using token-as-gas + /// @dev When req has no payload, no funds, and nativeValue > 0 (from swap), should route to GAS + function test_TokenGas_InferGAS_Type() public { + // Arrange: Swap tokenA for gas, no payload, no funds + // Use smaller amount to stay within USD caps: $1-$10 range + // At $2000/ETH: need 0.0005 ETH to 0.005 ETH (0.001 ETH = $2, within caps) + // Swap rate: 1e15 means 1 tokenA = 0.001 ETH + // To get 0.001 ETH: need 1 tokenA = 1e18 wei + uint256 gasAmount = 1 ether; // 1 tokenA = 0.001 ETH = $2, within caps + uint256 expectedETH = (gasAmount * 1e15) / 1e18; // = 0.001 ETH + uint256 amountOutMinETH = expectedETH; // Use exact amount (mock returns exact) + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), + 0, // No funds + address(tokenA), + gasAmount, + bytes(""), // No payload + amountOutMinETH, + block.timestamp + 1 hours + ); + + uint256 tssBalanceBefore = tss.balance; + + // Act: Expect GAS event + // Note: The actual swap output will be 1 ETH (1000 tokenA * 1e15 / 1e18 = 1 ETH) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx( + user1, + address(0), + address(0), + expectedETH, // nativeValue from swap = 1 ETH + bytes(""), + req.revertRecipient, + TX_TYPE.GAS, + bytes("") + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + + // Assert: TSS received ETH from swap + assertGe(tss.balance, tssBalanceBefore + amountOutMinETH, "TSS should receive swapped ETH"); + } + + /// @notice Test that TX_TYPE.GAS_AND_PAYLOAD is correctly inferred when using token-as-gas with payload + /// @dev When req has payload, no funds, and nativeValue > 0 (from swap), should route to GAS_AND_PAYLOAD + function test_TokenGas_InferGAS_AND_PAYLOAD_Type() public { + // Arrange: Swap tokenA for gas, with payload, no funds + uint256 gasAmount = 1 ether; // 1 tokenA = 0.001 ETH = $2, within caps + uint256 expectedETH = (gasAmount * 1e15) / 1e18; // = 0.001 ETH + uint256 amountOutMinETH = expectedETH - 1; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory payloadBytes = abi.encode(payload); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), + 0, // No funds + address(tokenA), + gasAmount, + payloadBytes, // Has payload + amountOutMinETH, + block.timestamp + 1 hours + ); + + // Act: Expect GAS_AND_PAYLOAD event + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx( + user1, + address(0), + address(0), + expectedETH, + payloadBytes, + req.revertRecipient, + TX_TYPE.GAS_AND_PAYLOAD, + bytes("") + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + /// @notice Test that TX_TYPE.FUNDS is correctly inferred when using token-as-gas with funds + /// @dev When req has funds (amount > 0), should route to FUNDS regardless of nativeValue + function test_TokenGas_InferFUNDS_Type() public { + // Arrange: Swap tokenA for gas, with funds (native) + uint256 gasAmount = 1 ether; // 1 tokenA = 0.001 ETH = $2, within caps + uint256 expectedETH = (gasAmount * 1e15) / 1e18; // = 0.001 ETH + uint256 amountOutMinETH = expectedETH - 1; + uint256 fundsAmount = 0.001 ether; // Native funds = $2, within caps + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), // Native token + fundsAmount, // Has funds + address(tokenA), + gasAmount, + bytes(""), // No payload + amountOutMinETH, + block.timestamp + 1 hours + ); + + // Act: Expect FUNDS event (funds take precedence) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx( + user1, + address(0), + address(0), // Native token + fundsAmount, // Funds amount, not gas amount + bytes(""), + req.revertRecipient, + TX_TYPE.FUNDS, + bytes("") + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); // Send funds with msg.value + } + + /// @notice Test that TX_TYPE.FUNDS_AND_PAYLOAD is correctly inferred when using token-as-gas with funds and payload + /// @dev When req has both funds and payload, should route to FUNDS_AND_PAYLOAD + function test_TokenGas_InferFUNDS_AND_PAYLOAD_Type() public { + // Arrange: Swap tokenA for gas, with funds and payload + uint256 gasAmount = 1 ether; // 1 tokenA = 0.001 ETH = $2, within caps + uint256 expectedETH = (gasAmount * 1e15) / 1e18; // = 0.001 ETH + uint256 amountOutMinETH = expectedETH - 1; + uint256 fundsAmount = 0.001 ether; // Native funds = $2, within caps + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory payloadBytes = abi.encode(payload); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), // Native token + fundsAmount, // Has funds + address(tokenA), + gasAmount, + payloadBytes, // Has payload + amountOutMinETH, + block.timestamp + 1 hours + ); + + // Act: Expect FUNDS_AND_PAYLOAD event + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx( + user1, + address(0), + address(0), + fundsAmount, + payloadBytes, + req.revertRecipient, + TX_TYPE.FUNDS_AND_PAYLOAD, + bytes("") + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + } + + // ========================= + // MSG.VALUE SEMANTICS TESTS + // ========================= + + /// @notice Test that msg.value is accepted but ignored for token-as-gas entrypoint + /// @dev Currently, msg.value is not used in the token-as-gas path (nativeValue comes from swap) + /// But the function is payable, so msg.value > 0 should not revert + function test_TokenGas_AcceptsMsgValue() public { + // Arrange: Send msg.value along with token-as-gas request + uint256 gasAmount = 1 ether; // 1 tokenA = 0.001 ETH = $2, within caps + uint256 expectedETH = (gasAmount * 1e15) / 1e18; // = 0.001 ETH + uint256 amountOutMinETH = expectedETH - 1; + uint256 msgValue = 0.1 ether; // Extra ETH sent + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(address(tokenA), gasAmount, amountOutMinETH); + + // Act: Should succeed even with msg.value + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx( + user1, + address(0), + address(0), + expectedETH, // nativeValue from swap, not msg.value + bytes(""), + req.revertRecipient, + TX_TYPE.GAS, + bytes("") + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: msg.value was accepted but not used (gateway balance increased) + assertEq(address(gatewayTemp).balance, msgValue, "Gateway should receive msg.value"); + } + + /// @notice Test that msg.value does not affect nativeValue calculation + /// @dev nativeValue comes from swapToNative, not msg.value + function test_TokenGas_MsgValueDoesNotAffectNativeValue() public { + // Arrange: Same swap with different msg.value amounts + uint256 gasAmount = 1 ether; // 1 tokenA = 0.001 ETH = $2, within caps + uint256 expectedETH = (gasAmount * 1e15) / 1e18; // = 0.001 ETH + uint256 amountOutMinETH = expectedETH - 1; + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(address(tokenA), gasAmount, amountOutMinETH); + + uint256 tssBalanceBefore = tss.balance; + + // Act: Send with msg.value = 0 + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + + uint256 tssBalanceAfterZero = tss.balance; + uint256 ethReceivedZero = tssBalanceAfterZero - tssBalanceBefore; + + // Reset and send with msg.value > 0 + vm.roll(block.number + 1); + tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 1 ether }(req); + + uint256 tssBalanceAfterNonZero = tss.balance; + uint256 ethReceivedNonZero = tssBalanceAfterNonZero - tssBalanceBefore; + + // Assert: nativeValue (sent to TSS) is the same regardless of msg.value + assertEq(ethReceivedZero, ethReceivedNonZero, "nativeValue should be same regardless of msg.value"); + assertEq(ethReceivedZero, expectedETH, "nativeValue should come from swap"); + } + + // ========================= + // INTEGRATION WITH ROUTING TESTS - PENDING + // ========================= + + /// @notice Test that UniversalTxRequest is correctly built from UniversalTokenTxRequest + /// @dev Verify that recipient, token, amount, payload, revertInstruction, signatureData are preserved + function test_TokenGas_BuildsCorrectUniversalTxRequest() public { + // This test verifies the conversion from UniversalTokenTxRequest to UniversalTxRequest + // The conversion happens at lines 317-324 in UniversalGateway.sol + // We can't easily test this without successful swap, but we document the expected behavior + } + + /// @notice Test that _routeUniversalTx is called with correct parameters + /// @dev Verify that req, caller, nativeValue (from swap), and txType are passed correctly + function test_TokenGas_RoutesCorrectly() public { + // This test verifies the routing call at line 327 + // Requires successful swap to fully test + } + + // ========================= + // ERROR PATH TESTS + // ========================= + + /// @notice Test revert when user has insufficient gasToken balance + function test_TokenGas_RevertOn_InsufficientBalance() public { + // Setup: User has no tokens + uint256 gasAmount = 1 ether; + uint256 userBalance = tokenA.balanceOf(user1); + + // Ensure user has insufficient balance + if (userBalance >= gasAmount) { + vm.prank(user1); + tokenA.transfer(address(0xDEAD), userBalance - gasAmount + 1); + } + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(address(tokenA), gasAmount, 0.001 ether); + + // Should revert when swapToNative tries to transfer tokens + // Will revert with ERC20InsufficientBalance when transferFrom fails + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientBalance.selector, user1, tokenA.balanceOf(user1), gasAmount + ) + ); + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + /// @notice Test revert when user has insufficient gasToken allowance + function test_TokenGas_RevertOn_InsufficientAllowance() public { + // Setup: Revoke approval + vm.prank(user1); + tokenA.approve(address(gatewayTemp), 0); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(address(tokenA), 1 ether, 0.001 ether); + + // Should revert when swapToNative tries to transfer tokens + // Will revert with ERC20InsufficientAllowance when transferFrom fails + vm.expectRevert( + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(gatewayTemp), 0, 1 ether) + ); + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + /// @notice Test deadline = 0 uses default deadline + /// @dev When deadline is 0, swapToNative should use block.timestamp + defaultSwapDeadlineSec + function test_TokenGas_ZeroDeadlineUsesDefault() public { + // Arrange: Set deadline to 0 + uint256 gasAmount = 1 ether; // 1 tokenA = 0.001 ETH = $2, within caps + uint256 expectedETH = (gasAmount * 1e15) / 1e18; // = 0.001 ETH + uint256 amountOutMinETH = expectedETH - 1; + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), + 0, + address(tokenA), + gasAmount, + bytes(""), + amountOutMinETH, + 0 // Zero deadline should use default + ); + + // Act: Should succeed with default deadline + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx( + user1, address(0), address(0), expectedETH, bytes(""), req.revertRecipient, TX_TYPE.GAS, bytes("") + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + /// @notice Test slippage protection when swap output is below amountOutMinETH + /// @dev swapToNative should revert with SlippageExceededOrExpired if ethOut < amountOutMinETH + function test_TokenGas_RevertOn_SlippageExceeded() public { + // Arrange: Set very high amountOutMinETH (higher than swap output) + uint256 gasAmount = 1 ether; // 1 tokenA = 0.001 ETH = $2, within caps + uint256 expectedETH = (gasAmount * 1e15) / 1e18; // = 0.001 ETH + uint256 amountOutMinETH = expectedETH + 1; // Higher than expected output + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(address(tokenA), gasAmount, amountOutMinETH); + + // Act & Assert: Should revert on slippage check + vm.expectRevert(Errors.SlippageExceededOrExpired.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + // ========================= + // EDGE CASE TESTS + // ========================= + + /// @notice Test that revertInstruction validation is preserved + /// @dev The UniversalTxRequest built from UniversalTokenTxRequest should preserve revertInstruction + /// and _routeUniversalTx should validate it (e.g., revertRecipient != address(0) for GAS routes) + function test_TokenGas_PreservesRevertInstruction() public { + // Arrange: Zero revertRecipient should revert for GAS route + uint256 gasAmount = 1 ether; // 1 tokenA = 0.001 ETH = $2, within caps + uint256 expectedETH = (gasAmount * 1e15) / 1e18; // = 0.001 ETH + uint256 amountOutMinETH = expectedETH - 1; + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), address(0), 0, address(tokenA), gasAmount, bytes(""), amountOutMinETH, block.timestamp + 1 hours + ); + req.revertRecipient = address(0); // Invalid + + // Act & Assert: Should revert on invalid revertInstruction + vm.expectRevert(Errors.InvalidRecipient.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + /// @notice Test that signatureData is preserved + /// @dev The UniversalTxRequest should preserve signatureData from UniversalTokenTxRequest + function test_TokenGas_PreservesSignatureData() public { + bytes memory customSignature = abi.encode("custom signature data"); + uint256 gasAmount = 1 ether; // 1 tokenA = 0.001 ETH = $2, within caps + uint256 expectedETH = (gasAmount * 1e15) / 1e18; // = 0.001 ETH + uint256 amountOutMinETH = expectedETH - 1; + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), address(0), 0, address(tokenA), gasAmount, bytes(""), amountOutMinETH, block.timestamp + 1 hours + ); + req.signatureData = customSignature; + + // Act: Verify signatureData is preserved in event + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx( + user1, + address(0), + address(0), + expectedETH, + bytes(""), + req.revertRecipient, + TX_TYPE.GAS, + customSignature // Preserved + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } + + /// @notice Test maximum values for gasAmount and amountOutMinETH + function test_TokenGas_MaximumValues() public { + uint256 maxGasAmount = type(uint256).max; + uint256 maxAmountOutMinETH = type(uint256).max; + + UniversalTokenTxRequest memory req = + _buildMinimalTokenGasRequest(address(tokenA), maxGasAmount, maxAmountOutMinETH); + + // Should revert when swapToNative tries to transfer tokens + // Will revert with ERC20InsufficientBalance (user doesn't have type(uint256).max tokens) + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientBalance.selector, user1, tokenA.balanceOf(user1), maxGasAmount + ) + ); + vm.prank(user1); + gatewayTemp.sendUniversalTx(req); + } +} diff --git a/contracts/evm-gateway/test/gateway/3_sendUniversalTx_token_fork.t.sol b/contracts/evm-gateway/test/gateway/3_sendUniversalTx_token_fork.t.sol new file mode 100644 index 0000000..8e84b63 --- /dev/null +++ b/contracts/evm-gateway/test/gateway/3_sendUniversalTx_token_fork.t.sol @@ -0,0 +1,888 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BaseTest } from "../BaseTest.t.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { TX_TYPE, RevertInstructions, UniversalPayload, UniversalTokenTxRequest } from "../../src/libraries/Types.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import { IWETH } from "../../src/interfaces/IWETH.sol"; +import { IUniversalGateway } from "../../src/interfaces/IUniversalGateway.sol"; +import { MockERC20 } from "../mocks/MockERC20.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +// USDT interface for non-standard transfer and approve functions +interface TetherToken { + function transfer(address to, uint256 amount) external; + function approve(address spender, uint256 amount) external; + function balanceOf(address account) external view returns (uint256); +} + +/** + * @title GatewaySendUniversalTxTokenGasFork Test Suite + * @notice Fork-based tests for sendUniversalTx(UniversalTokenTxRequest) - token-as-gas entrypoint + * @dev Tests using mainnet fork with real Uniswap contracts and real tokens: + * - Parameter validation (gasToken, gasAmount, amountOutMinETH, deadline) + * - swapToNative integration with real Uniswap swaps (WETH fast-path and ERC20 swaps) + * - TX_TYPE inference when nativeValue comes from swap + * - msg.value semantics + * - Error paths (no pool, slippage, deadline, paused state) + * + * @dev Note: This test suite uses mainnet fork to test real-world integration. + * Uses real mainnet tokens (USDC, USDT, DAI, WETH) and real Uniswap V3 pools. + */ +contract GatewaySendUniversalTxTokenGasForkTest is BaseTest { + // ========================= + // MAINNET CONTRACTS + // ========================= + // Mainnet WETH address + address constant MAINNET_WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + // Mainnet USDC address + address constant MAINNET_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + + // Mainnet USDT address + address constant MAINNET_USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + + // Mainnet DAI address + address constant MAINNET_DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + + // Mainnet Uniswap V3 Router + address constant MAINNET_UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + + // Mainnet Uniswap V3 Factory + address constant MAINNET_UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + + // Mainnet Chainlink ETH/USD Feed + address constant MAINNET_ETH_USD_FEED = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; + + // Real mainnet token contracts + IERC20 mainnetWETH; + IERC20 mainnetUSDC; + TetherToken mainnetUSDT; + IERC20 mainnetDAI; + + // UniversalGateway instance for fork tests + UniversalGateway public gatewayFork; + + // ========================= + // EVENTS + // ========================= + event UniversalTx( + address indexed sender, + address indexed recipient, + address token, + uint256 amount, + bytes payload, + address revertRecipient, + TX_TYPE txType, + bytes signatureData + ); + + // ========================= + // SETUP + // ========================= + function setUp() public override { + // Use mainnet fork for Uniswap integration + // Read RPC URL from ETH_RPC_URL environment variable (must be set in .env file) + string memory rpcUrl = vm.envString("ETH_MAINNET_RPC_URL"); + vm.createSelectFork(rpcUrl); + super.setUp(); + + // Redeploy gateway with mainnet WETH address + _redeployGatewayWithMainnetWETH(); + + // Override gateway configuration to use mainnet contracts + vm.prank(admin); + gatewayFork.setRouters(MAINNET_UNISWAP_V3_FACTORY, MAINNET_UNISWAP_V3_ROUTER); + + // Initialize real mainnet token contracts + mainnetWETH = IERC20(MAINNET_WETH); + mainnetUSDC = IERC20(MAINNET_USDC); + mainnetUSDT = TetherToken(MAINNET_USDT); + mainnetDAI = IERC20(MAINNET_DAI); + + // Enable mainnet ERC20 token support for testing + address[] memory tokens = new address[](5); + uint256[] memory thresholds = new uint256[](5); + tokens[0] = address(0); // Native token + tokens[1] = MAINNET_WETH; + tokens[2] = MAINNET_USDC; + tokens[3] = MAINNET_USDT; + tokens[4] = MAINNET_DAI; + thresholds[0] = 1000000 ether; // Large threshold for native + thresholds[1] = 1000000 ether; // Large threshold for WETH + thresholds[2] = 1000000e6; // Large threshold for USDC (6 decimals) + thresholds[3] = 1000000e6; // Large threshold for USDT (6 decimals) + thresholds[4] = 1000000 ether; // Large threshold for DAI (18 decimals) + + vm.prank(admin); + gatewayFork.setTokenLimitThresholds(tokens, thresholds); + + // Set up Chainlink oracle + vm.prank(admin); + gatewayFork.setEthUsdFeed(MAINNET_ETH_USD_FEED); + + // Set the correct fee order for Uniswap V3 + vm.prank(admin); + gatewayFork.setV3FeeOrder(500, 3000, 10000); + } + + // ========================= + // HELPER FUNCTIONS + // ========================= + + /// @notice Redeploy gateway with mainnet WETH address + function _redeployGatewayWithMainnetWETH() internal { + // Deploy new implementation + UniversalGateway newImplementation = new UniversalGateway(); + + // Create initialization data with mainnet WETH + bytes memory initData = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, // admin + tss, // tss + address(this), // vault address + MIN_CAP_USD, + MAX_CAP_USD, + MAINNET_UNISWAP_V3_FACTORY, // Use mainnet factory + MAINNET_UNISWAP_V3_ROUTER, // Use mainnet router + MAINNET_WETH // Use mainnet WETH instead of mock + ); + + // Deploy new proxy + gatewayProxy = new TransparentUpgradeableProxy(address(newImplementation), address(proxyAdmin), initData); + + // Update gateway reference + gatewayFork = UniversalGateway(payable(address(gatewayProxy))); + + // Label for debugging + vm.label(address(gatewayFork), "UniversalGateway-Fork"); + vm.label(address(gatewayProxy), "GatewayProxy-Fork"); + } + + /// @notice Fund user with real mainnet tokens by impersonating a whale + function fundUserWithMainnetTokens(address user, address token, uint256 amount) internal override { + // Find a whale address that has the token + address whale; + if (token == MAINNET_WETH) { + whale = 0x28C6c06298d514Db089934071355E5743bf21d60; // Binance 14 + } else if (token == MAINNET_USDC) { + whale = 0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341; // SKY + } else if (token == MAINNET_USDT) { + whale = 0xF977814e90dA44bFA03b6295A0616a897441aceC; // Binance 20 + } else if (token == MAINNET_DAI) { + whale = 0x28C6c06298d514Db089934071355E5743bf21d60; // Binance 14 + } else { + revert("Unknown token"); + } + + // Check if whale has enough tokens + uint256 whaleBalance = IERC20(token).balanceOf(whale); + require(whaleBalance >= amount, "Whale doesn't have enough tokens"); + + // Impersonate whale and transfer tokens + vm.startPrank(whale); + + // Handle USDT specially since it has non-standard transfer function + if (token == MAINNET_USDT) { + // USDT transfer returns void, not bool + TetherToken(token).transfer(user, amount); + } else { + IERC20(token).transfer(user, amount); + } + + vm.stopPrank(); + + // Verify transfer was successful + assertGe(IERC20(token).balanceOf(user), amount, "User should receive tokens"); + } + + /// @notice Build a UniversalTokenTxRequest for testing + function _buildTokenGasRequest( + address recipient, + address token, + uint256 amount, + address gasToken, + uint256 gasAmount, + bytes memory payload, + uint256 amountOutMinETH, + uint256 deadline + ) internal pure returns (UniversalTokenTxRequest memory) { + return UniversalTokenTxRequest({ + recipient: recipient, + token: token, + amount: amount, + gasToken: gasToken, + gasAmount: gasAmount, + payload: payload, + revertRecipient: address(0x456), + signatureData: bytes(""), + amountOutMinETH: amountOutMinETH, + deadline: deadline + }); + } + + /// @notice Build a minimal valid UniversalTokenTxRequest for GAS route + function _buildMinimalTokenGasRequest(address gasToken, uint256 gasAmount, uint256 amountOutMinETH) + internal + view + returns (UniversalTokenTxRequest memory) + { + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), // recipient + address(0), // token + 0, // amount + gasToken, + gasAmount, + bytes(""), // empty payload + amountOutMinETH, + block.timestamp + 1 hours // deadline + ); + // Set revertRecipient to non-zero for GAS routes (required by _routeUniversalTx) + req.revertRecipient = address(0x456); + return req; + } + + /// @notice Calculate expected ETH output from token amount using real market price + /// @dev Uses gateway's getEthUsdPrice() to calculate expected ETH amount + /// This is approximate due to real market slippage + function _calculateExpectedETH(address token, uint256 tokenAmount) internal view returns (uint256) { + // Get current ETH/USD price + (uint256 ethUsdPrice,) = gatewayFork.getEthUsdPrice(); + + // For stablecoins (USDC, USDT, DAI), assume 1:1 with USD + // Calculate USD value, then convert to ETH + uint256 usdValue; + if (token == MAINNET_USDC || token == MAINNET_USDT) { + // 6 decimals: convert to 18 decimals for USD calculation + usdValue = tokenAmount * 1e12; // Scale from 6 to 18 decimals + } else if (token == MAINNET_DAI) { + // 18 decimals: already in correct format + usdValue = tokenAmount; + } else { + // For other tokens, assume 1:1 USD for simplicity + usdValue = tokenAmount; + } + + // Convert USD to ETH: ethAmount = (usdValue * 1e18) / ethUsdPrice + // Apply 5% slippage tolerance for real swaps + return (usdValue * 95) / 100 / (ethUsdPrice / 1e18); + } + + // ========================= + // PARAMETER VALIDATION TESTS + // ========================= + + /// @notice Test revert when gasToken is zero address + function test_TokenGas_RevertOn_ZeroGasToken() public { + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(address(0), 1 ether, 0.001 ether); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + /// @notice Test revert when gasAmount is zero + function test_TokenGas_RevertOn_ZeroGasAmount() public { + fundUserWithMainnetTokens(user1, MAINNET_USDC, 1000e6); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), type(uint256).max); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_USDC, 0, 0.001 ether); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + /// @notice Test revert when amountOutMinETH is zero + function test_TokenGas_RevertOn_ZeroAmountOutMinETH() public { + fundUserWithMainnetTokens(user1, MAINNET_USDC, 1000e6); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), type(uint256).max); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_USDC, 10e6, 0); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + /// @notice Test revert when deadline is in the past + function test_TokenGas_RevertOn_ExpiredDeadline() public { + fundUserWithMainnetTokens(user1, MAINNET_USDC, 1000e6); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), type(uint256).max); + + // Set deadline to past timestamp + vm.warp(block.timestamp + 1000); + uint256 pastDeadline = block.timestamp - 100; + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), + 0, + MAINNET_USDC, + 10e6, // 10 USDC + bytes(""), + 1e15, // Small min ETH output + pastDeadline + ); + req.revertRecipient = address(0x456); + + vm.expectRevert(Errors.SlippageExceededOrExpired.selector); + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + /// @notice Test revert when contract is paused + function test_TokenGas_RevertOn_Paused() public { + vm.prank(admin); + gatewayFork.pause(); + + fundUserWithMainnetTokens(user1, MAINNET_USDC, 1000e6); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), type(uint256).max); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_USDC, 10e6, 0.001 ether); + + vm.expectRevert(); + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + // ========================= + // SWAP INTEGRATION TESTS + // ========================= + + /// @notice Test revert when Uniswap router/factory are not configured + function test_TokenGas_RevertOn_UniswapNotConfigured() public { + // Create a new gateway without Uniswap configured + UniversalGateway implementation2 = new UniversalGateway(); + bytes memory initData2 = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, + tss, + address(this), + MIN_CAP_USD, + MAX_CAP_USD, + address(0), // No factory + address(0), // No router + MAINNET_WETH + ); + TransparentUpgradeableProxy proxy2 = + new TransparentUpgradeableProxy(address(implementation2), address(proxyAdmin), initData2); + UniversalGateway gatewayNoUniswap = UniversalGateway(payable(address(proxy2))); + + fundUserWithMainnetTokens(user1, MAINNET_USDC, 1000e6); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayNoUniswap), type(uint256).max); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_USDC, 10e6, 0.001 ether); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayNoUniswap.sendUniversalTx(req); + } + + /// @notice Test WETH fast-path when Uniswap is configured + /// @dev When gasToken == WETH, swapToNative uses fast-path: pull WETH, unwrap to native + function test_TokenGas_WETHFastPath_Success() public { + // Arrange: User has WETH + uint256 gasAmount = 0.001 ether; // 0.001 ETH = $2, within caps + uint256 amountOutMinETH = (gasAmount * 99) / 100; // Allow 1% slippage + + // Fund user with ETH and convert to WETH + vm.deal(user1, gasAmount); + vm.prank(user1); + IWETH(MAINNET_WETH).deposit{ value: gasAmount }(); + + vm.prank(user1); + mainnetWETH.approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), address(0), 0, MAINNET_WETH, gasAmount, bytes(""), amountOutMinETH, block.timestamp + 1 hours + ); + + uint256 tssBalanceBefore = tss.balance; + uint256 userWethBalanceBefore = mainnetWETH.balanceOf(user1); + + // Act: Expect UniversalTx event with TX_TYPE.GAS + vm.expectEmit(true, true, false, true, address(gatewayFork)); + emit UniversalTx( + user1, + address(0), + address(0), + gasAmount, // nativeValue from unwrap + bytes(""), + req.revertRecipient, + TX_TYPE.GAS, + bytes("") + ); + + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + + // Assert: WETH was unwrapped and sent to TSS + assertEq(tss.balance, tssBalanceBefore + gasAmount, "TSS should receive unwrapped ETH"); + assertEq(mainnetWETH.balanceOf(user1), userWethBalanceBefore - gasAmount, "User WETH should be consumed"); + } + + /// @notice Test revert when pool is not found for non-WETH token + /// @dev _findV3PoolWithNative reverts with InvalidInput when no pool exists + function test_TokenGas_RevertOn_NoPoolFound() public { + // Use a token that doesn't have a WETH pair on Uniswap + // Create a mock token with no liquidity + MockERC20 fakeToken = new MockERC20("Fake", "FAKE", 18, 0); + fakeToken.mint(user1, 1e18); + + vm.prank(user1); + fakeToken.approve(address(gatewayFork), 1e18); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), address(0), 0, address(fakeToken), 1e18, bytes(""), 0.001 ether, block.timestamp + 1 hours + ); + + // Act & Assert: Should revert when pool is not found + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + // ========================= + // TX_TYPE INFERENCE TESTS + // ========================= + + /// @notice Test that TX_TYPE.GAS is correctly inferred when using token-as-gas + /// @dev When req has no payload, no funds, and nativeValue > 0 (from swap), should route to GAS + function test_TokenGas_InferGAS_Type() public { + // Arrange: Swap USDC for gas, no payload, no funds + // Use amount that swaps to between $1-$10 worth of ETH (MIN_CAP to MAX_CAP) + // At ~$3000/ETH: $1 = ~0.00033 ETH, $10 = ~0.0033 ETH + // Use ~5 USDC to get ~$5 worth of ETH (within caps, accounting for slippage) + uint256 gasAmount = 5e6; // 5 USDC (swaps to ~$5 worth of ETH, within $1-$10 caps) + uint256 amountOutMinETH = 0.0001 ether; // Min output (conservative to allow slippage) + + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), + 0, // No funds + MAINNET_USDC, + gasAmount, + bytes(""), // No payload + amountOutMinETH, + block.timestamp + 1 hours + ); + + uint256 tssBalanceBefore = tss.balance; + + // Act: Call sendUniversalTx (don't check event as amount is unpredictable from real swap) + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + + // Assert: TSS received ETH from swap (approximate check) + assertGt(tss.balance, tssBalanceBefore, "TSS should receive swapped ETH"); + assertGe(tss.balance, tssBalanceBefore + amountOutMinETH, "TSS should receive at least min ETH"); + } + + /// @notice Test that TX_TYPE.GAS_AND_PAYLOAD is correctly inferred when using token-as-gas with payload + function test_TokenGas_InferGAS_AND_PAYLOAD_Type() public { + // Arrange: Swap USDC for gas, with payload, no funds + // Use amount that swaps to between $1-$10 worth of ETH + uint256 gasAmount = 5e6; // 5 USDC (swaps to ~$5 worth of ETH, within $1-$10 caps) + uint256 amountOutMinETH = 0.0001 ether; // Min output (conservative to allow slippage) + + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory payloadBytes = abi.encode(payload); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), + 0, // No funds + MAINNET_USDC, + gasAmount, + payloadBytes, // Has payload + amountOutMinETH, + block.timestamp + 1 hours + ); + + // Act: Call sendUniversalTx (don't check event as amount is unpredictable from real swap) + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + /// @notice Test that TX_TYPE.FUNDS is correctly inferred when using token-as-gas with funds + /// @dev Note: When using token-as-gas with native funds, the swap happens first. + /// However, _fetchTxType sees hasFunds=true and fundsIsNative=true, so it routes to FUNDS. + /// But _sendTxWithFunds checks if nativeValue (from swap) == req.amount (fundsAmount), + /// which will fail. This test documents that token-as-gas with native funds is not a valid combination. + /// Instead, users should either: + /// - Use native funds without token-as-gas (sendUniversalTx with UniversalTxRequest) + /// - Use ERC20 funds with token-as-gas (token != address(0)) + function test_TokenGas_InferFUNDS_Type() public { + // Arrange: Swap USDC for gas, with funds (native) + // NOTE: This combination will actually revert because: + // - Swap produces nativeValue from token + // - _sendTxWithFunds checks: if (fundsIsNative && hasNativeValue) { if (_req.amount != nativeValue) revert } + // - Since nativeValue != fundsAmount, it reverts + uint256 gasAmount = 100e6; // 100 USDC + uint256 amountOutMinETH = 0.0003 ether; + uint256 fundsAmount = 0.001 ether; // Native funds = $2, within caps + + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), // Native token + fundsAmount, // Has funds + MAINNET_USDC, + gasAmount, + bytes(""), // No payload + amountOutMinETH, + block.timestamp + 1 hours + ); + + // Act & Assert: This combination will revert because nativeValue from swap != fundsAmount + // The swap happens first, producing nativeValue, but then _sendTxWithFunds expects nativeValue == fundsAmount + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayFork.sendUniversalTx{ value: fundsAmount }(req); // Send funds with msg.value + } + + /// @notice Test that TX_TYPE.FUNDS_AND_PAYLOAD is correctly inferred when using token-as-gas with funds and payload + function test_TokenGas_InferFUNDS_AND_PAYLOAD_Type() public { + // Arrange: Swap USDC for gas, with funds and payload + uint256 gasAmount = 5e6; // 5 USDC (swaps to ~$5 worth of ETH, within $1-$10 caps) + uint256 amountOutMinETH = 0.0001 ether; + uint256 fundsAmount = 0.001 ether; // Native funds = $2, within caps + + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory payloadBytes = abi.encode(payload); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), // Native token + fundsAmount, // Has funds + MAINNET_USDC, + gasAmount, + payloadBytes, // Has payload + amountOutMinETH, + block.timestamp + 1 hours + ); + + // Act: Call sendUniversalTx (don't check event as amount is unpredictable from real swap) + vm.prank(user1); + gatewayFork.sendUniversalTx{ value: fundsAmount }(req); + } + + // ========================= + // MSG.VALUE SEMANTICS TESTS + // ========================= + + /// @notice Test that msg.value is accepted but ignored for token-as-gas entrypoint + function test_TokenGas_AcceptsMsgValue() public { + // Arrange: Send msg.value along with token-as-gas request + // Use amount that swaps to between $1-$10 worth of ETH + uint256 gasAmount = 5e6; // 5 USDC (swaps to ~$5 worth of ETH, within $1-$10 caps) + uint256 amountOutMinETH = 0.0001 ether; + uint256 msgValue = 0.1 ether; // Extra ETH sent + + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_USDC, gasAmount, amountOutMinETH); + + uint256 gatewayBalanceBefore = address(gatewayFork).balance; + + // Act: Should succeed even with msg.value (don't check event as amount is unpredictable) + vm.prank(user1); + gatewayFork.sendUniversalTx{ value: msgValue }(req); + + // Assert: msg.value was accepted but not used (gateway balance increased) + assertEq(address(gatewayFork).balance, gatewayBalanceBefore + msgValue, "Gateway should receive msg.value"); + } + + /// @notice Test that msg.value does not affect nativeValue calculation + function test_TokenGas_MsgValueDoesNotAffectNativeValue() public { + // Arrange: Same swap with different msg.value amounts + // Use amount that swaps to between $1-$10 worth of ETH + uint256 gasAmount = 5e6; // 5 USDC (swaps to ~$5 worth of ETH, within $1-$10 caps) + uint256 amountOutMinETH = 0.0001 ether; + + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount * 2); // Fund for two swaps + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount * 2); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_USDC, gasAmount, amountOutMinETH); + + uint256 tssBalanceBefore = tss.balance; + + // Act: Send with msg.value = 0 + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + + uint256 tssBalanceAfterZero = tss.balance; + uint256 ethReceivedZero = tssBalanceAfterZero - tssBalanceBefore; + + // Reset and send with msg.value > 0 + vm.roll(block.number + 1); + tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayFork.sendUniversalTx{ value: 1 ether }(req); + + uint256 tssBalanceAfterNonZero = tss.balance; + uint256 ethReceivedNonZero = tssBalanceAfterNonZero - tssBalanceBefore; + + // Assert: nativeValue (sent to TSS) is approximately the same regardless of msg.value + // Allow small tolerance due to real market conditions + assertApproxEqAbs( + ethReceivedZero, ethReceivedNonZero, 1e15, "nativeValue should be same regardless of msg.value" + ); + } + + // ========================= + // ERROR PATH TESTS + // ========================= + + /// @notice Test revert when user has insufficient gasToken balance + function test_TokenGas_RevertOn_InsufficientBalance() public { + // Setup: User has no tokens + uint256 gasAmount = 1000e6; // 1000 USDC + + // Don't fund user + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_USDC, gasAmount, 0.0003 ether); + + // Should revert when swapToNative tries to transfer tokens + // USDC uses old-style string errors, so we check for any revert + vm.expectRevert(); + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + /// @notice Test revert when user has insufficient gasToken allowance + function test_TokenGas_RevertOn_InsufficientAllowance() public { + // Setup: Fund user but don't approve + uint256 gasAmount = 100e6; // Use larger amount (test will revert before USD cap check) + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount); + + // Don't approve + // vm.prank(user1); + // mainnetUSDC.approve(address(gatewayFork), 0); // Already 0 + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_USDC, gasAmount, 0.0003 ether); + + // Should revert when swapToNative tries to transfer tokens + // USDC uses old-style string errors, so we check for any revert + vm.expectRevert(); + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + /// @notice Test deadline = 0 uses default deadline + function test_TokenGas_ZeroDeadlineUsesDefault() public { + // Arrange: Set deadline to 0 + uint256 gasAmount = 5e6; // 5 USDC (swaps to ~$5 worth of ETH, within $1-$10 caps) + uint256 amountOutMinETH = 0.0001 ether; + + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), + address(0), + 0, + MAINNET_USDC, + gasAmount, + bytes(""), + amountOutMinETH, + 0 // Zero deadline should use default + ); + + // Act: Should succeed with default deadline (don't check event as amount is unpredictable) + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + /// @notice Test slippage protection when swap output is below amountOutMinETH + function test_TokenGas_RevertOn_SlippageExceeded() public { + // Arrange: Set very high amountOutMinETH (higher than swap output) + uint256 gasAmount = 10e6; // 10 USDC + uint256 amountOutMinETH = 100 ether; // Extremely high min output (will fail) + + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_USDC, gasAmount, amountOutMinETH); + + // Act & Assert: Should revert on slippage check + // Uniswap router reverts with "Too little received" string error when slippage is exceeded + // This happens at the router level before the gateway's slippage check + vm.expectRevert("Too little received"); + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + // ========================= + // EDGE CASE TESTS + // ========================= + + /// @notice Test that revertInstruction validation is preserved + function test_TokenGas_PreservesRevertInstruction() public { + // Arrange: Zero revertRecipient should revert for GAS route + uint256 gasAmount = 100e6; // Use larger amount to meet USD caps + uint256 amountOutMinETH = 0.0003 ether; + + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), address(0), 0, MAINNET_USDC, gasAmount, bytes(""), amountOutMinETH, block.timestamp + 1 hours + ); + req.revertRecipient = address(0); // Invalid + + // Act & Assert: Should revert on invalid revertInstruction + vm.expectRevert(Errors.InvalidRecipient.selector); + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + /// @notice Test that signatureData is preserved + function test_TokenGas_PreservesSignatureData() public { + bytes memory customSignature = abi.encode("custom signature data"); + uint256 gasAmount = 5e6; // 5 USDC (swaps to ~$5 worth of ETH, within $1-$10 caps) + uint256 amountOutMinETH = 0.0001 ether; + + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildTokenGasRequest( + address(0), address(0), 0, MAINNET_USDC, gasAmount, bytes(""), amountOutMinETH, block.timestamp + 1 hours + ); + req.signatureData = customSignature; + + // Act: Call sendUniversalTx (don't check event as amount is unpredictable from real swap) + // Note: signatureData is preserved in the request and will be in the emitted event + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + /// @notice Test maximum values for gasAmount and amountOutMinETH + function test_TokenGas_MaximumValues() public { + uint256 maxGasAmount = type(uint256).max; + uint256 maxAmountOutMinETH = type(uint256).max; + + UniversalTokenTxRequest memory req = + _buildMinimalTokenGasRequest(MAINNET_USDC, maxGasAmount, maxAmountOutMinETH); + + // Should revert when swapToNative tries to transfer tokens + // USDC uses old-style string errors, so we check for any revert + vm.expectRevert(); + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + } + + // ========================= + // REAL SWAP TESTS WITH VARIOUS TOKENS + // ========================= + + /// @notice Test swapToNative with USDC + function test_TokenGas_SwapUSDC_Success() public { + uint256 gasAmount = 5e6; // 5 USDC (swaps to ~$5 worth of ETH, within $1-$10 caps) + uint256 amountOutMinETH = 0.0001 ether; // Min output (conservative to allow slippage) + + fundUserWithMainnetTokens(user1, MAINNET_USDC, gasAmount); + vm.prank(user1); + mainnetUSDC.approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_USDC, gasAmount, amountOutMinETH); + + uint256 tssBalanceBefore = tss.balance; + uint256 userBalanceBefore = mainnetUSDC.balanceOf(user1); + + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + + // Verify user's token balance decreased + assertEq(mainnetUSDC.balanceOf(user1), userBalanceBefore - gasAmount, "User token balance should decrease"); + + // Verify TSS received ETH (approximate check) + assertGt(tss.balance, tssBalanceBefore, "TSS should receive ETH from token swap"); + assertGe(tss.balance, tssBalanceBefore + amountOutMinETH, "TSS should receive at least min ETH"); + } + + /// @notice Test swapToNative with USDT + function test_TokenGas_SwapUSDT_Success() public { + uint256 gasAmount = 5e6; // 5 USDT (swaps to ~$5 worth of ETH, within $1-$10 caps) + uint256 amountOutMinETH = 0.0001 ether; // Min output (conservative to allow slippage) + + fundUserWithMainnetTokens(user1, MAINNET_USDT, gasAmount); + vm.prank(user1); + // USDT approve returns void, not bool + TetherToken(MAINNET_USDT).approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_USDT, gasAmount, amountOutMinETH); + + uint256 tssBalanceBefore = tss.balance; + uint256 userBalanceBefore = mainnetUSDT.balanceOf(user1); + + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + + // Verify user's token balance decreased + assertEq(mainnetUSDT.balanceOf(user1), userBalanceBefore - gasAmount, "User token balance should decrease"); + + // Verify TSS received ETH + assertGt(tss.balance, tssBalanceBefore, "TSS should receive ETH from token swap"); + } + + /// @notice Test swapToNative with DAI + function test_TokenGas_SwapDAI_Success() public { + uint256 gasAmount = 5e18; // 5 DAI (swaps to ~$5 worth of ETH, within $1-$10 caps) + uint256 amountOutMinETH = 0.0001 ether; // Min output (conservative to allow slippage) + + fundUserWithMainnetTokens(user1, MAINNET_DAI, gasAmount); + vm.prank(user1); + mainnetDAI.approve(address(gatewayFork), gasAmount); + + UniversalTokenTxRequest memory req = _buildMinimalTokenGasRequest(MAINNET_DAI, gasAmount, amountOutMinETH); + + uint256 tssBalanceBefore = tss.balance; + uint256 userBalanceBefore = mainnetDAI.balanceOf(user1); + + vm.prank(user1); + gatewayFork.sendUniversalTx(req); + + // Verify user's token balance decreased + assertEq(mainnetDAI.balanceOf(user1), userBalanceBefore - gasAmount, "User token balance should decrease"); + + // Verify TSS received ETH + assertGt(tss.balance, tssBalanceBefore, "TSS should receive ETH from token swap"); + } +} diff --git a/contracts/evm-gateway/test/gateway/4_sendUniversalTx_GasTxType.t.sol b/contracts/evm-gateway/test/gateway/4_sendUniversalTx_GasTxType.t.sol new file mode 100644 index 0000000..2a7467a --- /dev/null +++ b/contracts/evm-gateway/test/gateway/4_sendUniversalTx_GasTxType.t.sol @@ -0,0 +1,738 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BaseTest } from "../BaseTest.t.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { TX_TYPE, RevertInstructions, UniversalPayload, UniversalTxRequest } from "../../src/libraries/Types.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/** + * @title GatewaySendUniversalTxWithGas Test Suite + * @notice Comprehensive tests for _sendTxWithGas (instant route) via sendUniversalTx + * @dev Tests GAS and GAS_AND_PAYLOAD transaction types with focus on: + * - Validation rules (_validateUniversalTxWithGas) + * - Per-tx USD caps (_checkUSDCaps) + * - Per-block USD caps (_checkBlockUSDCap) + * - Native forwarding to TSS + * - Event emission correctness + */ +contract GatewaySendUniversalTxWithGasTest is BaseTest { + // UniversalGateway instance + UniversalGateway public gatewayTemp; + + // ========================= + // EVENTS + // ========================= + event UniversalTx( + address indexed sender, + address indexed recipient, + address token, + uint256 amount, + bytes payload, + address revertRecipient, + TX_TYPE txType, + bytes signatureData + ); + + // ========================= + // SETUP + // ========================= + function setUp() public override { + super.setUp(); + + // Deploy UniversalGateway + _deployGatewayTemp(); + + // Wire oracle to the new gateway instance + vm.prank(admin); + gatewayTemp.setEthUsdFeed(address(ethUsdFeedMock)); + + // Setup token support on gatewayTemp (native + all mock ERC20s) + address[] memory tokens = new address[](4); + uint256[] memory thresholds = new uint256[](4); + tokens[0] = address(0); // Native token + tokens[1] = address(tokenA); // Mock ERC20 tokenA + tokens[2] = address(usdc); // Mock ERC20 usdc + tokens[3] = address(weth); // Mock WETH + thresholds[0] = 1000000 ether; // Large threshold for native + thresholds[1] = 1000000 ether; // Large threshold for tokenA + thresholds[2] = 1000000e6; // Large threshold for usdc (6 decimals) + thresholds[3] = 1000000 ether; // Large threshold for weth + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + // Re-approve tokens to gatewayTemp + address[] memory users = new address[](5); + users[0] = user1; + users[1] = user2; + users[2] = user3; + users[3] = user4; + users[4] = attacker; + + for (uint256 i = 0; i < users.length; i++) { + vm.prank(users[i]); + tokenA.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + usdc.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + weth.approve(address(gatewayTemp), type(uint256).max); + } + } + + /// @notice Deploy UniversalGateway + function _deployGatewayTemp() internal { + UniversalGateway implementation = new UniversalGateway(); + + bytes memory initData = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, + tss, + address(this), + MIN_CAP_USD, + MAX_CAP_USD, + uniV3Factory, + uniV3Router, + address(weth) + ); + + TransparentUpgradeableProxy tempProxy = + new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); + + gatewayTemp = UniversalGateway(payable(address(tempProxy))); + vm.label(address(gatewayTemp), "UniversalGateway"); + } + + /// @notice Helper to build UniversalTxRequest structs + function buildUniversalTxRequest(address recipient_, address token, uint256 amount, bytes memory payload) + internal + pure + returns (UniversalTxRequest memory) + { + return UniversalTxRequest({ + recipient: recipient_, + token: token, + amount: amount, + payload: payload, + revertRecipient: address(0x456), + signatureData: bytes("") + }); + } + + // ========================= + // C1: VALIDATION RULES + // ========================= + + /// @notice Test GAS requires empty payload + /// @dev txType=GAS with non-empty payload should revert + function test_SendTxWithGas_GAS_RevertOn_NonEmptyPayload() public { + // Arrange + uint256 gasAmount = 0.001 ether; + bytes memory nonEmptyPayload = abi.encode(buildDefaultPayload()); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + nonEmptyPayload + ); + + // Matrix will infer GAS_AND_PAYLOAD (hasPayload && !hasFunds && hasNativeValue) + // The deprecated txType field is ignored - call should succeed + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + /// @notice Test GAS accepts empty payload + /// @dev txType=GAS with empty payload should succeed + function test_SendTxWithGas_GAS_SucceedsWith_EmptyPayload() public { + // Arrange + uint256 gasAmount = 0.001 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") // ✅ Empty payload for GAS + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + assertEq(tss.balance, tssBalanceBefore + gasAmount, "TSS should receive gas amount"); + } + + /// @notice Test that empty payload with amount=0 routes to GAS (matrix inference) + /// @dev Matrix infers GAS when !hasPayload && !hasFunds && hasNativeValue + /// The deprecated txType field is ignored for routing + function test_SendTxWithGas_GAS_AND_PAYLOAD_RevertOn_EmptyPayload() public { + // Arrange + uint256 gasAmount = 0.002 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + // This field is ignored - matrix will infer GAS + address(0), + address(0), + 0, // amount must be 0 for GAS_AND_PAYLOAD route (matrix requires !hasFunds) + bytes("") + ); + + // Should succeed - matrix routes to GAS + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + /// @notice Test GAS_AND_PAYLOAD accepts non-empty payload + /// @dev txType=GAS_AND_PAYLOAD with non-empty payload should succeed + function test_SendTxWithGas_GAS_AND_PAYLOAD_SucceedsWith_NonEmptyPayload() public { + // Arrange + uint256 gasAmount = 0.002 ether; + bytes memory nonEmptyPayload = abi.encode(buildDefaultPayload()); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS_AND_PAYLOAD route (matrix requires !hasFunds) + nonEmptyPayload // ✅ Non-empty payload for GAS_AND_PAYLOAD + ); + + uint256 tssBalanceBefore = tss.balance; + + // Act + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + // Assert + assertEq(tss.balance, tssBalanceBefore + gasAmount, "TSS should receive gas amount"); + } + + /// @notice Test GAS_AND_PAYLOAD allows zero gas when only payload is provided + /// @dev User can send payload-only requests (no gas, no funds) - _fetchTxType should route to GAS_AND_PAYLOAD + /// This covers the case where user already has funds on Push Chain and only needs to send payload + function test_SendTxWithGas_GAS_AND_PAYLOAD_AllowsZeroGas() public { + // Arrange: Payload-only request (hasPayload=true, hasFunds=false, hasNativeValue=false) + // _fetchTxType should infer TX_TYPE.GAS_AND_PAYLOAD for this combination + uint256 gasAmount = 0; // No native value sent + bytes memory nonEmptyPayload = abi.encode(buildDefaultPayload()); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // No funds (amount = 0) + nonEmptyPayload // Payload is present + ); + + uint256 tssBalanceBefore = tss.balance; + + // Act & Assert: Expect GAS_AND_PAYLOAD event with zero amount + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // address(0) for UEA credit + token: address(0), // Native token (even though amount is 0) + amount: gasAmount, // Zero amount + payload: nonEmptyPayload, // Payload is present + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + // Assert: No native ETH forwarded to TSS when gasAmount is zero + assertEq(tss.balance, tssBalanceBefore, "TSS balance should remain unchanged when gasAmount is zero"); + } + + /// @notice Test revertInstruction.revertRecipient must be non-zero for GAS + /// @dev Zero revertRecipient should revert with InvalidRecipient + function test_SendTxWithGas_GAS_RevertOn_ZerorevertRecipient() public { + // Arrange + uint256 gasAmount = 0.001 ether; + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), + token: address(0), + amount: 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + payload: bytes(""), + revertRecipient: address(0), // ❌ Zero address + signatureData: bytes("") + }); + + vm.expectRevert(Errors.InvalidRecipient.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + /// @notice Test revertInstruction.revertRecipient must be non-zero for GAS_AND_PAYLOAD + /// @dev Zero revertRecipient should revert with InvalidRecipient + function test_SendTxWithGas_GAS_AND_PAYLOAD_RevertOn_ZerorevertRecipient() public { + uint256 gasAmount = 0.002 ether; + bytes memory payload = abi.encode(buildDefaultPayload()); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), + token: address(0), + amount: 0, // amount must be 0 for GAS_AND_PAYLOAD route (matrix requires !hasFunds) + payload: payload, + revertRecipient: address(0), + signatureData: bytes("") + }); + + vm.expectRevert(Errors.InvalidRecipient.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + /// @notice Test amount=0 fails USD cap check + /// @dev Zero amount results in 0 USD which is below MIN_CAP_UNIVERSAL_TX_USD + function test_SendTxWithGas_GAS_RevertOn_ZeroAmount() public { + // Arrange + UniversalTxRequest memory req = buildUniversalTxRequest(address(0), address(0), 0, bytes("")); + + vm.expectRevert(Errors.InvalidInput.selector); // _fetchTxType throws InvalidInput for zero native value + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + // ========================= + // C2: PER-TX USD CAP RANGE + // ========================= + + /// @notice Test amount below MIN_CAP_UNIVERSAL_TX_USD reverts + /// @dev At $2000/ETH, $1 min = 0.0005 ETH. Test with 0.0004 ETH ($0.80) + function test_SendTxWithGas_RevertOn_BelowMinCap() public { + // Arrange: At $2000/ETH, 0.0004 ETH = $0.80 (below $1 min) + uint256 gasAmount = 0.0004 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + /// @notice Test amount above MAX_CAP_UNIVERSAL_TX_USD reverts + /// @dev At $2000/ETH, $10 max = 0.005 ETH. Test with 0.006 ETH ($12) + function test_SendTxWithGas_RevertOn_AboveMaxCap() public { + // Arrange: At $2000/ETH, 0.006 ETH = $12 (above $10 max) + uint256 gasAmount = 0.006 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + /// @notice Test amount exactly at MIN_CAP_UNIVERSAL_TX_USD succeeds + /// @dev At $2000/ETH, $1 min = 0.0005 ETH exactly + function test_SendTxWithGas_SucceedsAt_ExactMinCap() public { + // Arrange: At $2000/ETH, 0.0005 ETH = exactly $1 + uint256 gasAmount = 0.0005 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + + uint256 tssBalanceBefore = tss.balance; + + // Act + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + // Assert + assertEq(tss.balance, tssBalanceBefore + gasAmount, "TSS should receive exact min cap amount"); + } + + // ========================= + // C3: PER-BLOCK USD CAP + // ========================= + + // NOTE: Most of these tests are already in test/gateway/5_GatewayBlockRateLimit.t.sol - Skipping here + + /// @notice Test block cap disabled (cap=0) allows unlimited calls + /// @dev With BLOCK_USD_CAP=0, should accept any number of calls in same block + function test_SendTxWithGas_BlockCap_Disabled_AllowsUnlimited() public { + // Arrange: Ensure block cap is 0 (disabled by default) + assertEq(gatewayTemp.BLOCK_USD_CAP(), 0, "Block cap should be 0 by default"); + + uint256 gasAmount = 0.002 ether; // $4 per call at $2000/ETH + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + + uint256 tssBalanceBefore = tss.balance; + + // Act: Make 5 calls in same block (total $20, no limit) + for (uint256 i = 0; i < 5; i++) { + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + // Assert: All calls succeeded + assertEq(tss.balance, tssBalanceBefore + (gasAmount * 5), "All 5 calls should succeed"); + } + + /// @notice Test single call exceeding block cap reverts + /// @dev Set cap=$5, attempt call worth $6 + function test_SendTxWithGas_BlockCap_RevertOn_SingleCallExceedsCap() public { + // Arrange: Set block cap to $5 (5e18) + vm.prank(admin); + gatewayTemp.setBlockUsdCap(5e18); + + // At $2000/ETH, 0.003 ETH = $6 (exceeds $5 cap) + uint256 gasAmount = 0.003 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + + // Act & Assert + vm.expectRevert(Errors.BlockCapLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + /// @notice Test cumulative calls exceeding block cap reverts + /// @dev Set cap=$10, first call $6 (60%), second call $5 (50%) should revert + function test_SendTxWithGas_BlockCap_RevertOn_CumulativeExceedsCap() public { + // Arrange: Set block cap to $10 (10e18) + vm.prank(admin); + gatewayTemp.setBlockUsdCap(10e18); + + // First call: 0.003 ETH = $6 at $2000/ETH (60% of cap) + uint256 firstAmount = 0.003 ether; + UniversalTxRequest memory req1 = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + + // Second call: 0.0025 ETH = $5 at $2000/ETH (would be 110% cumulative) + uint256 secondAmount = 0.0025 ether; + UniversalTxRequest memory req2 = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + + // Act: First call succeeds + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: firstAmount }(req1); + + // Act & Assert: Second call reverts (cumulative $11 > $10 cap) + vm.expectRevert(Errors.BlockCapLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: secondAmount }(req2); + } + + // ========================= + // C4: NATIVE FORWARDING & EVENT + // ========================= + + /// @notice Test native ETH forwarded to TSS for GAS + /// @dev Verify TSS balance increases by exact amount + function test_SendTxWithGas_GAS_ForwardsNative_ToTSS() public { + uint256 gasAmount = 0.002 ether; + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + + uint256 tssBalanceBefore = tss.balance; + uint256 gatewayBalanceBefore = address(gatewayTemp).balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + assertEq(tss.balance, tssBalanceBefore + gasAmount, "TSS should receive exact gas amount"); + + assertEq(address(gatewayTemp).balance, gatewayBalanceBefore, "Gateway should not hold funds"); + } + + /// @notice Test native ETH forwarded to TSS for GAS_AND_PAYLOAD + /// @dev Verify TSS balance increases by exact amount + function test_SendTxWithGas_GAS_AND_PAYLOAD_ForwardsNative_ToTSS() public { + uint256 gasAmount = 0.003 ether; + bytes memory payload = abi.encode(buildDefaultPayload()); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS_AND_PAYLOAD route (matrix requires !hasFunds) + payload + ); + + uint256 tssBalanceBefore = tss.balance; + uint256 gatewayBalanceBefore = address(gatewayTemp).balance; + + // Act + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + // Assert: TSS received funds + assertEq(tss.balance, tssBalanceBefore + gasAmount, "TSS should receive exact gas amount"); + + // Assert: Gateway balance unchanged + assertEq(address(gatewayTemp).balance, gatewayBalanceBefore, "Gateway should not hold funds"); + } + + /// @notice Test UniversalTx event correctness for GAS + /// @dev Verify all event parameters match expected values + function test_SendTxWithGas_GAS_EmitsCorrect_UniversalTxEvent() public { + // Arrange + uint256 gasAmount = 0.002 ether; + RevertInstructions memory revertInst = + RevertInstructions({ revertRecipient: address(0x789), revertMsg: bytes("test context") }); + bytes memory sigData = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2))); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), + token: address(0), + amount: 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + payload: bytes(""), + revertRecipient: revertInst.revertRecipient, + signatureData: sigData + }); + + // Act & Assert: Expect event with exact parameters + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), // Always address(0) for gas routes (UEA credit) + token: address(0), // Native token + amount: gasAmount, + payload: bytes(""), // Empty for GAS + revertRecipient: revertInst.revertRecipient, + signatureData: sigData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + /// @notice Test UniversalTx event correctness for GAS_AND_PAYLOAD + /// @dev Verify all event parameters including non-empty payload + function test_SendTxWithGas_GAS_AND_PAYLOAD_EmitsCorrect_UniversalTxEvent() public { + uint256 gasAmount = 0.003 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + RevertInstructions memory revertInst = + RevertInstructions({ revertRecipient: address(0xABC), revertMsg: bytes("payload context") }); + bytes memory sigData = abi.encodePacked(bytes32(uint256(3)), bytes32(uint256(4))); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), + token: address(0), + amount: 0, // amount must be 0 for GAS_AND_PAYLOAD route (matrix requires !hasFunds) + payload: encodedPayload, + revertRecipient: revertInst.revertRecipient, + signatureData: sigData + }); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // Always address(0) for gas routes (UEA credit) + token: address(0), // Native token + amount: gasAmount, + payload: encodedPayload, // Non-empty for GAS_AND_PAYLOAD + revertRecipient: revertInst.revertRecipient, + signatureData: sigData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + /// @notice Test event emitted with empty signatureData + /// @dev Verify empty signature data is acceptable + function test_SendTxWithGas_Event_WithEmpty_SignatureData() public { + uint256 gasAmount = 0.001 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + // signatureData is already empty + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), + token: address(0), + amount: gasAmount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: bytes("") // Empty + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + // ========================= + // ADDITIONAL EDGE CASES + // ========================= + + /// @notice Test multiple users can call in same block (within caps) + /// @dev Verify different users share the same block cap + function test_SendTxWithGas_MultipleUsers_ShareBlockCap() public { + // Arrange: Set block cap to $10 + vm.prank(admin); + gatewayTemp.setBlockUsdCap(10e18); + + // Each user sends $3 worth + uint256 gasAmount = 0.0015 ether; // $3 at $2000/ETH + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + + uint256 tssBalanceBefore = tss.balance; + + // Act: user1 sends $3 (total $3) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + // Act: user2 sends $3 (total $6) + vm.prank(user2); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + // Act: user3 sends $3 (total $9) + vm.prank(user3); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + // Act & Assert: user4 tries to send $3 (would be $12 total) - should revert + vm.expectRevert(Errors.BlockCapLimitExceeded.selector); + vm.prank(user4); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + // Assert: First 3 users succeeded + assertEq(tss.balance, tssBalanceBefore + (gasAmount * 3), "First 3 users should succeed"); + } + + /// @notice Test GAS and GAS_AND_PAYLOAD share the same block cap + /// @dev Both transaction types consume from the same block budget + function test_SendTxWithGas_GAS_And_GAS_AND_PAYLOAD_ShareBlockCap() public { + // Arrange: Set block cap to $10 + vm.prank(admin); + gatewayTemp.setBlockUsdCap(10e18); + + // GAS call: $6 + uint256 gasAmount1 = 0.003 ether; + UniversalTxRequest memory req1 = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + + // GAS_AND_PAYLOAD call: $5 (would exceed cap) + uint256 gasAmount2 = 0.0025 ether; + bytes memory payload = abi.encode(buildDefaultPayload()); + UniversalTxRequest memory req2 = buildUniversalTxRequest( + address(0), // amount must be 0 for GAS_AND_PAYLOAD route (matrix requires !hasFunds) + address(0), + 0, + payload + ); + + // Act: GAS call succeeds + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount1 }(req1); + + // Act & Assert: GAS_AND_PAYLOAD call reverts (cumulative $11 > $10) + vm.expectRevert(Errors.BlockCapLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount2 }(req2); + } + + /// @notice Test large payload doesn't affect USD cap checks + /// @dev USD caps only check amount, not payload size + function test_SendTxWithGas_LargePayload_DoesNotAffect_USDCaps() public { + bytes memory largePayload = new bytes(10000); + for (uint256 i = 0; i < 10000; i++) { + largePayload[i] = bytes1(uint8(i % 256)); + } + bytes memory encodedPayload = abi.encode(largePayload); + + uint256 gasAmount = 0.002 ether; // $4 at $2000/ETH (within caps) + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS_AND_PAYLOAD route (matrix requires !hasFunds) + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + // Act: Should succeed despite large payload + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + + // Assert + assertEq(tss.balance, tssBalanceBefore + gasAmount, "Large payload should not affect USD cap check"); + } + + /// @notice Test contract balance remains zero after forwarding + /// @dev Gateway should not accumulate native ETH + function test_SendTxWithGas_Gateway_DoesNotAccumulate_NativeETH() public { + uint256 gasAmount = 0.002 ether; + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), + address(0), + 0, // amount must be 0 for GAS route (matrix requires !hasFunds) + bytes("") + ); + + // Act: Make multiple calls + for (uint256 i = 0; i < 5; i++) { + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: gasAmount }(req); + } + + // Assert: Gateway balance should be 0 + assertEq(address(gatewayTemp).balance, 0, "Gateway should not hold any native ETH"); + } +} diff --git a/contracts/evm-gateway/test/gateway/5_sendUniversalTx_FundsTxType_Case_1.t.sol b/contracts/evm-gateway/test/gateway/5_sendUniversalTx_FundsTxType_Case_1.t.sol new file mode 100644 index 0000000..e4eee99 --- /dev/null +++ b/contracts/evm-gateway/test/gateway/5_sendUniversalTx_FundsTxType_Case_1.t.sol @@ -0,0 +1,663 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BaseTest } from "../BaseTest.t.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { TX_TYPE, RevertInstructions, UniversalPayload, UniversalTxRequest } from "../../src/libraries/Types.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { MockERC20 } from "../mocks/MockERC20.sol"; + +/** + * @title GatewaySendUniversalTxWithFunds Test Suite + * @notice Comprehensive tests for FUNDS route (standard route) via sendUniversalTx + * @dev Tests FUNDS and FUNDS_AND_PAYLOAD transaction types with focus on: + * All paths are exercised through sendUniversalTx() which internally routes to the standard route. + * Phase 1: TX_TYPE.FUNDS (native and ERC20) + * Phase 2: TX_TYPE.FUNDS_AND_PAYLOAD - Case 2.1 (No batching) + * Phase 3: TX_TYPE.FUNDS_AND_PAYLOAD - Case 2.2 (Native batching) + * Phase 4: TX_TYPE.FUNDS_AND_PAYLOAD - Case 2.3 (ERC20 + native batching) + * + * Current Implementation: Phase 1 - TX_TYPE.FUNDS + */ +contract GatewaySendUniversalTxWithFundsTest is BaseTest { + // UniversalGateway instance + UniversalGateway public gatewayTemp; + + // ========================= + // EVENTS + // ========================= + event UniversalTx( + address indexed sender, + address indexed recipient, + address token, + uint256 amount, + bytes payload, + address revertRecipient, + TX_TYPE txType, + bytes signatureData + ); + + // ========================= + // SETUP + // ========================= + function setUp() public override { + super.setUp(); + + // Deploy UniversalGateway + _deployGatewayTemp(); + + // Wire oracle to the new gateway instance + vm.prank(admin); + gatewayTemp.setEthUsdFeed(address(ethUsdFeedMock)); + + // Setup token support on gatewayTemp (native + all mock ERC20s) + address[] memory tokens = new address[](4); + uint256[] memory thresholds = new uint256[](4); + tokens[0] = address(0); // Native token + tokens[1] = address(tokenA); // Mock ERC20 tokenA + tokens[2] = address(usdc); // Mock ERC20 usdc + tokens[3] = address(weth); // Mock WETH + thresholds[0] = 1000000 ether; // Large threshold for native + thresholds[1] = 1000000 ether; // Large threshold for tokenA + thresholds[2] = 1000000e6; // Large threshold for usdc (6 decimals) + thresholds[3] = 1000000 ether; // Large threshold for weth + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + // Re-approve tokens to gatewayTemp + address[] memory users = new address[](5); + users[0] = user1; + users[1] = user2; + users[2] = user3; + users[3] = user4; + users[4] = attacker; + + for (uint256 i = 0; i < users.length; i++) { + vm.prank(users[i]); + tokenA.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + usdc.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + weth.approve(address(gatewayTemp), type(uint256).max); + } + } + + /// @notice Deploy UniversalGateway + function _deployGatewayTemp() internal { + UniversalGateway implementation = new UniversalGateway(); + + bytes memory initData = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, + tss, + address(this), + MIN_CAP_USD, + MAX_CAP_USD, + uniV3Factory, + uniV3Router, + address(weth) + ); + + TransparentUpgradeableProxy tempProxy = + new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); + + gatewayTemp = UniversalGateway(payable(address(tempProxy))); + vm.label(address(gatewayTemp), "UniversalGateway"); + } + + /// @notice Helper to build UniversalTxRequest structs + function buildUniversalTxRequest(address recipient_, address token, uint256 amount, bytes memory payload) + internal + pure + returns (UniversalTxRequest memory) + { + return UniversalTxRequest({ + recipient: recipient_, + token: token, + amount: amount, + payload: payload, + revertRecipient: address(0x456), + signatureData: bytes("") + }); + } + + // ========================================================================= + // PHASE 1: TX_TYPE.FUNDS TESTS + // ========================================================================= + + // ========================= + // D1.1: NATIVE FUNDS (Case 1.1) + // ========================= + + /// @notice Test FUNDS with native token - happy path + /// @dev Verifies: + /// - Native token forwarded to TSS + /// - Rate limit consumed correctly + /// - Event emitted with correct parameters + /// - Recipient must be address(0) for FUNDS type + function test_SendTxWithFunds_FUNDS_Native_HappyPath() public { + uint256 fundsAmount = 100 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(0), // Native token + fundsAmount, + bytes("") // Empty payload for FUNDS + ); + + uint256 tssBalanceBefore = tss.balance; + (uint256 usedBefore,) = gatewayTemp.currentTokenUsage(address(0)); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS, + sender: user1, + recipient: address(0), // FUNDS always has recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + + assertEq(tss.balance, tssBalanceBefore + fundsAmount, "TSS should receive native funds"); + + (uint256 usedAfter,) = gatewayTemp.currentTokenUsage(address(0)); + assertEq(usedAfter, usedBefore + fundsAmount, "Rate limit should be consumed"); + } + + /// @notice Test FUNDS with native token - recipient can be zero + /// @dev Unlike GAS routes, FUNDS allows zero recipient ( zero recipients = UEA on Push Chain) + function test_SendTxWithFunds_FUNDS_Native_AllowsZeroRecipient() public { + uint256 fundsAmount = 50 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // Zero recipient is allowed for FUNDS + address(0), // Native token + fundsAmount, + bytes("") + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + + assertEq(tss.balance, tssBalanceBefore + fundsAmount, "TSS should receive funds even with zero recipient"); + } + + /// @notice Test FUNDS native - msg.value must equal amount + /// @dev Revert if msg.value != amount + function test_SendTxWithFunds_FUNDS_Native_RevertOn_MsgValueMismatch_TooLow() public { + uint256 fundsAmount = 100 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(0), + fundsAmount, + bytes("") + ); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 50 ether }(req); // msg.value < amount + } + + /// @notice Test FUNDS native - zero amount reverts + /// @dev Amount must be > 0 + function test_SendTxWithFunds_FUNDS_Native_RevertOn_ZeroAmount() public { + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(0), + 0, // Zero amount + bytes("") + ); + + vm.expectRevert(Errors.InvalidInput.selector); // _fetchTxType throws InvalidInput for invalid combinations + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test FUNDS native - rate limit enforcement + /// @dev Should revert when exceeding threshold + function test_SendTxWithFunds_FUNDS_Native_RevertOn_RateLimitExceeded() public { + // Set a low threshold for native token + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(0); + thresholds[0] = 100 ether; // Low threshold + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + uint256 fundsAmount = 150 ether; // Exceeds threshold + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(0), + fundsAmount, + bytes("") + ); + + vm.expectRevert(Errors.RateLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + } + + /// @notice Test FUNDS native - cumulative rate limit exceeded + /// @dev Third call should fail when cumulative exceeds threshold + function test_SendTxWithFunds_FUNDS_Native_RevertOn_CumulativeRateLimitExceeded() public { + // Set threshold + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(0); + thresholds[0] = 200 ether; + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + uint256 firstAmount = 120 ether; + uint256 secondAmount = 90 ether; // Total 210 ether (exceeds 200) + + UniversalTxRequest memory req1 = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(0), + firstAmount, + bytes("") + ); + + UniversalTxRequest memory req2 = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(0), + secondAmount, + bytes("") + ); + + // First call succeeds + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: firstAmount }(req1); + + // Second call fails (cumulative 210 > 200) + vm.expectRevert(Errors.RateLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: secondAmount }(req2); + } + + /// @notice Test FUNDS native - rate limit resets in new epoch + /// @dev After epoch duration, rate limit should reset + function test_SendTxWithFunds_FUNDS_Native_RateLimitResetsInNewEpoch() public { + // Set threshold + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(0); + thresholds[0] = 100 ether; + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + uint256 fundsAmount = 90 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(0), + fundsAmount, + bytes("") + ); + + // First call in epoch 1 + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + + // Verify usage + (uint256 usedEpoch1,) = gatewayTemp.currentTokenUsage(address(0)); + assertEq(usedEpoch1, fundsAmount, "Usage in epoch 1"); + + // Advance time to next epoch (default epoch duration is 86400 seconds / 1 day) + vm.warp(block.timestamp + 86400); + + // Second call in epoch 2 (should succeed as limit reset) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + + // Verify usage reset + (uint256 usedEpoch2,) = gatewayTemp.currentTokenUsage(address(0)); + assertEq(usedEpoch2, fundsAmount, "Usage should reset in new epoch"); + } + + /// @notice Test FUNDS native - gateway does not accumulate ETH + /// @dev All native ETH should be forwarded to TSS + function test_SendTxWithFunds_FUNDS_Native_GatewayDoesNotAccumulate() public { + uint256 fundsAmount = 100 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(0), + fundsAmount, + bytes("") + ); + + uint256 gatewayBalanceBefore = address(gatewayTemp).balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + + // Gateway balance should remain unchanged + assertEq(address(gatewayTemp).balance, gatewayBalanceBefore, "Gateway should not hold native ETH"); + } + + // ========================= + // D1.2: ERC20 FUNDS (Case 1.2) + // ========================= + + /// @notice Test FUNDS with ERC20 - happy path + /// @dev Verifies: + /// - ERC20 transferred to vault + /// - Rate limit consumed correctly + /// - Event emitted with correct parameters + /// - Recipient must be address(0) for FUNDS type + function test_SendTxWithFunds_FUNDS_ERC20_HappyPath() public { + uint256 fundsAmount = 1000 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(tokenA), // ERC20 token + fundsAmount, + bytes("") + ); + + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + (uint256 usedBefore,) = gatewayTemp.currentTokenUsage(address(tokenA)); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS, + sender: user1, + recipient: address(0), // FUNDS always has recipient == address(0) + token: address(tokenA), + amount: fundsAmount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); // No native value for ERC20 + + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + fundsAmount, "Vault should receive ERC20"); + + (uint256 usedAfter,) = gatewayTemp.currentTokenUsage(address(tokenA)); + assertEq(usedAfter, usedBefore + fundsAmount, "Rate limit should be consumed"); + } + + /// @notice Test FUNDS with ERC20 - msg.value must be zero + /// @dev Revert if msg.value > 0 for ERC20 transfers + function test_SendTxWithFunds_FUNDS_ERC20_RevertOn_NonZeroMsgValue() public { + uint256 fundsAmount = 1000 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(tokenA), + fundsAmount, + bytes("") + ); + + vm.expectRevert(Errors.InvalidInput.selector); // _fetchTxType throws InvalidInput for invalid combinations + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 1 ether }(req); // msg.value > 0 not allowed for ERC20 + } + + /// @notice Test FUNDS with ERC20 - unsupported token reverts + /// @dev Token with threshold=0 should revert with NotSupported + function test_SendTxWithFunds_FUNDS_ERC20_RevertOn_UnsupportedToken() public { + // Deploy a new token that's not configured + MockERC20 unsupportedToken = new MockERC20("Unsupported", "UNSUP", 18, 0); + unsupportedToken.mint(user1, 1000 ether); + + vm.prank(user1); + unsupportedToken.approve(address(gatewayTemp), type(uint256).max); + + uint256 fundsAmount = 100 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(unsupportedToken), + fundsAmount, + bytes("") + ); + + vm.expectRevert(Errors.NotSupported.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test FUNDS with ERC20 - insufficient allowance reverts + /// @dev Should revert with ERC20InsufficientAllowance + function test_SendTxWithFunds_FUNDS_ERC20_RevertOn_InsufficientAllowance() public { + uint256 fundsAmount = 1000 ether; + + // Create a user with no approval + address userNoApproval = address(0x7777); + tokenA.mint(userNoApproval, fundsAmount); + // No approval given + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(tokenA), + fundsAmount, + bytes("") + ); + + vm.expectRevert(); // ERC20InsufficientAllowance + vm.prank(userNoApproval); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test FUNDS with ERC20 - insufficient balance reverts + /// @dev Should revert with ERC20InsufficientBalance + function test_SendTxWithFunds_FUNDS_ERC20_RevertOn_InsufficientBalance() public { + uint256 fundsAmount = 1000 ether; + + // Create a user with approval but no balance + address userNoBalance = address(0x8888); + // No tokens minted + + vm.prank(userNoBalance); + tokenA.approve(address(gatewayTemp), type(uint256).max); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(tokenA), + fundsAmount, + bytes("") + ); + + vm.expectRevert(); // ERC20InsufficientBalance + vm.prank(userNoBalance); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test FUNDS with ERC20 - rate limit enforcement + /// @dev Should revert when exceeding threshold + function test_SendTxWithFunds_FUNDS_ERC20_RevertOn_RateLimitExceeded() public { + // Set a low threshold for tokenA + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(tokenA); + thresholds[0] = 500 ether; // Low threshold + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + uint256 fundsAmount = 600 ether; // Exceeds threshold + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(tokenA), + fundsAmount, + bytes("") + ); + + vm.expectRevert(Errors.RateLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test FUNDS with ERC20 - different tokens have separate rate limits + /// @dev tokenA and usdc should have independent rate limits + function test_SendTxWithFunds_FUNDS_ERC20_SeparateRateLimitsPerToken() public { + // Set thresholds + address[] memory tokens = new address[](2); + uint256[] memory thresholds = new uint256[](2); + tokens[0] = address(tokenA); + tokens[1] = address(usdc); + thresholds[0] = 500 ether; + thresholds[1] = 500e6; // USDC has 6 decimals + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + uint256 tokenAAmount = 400 ether; + uint256 usdcAmount = 400e6; + + UniversalTxRequest memory reqA = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(tokenA), + tokenAAmount, + bytes("") + ); + + UniversalTxRequest memory reqU = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(usdc), + usdcAmount, + bytes("") + ); + + // Send tokenA + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(reqA); + + // Send usdc (should succeed - separate limit) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(reqU); + + // Verify separate usage tracking + (uint256 usedA,) = gatewayTemp.currentTokenUsage(address(tokenA)); + (uint256 usedU,) = gatewayTemp.currentTokenUsage(address(usdc)); + assertEq(usedA, tokenAAmount, "TokenA usage"); + assertEq(usedU, usdcAmount, "USDC usage"); + } + + // ========================= + // D1.3: VALIDATION TESTS (Common to both native and ERC20) + // ========================= + + /// @notice Test FUNDS - payload must be empty + /// @dev Non-empty payload should revert + function test_SendTxWithFunds_FUNDS_RevertOn_NonEmptyPayload() public { + uint256 fundsAmount = 100 ether; + bytes memory nonEmptyPayload = abi.encode(buildDefaultPayload()); + + UniversalTxRequest memory req = buildUniversalTxRequest( + // This field is ignored - matrix will infer FUNDS_AND_PAYLOAD + address(0), // FUNDS requires recipient == address(0) + address(0), + fundsAmount, + nonEmptyPayload // Has payload - matrix will route to FUNDS_AND_PAYLOAD (Case 2.2) + ); + + // Should succeed - matrix routes to FUNDS_AND_PAYLOAD (native batching) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + } + + /// @notice Test FUNDS - revertInstruction.revertRecipient must be non-zero + /// @dev Zero revertRecipient should revert + function test_SendTxWithFunds_FUNDS_RevertOn_ZerorevertRecipient() public { + uint256 fundsAmount = 100 ether; + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS requires recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: bytes(""), + revertRecipient: address(0), // Zero address not allowed + signatureData: bytes("") + }); + + vm.expectRevert(Errors.InvalidRecipient.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + } + + /// @notice Test FUNDS - multiple users can send independently + /// @dev Different users should be able to send funds independently + function test_SendTxWithFunds_FUNDS_MultipleUsersIndependent() public { + uint256 fundsAmount = 50 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(0), + fundsAmount, + bytes("") + ); + + uint256 tssBalanceBefore = tss.balance; + + // user1 sends + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + + // user2 sends + vm.prank(user2); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + + // user3 sends + vm.prank(user3); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + + // Assert: All succeeded + assertEq(tss.balance, tssBalanceBefore + (fundsAmount * 3), "All users should succeed"); + } + + /// @notice Test FUNDS - event preserves revertMsg + /// @dev revertMsg should be emitted correctly + function test_SendTxWithFunds_FUNDS_EventPreservesrevertMsg() public { + uint256 fundsAmount = 100 ether; + bytes memory revertMsg = abi.encodePacked("custom revert data", uint256(12345)); + + RevertInstructions memory revertInst = + RevertInstructions({ revertRecipient: address(0x999), revertMsg: revertMsg }); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS requires recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: bytes(""), + revertRecipient: revertInst.revertRecipient, + signatureData: bytes("") + }); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS, + sender: user1, + recipient: address(0), // FUNDS always has recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: bytes(""), + revertRecipient: revertInst.revertRecipient, // Full struct with revertMsg + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: fundsAmount }(req); + } +} diff --git a/contracts/evm-gateway/test/gateway/6_sendUniversalTxWithFUNDS_FundsTxType_Case2_1.t.sol b/contracts/evm-gateway/test/gateway/6_sendUniversalTxWithFUNDS_FundsTxType_Case2_1.t.sol new file mode 100644 index 0000000..241a9af --- /dev/null +++ b/contracts/evm-gateway/test/gateway/6_sendUniversalTxWithFUNDS_FundsTxType_Case2_1.t.sol @@ -0,0 +1,724 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BaseTest } from "../BaseTest.t.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { + TX_TYPE, + RevertInstructions, + UniversalPayload, + UniversalTxRequest, + VerificationType +} from "../../src/libraries/Types.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { MockERC20 } from "../mocks/MockERC20.sol"; + +/** + * @title GatewaySendUniversalTxWithFunds_PAYLOAD_Case2_1 Test Suite + * @notice Comprehensive tests for FUNDS_AND_PAYLOAD route (standard route) via sendUniversalTx + * @dev Tests FUNDS_AND_PAYLOAD transaction type - Case 2.1: No Batching + * All paths are exercised through sendUniversalTx() which internally routes to the standard route. + * + * Phase 2 - TX_TYPE.FUNDS_AND_PAYLOAD - Case 2.1 (No batching, msg.value == 0) + * + * Case 2.1: No Batching (msg.value == 0) + * - User already has UEA with PC token (gas) on Push Chain to execute payloads + * - User sends ERC20 token with payload + * - No gas leg executed (no gas route triggered) + * - Only ERC20 transferred to vault with payload + */ +contract GatewaySendUniversalTxWithFunds_PAYLOAD_Case2_1_Test is BaseTest { + // UniversalGateway instance + UniversalGateway public gatewayTemp; + + // ========================= + // EVENTS + // ========================= + event UniversalTx( + address indexed sender, + address indexed recipient, + address token, + uint256 amount, + bytes payload, + address revertRecipient, + TX_TYPE txType, + bytes signatureData + ); + + // ========================= + // SETUP + // ========================= + function setUp() public override { + super.setUp(); + + // Deploy UniversalGateway + _deployGatewayTemp(); + + // Wire oracle to the new gateway instance + vm.prank(admin); + gatewayTemp.setEthUsdFeed(address(ethUsdFeedMock)); + + // Setup token support on gatewayTemp (native + all mock ERC20s) + address[] memory tokens = new address[](4); + uint256[] memory thresholds = new uint256[](4); + tokens[0] = address(0); // Native token + tokens[1] = address(tokenA); // Mock ERC20 tokenA + tokens[2] = address(usdc); // Mock ERC20 usdc + tokens[3] = address(weth); // Mock WETH + thresholds[0] = 1000000 ether; // Large threshold for native + thresholds[1] = 1000000 ether; // Large threshold for tokenA + thresholds[2] = 1000000e6; // Large threshold for usdc (6 decimals) + thresholds[3] = 1000000 ether; // Large threshold for weth + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + // Re-approve tokens to gatewayTemp + address[] memory users = new address[](5); + users[0] = user1; + users[1] = user2; + users[2] = user3; + users[3] = user4; + users[4] = attacker; + + for (uint256 i = 0; i < users.length; i++) { + vm.prank(users[i]); + tokenA.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + usdc.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + weth.approve(address(gatewayTemp), type(uint256).max); + } + } + + /// @notice Deploy UniversalGateway + function _deployGatewayTemp() internal { + UniversalGateway implementation = new UniversalGateway(); + + bytes memory initData = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, + tss, + address(this), + MIN_CAP_USD, + MAX_CAP_USD, + uniV3Factory, + uniV3Router, + address(weth) + ); + + TransparentUpgradeableProxy tempProxy = + new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); + + gatewayTemp = UniversalGateway(payable(address(tempProxy))); + vm.label(address(gatewayTemp), "UniversalGateway"); + } + + /// @notice Helper to build UniversalTxRequest structs + function buildUniversalTxRequest(address recipient_, address token, uint256 amount, bytes memory payload) + internal + pure + returns (UniversalTxRequest memory) + { + return UniversalTxRequest({ + recipient: recipient_, + token: token, + amount: amount, + payload: payload, + revertRecipient: address(0x456), + signatureData: bytes("") + }); + } + + // ========================================================================= + // PHASE 2: TX_TYPE.FUNDS_AND_PAYLOAD - CASE 2.1 (NO BATCHING) + // ========================================================================= + + // ========================= + // D2.1: NO BATCHING - ERC20 WITH PAYLOAD (msg.value == 0) + // ========================= + + /// @notice Test FUNDS_AND_PAYLOAD Case 2.1 - ERC20 with payload, no batching - happy path + /// @dev Verifies: + /// - No gas leg executed (no gas route triggered) + /// - ERC20 transferred to vault + /// - Rate limit consumed for ERC20 + /// - Event emitted with payload + /// - msg.value must be 0 + function test_Case2_1_FUNDS_AND_PAYLOAD_ERC20_NoBatching_HappyPath() public { + uint256 fundsAmount = 1000 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), // ERC20 token (not native) + fundsAmount, + encodedPayload // Non-empty payload required + ); + + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + uint256 tssBalanceBefore = tss.balance; + (uint256 usedBefore,) = gatewayTemp.currentTokenUsage(address(tokenA)); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), + amount: fundsAmount, + payload: encodedPayload, // Payload preserved + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); // msg.value == 0 (no batching) + + // Assert: Vault received ERC20 + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + fundsAmount, "Vault should receive ERC20"); + + // Assert: TSS did NOT receive any native ETH (no gas leg) + assertEq(tss.balance, tssBalanceBefore, "TSS should not receive ETH in no-batching mode"); + + // Assert: Rate limit consumed for ERC20 + (uint256 usedAfter,) = gatewayTemp.currentTokenUsage(address(tokenA)); + assertEq(usedAfter, usedBefore + fundsAmount, "Rate limit should be consumed for ERC20"); + } + + /// @notice Test Case 2.1 - Multiple ERC20 tokens can be sent with payloads + /// @dev Different ERC20 tokens should work independently + function test_Case2_1_FUNDS_AND_PAYLOAD_MultipleERC20Tokens() public { + uint256 tokenAAmount = 500 ether; + uint256 usdcAmount = 500e6; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory reqA = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + tokenAAmount, + encodedPayload + ); + + UniversalTxRequest memory reqU = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(usdc), + usdcAmount, + encodedPayload + ); + + uint256 vaultTokenABefore = tokenA.balanceOf(address(this)); + uint256 vaultUsdcBefore = usdc.balanceOf(address(this)); + + // Send tokenA + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(reqA); + + // Send usdc + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(reqU); + + // Assert: Both tokens received + assertEq(tokenA.balanceOf(address(this)), vaultTokenABefore + tokenAAmount, "Vault should receive tokenA"); + assertEq(usdc.balanceOf(address(this)), vaultUsdcBefore + usdcAmount, "Vault should receive usdc"); + } + + /// @notice Test Case 2.1 - Payload content is preserved in event + /// @dev Verify payload is not modified + function test_Case2_1_FUNDS_AND_PAYLOAD_PayloadPreserved() public { + uint256 fundsAmount = 500 ether; + + // Create custom payload with specific data + UniversalPayload memory customPayload = UniversalPayload({ + to: address(0xABCD), + value: 0, + data: abi.encodeWithSignature("customFunction(uint256,address)", 12345, address(0x9999)), + gasLimit: 500000, + maxFeePerGas: 0, + maxPriorityFeePerGas: 0, + nonce: 42, + deadline: 0, + vType: VerificationType.signedVerification + }); + bytes memory encodedPayload = abi.encode(customPayload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + fundsAmount, + encodedPayload + ); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), + amount: fundsAmount, + payload: encodedPayload, // Exact payload preserved + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test Case 2.1 - Large payload is handled correctly + /// @dev Verify large payloads don't cause issues + function test_Case2_1_FUNDS_AND_PAYLOAD_LargePayload() public { + uint256 fundsAmount = 500 ether; + + // Create large payload (10KB of data) + bytes memory largeData = new bytes(10000); + for (uint256 i = 0; i < 10000; i++) { + largeData[i] = bytes1(uint8(i % 256)); + } + + UniversalPayload memory largePayload = UniversalPayload({ + to: address(0xABCD), + value: 0, + data: largeData, + gasLimit: 1000000, + maxFeePerGas: 0, + maxPriorityFeePerGas: 0, + nonce: 1, + deadline: 0, + vType: VerificationType.signedVerification + }); + bytes memory encodedPayload = abi.encode(largePayload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + fundsAmount, + encodedPayload + ); + + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + + // Assert: Should succeed with large payload + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + fundsAmount, "Should handle large payload"); + } + + /// @notice Test Case 2.1 - Rate limit enforcement for ERC20 + /// @dev Should revert when exceeding ERC20 threshold + function test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_RateLimitExceeded() public { + // Set a low threshold for tokenA + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(tokenA); + thresholds[0] = 500 ether; // Low threshold + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + uint256 fundsAmount = 600 ether; // Exceeds threshold + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + fundsAmount, + encodedPayload + ); + + vm.expectRevert(Errors.RateLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test Case 2.1 - Cumulative rate limit for ERC20 + /// @dev Multiple calls should accumulate towards rate limit + function test_Case2_1_FUNDS_AND_PAYLOAD_CumulativeRateLimit() public { + // Set threshold + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(tokenA); + thresholds[0] = 1000 ether; + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + uint256 firstAmount = 600 ether; + uint256 secondAmount = 300 ether; // Total 900 ether (within limit) + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req1 = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + firstAmount, + encodedPayload + ); + + UniversalTxRequest memory req2 = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + secondAmount, + encodedPayload + ); + + // First call succeeds + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req1); + + // Second call succeeds (cumulative 900 < 1000) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req2); + + // Verify cumulative usage + (uint256 used,) = gatewayTemp.currentTokenUsage(address(tokenA)); + assertEq(used, firstAmount + secondAmount, "Cumulative usage should match"); + } + + // ========================= + // D2.1: VALIDATION & REVERT CASES + // ========================= + + /// @notice Test Case 2.1 - Native token not allowed in no-batching mode + /// @dev token == address(0) with msg.value == 0 should revert + function test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_NativeToken_NoBatching() public { + uint256 fundsAmount = 100 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), // Native token NOT allowed in no-batching mode + fundsAmount, + encodedPayload + ); + + vm.expectRevert(Errors.InvalidInput.selector); // _fetchTxType throws InvalidInput for invalid combinations + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); // msg.value == 0 + } + + /// @notice Test Case 2.1 - Empty payload routes to FUNDS (matrix inference) + /// @dev Matrix infers FUNDS when !hasPayload && hasFunds && !fundsIsNative && !hasNativeValue + /// FUNDS with recipient != address(0) triggers InvalidRecipient + function test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_EmptyPayload() public { + uint256 fundsAmount = 500 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(tokenA), + fundsAmount, + bytes("") // Empty payload → matrix routes to FUNDS + ); + + // Empty payload routes to FUNDS, which succeeds with recipient == address(0) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS, + sender: user1, + recipient: address(0), + token: address(tokenA), + amount: fundsAmount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test Case 2.1 - Zero amount routes to GAS_AND_PAYLOAD (payload-only) + /// @dev Ensure payload-only requests emit GAS_AND_PAYLOAD event and do not move funds + function test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_ZeroAmount() public { + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + 0, // Zero amount forces routing to GAS_AND_PAYLOAD (payload-only) + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + uint256 tokenABalanceBefore = tokenA.balanceOf(address(gatewayTemp)); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS_AND_PAYLOAD, + sender: user1, + recipient: address(0), + token: address(0), + amount: 0, + payload: encodedPayload, + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + + assertEq(tss.balance, tssBalanceBefore, "TSS balance should remain unchanged when gasAmount is zero"); + assertEq( + tokenA.balanceOf(address(gatewayTemp)), + tokenABalanceBefore, + "Gateway should not receive ERC20 when amount is zero" + ); + } + + /// @notice Test Case 2.1 - Zero revertRecipient reverts + /// @dev revertInstruction.revertRecipient must be non-zero + function test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_ZerorevertRecipient() public { + uint256 fundsAmount = 500 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: address(0), // Zero address not allowed + signatureData: bytes("") + }); + + vm.expectRevert(Errors.InvalidRecipient.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test Case 2.1 - Unsupported token reverts + /// @dev Token with threshold=0 should revert with NotSupported + function test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_UnsupportedToken() public { + // Deploy a new token that's not configured + MockERC20 unsupportedToken = new MockERC20("Unsupported", "UNSUP", 18, 0); + unsupportedToken.mint(user1, 1000 ether); + + vm.prank(user1); + unsupportedToken.approve(address(gatewayTemp), type(uint256).max); + + uint256 fundsAmount = 100 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(unsupportedToken), + fundsAmount, + encodedPayload + ); + + vm.expectRevert(Errors.NotSupported.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test Case 2.1 - Insufficient allowance reverts + /// @dev Should revert with ERC20InsufficientAllowance + function test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_InsufficientAllowance() public { + uint256 fundsAmount = 1000 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + // Create a user with no approval + address userNoApproval = address(0x7777); + tokenA.mint(userNoApproval, fundsAmount); + // No approval given + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + fundsAmount, + encodedPayload + ); + + vm.expectRevert(); // ERC20InsufficientAllowance + vm.prank(userNoApproval); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test Case 2.1 - Insufficient balance reverts + /// @dev Should revert with ERC20InsufficientBalance + function test_Case2_1_FUNDS_AND_PAYLOAD_RevertOn_InsufficientBalance() public { + uint256 fundsAmount = 1000 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + // Create a user with approval but no balance + address userNoBalance = address(0x8888); + // No tokens minted + + vm.prank(userNoBalance); + tokenA.approve(address(gatewayTemp), type(uint256).max); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + fundsAmount, + encodedPayload + ); + + vm.expectRevert(); // ERC20InsufficientBalance + vm.prank(userNoBalance); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + // ========================= + // D2.1: EDGE CASES & ADDITIONAL TESTS + // ========================= + + /// @notice Test Case 2.1 - Event preserves revertMsg + /// @dev revertMsg should be emitted correctly + function test_Case2_1_FUNDS_AND_PAYLOAD_EventPreservesrevertMsg() public { + uint256 fundsAmount = 500 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + bytes memory revertMsg = abi.encodePacked("custom revert data", uint256(12345)); + + RevertInstructions memory revertInst = + RevertInstructions({ revertRecipient: address(0x999), revertMsg: revertMsg }); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: revertInst.revertRecipient, + signatureData: bytes("") + }); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: revertInst.revertRecipient, // Full struct with revertMsg + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test Case 2.1 - Event preserves signatureData + /// @dev SignatureData should be emitted correctly + function test_Case2_1_FUNDS_AND_PAYLOAD_EventPreservesSignatureData() public { + uint256 fundsAmount = 500 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + bytes memory sigData = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2))); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: address(0x456), + signatureData: sigData + }); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: req.revertRecipient, + signatureData: sigData // Should preserve signature data + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test Case 2.1 - Recipient can be zero (UEA on Push Chain) + /// @dev Zero recipient is allowed for FUNDS_AND_PAYLOAD + function test_Case2_1_FUNDS_AND_PAYLOAD_AllowsZeroRecipient() public { + uint256 fundsAmount = 500 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // Zero recipient allowed (UEA) + address(tokenA), + fundsAmount, + encodedPayload + ); + + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + fundsAmount, "Should allow zero recipient"); + } + + /// @notice Test Case 2.1 - Non-zero recipient reverts + /// @dev FUNDS_AND_PAYLOAD requires recipient == address(0) + function test_Case2_1_FUNDS_AND_PAYLOAD_RecipientEmittedAlwaysZero() public { + uint256 fundsAmount = 500 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: address(0x456), + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }( + buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + fundsAmount, + encodedPayload + ) + ); + } + + /// @notice Test Case 2.1 - Gateway does not accumulate ETH + /// @dev Gateway should not hold any native ETH in no-batching mode + function test_Case2_1_FUNDS_AND_PAYLOAD_GatewayDoesNotAccumulateETH() public { + uint256 fundsAmount = 500 ether; + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + fundsAmount, + encodedPayload + ); + + uint256 gatewayBalanceBefore = address(gatewayTemp).balance; + + // Make multiple calls + for (uint256 i = 0; i < 3; i++) { + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + // Gateway balance should remain unchanged + assertEq(address(gatewayTemp).balance, gatewayBalanceBefore, "Gateway should not hold native ETH"); + } +} diff --git a/contracts/evm-gateway/test/gateway/7_sendUniversalTxWithFUNDS_FundsTxType_Case2_2.t.sol b/contracts/evm-gateway/test/gateway/7_sendUniversalTxWithFUNDS_FundsTxType_Case2_2.t.sol new file mode 100644 index 0000000..509be89 --- /dev/null +++ b/contracts/evm-gateway/test/gateway/7_sendUniversalTxWithFUNDS_FundsTxType_Case2_2.t.sol @@ -0,0 +1,1264 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BaseTest } from "../BaseTest.t.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { + TX_TYPE, + RevertInstructions, + UniversalPayload, + UniversalTxRequest, + VerificationType +} from "../../src/libraries/Types.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/** + * @title GatewaySendUniversalTxWithFunds_PAYLOAD_Case2_2 Test Suite + * @notice Comprehensive tests for FUNDS_AND_PAYLOAD route (standard route) via sendUniversalTx + * @dev Tests FUNDS_AND_PAYLOAD transaction type - Case 2.2: Native Batching + * All paths are exercised through sendUniversalTx() which internally routes to both instant and standard routes. + * + * Phase 3 - TX_TYPE.FUNDS_AND_PAYLOAD - Case 2.2 (Native batching, msg.value > 0, token == native) + * + * Case 2.2: Batching of Gas + Funds_and_Payload (msg.value > 0, token == native) + * - User refills UEA's gas AND bridges native token in one transaction + * - Split Logic: msg.value is split between gasAmount and fundsAmount + * - gasAmount = msg.value - _req.amount + * - Dual Execution: + * 1. Gas route triggered if gasAmount > 0 (instant route with USD caps) + * 2. Native token rate limit consumed for _req.amount only + * 3. All msg.value forwarded to TSS + */ +contract GatewaySendUniversalTxWithFunds_PAYLOAD_Case2_2_Test is BaseTest { + // UniversalGateway instance + UniversalGateway public gatewayTemp; + + // ========================= + // EVENTS + // ========================= + event UniversalTx( // Placeholder value - ignored by matrix inference but required for struct + address indexed sender, + address indexed recipient, + address token, + uint256 amount, + bytes payload, + address revertRecipient, + TX_TYPE txType, + bytes signatureData + ); + + // ========================= + // SETUP + // ========================= + function setUp() public override { + super.setUp(); + + // Deploy UniversalGateway + _deployGatewayTemp(); + + // Wire oracle to the new gateway instance + vm.prank(admin); + gatewayTemp.setEthUsdFeed(address(ethUsdFeedMock)); + + // Setup token support on gatewayTemp (native + all mock ERC20s) + address[] memory tokens = new address[](4); + uint256[] memory thresholds = new uint256[](4); + tokens[0] = address(0); // Native token + tokens[1] = address(tokenA); // Mock ERC20 tokenA + tokens[2] = address(usdc); // Mock ERC20 usdc + tokens[3] = address(weth); // Mock WETH + thresholds[0] = 1000000 ether; // Large threshold for native + thresholds[1] = 1000000 ether; // Large threshold for tokenA + thresholds[2] = 1000000e6; // Large threshold for usdc (6 decimals) + thresholds[3] = 1000000 ether; // Large threshold for weth + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + // Re-approve tokens to gatewayTemp + address[] memory users = new address[](5); + users[0] = user1; + users[1] = user2; + users[2] = user3; + users[3] = user4; + users[4] = attacker; + + for (uint256 i = 0; i < users.length; i++) { + vm.prank(users[i]); + tokenA.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + usdc.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + weth.approve(address(gatewayTemp), type(uint256).max); + } + } + + /// @notice Deploy UniversalGateway + function _deployGatewayTemp() internal { + UniversalGateway implementation = new UniversalGateway(); + + bytes memory initData = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, + tss, + address(this), + MIN_CAP_USD, + MAX_CAP_USD, + uniV3Factory, + uniV3Router, + address(weth) + ); + + TransparentUpgradeableProxy tempProxy = + new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); + + gatewayTemp = UniversalGateway(payable(address(tempProxy))); + vm.label(address(gatewayTemp), "UniversalGateway"); + } + + /// @notice Helper to build UniversalTxRequest structs + function buildUniversalTxRequest(address recipient_, address token, uint256 amount, bytes memory payload) + internal + pure + returns (UniversalTxRequest memory) + { + return UniversalTxRequest({ + recipient: recipient_, + token: token, + amount: amount, + payload: payload, + revertRecipient: address(0x456), + signatureData: bytes("") + }); + } + + // ========================================================================= + // PHASE 3: TX_TYPE.FUNDS_AND_PAYLOAD - CASE 2.2 (NATIVE BATCHING) + // ========================================================================= + + // ========================= + // CATEGORY 1: HAPPY PATH & CORE FUNCTIONALITY + // ========================= + + /// @notice Test Case 2.2 - Native batching happy path with split + /// @dev Verifies: + /// - msg.value split correctly: gasAmount = msg.value - amount + /// - Gas route called with gasAmount + /// - Funds route called with amount + /// - TSS receives full msg.value + /// - Two events emitted (gas + funds) + /// - Native rate limit consumed for amount only (not gasAmount) + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_Batching_HappyPath() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + uint256 expectedGasAmount = msgValue - fundsAmount; // 0.002 ether = $4 + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), // Native token + fundsAmount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + (uint256 nativeUsedBefore,) = gatewayTemp.currentTokenUsage(address(0)); + + // Expect two events: Gas event + Funds event + // Event 1: Gas event (gasAmount = 4 ETH) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), // Gas always credits UEA + token: address(0), + amount: expectedGasAmount, + payload: bytes(""), // Gas event has empty payload + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + // Event 2: Funds event (amount = 6 ETH) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: encodedPayload, // Funds event has full payload + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: TSS received full msg.value + assertEq(tss.balance, tssBalanceBefore + msgValue, "TSS should receive full msg.value"); + + // Assert: Native rate limit consumed for fundsAmount only (not gasAmount) + (uint256 nativeUsedAfter,) = gatewayTemp.currentTokenUsage(address(0)); + assertEq(nativeUsedAfter, nativeUsedBefore + fundsAmount, "Rate limit should only consume fundsAmount"); + } + + /// @notice Test Case 2.2 - Exact amount (no gas, gasAmount = 0 - means no batching) + /// @dev When msg.value == amount, gasAmount = 0, no gas route called + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_ExactAmount_NoGas() public { + uint256 msgValue = 5 ether; + uint256 fundsAmount = 5 ether; // Exact match + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + // Expect only ONE event: Funds event (no gas event) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + assertEq(tss.balance, tssBalanceBefore + msgValue, "TSS should receive full msg.value"); + } + + /// @notice Test Case 2.2 - Small gas amount with large funds + /// @dev Verify split works with opposite asymmetric distribution + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_SmallGasLargeFunds() public { + uint256 msgValue = 10 ether; + uint256 fundsAmount = 9.999 ether; + uint256 expectedGasAmount = 0.001 ether; // $2 at $2000/ETH (within caps) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: TSS received full msg.value + assertEq(tss.balance, tssBalanceBefore + msgValue, "TSS should receive full msg.value"); + } + + /// @notice Test Case 2.2 - Payload preserved in funds event + /// @dev Gas event has empty payload, funds event has full payload + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_PayloadPreserved() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.002 ETH = $4 (within caps) + + // Custom payload + UniversalPayload memory customPayload = UniversalPayload({ + to: address(0xABCD), + value: 0, + data: abi.encodeWithSignature("customFunction(uint256)", 12345), + gasLimit: 500000, + maxFeePerGas: 0, + maxPriorityFeePerGas: 0, + nonce: 42, + deadline: 0, + vType: VerificationType.signedVerification + }); + bytes memory encodedPayload = abi.encode(customPayload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + // Gas event: empty payload + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), + token: address(0), + amount: msgValue - fundsAmount, + payload: bytes(""), // Empty for gas event + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + // Funds event: full payload + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: encodedPayload, // Full payload preserved + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Multiple users can send independently + /// @dev Different users should be able to send batched transactions + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_MultipleUsers() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.002 ETH = $4 (within caps) + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + // user1 sends + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // user2 sends + vm.prank(user2); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // user3 sends + vm.prank(user3); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: All succeeded + assertEq(tss.balance, tssBalanceBefore + (msgValue * 3), "All users should succeed"); + } + + // ========================= + // CATEGORY 2: VALIDATION & REVERT CASES + // ========================= + + /// @notice Test Case 2.2 - Revert when msg.value < amount + /// @dev Critical: msg.value must be >= amount for split to work + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_MsgValueLessThanAmount() public { + uint256 msgValue = 5 ether; + uint256 fundsAmount = 6 ether; // More than msg.value + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Empty payload routes to FUNDS (not FUNDS_AND_PAYLOAD) + /// @dev Empty payload means hasPayload=false, so _fetchTxType routes to FUNDS + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_EmptyPayload() public { + uint256 fundsAmount = 1 ether; + // For FUNDS, msg.value must equal amount + uint256 msgValue = fundsAmount; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS requires recipient == address(0) + address(0), + fundsAmount, + bytes("") // Empty payload - routes to FUNDS, not FUNDS_AND_PAYLOAD + ); + + // Empty payload routes to FUNDS (not FUNDS_AND_PAYLOAD), which succeeds + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS, + sender: user1, + recipient: address(0), + token: address(0), + amount: fundsAmount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Zero amount reverts + /// @dev Amount must be > 0 + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_ZeroAmount() public { + uint256 msgValue = 1 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + 0, // Zero amount + encodedPayload + ); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Zero msg.value routes to Case 2.1 (which reverts for native) + /// @dev Ensures Case 2.2 is NOT triggered when msg.value == 0 + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_ZeroMsgValue() public { + uint256 fundsAmount = 1 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), // Native token + fundsAmount, + encodedPayload + ); + + // Should route to Case 2.1, which reverts for native token with msg.value == 0 + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test Case 2.2 - Zero revertRecipient reverts + /// @dev revertInstruction.revertRecipient must be non-zero + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_ZerorevertRecipient() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: address(0), // Zero address + signatureData: bytes("") + }); + + vm.expectRevert(Errors.InvalidRecipient.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Gas amount below min USD cap reverts + /// @dev At $2000/ETH, min cap = $1 = 0.0005 ETH + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_GasAmountBelowMinUSDCap() public { + uint256 msgValue = 1.0004 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.0004 ETH = $0.80 (below $1 min cap) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Gas amount above max USD cap reverts + /// @dev At $2000/ETH, max cap = $10 = 0.005 ETH + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_GasAmountAboveMaxUSDCap() public { + uint256 msgValue = 1.006 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.006 ETH = $12 (above $10 max cap) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Gas amount exceeds block cap reverts + /// @dev Set block cap and verify gas route respects it + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_GasAmountExceedsBlockCap() public { + // Set block cap to $5 + vm.prank(admin); + gatewayTemp.setBlockUsdCap(5e18); + + uint256 msgValue = 1.003 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.003 ETH = $6 (exceeds $5 block cap) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + vm.expectRevert(Errors.BlockCapLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + // ========================= + // CATEGORY 3: RATE LIMITING + // ========================= + + /// @notice Test Case 2.2 - Rate limit only for funds amount (not gas amount) + /// @dev Critical: Only fundsAmount consumes native rate limit, gasAmount uses USD caps + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RateLimitOnlyForFunds() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.002 ether = $4 (does NOT consume rate limit) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + (uint256 usedBefore,) = gatewayTemp.currentTokenUsage(address(0)); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: Rate limit consumed for fundsAmount only (6 ETH, not 10 ETH) + (uint256 usedAfter,) = gatewayTemp.currentTokenUsage(address(0)); + assertEq(usedAfter, usedBefore + fundsAmount, "Rate limit should only consume fundsAmount"); + } + + /// @notice Test Case 2.2 - Funds amount exceeds rate limit reverts + /// @dev Even if msg.value is large enough for gas, funds must respect rate limit + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_FundsExceedRateLimit() public { + // Set low threshold for native + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(0); + thresholds[0] = 5 ether; + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + uint256 msgValue = 6.001 ether; + uint256 fundsAmount = 6 ether; // Exceeds 5 ETH threshold + // gasAmount = 0.001 ether = $2 (within USD caps) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + vm.expectRevert(Errors.RateLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Cumulative rate limit for funds + /// @dev Multiple calls accumulate towards rate limit + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_CumulativeRateLimit() public { + // Set threshold + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(0); + thresholds[0] = 10 ether; + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + // Call 1: fundsAmount = 6 ETH + UniversalTxRequest memory req1 = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + 6 ether, + encodedPayload + ); + + // Call 2: fundsAmount = 3 ETH (cumulative 9 ETH < 10 ETH) + UniversalTxRequest memory req2 = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + 3 ether, + encodedPayload + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 6.001 ether }(req1); // 6 ETH funds + 0.001 ETH gas ($2) + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 3.001 ether }(req2); // 3 ETH funds + 0.001 ETH gas ($2) + + // Verify cumulative usage + (uint256 used,) = gatewayTemp.currentTokenUsage(address(0)); + assertEq(used, 9 ether, "Cumulative rate limit should be 9 ETH"); + } + + /// @notice Test Case 2.2 - Cumulative rate limit exceeded reverts + /// @dev Second call should fail when cumulative exceeds threshold + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RevertOn_CumulativeRateLimitExceeded() public { + // Set threshold + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(0); + thresholds[0] = 10 ether; + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + // Call 1: fundsAmount = 6 ETH + UniversalTxRequest memory req1 = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + 6 ether, + encodedPayload + ); + + // Call 2: fundsAmount = 5 ETH (cumulative 11 ETH > 10 ETH) + UniversalTxRequest memory req2 = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + 5 ether, + encodedPayload + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 6.001 ether }(req1); // 6 ETH funds + 0.001 ETH gas + + vm.expectRevert(Errors.RateLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 5.001 ether }(req2); // 5 ETH funds + 0.001 ETH gas + } + + /// @notice Test Case 2.2 - Rate limit resets in new epoch + /// @dev After epoch duration, rate limit should reset + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_RateLimitResetsInNewEpoch() public { + // Set threshold + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(0); + thresholds[0] = 5 ether; + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + 4.5 ether, + encodedPayload + ); + + // First call in epoch 1 + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 4.501 ether }(req); // 4.5 ETH funds + 0.001 ETH gas ($2) + + // Advance time to next epoch + vm.warp(block.timestamp + 86401); + vm.roll(block.number + 1); + + // Update oracle timestamp to prevent stale data error + ethUsdFeedMock.setAnswer(2000e8, block.timestamp); + + // Second call in epoch 2 (should succeed as limit reset) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 4.501 ether }(req); // 4.5 ETH funds + 0.001 ETH gas ($2) + + // Verify usage reset + (uint256 used,) = gatewayTemp.currentTokenUsage(address(0)); + assertEq(used, 4.5 ether, "Usage should reset in new epoch"); + } + + // ========================= + // CATEGORY 4: EVENT EMISSION & DUAL EVENTS + // ========================= + + /// @notice Test Case 2.2 - Two events emitted when gasAmount > 0 + /// @dev Verify both gas and funds events are emitted + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_EmitsTwoEvents_WhenGasAmountPositive() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + uint256 gasAmount = 0.002 ether; // $4 + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + // Event 1: Gas + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), + token: address(0), + amount: gasAmount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + // Event 2: Funds + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - One event emitted when gasAmount = 0 + /// @dev Only funds event when msg.value == amount + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_EmitsOneEvent_WhenGasAmountZero() public { + uint256 msgValue = 5 ether; + uint256 fundsAmount = 5 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + // Only funds event (no gas event) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Gas event has empty payload + /// @dev Gas event always has empty payload, funds event has full payload + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_GasEvent_HasEmptyPayload() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + uint256 gasAmount = 0.002 ether; // $4 (within caps) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + // Gas event: empty payload + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), + token: address(0), + amount: gasAmount, + payload: bytes(""), // Empty + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Gas event recipient always address(0) + /// @dev Gas always credits UEA, funds preserves recipient + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_GasEvent_RecipientAlwaysZero() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + uint256 gasAmount = 0.002 ether; // $4 (within caps) + address explicitRecipient = address(0x999); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + // Gas event: recipient = address(0) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), // Always zero for gas + token: address(0), + amount: gasAmount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + // Funds event: recipient preserved + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Events preserve revertMsg + /// @dev Both events should preserve revertMsg + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_EventsPreserverevertMsg() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.002 ETH = $4 (within caps) + bytes memory revertMsg = abi.encodePacked("custom revert", uint256(999)); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + RevertInstructions memory revertInst = + RevertInstructions({ revertRecipient: address(0x456), revertMsg: revertMsg }); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: revertInst.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + // Both events should have preserved revertMsg (verified implicitly) + } + + /// @notice Test Case 2.2 - Events preserve signatureData + /// @dev Both events should preserve signatureData + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_EventsPreserveSignatureData() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.002 ETH = $4 (within caps) + bytes memory sigData = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2))); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + token: address(0), + amount: fundsAmount, + payload: encodedPayload, + revertRecipient: address(0x456), + signatureData: sigData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + // Both events should have preserved signatureData (verified implicitly) + } + + // ========================= + // CATEGORY 5: TSS BALANCE & FUND FLOW + // ========================= + + /// @notice Test Case 2.2 - TSS receives full msg.value + /// @dev All native ETH should go to TSS (gas + funds) + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_TSS_ReceivesFullMsgValue() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.002 ether = $4 (within USD caps) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + uint256 gatewayBalanceBefore = address(gatewayTemp).balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: TSS received full msg.value + assertEq(tss.balance, tssBalanceBefore + msgValue, "TSS should receive full msg.value"); + + // Assert: Gateway balance unchanged + assertEq(address(gatewayTemp).balance, gatewayBalanceBefore, "Gateway should not hold ETH"); + } + + /// @notice Test Case 2.2 - Gateway does not accumulate ETH + /// @dev Gateway should not hold any native ETH after multiple calls + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_Gateway_DoesNotAccumulate() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.002 ETH = $4 (within caps) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + uint256 gatewayBalanceBefore = address(gatewayTemp).balance; + + // Make multiple calls + for (uint256 i = 0; i < 5; i++) { + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + // Gateway balance should remain unchanged + assertEq(address(gatewayTemp).balance, gatewayBalanceBefore, "Gateway should not accumulate ETH"); + } + + /// @notice Test Case 2.2 - Fund split correct distribution + /// @dev Verify gas and funds routes receive correct amounts + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_FundSplit_CorrectDistribution() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + uint256 expectedGasAmount = 0.002 ether; // $4 (within USD caps) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: Total received equals msg.value + assertEq(tss.balance - tssBalanceBefore, msgValue, "Total should equal msg.value"); + } + + /// @notice Test Case 2.2 - Exact amount sends all to funds route + /// @dev When msg.value == amount, all goes to funds (no gas route) + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_ExactAmount_AllToFunds() public { + uint256 msgValue = 5 ether; + uint256 fundsAmount = 5 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: TSS received full amount + assertEq(tss.balance, tssBalanceBefore + msgValue, "TSS should receive full amount"); + } + + // ========================= + // CATEGORY 6: EDGE CASES & BOUNDARY CONDITIONS + // ========================= + + /// @notice Test Case 2.2 - Minimal gas amount at min cap + /// @dev gasAmount = 0.0005 ETH (exactly $1 at $2000/ETH) + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_MinimalGasAmount_AtMinCap() public { + uint256 msgValue = 1.0005 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.0005 ETH = $1 (at min cap) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + assertEq(tss.balance, tssBalanceBefore + msgValue, "Should succeed at min cap"); + } + + /// @notice Test Case 2.2 - Maximal gas amount at max cap + /// @dev gasAmount = 0.005 ETH (exactly $10 at $2000/ETH) + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_MaximalGasAmount_AtMaxCap() public { + uint256 msgValue = 1.005 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.005 ETH = $10 (at max cap) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + assertEq(tss.balance, tssBalanceBefore + msgValue, "Should succeed at max cap"); + } + + /// @notice Test Case 2.2 - Large payload does not affect gas caps + /// @dev Gas USD caps only check amount, not payload size + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_LargePayload_DoesNotAffectGasCaps() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.002 ETH = $4 (within caps) + + // Create large payload (10KB) + bytes memory largeData = new bytes(10000); + for (uint256 i = 0; i < 10000; i++) { + largeData[i] = bytes1(uint8(i % 256)); + } + + UniversalPayload memory largePayload = UniversalPayload({ + to: address(0xABCD), + value: 0, + data: largeData, + gasLimit: 1000000, + maxFeePerGas: 0, + maxPriorityFeePerGas: 0, + nonce: 1, + deadline: 0, + vType: VerificationType.signedVerification + }); + bytes memory encodedPayload = abi.encode(largePayload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + assertEq(tss.balance, tssBalanceBefore + msgValue, "Large payload should not affect gas caps"); + } + + /// @notice Test Case 2.2 - Very large funds within rate limit + /// @dev Should handle large amounts correctly + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_VeryLargeFunds_WithinRateLimit() public { + uint256 fundsAmount = 5000 ether; + uint256 msgValue = fundsAmount + 0.001 ether; // Add minimal gas + + // Give user enough ETH + vm.deal(user1, msgValue); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + assertEq(tss.balance, tssBalanceBefore + msgValue, "Should handle large amounts"); + } + + /// @notice Test Case 2.2 - Multiple calls same block respect gas block cap + /// @dev Cumulative gas amounts checked against block cap + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_MultipleCallsSameBlock_GasBlockCap() public { + // Set block cap to $8 + vm.prank(admin); + gatewayTemp.setBlockUsdCap(8e18); + + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.002 ETH = $4 per call + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(0), + fundsAmount, + encodedPayload + ); + + // First call: $4 gas (within $8 cap) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Second call: $4 gas (cumulative $8, at cap) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Third call: $4 gas (cumulative $12, exceeds $8 cap) + vm.expectRevert(Errors.BlockCapLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.2 - Different recipients work correctly + /// @dev Both zero and non-zero recipients should work + function test_Case2_2_FUNDS_AND_PAYLOAD_Native_DifferentRecipients_Work() public { + uint256 msgValue = 1.002 ether; + uint256 fundsAmount = 1 ether; + // gasAmount = 0.002 ETH = $4 (within caps) + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + // Test with zero recipient + UniversalTxRequest memory req1 = buildUniversalTxRequest( + address(0), // Zero recipient + address(0), + fundsAmount, + encodedPayload + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req1); + + // Test with zero recipient (non-zero not allowed for FUNDS_AND_PAYLOAD) + UniversalTxRequest memory req2 = buildUniversalTxRequest( + address(0), // Zero recipient (required) + address(0), + fundsAmount, + encodedPayload + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req2); + + // Both should succeed + } +} diff --git a/contracts/evm-gateway/test/gateway/8_GatewayPC.t.sol b/contracts/evm-gateway/test/gateway/8_GatewayPC.t.sol deleted file mode 100644 index a3e4026..0000000 --- a/contracts/evm-gateway/test/gateway/8_GatewayPC.t.sol +++ /dev/null @@ -1,1133 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import { Test } from "forge-std/Test.sol"; -import { Vm } from "forge-std/Vm.sol"; -import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import { ProxyAdmin } from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; - -import { UniversalGatewayPC } from "../../src/UniversalGatewayPC.sol"; -import { IUniversalGatewayPC } from "../../src/interfaces/IUniversalGatewayPC.sol"; -import { RevertInstructions } from "../../src/libraries/Types.sol"; -import { Errors } from "../../src/libraries/Errors.sol"; -import { MockPRC20 } from "../mocks/MockPRC20.sol"; -import { MockUniversalCoreReal } from "../mocks/MockUniversalCoreReal.sol"; -import { MockReentrantContract } from "../mocks/MockReentrantContract.sol"; - -/** - * @title UniversalGatewayPCTest - * @notice Comprehensive test suite for UniversalGatewayPC contract - * @dev Tests initialization, admin functions, and user withdrawal flows - */ -contract UniversalGatewayPCTest is Test { - // ========================= - // ACTORS - // ========================= - address public admin; - address public pauser; - address public user1; - address public user2; - address public attacker; - address public uem; - address public vaultPC; - - // ========================= - // CONTRACTS - // ========================= - UniversalGatewayPC public gateway; - TransparentUpgradeableProxy public gatewayProxy; - ProxyAdmin public proxyAdmin; - - // ========================= - // MOCKS - // ========================= - MockUniversalCoreReal public universalCore; - MockPRC20 public prc20Token; - MockPRC20 public gasToken; - - // ========================= - // TEST CONSTANTS - // ========================= - uint256 public constant LARGE_AMOUNT = 1000000 * 1e18; - uint256 public constant DEFAULT_GAS_LIMIT = 500_000; // Matches UniversalCore.BASE_GAS_LIMIT - uint256 public constant DEFAULT_PROTOCOL_FEE = 0.01 ether; - uint256 public constant DEFAULT_GAS_PRICE = 20 gwei; - string public constant SOURCE_CHAIN_ID = "1"; // Ethereum mainnet - string public constant SOURCE_TOKEN_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC - - // ========================= - // SETUP - // ========================= - function setUp() public { - _createActors(); - _deployMocks(); - _deployGateway(); - _initializeGateway(); - _setupTokens(); - } - - // ========================= - // HELPER FUNCTIONS - // ========================= - function buildRevertInstructions(address fundRecipient) internal pure returns (RevertInstructions memory) { - return RevertInstructions({ fundRecipient: fundRecipient, revertContext: bytes("") }); - } - - function buildRevertInstructionsWithMsg(address fundRecipient, string memory revertContext) - internal - pure - returns (RevertInstructions memory) - { - return RevertInstructions({ fundRecipient: fundRecipient, revertContext: bytes(revertContext) }); - } - - function calculateExpectedGasFee(uint256 gasLimit) internal view returns (uint256) { - return DEFAULT_GAS_PRICE * gasLimit + DEFAULT_PROTOCOL_FEE; - } - - - function testInitializeSuccess() public { - // Deploy new gateway for testing initialization - UniversalGatewayPC newImplementation = new UniversalGatewayPC(); - ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); - - bytes memory initData = abi.encodeWithSelector( - UniversalGatewayPC.initialize.selector, - admin, - pauser, - address(universalCore), - vaultPC - ); - - TransparentUpgradeableProxy newProxy = new TransparentUpgradeableProxy( - address(newImplementation), - address(newProxyAdmin), - initData - ); - - UniversalGatewayPC newGateway = UniversalGatewayPC(address(newProxy)); - - // Verify initialization - assertEq(newGateway.UNIVERSAL_CORE(), address(universalCore)); - assertEq(address(newGateway.VAULT_PC()), vaultPC); - assertTrue(newGateway.hasRole(newGateway.DEFAULT_ADMIN_ROLE(), admin)); - assertTrue(newGateway.hasRole(newGateway.PAUSER_ROLE(), pauser)); - } - - function testInitializeRevertZeroAdmin() public { - UniversalGatewayPC newImplementation = new UniversalGatewayPC(); - ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); - - bytes memory initData = abi.encodeWithSelector( - UniversalGatewayPC.initialize.selector, - address(0), // zero admin - pauser, - address(universalCore), - vaultPC - ); - - vm.expectRevert(); // Proxy wraps the error - new TransparentUpgradeableProxy(address(newImplementation), address(newProxyAdmin), initData); - } - - function testInitializeRevertZeroPauser() public { - UniversalGatewayPC newImplementation = new UniversalGatewayPC(); - ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); - - bytes memory initData = abi.encodeWithSelector( - UniversalGatewayPC.initialize.selector, - admin, - address(0), // zero pauser - address(universalCore), - vaultPC - ); - - vm.expectRevert(); // Proxy wraps the error - new TransparentUpgradeableProxy(address(newImplementation), address(newProxyAdmin), initData); - } - - function testInitializeRevertZeroUniversalCore() public { - UniversalGatewayPC newImplementation = new UniversalGatewayPC(); - ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); - - bytes memory initData = abi.encodeWithSelector( - UniversalGatewayPC.initialize.selector, - admin, - pauser, - address(0), // zero universal core - vaultPC - ); - - vm.expectRevert(); // Proxy wraps the error - new TransparentUpgradeableProxy(address(newImplementation), address(newProxyAdmin), initData); - } - - function testInitializeRevertZeroVaultPC() public { - UniversalGatewayPC newImplementation = new UniversalGatewayPC(); - ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); - - bytes memory initData = abi.encodeWithSelector( - UniversalGatewayPC.initialize.selector, - admin, - pauser, - address(universalCore), - address(0) // zero vaultPC - ); - - vm.expectRevert(); // Proxy wraps the error - new TransparentUpgradeableProxy(address(newImplementation), address(newProxyAdmin), initData); - } - - function testInitializeRevertDoubleInit() public { - UniversalGatewayPC newImplementation = new UniversalGatewayPC(); - ProxyAdmin newProxyAdmin = new ProxyAdmin(admin); - - bytes memory initData = abi.encodeWithSelector( - UniversalGatewayPC.initialize.selector, - admin, - pauser, - address(universalCore), - vaultPC - ); - - TransparentUpgradeableProxy newProxy = new TransparentUpgradeableProxy( - address(newImplementation), - address(newProxyAdmin), - initData - ); - - UniversalGatewayPC newGateway = UniversalGatewayPC(address(newProxy)); - - // Try to initialize again - vm.expectRevert(); - newGateway.initialize(admin, pauser, address(universalCore), vaultPC); - } - - // ========================= - // ADMIN FUNCTION TESTS - // ========================= - - function testSetVaultPCSuccess() public { - address newVaultPC = address(0x999); - - // Admin sets new VaultPC - vm.prank(admin); - vm.expectEmit(true, true, false, false); - emit IUniversalGatewayPC.VaultPCUpdated(vaultPC, newVaultPC); - gateway.setVaultPC(newVaultPC); - - // Verify state changes - assertEq(address(gateway.VAULT_PC()), newVaultPC); - } - - function testSetVaultPCRevertNonAdmin() public { - address newVaultPC = address(0x999); - - vm.prank(attacker); - vm.expectRevert(); - gateway.setVaultPC(newVaultPC); - } - - function testSetVaultPCRevertZeroAddress() public { - vm.prank(admin); - vm.expectRevert(Errors.ZeroAddress.selector); - gateway.setVaultPC(address(0)); - } - - function testSetVaultPCRevertWhenPaused() public { - // Pause the gateway first - vm.prank(pauser); - gateway.pause(); - - address newVaultPC = address(0x999); - - // Attempt to set VaultPC while paused should revert - vm.prank(admin); - vm.expectRevert(); - gateway.setVaultPC(newVaultPC); - } - - - function testPauseSuccess() public { - assertFalse(gateway.paused()); - - vm.prank(pauser); - gateway.pause(); - - assertTrue(gateway.paused()); - } - - function testPauseRevertNonPauser() public { - vm.prank(attacker); - vm.expectRevert(); - gateway.pause(); - } - - function testPauseRevertAlreadyPaused() public { - // Pause the contract - vm.prank(pauser); - gateway.pause(); - - // Try to pause again - vm.prank(pauser); - vm.expectRevert(); - gateway.pause(); - } - - function testUnpauseSuccess() public { - // Pause the contract first - vm.prank(pauser); - gateway.pause(); - assertTrue(gateway.paused()); - - // Pauser unpauses the contract - vm.prank(pauser); - gateway.unpause(); - - // Verify contract is unpaused - assertFalse(gateway.paused()); - } - - function testUnpauseRevertNonPauser() public { - // Pause the contract first - vm.prank(pauser); - gateway.pause(); - - // Non-pauser tries to unpause - vm.prank(attacker); - vm.expectRevert(); - gateway.unpause(); - } - - function testUnpauseRevertNotPaused() public { - // Contract is not paused initially - assertFalse(gateway.paused()); - - // Try to unpause - vm.prank(pauser); - vm.expectRevert(); - gateway.unpause(); - } - - function testAdminFunctionsWorkWhenPaused() public { - // Pause the contract - vm.prank(pauser); - gateway.pause(); - - // Verify that the contract is paused - assertTrue(gateway.paused()); - - // Unpause should still work - vm.prank(pauser); - gateway.unpause(); - - assertFalse(gateway.paused()); - } - - // ========================= - // WITHDRAW FUNCTION TESTS - // ========================= - - function testWithdrawSuccessWithCustomGasLimit() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = 150_000; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - // Ensure user has enough balance - uint256 userBalance = prc20Token.balanceOf(user1); - if (userBalance < amount) { - prc20Token.mint(user1, amount); - vm.prank(user1); - prc20Token.approve(address(gateway), amount); - } - - uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); - uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); - uint256 initialPrc20Balance = prc20Token.balanceOf(user1); - - vm.prank(user1); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - - // Verify token balances - assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); - assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); - } - - function testWithdrawEventEmission() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = 150_000; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.recordLogs(); - vm.prank(user1); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertTrue(logs.length >= 1, "At least one event should be emitted"); - } - - function testWithdrawSuccessWithDefaultGasLimit() public { - uint256 amount = 1000 * 1e6; // 1000 USDC (6 decimals) - uint256 gasLimit = 0; // Use default gas limit - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - uint256 expectedGasFee = calculateExpectedGasFee(DEFAULT_GAS_LIMIT); - uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); - uint256 initialPrc20Balance = prc20Token.balanceOf(user1); - - vm.prank(user1); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - - // Verify token balances - assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); - assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); - } - - function testWithdrawRevertEmptyTarget() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = bytes(""); // Empty target - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.prank(user1); - vm.expectRevert(Errors.InvalidInput.selector); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - } - - function testWithdrawRevertZeroToken() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.prank(user1); - vm.expectRevert(Errors.ZeroAddress.selector); - gateway.withdraw(to, address(0), amount, gasLimit, revertCfg); - } - - function testWithdrawRevertZeroAmount() public { - uint256 amount = 0; // Zero amount - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.prank(user1); - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - } - - function testWithdrawRevertInvalidRecipient() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(address(0)); // Zero recipient - - vm.prank(user1); - vm.expectRevert(Errors.InvalidRecipient.selector); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - } - - function testWithdrawRevertWhenPaused() public { - vm.prank(pauser); - gateway.pause(); - - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.prank(user1); - vm.expectRevert(); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - } - - function testWithdrawRevertInsufficientGasTokenBalance() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - // Calculate required gas fee - uint256 requiredGasFee = calculateExpectedGasFee(gasLimit); - - // Set user1's gas token balance to less than required - uint256 currentBalance = gasToken.balanceOf(user1); - vm.prank(user1); - gasToken.transfer(address(0xdead), currentBalance); - - // Give user1 insufficient gas tokens (less than required fee) - if (requiredGasFee > 1) { - gasToken.mint(user1, requiredGasFee - 1); - vm.prank(user1); - gasToken.approve(address(gateway), type(uint256).max); - } - - vm.prank(user1); - vm.expectRevert(); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - } - - function testWithdrawRevertInsufficientGasTokenAllowance() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - // Remove gas token allowance - vm.prank(user1); - gasToken.approve(address(gateway), 0); - - vm.prank(user1); - vm.expectRevert("MockPRC20: insufficient allowance"); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - } - - function testWithdrawRevertInsufficientPrc20Balance() public { - uint256 amount = LARGE_AMOUNT + 1; // More than user has - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.prank(user1); - vm.expectRevert("MockPRC20: insufficient balance"); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - } - - // ========================= - // WITHDRAW AND EXECUTE FUNCTION TESTS - // ========================= - - function testWithdrawAndExecuteSuccessWithCustomGasLimit() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = 200_000; - bytes memory target = abi.encodePacked(user2); - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); - uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); - uint256 initialPrc20Balance = prc20Token.balanceOf(user1); - - vm.prank(user1); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - - // Verify token balances - assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); - assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); - } - - function testWithdrawAndExecuteEventEmission() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = 200_000; - bytes memory target = abi.encodePacked(user2); - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.recordLogs(); - vm.prank(user1); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertTrue(logs.length >= 1, "At least one event should be emitted"); - } - - function testWithdrawAndExecuteSuccessWithDefaultGasLimit() public { - uint256 amount = 1000 * 1e6; // 1000 USDC (6 decimals) - uint256 gasLimit = 0; // Use default gas limit - bytes memory target = abi.encodePacked(user2); - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - uint256 expectedGasFee = calculateExpectedGasFee(DEFAULT_GAS_LIMIT); - uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); - uint256 initialPrc20Balance = prc20Token.balanceOf(user1); - - vm.prank(user1); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - - // Verify token balances - assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); - assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); - } - - function testWithdrawAndExecuteSuccessWithEmptyPayload() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = abi.encodePacked(user2); - bytes memory payload = bytes(""); // Empty payload - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); - uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); - uint256 initialPrc20Balance = prc20Token.balanceOf(user1); - - vm.prank(user1); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - - // Verify token balances - assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); - assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); - } - - function testWithdrawAndExecuteSuccessWithComplexPayload() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = abi.encodePacked(user2); - - // Complex payload with multiple parameters - bytes memory payload = abi.encodeWithSignature( - "complexFunction(address,uint256,bytes32,string)", - user2, - 1000, - keccak256("test"), - "complex string parameter" - ); - - RevertInstructions memory revertCfg = buildRevertInstructionsWithMsg(user2, "Complex operation failed"); - - uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); - uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); - uint256 initialPrc20Balance = prc20Token.balanceOf(user1); - - vm.prank(user1); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - - // Verify token balances - assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); - assertEq(prc20Token.balanceOf(user1), initialPrc20Balance - amount); - } - - function testWithdrawAndExecuteRevertEmptyTarget() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = bytes(""); // Empty target - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.prank(user1); - vm.expectRevert(Errors.InvalidInput.selector); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - } - - function testWithdrawAndExecuteRevertZeroToken() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = abi.encodePacked(user2); - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.prank(user1); - vm.expectRevert(Errors.ZeroAddress.selector); - gateway.withdrawAndExecute(target, address(0), amount, payload, gasLimit, revertCfg); - } - - function testWithdrawAndExecuteRevertZeroAmount() public { - uint256 amount = 0; // Zero amount - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = abi.encodePacked(user2); - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.prank(user1); - vm.expectRevert(Errors.InvalidAmount.selector); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - } - - function testWithdrawAndExecuteRevertInvalidRecipient() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = abi.encodePacked(user2); - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(address(0)); // Zero recipient - - vm.prank(user1); - vm.expectRevert(Errors.InvalidRecipient.selector); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - } - - function testWithdrawAndExecuteRevertWhenPaused() public { - vm.prank(pauser); - gateway.pause(); - - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = abi.encodePacked(user2); - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.prank(user1); - vm.expectRevert(); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - } - - function testWithdrawAndExecuteRevertInsufficientGasTokenBalance() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = abi.encodePacked(user2); - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - // Calculate required gas fee - uint256 requiredGasFee = calculateExpectedGasFee(gasLimit); - - // Set user1's gas token balance to less than required - uint256 currentBalance = gasToken.balanceOf(user1); - vm.prank(user1); - gasToken.transfer(address(0xdead), currentBalance); - - // Give user1 insufficient gas tokens (less than required fee) - if (requiredGasFee > 1) { - gasToken.mint(user1, requiredGasFee - 1); - vm.prank(user1); - gasToken.approve(address(gateway), type(uint256).max); - } - - vm.prank(user1); - vm.expectRevert(); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - } - - function testWithdrawAndExecuteRevertInsufficientGasTokenAllowance() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = abi.encodePacked(user2); - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - // Remove gas token allowance - vm.prank(user1); - gasToken.approve(address(gateway), 0); - - vm.prank(user1); - vm.expectRevert("MockPRC20: insufficient allowance"); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - } - - function testWithdrawAndExecuteRevertInsufficientPrc20Balance() public { - uint256 amount = LARGE_AMOUNT + 1; // More than user has - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = abi.encodePacked(user2); - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - vm.prank(user1); - vm.expectRevert("MockPRC20: insufficient balance"); - gateway.withdrawAndExecute(target, address(prc20Token), amount, payload, gasLimit, revertCfg); - } - - function testWithdrawAndExecuteDifferentPayloadSizes() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - uint256 initialBalance = prc20Token.balanceOf(user1); - - // Test with small payload - bytes memory smallPayload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - - vm.prank(user1); - gateway.withdrawAndExecute(target, address(prc20Token), amount, smallPayload, gasLimit, revertCfg); - - // Reset balances for next test - prc20Token.mint(user1, amount); - vm.prank(user1); - prc20Token.approve(address(gateway), amount); - - // Test with large payload - bytes memory largePayload = abi.encodeWithSignature( - "largeFunction(address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)", - user2, 1, 2, 3, 4, 5, 6, 7, 8, 9 - ); - - vm.prank(user1); - gateway.withdrawAndExecute(target, address(prc20Token), amount, largePayload, gasLimit, revertCfg); - - // Both should succeed - verify final balance - uint256 finalBalance = prc20Token.balanceOf(user1); - assertEq(finalBalance, initialBalance - amount); - } - - // ========================= - // EDGE CASES & ADDITIONAL TESTS - // ========================= - - function testReentrancyProtection() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - // Create a contract that calls the gateway - MockReentrantContract reentrantContract = new MockReentrantContract( - address(gateway), - address(prc20Token), - address(gasToken) - ); - - // Fund the contract - prc20Token.mint(address(reentrantContract), amount); - gasToken.mint(address(reentrantContract), LARGE_AMOUNT); - - vm.prank(address(reentrantContract)); - prc20Token.approve(address(gateway), amount); - vm.prank(address(reentrantContract)); - gasToken.approve(address(gateway), type(uint256).max); - - // Call should succeed (reentrancy protection is for preventing recursive calls during execution) - vm.prank(address(reentrantContract)); - reentrantContract.attemptReentrancy(to, amount, gasLimit, revertCfg); - - // Verify the withdrawal succeeded - assertEq(prc20Token.balanceOf(address(reentrantContract)), 0); - } - - function testReentrancyProtectionWithExecute() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory target = abi.encodePacked(user2); - bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", user2, 100); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - // Create a contract that calls the gateway - MockReentrantContract reentrantContract = new MockReentrantContract( - address(gateway), - address(prc20Token), - address(gasToken) - ); - - // Fund the contract - prc20Token.mint(address(reentrantContract), amount); - gasToken.mint(address(reentrantContract), LARGE_AMOUNT); - - vm.prank(address(reentrantContract)); - prc20Token.approve(address(gateway), amount); - vm.prank(address(reentrantContract)); - gasToken.approve(address(gateway), type(uint256).max); - - // Call should succeed (reentrancy protection is for preventing recursive calls during execution) - vm.prank(address(reentrantContract)); - reentrantContract.attemptReentrancyWithExecute(target, amount, payload, gasLimit, revertCfg); - - // Verify the withdrawal succeeded - assertEq(prc20Token.balanceOf(address(reentrantContract)), 0); - } - - function testMaxGasLimit() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = 1_000_000; // Large gas limit - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); - uint256 initialGasTokenBalance = gasToken.balanceOf(vaultPC); - - // Ensure user has enough gas tokens for the fee - uint256 userGasBalance = gasToken.balanceOf(user1); - if (userGasBalance < expectedGasFee) { - gasToken.mint(user1, expectedGasFee - userGasBalance + 1 ether); - vm.prank(user1); - gasToken.approve(address(gateway), type(uint256).max); - } - - vm.prank(user1); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - - // Verify withdrawal with max gas limit succeeded - assertEq(gasToken.balanceOf(vaultPC), initialGasTokenBalance + expectedGasFee); - } - - function testGasFeeCalculationAccuracy() public { - uint256 amount = 1000 * 1e6; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - // Test specific gas limits individually - _testGasFeeForLimit(amount, to, revertCfg, 50_000); - _testGasFeeForLimit(amount, to, revertCfg, 100_000); - _testGasFeeForLimit(amount, to, revertCfg, 200_000); - _testGasFeeForLimit(amount, to, revertCfg, 500_000); - _testGasFeeForLimit(amount, to, revertCfg, 1_000_000); - } - - function _testGasFeeForLimit(uint256 amount, bytes memory to, RevertInstructions memory revertCfg, uint256 gasLimit) internal { - uint256 expectedGasFee = calculateExpectedGasFee(gasLimit); - uint256 balanceBefore = gasToken.balanceOf(vaultPC); - - vm.prank(user1); - gateway.withdraw(to, address(prc20Token), amount, gasLimit, revertCfg); - - uint256 balanceAfter = gasToken.balanceOf(vaultPC); - assertEq(balanceAfter - balanceBefore, expectedGasFee); - - // Reset for next iteration - prc20Token.mint(user1, amount); - vm.prank(user1); - prc20Token.approve(address(gateway), amount); - } - - function testSetVaultPCToZeroReverts() public { - // Attempt to set VaultPC to zero should revert - vm.prank(admin); - vm.expectRevert(Errors.ZeroAddress.selector); - gateway.setVaultPC(address(0)); - } - - function testInvalidFeeQuoteZeroGasToken() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - // Create token with unconfigured chain ID (no gas token set for this chain) - string memory unconfiguredChainId = "999"; // Chain ID not configured in universalCore - MockPRC20 invalidToken = new MockPRC20( - "Invalid Token", - "INV", - 6, - unconfiguredChainId, - MockPRC20.TokenType.ERC20, - DEFAULT_PROTOCOL_FEE, - address(universalCore), - SOURCE_TOKEN_ADDRESS - ); - - // Setup token for user1 - invalidToken.mint(user1, amount); - vm.prank(user1); - invalidToken.approve(address(gateway), amount); - - // Withdrawal should fail with "MockUniversalCore: zero gas token" error - vm.prank(user1); - vm.expectRevert("MockUniversalCore: zero gas token"); - gateway.withdraw(to, address(invalidToken), amount, gasLimit, revertCfg); - } - - function _createInvalidToken() internal returns (MockPRC20) { - return new MockPRC20( - "Invalid Token", - "INV", - 6, - SOURCE_CHAIN_ID, - MockPRC20.TokenType.ERC20, - DEFAULT_PROTOCOL_FEE, - address(universalCore), - SOURCE_TOKEN_ADDRESS - ); - } - - function _createInvalidCoreWithZeroGasToken() internal returns (MockUniversalCoreReal) { - MockUniversalCoreReal invalidCore = new MockUniversalCoreReal(uem); - vm.prank(uem); - invalidCore.setGasPrice(SOURCE_CHAIN_ID, DEFAULT_GAS_PRICE); - vm.prank(uem); - invalidCore.setGasTokenPRC20(SOURCE_CHAIN_ID, address(0)); // Zero gas token - return invalidCore; - } - - function testInvalidFeeQuoteZeroGasFee() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - // Create token with a chain ID that has gas token but no gas price configured - string memory chainWithTokenNoPrice = "777"; - - // Configure this chain in universalCore with gas token but NO gas price - vm.prank(uem); - universalCore.setGasTokenPRC20(chainWithTokenNoPrice, address(gasToken)); - // Intentionally NOT setting gas price for this chain - - MockPRC20 invalidToken = new MockPRC20( - "Invalid Token", - "INV", - 6, - chainWithTokenNoPrice, - MockPRC20.TokenType.ERC20, - DEFAULT_PROTOCOL_FEE, - address(universalCore), - SOURCE_TOKEN_ADDRESS - ); - - // Setup token for user1 - invalidToken.mint(user1, amount); - vm.prank(user1); - invalidToken.approve(address(gateway), amount); - - // Withdrawal should fail with "MockUniversalCore: zero gas price" error - vm.prank(user1); - vm.expectRevert("MockUniversalCore: zero gas price"); - gateway.withdraw(to, address(invalidToken), amount, gasLimit, revertCfg); - } - - function _createInvalidCoreWithZeroGasPrice() internal returns (MockUniversalCoreReal) { - MockUniversalCoreReal invalidCore = new MockUniversalCoreReal(uem); - vm.prank(uem); - invalidCore.setGasPrice(SOURCE_CHAIN_ID, 0); // Zero gas price - vm.prank(uem); - invalidCore.setGasTokenPRC20(SOURCE_CHAIN_ID, address(gasToken)); - return invalidCore; - } - - function testTokenBurnFailure() public { - uint256 amount = 1000 * 1e6; - uint256 gasLimit = DEFAULT_GAS_LIMIT; - bytes memory to = abi.encodePacked(user2); - RevertInstructions memory revertCfg = buildRevertInstructions(user2); - - // Create failing token - MockPRC20 failingToken = _createFailingToken(); - - // Setup token for user1 - failingToken.mint(user1, amount); - vm.prank(user1); - failingToken.approve(address(gateway), amount); - - // Mock the burn function to fail by setting balance to 0 - failingToken.setBalance(user1, 0); - - // Withdrawal should fail with transfer failure - vm.prank(user1); - vm.expectRevert("MockPRC20: insufficient balance"); - gateway.withdraw(to, address(failingToken), amount, gasLimit, revertCfg); - } - - function _createFailingToken() internal returns (MockPRC20) { - return new MockPRC20( - "Failing Token", - "FAIL", - 6, - SOURCE_CHAIN_ID, - MockPRC20.TokenType.ERC20, - DEFAULT_PROTOCOL_FEE, - address(universalCore), - SOURCE_TOKEN_ADDRESS - ); - } - - // ========================= - // INTERNAL FUNCTIONS - // ========================= - - function _createActors() internal { - admin = address(0x1); - pauser = address(0x2); - user1 = address(0x3); - user2 = address(0x4); - attacker = address(0x5); - uem = address(0x6); - vaultPC = address(0x7); - - vm.label(admin, "admin"); - vm.label(pauser, "pauser"); - vm.label(user1, "user1"); - vm.label(user2, "user2"); - vm.label(attacker, "attacker"); - vm.label(uem, "uem"); - vm.label(vaultPC, "vaultPC"); - - vm.deal(admin, 100 ether); - vm.deal(pauser, 100 ether); - vm.deal(user1, 1000 ether); - vm.deal(user2, 1000 ether); - vm.deal(attacker, 1000 ether); - } - - function _deployMocks() internal { - // Deploy UniversalCore mock - universalCore = new MockUniversalCoreReal(uem); - - // Deploy gas token (PC native token) - gasToken = new MockPRC20( - "Push Chain Native", - "PC", - 18, - SOURCE_CHAIN_ID, - MockPRC20.TokenType.PC, - DEFAULT_PROTOCOL_FEE, - address(universalCore), - "" - ); - - // Deploy PRC20 token (wrapped USDC) - prc20Token = new MockPRC20( - "USDC on Push Chain", - "USDC", - 6, - SOURCE_CHAIN_ID, - MockPRC20.TokenType.ERC20, - DEFAULT_PROTOCOL_FEE, - address(universalCore), - SOURCE_TOKEN_ADDRESS - ); - - // Configure UniversalCore with gas settings - vm.prank(uem); - universalCore.setGasPrice(SOURCE_CHAIN_ID, DEFAULT_GAS_PRICE); - vm.prank(uem); - universalCore.setGasTokenPRC20(SOURCE_CHAIN_ID, address(gasToken)); - - vm.label(address(universalCore), "UniversalCore"); - vm.label(address(prc20Token), "PRC20Token"); - vm.label(address(gasToken), "GasToken"); - } - - function _deployGateway() internal { - // Deploy implementation - UniversalGatewayPC implementation = new UniversalGatewayPC(); - - // Deploy proxy admin - proxyAdmin = new ProxyAdmin(admin); - - // Deploy transparent upgradeable proxy - bytes memory initData = abi.encodeWithSelector( - UniversalGatewayPC.initialize.selector, - admin, - pauser, - address(universalCore), - vaultPC - ); - - gatewayProxy = new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); - - // Cast proxy to gateway interface - gateway = UniversalGatewayPC(address(gatewayProxy)); - - vm.label(address(gateway), "UniversalGatewayPC"); - vm.label(address(gatewayProxy), "GatewayProxy"); - vm.label(address(proxyAdmin), "ProxyAdmin"); - } - - function _initializeGateway() internal view { - // Gateway is already initialized via proxy constructor - // Verify initialization - assertEq(gateway.UNIVERSAL_CORE(), address(universalCore)); - assertEq(address(gateway.VAULT_PC()), vaultPC); - assertTrue(gateway.hasRole(gateway.DEFAULT_ADMIN_ROLE(), admin)); - assertTrue(gateway.hasRole(gateway.PAUSER_ROLE(), pauser)); - } - - function _setupTokens() internal { - // Mint tokens to users - prc20Token.mint(user1, LARGE_AMOUNT); - prc20Token.mint(user2, LARGE_AMOUNT); - gasToken.mint(user1, LARGE_AMOUNT); - gasToken.mint(user2, LARGE_AMOUNT); - - // Approve gateway to spend tokens - vm.prank(user1); - prc20Token.approve(address(gateway), type(uint256).max); - vm.prank(user1); - gasToken.approve(address(gateway), type(uint256).max); - - vm.prank(user2); - prc20Token.approve(address(gateway), type(uint256).max); - vm.prank(user2); - gasToken.approve(address(gateway), type(uint256).max); - } -} \ No newline at end of file diff --git a/contracts/evm-gateway/test/gateway/8_sendUniversalTxWithFUNDS_FundsTxType_Case2_3.t.sol b/contracts/evm-gateway/test/gateway/8_sendUniversalTxWithFUNDS_FundsTxType_Case2_3.t.sol new file mode 100644 index 0000000..b6d9469 --- /dev/null +++ b/contracts/evm-gateway/test/gateway/8_sendUniversalTxWithFUNDS_FundsTxType_Case2_3.t.sol @@ -0,0 +1,1379 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BaseTest } from "../BaseTest.t.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { + TX_TYPE, + RevertInstructions, + UniversalPayload, + UniversalTxRequest, + VerificationType +} from "../../src/libraries/Types.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { MockERC20 } from "../mocks/MockERC20.sol"; + +/** + * @title GatewaySendUniversalTxWithFunds_PAYLOAD_Case2_3 Test Suite + * @notice Comprehensive tests for FUNDS_AND_PAYLOAD route (standard route) via sendUniversalTx + * @dev Tests FUNDS_AND_PAYLOAD transaction type - Case 2.3: ERC20 + Native Batching + * All paths are exercised through sendUniversalTx() which internally routes to both instant and standard routes. + * + * Phase 4 - TX_TYPE.FUNDS_AND_PAYLOAD - Case 2.3 (ERC20 batching, msg.value > 0, token != native) + * + * Case 2.3: Batching of Gas + Funds_and_Payload (msg.value > 0, token != native) + * - User refills UEA's gas (native ETH) AND bridges ERC20 token in one transaction + * - No Split Logic: gasAmount = msg.value (entire msg.value is gas) + * - Dual Token: Native for gas, ERC20 for funds + * - Dual Destination: Native to TSS, ERC20 to Vault + * - Dual Execution: + * 1. Gas route ALWAYS triggered with full msg.value (instant route with USD caps) + * 2. ERC20 rate limit consumed for _req.amount + * 3. ERC20 transferred to vault + * 4. Native ETH forwarded to TSS + */ +contract GatewaySendUniversalTxWithFunds_PAYLOAD_Case2_3_Test is BaseTest { + // UniversalGateway instance + UniversalGateway public gatewayTemp; + + // ========================= + // EVENTS + // ========================= + event UniversalTx( // Placeholder value - ignored by matrix inference but required for struct + address indexed sender, + address indexed recipient, + address token, + uint256 amount, + bytes payload, + address revertRecipient, + TX_TYPE txType, + bytes signatureData + ); + + // ========================= + // SETUP + // ========================= + function setUp() public override { + super.setUp(); + + // Deploy UniversalGateway + _deployGatewayTemp(); + + // Wire oracle to the new gateway instance + vm.prank(admin); + gatewayTemp.setEthUsdFeed(address(ethUsdFeedMock)); + + // Setup token support on gatewayTemp (native + all mock ERC20s) + address[] memory tokens = new address[](4); + uint256[] memory thresholds = new uint256[](4); + tokens[0] = address(0); // Native token + tokens[1] = address(tokenA); // Mock ERC20 tokenA + tokens[2] = address(usdc); // Mock ERC20 usdc + tokens[3] = address(weth); // Mock WETH + thresholds[0] = 1000000 ether; // Large threshold for native + thresholds[1] = 1000000 ether; // Large threshold for tokenA + thresholds[2] = 1000000e6; // Large threshold for usdc (6 decimals) + thresholds[3] = 1000000 ether; // Large threshold for weth + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + // Re-approve tokens to gatewayTemp + address[] memory users = new address[](5); + users[0] = user1; + users[1] = user2; + users[2] = user3; + users[3] = user4; + users[4] = attacker; + + for (uint256 i = 0; i < users.length; i++) { + vm.prank(users[i]); + tokenA.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + usdc.approve(address(gatewayTemp), type(uint256).max); + + vm.prank(users[i]); + weth.approve(address(gatewayTemp), type(uint256).max); + } + } + + /// @notice Deploy UniversalGateway + function _deployGatewayTemp() internal { + UniversalGateway implementation = new UniversalGateway(); + + bytes memory initData = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, + tss, + address(this), + MIN_CAP_USD, + MAX_CAP_USD, + uniV3Factory, + uniV3Router, + address(weth) + ); + + TransparentUpgradeableProxy tempProxy = + new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); + + gatewayTemp = UniversalGateway(payable(address(tempProxy))); + vm.label(address(gatewayTemp), "UniversalGateway"); + } + + /// @notice Helper to build UniversalTxRequest structs + function buildUniversalTxRequest(address recipient_, address token, uint256 amount, bytes memory payload) + internal + pure + returns (UniversalTxRequest memory) + { + return UniversalTxRequest({ + recipient: recipient_, + token: token, + amount: amount, + payload: payload, + revertRecipient: address(0x456), + signatureData: bytes("") + }); + } + + // ========================================================================= + // PHASE 4: TX_TYPE.FUNDS_AND_PAYLOAD - CASE 2.3 (ERC20 + NATIVE BATCHING) + // ========================================================================= + + // ========================= + // CATEGORY 1: HAPPY PATH & CORE FUNCTIONALITY + // ========================= + + /// @notice Test Case 2.3 - ERC20 + Native batching happy path + /// @dev Verifies: + /// - Full msg.value goes to gas route (no split) + /// - ERC20 transferred to vault + /// - Native ETH goes to TSS + /// - Two events emitted (gas + funds) + /// - ERC20 rate limit consumed + /// - Native rate limit NOT consumed + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_Batching_HappyPath() public { + uint256 msgValue = 0.002 ether; // $4 for gas + uint256 erc20Amount = 100 ether; // 100 tokenA + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), // ERC20 token + erc20Amount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + (uint256 erc20UsedBefore,) = gatewayTemp.currentTokenUsage(address(tokenA)); + (uint256 nativeUsedBefore,) = gatewayTemp.currentTokenUsage(address(0)); + + // Expect two events: Gas event + Funds event + // Event 1: Gas event (full msg.value) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), // Gas always credits UEA + token: address(0), // Native token for gas + amount: msgValue, + payload: bytes(""), // Gas event has empty payload + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + // Event 2: Funds event (ERC20 amount) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), // ERC20 token for funds + amount: erc20Amount, + payload: encodedPayload, // Funds event has full payload + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + assertEq(tss.balance, tssBalanceBefore + msgValue, "TSS should receive native ETH"); + + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + erc20Amount, "Vault should receive ERC20"); + + (uint256 erc20UsedAfter,) = gatewayTemp.currentTokenUsage(address(tokenA)); + assertEq(erc20UsedAfter, erc20UsedBefore + erc20Amount, "ERC20 rate limit should be consumed"); + + (uint256 nativeUsedAfter,) = gatewayTemp.currentTokenUsage(address(0)); + assertEq(nativeUsedAfter, nativeUsedBefore, "Native rate limit should NOT be consumed"); + } + + /// @notice Test Case 2.3 - Payload preserved in funds event + /// @dev Gas event has empty payload, funds event has full payload + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_PayloadPreserved() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + + // Custom payload + UniversalPayload memory customPayload = UniversalPayload({ + to: address(0xABCD), + value: 0, + data: abi.encodeWithSignature("customFunction(uint256)", 12345), + gasLimit: 500000, + maxFeePerGas: 0, + maxPriorityFeePerGas: 0, + nonce: 42, + deadline: 0, + vType: VerificationType.signedVerification + }); + bytes memory encodedPayload = abi.encode(customPayload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + // Gas event: empty payload + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), + token: address(0), + amount: msgValue, + payload: bytes(""), // Empty for gas event + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + // Funds event: full payload + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), + amount: erc20Amount, + payload: encodedPayload, // Full payload preserved + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Small gas with large ERC20 funds + /// @dev Verify independent amounts work correctly (opposite asymmetry) + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_SmallGasLargeFunds() public { + uint256 msgValue = 0.0005 ether; // $1 for gas (at min cap) + uint256 erc20Amount = 10000 ether; // Large ERC20 amount + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + assertEq(tss.balance, tssBalanceBefore + msgValue, "TSS should receive full msg.value"); + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + erc20Amount, "Vault should receive ERC20"); + } + + /// @notice Test Case 2.3 - Minimal gas amount at min cap + /// @dev gasAmount = 0.0005 ETH (exactly $1 at $2000/ETH) + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_MinimalGasAmount() public { + uint256 msgValue = 0.0005 ether; // $1 (at min cap) + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + assertEq(tss.balance, tssBalanceBefore + msgValue, "Should succeed at min cap"); + } + + /// @notice Test Case 2.3 - Multiple users can send independently + /// @dev Different users should be able to send batched transactions + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_MultipleUsers() public { + uint256 msgValue = 0.001 ether; + uint256 erc20Amount = 50 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + + // user1 sends + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // user2 sends + vm.prank(user2); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // user3 sends + vm.prank(user3); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: All succeeded + assertEq(tss.balance, tssBalanceBefore + (msgValue * 3), "All users should succeed - TSS"); + assertEq( + tokenA.balanceOf(address(this)), vaultBalanceBefore + (erc20Amount * 3), "All users should succeed - Vault" + ); + } + + // ========================= + // CATEGORY 2: VALIDATION & REVERT CASES + // ========================= + + /// @notice Test Case 2.3 - Zero msg.value routes to Case 2.1 (should succeed for ERC20) + /// @dev Ensures Case 2.3 is NOT triggered when msg.value == 0 + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_ZeroMsgValue_RoutesToCase2_1() public { + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + + // Should route to Case 2.1 and succeed (no batching, ERC20 only) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + + // Assert: ERC20 transferred (Case 2.1 behavior) + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + erc20Amount, "Should route to Case 2.1"); + } + + /// @notice Test Case 2.3 - Empty payload reverts + /// @dev FUNDS_AND_PAYLOAD requires non-empty payload + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_EmptyPayload() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + bytes("") // Empty payload + ); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Zero amount with payload routes to GAS_AND_PAYLOAD (matrix inference) + /// @dev With amount=0, payload non-empty, msg.value>0, matrix infers GAS_AND_PAYLOAD (not FUNDS_AND_PAYLOAD) + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_ZeroAmount() public { + uint256 msgValue = 0.002 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + 0, // Zero amount + encodedPayload + ); + + // Matrix infers GAS_AND_PAYLOAD (hasPayload=true, hasFunds=false, hasNativeValue=true) + // This should succeed as a GAS_AND_PAYLOAD transaction + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // Gas routes always credit UEA + token: address(0), + amount: msgValue, + payload: encodedPayload, + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Zero revertRecipient reverts + /// @dev revertInstruction.revertRecipient must be non-zero + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_ZerorevertRecipient() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + token: address(tokenA), + amount: erc20Amount, + payload: encodedPayload, + revertRecipient: address(0), // Zero address + signatureData: bytes("") + }); + + vm.expectRevert(Errors.InvalidRecipient.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Gas amount below min USD cap reverts + /// @dev At $2000/ETH, min cap = $1 = 0.0005 ETH + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_GasAmountBelowMinUSDCap() public { + uint256 msgValue = 0.0004 ether; // $0.80 (below $1 min) + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Gas amount above max USD cap reverts + /// @dev At $2000/ETH, max cap = $10 = 0.005 ETH + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_GasAmountAboveMaxUSDCap() public { + uint256 msgValue = 0.006 ether; // $12 (above $10 max) + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + vm.expectRevert(Errors.InvalidAmount.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Gas amount exceeds block cap reverts + /// @dev Set block cap and verify gas route respects it + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_GasAmountExceedsBlockCap() public { + // Set block cap to $5 + vm.prank(admin); + gatewayTemp.setBlockUsdCap(5e18); + + uint256 msgValue = 0.003 ether; // $6 (exceeds $5 block cap) + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + vm.expectRevert(Errors.BlockCapLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Unsupported ERC20 token reverts + /// @dev Token with threshold=0 should revert with NotSupported + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_UnsupportedToken() public { + // Deploy a new token that's not configured + MockERC20 unsupportedToken = new MockERC20("Unsupported", "UNSUP", 18, 0); + unsupportedToken.mint(user1, 1000 ether); + + vm.prank(user1); + unsupportedToken.approve(address(gatewayTemp), type(uint256).max); + + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(unsupportedToken), + erc20Amount, + encodedPayload + ); + + vm.expectRevert(Errors.NotSupported.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Insufficient ERC20 allowance reverts + /// @dev Should revert with ERC20InsufficientAllowance + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_InsufficientAllowance() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 1000 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + // Create a user with no approval + address userNoApproval = address(0x7777); + tokenA.mint(userNoApproval, erc20Amount); + vm.deal(userNoApproval, msgValue); + // No approval given + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + vm.prank(userNoApproval); + vm.expectRevert(); // ERC20InsufficientAllowance + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Insufficient ERC20 balance reverts + /// @dev Should revert with ERC20InsufficientBalance + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_InsufficientBalance() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 1000 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + // Create a user with approval but no balance + address userNoBalance = address(0x8888); + vm.deal(userNoBalance, msgValue); + // No tokens minted + + vm.prank(userNoBalance); + tokenA.approve(address(gatewayTemp), type(uint256).max); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + vm.prank(userNoBalance); + vm.expectRevert(); // ERC20InsufficientBalance + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + // ========================= + // CATEGORY 3: RATE LIMITING - DUAL RATE LIMITS + // ========================= + + /// @notice Test Case 2.3 - Separate rate limits for gas and ERC20 + /// @dev Gas uses USD caps, ERC20 uses token rate limit - completely independent + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_SeparateRateLimits() public { + uint256 msgValue = 0.002 ether; // $4 for gas + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + (uint256 erc20UsedBefore,) = gatewayTemp.currentTokenUsage(address(tokenA)); + (uint256 nativeUsedBefore,) = gatewayTemp.currentTokenUsage(address(0)); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: ERC20 rate limit consumed + (uint256 erc20UsedAfter,) = gatewayTemp.currentTokenUsage(address(tokenA)); + assertEq(erc20UsedAfter, erc20UsedBefore + erc20Amount, "ERC20 rate limit should be consumed"); + + // Assert: Native rate limit NOT consumed (gas uses USD caps) + (uint256 nativeUsedAfter,) = gatewayTemp.currentTokenUsage(address(0)); + assertEq(nativeUsedAfter, nativeUsedBefore, "Native rate limit should NOT be consumed"); + } + + /// @notice Test Case 2.3 - ERC20 rate limit exceeded reverts + /// @dev Even if gas amount is fine, ERC20 must respect rate limit + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_ERC20RateLimitExceeded() public { + // Set low threshold for tokenA + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(tokenA); + thresholds[0] = 50 ether; // Low threshold + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + uint256 msgValue = 0.002 ether; // Gas is fine + uint256 erc20Amount = 60 ether; // Exceeds ERC20 threshold + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + vm.expectRevert(Errors.RateLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Cumulative ERC20 rate limit + /// @dev Multiple calls should accumulate towards ERC20 rate limit + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_CumulativeERC20RateLimit() public { + // Set threshold + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(tokenA); + thresholds[0] = 200 ether; + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + // Call 1: 120 tokenA + UniversalTxRequest memory req1 = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + 120 ether, + encodedPayload + ); + + // Call 2: 70 tokenA (cumulative 190 < 200) + UniversalTxRequest memory req2 = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + 70 ether, + encodedPayload + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0.001 ether }(req1); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0.001 ether }(req2); + + // Verify cumulative usage + (uint256 used,) = gatewayTemp.currentTokenUsage(address(tokenA)); + assertEq(used, 190 ether, "Cumulative ERC20 usage should be 190"); + } + + /// @notice Test Case 2.3 - Cumulative ERC20 rate limit exceeded reverts + /// @dev Second call should fail when cumulative exceeds threshold + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RevertOn_CumulativeERC20RateLimitExceeded() public { + // Set threshold + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(tokenA); + thresholds[0] = 200 ether; + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + // Call 1: 120 tokenA + UniversalTxRequest memory req1 = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + 120 ether, + encodedPayload + ); + + // Call 2: 90 tokenA (cumulative 210 > 200) + UniversalTxRequest memory req2 = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + 90 ether, + encodedPayload + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0.001 ether }(req1); + + vm.expectRevert(Errors.RateLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0.001 ether }(req2); + } + + /// @notice Test Case 2.3 - ERC20 rate limit resets in new epoch + /// @dev After epoch duration, ERC20 rate limit should reset + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_RateLimitResetsInNewEpoch() public { + // Set threshold + address[] memory tokens = new address[](1); + uint256[] memory thresholds = new uint256[](1); + tokens[0] = address(tokenA); + thresholds[0] = 100 ether; + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + 90 ether, + encodedPayload + ); + + // First call in epoch 1 + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0.001 ether }(req); + + // Advance time to next epoch (86400 seconds + 1 for safety) + vm.warp(block.timestamp + 86401); + // Also advance block number to ensure new epoch + vm.roll(block.number + 1); + + // Update oracle timestamp to prevent stale data error + ethUsdFeedMock.setAnswer(2000e8, block.timestamp); + + // Second call in epoch 2 (should succeed as limit reset) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0.001 ether }(req); + + // Verify usage reset + (uint256 used,) = gatewayTemp.currentTokenUsage(address(tokenA)); + assertEq(used, 90 ether, "Usage should reset in new epoch"); + } + + /// @notice Test Case 2.3 - Native rate limit NOT consumed + /// @dev Critical: Native ETH uses USD caps, not rate limits + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_NativeNotConsumedInRateLimit() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + (uint256 nativeUsedBefore,) = gatewayTemp.currentTokenUsage(address(0)); + + // Make multiple calls + for (uint256 i = 0; i < 5; i++) { + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + // Assert: Native rate limit still not consumed + (uint256 nativeUsedAfter,) = gatewayTemp.currentTokenUsage(address(0)); + assertEq(nativeUsedAfter, nativeUsedBefore, "Native rate limit should NEVER be consumed in Case 2.3"); + } + + // ========================= + // CATEGORY 4: EVENT EMISSION & DUAL EVENTS + // ========================= + + /// @notice Test Case 2.3 - Always emits two events + /// @dev Unlike Case 2.2, gas route is ALWAYS called (no condition) + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_EmitsTwoEvents_Always() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + // Event 1: Gas + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), + token: address(0), + amount: msgValue, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + // Event 2: Funds + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), + amount: erc20Amount, + payload: encodedPayload, + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Gas event has empty payload + /// @dev Gas event always has empty payload, funds event has full payload + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_GasEvent_HasEmptyPayload() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + // Gas event: empty payload + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), + token: address(0), + amount: msgValue, + payload: bytes(""), // Empty + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Gas event recipient always address(0) + /// @dev Gas always credits UEA, funds preserves recipient + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_GasEvent_RecipientAlwaysZero() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + address explicitRecipient = address(0x999); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + // Gas event: recipient = address(0) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), // Always zero for gas + token: address(0), + amount: msgValue, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + // Funds event: recipient preserved + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), + amount: erc20Amount, + payload: encodedPayload, + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Gas event uses native, funds event uses ERC20 + /// @dev Verify different tokens in each event + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_GasEvent_NativeToken_FundsEvent_ERC20Token() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + // Gas event: token = address(0) (native) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), + token: address(0), // Native token + amount: msgValue, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + // Funds event: token = tokenA (ERC20) + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // FUNDS_AND_PAYLOAD always has recipient == address(0) + token: address(tokenA), // ERC20 token + amount: erc20Amount, + payload: encodedPayload, + revertRecipient: req.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Events preserve revertMsg + /// @dev Both events should preserve revertMsg + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_EventsPreserverevertMsg() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + bytes memory revertMsg = abi.encodePacked("custom revert", uint256(999)); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + RevertInstructions memory revertInst = + RevertInstructions({ revertRecipient: address(0x456), revertMsg: revertMsg }); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + token: address(tokenA), + amount: erc20Amount, + payload: encodedPayload, + revertRecipient: revertInst.revertRecipient, + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + // Both events should have preserved revertMsg (verified implicitly) + } + + /// @notice Test Case 2.3 - Events preserve signatureData + /// @dev Both events should preserve signatureData + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_EventsPreserveSignatureData() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + bytes memory sigData = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2))); + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = UniversalTxRequest({ + recipient: address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + token: address(tokenA), + amount: erc20Amount, + payload: encodedPayload, + revertRecipient: address(0x456), + signatureData: sigData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + // Both events should have preserved signatureData (verified implicitly) + } + + // ========================= + // CATEGORY 5: FUND FLOW & DESTINATIONS + // ========================= + + /// @notice Test Case 2.3 - Native to TSS, ERC20 to Vault + /// @dev Verify dual destinations work correctly + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_NativeToTSS_ERC20ToVault() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + uint256 gatewayNativeBefore = address(gatewayTemp).balance; + uint256 gatewayERC20Before = tokenA.balanceOf(address(gatewayTemp)); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: TSS received native ETH + assertEq(tss.balance, tssBalanceBefore + msgValue, "TSS should receive native ETH"); + + // Assert: Vault received ERC20 + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + erc20Amount, "Vault should receive ERC20"); + + // Assert: Gateway holds nothing + assertEq(address(gatewayTemp).balance, gatewayNativeBefore, "Gateway should not hold native ETH"); + assertEq(tokenA.balanceOf(address(gatewayTemp)), gatewayERC20Before, "Gateway should not hold ERC20"); + } + + /// @notice Test Case 2.3 - Gateway does not accumulate + /// @dev Gateway should not hold any tokens after multiple calls + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_Gateway_DoesNotAccumulate() public { + uint256 msgValue = 0.001 ether; + uint256 erc20Amount = 50 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + uint256 gatewayNativeBefore = address(gatewayTemp).balance; + uint256 gatewayERC20Before = tokenA.balanceOf(address(gatewayTemp)); + + // Make multiple calls + for (uint256 i = 0; i < 5; i++) { + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + // Gateway balance should remain unchanged + assertEq(address(gatewayTemp).balance, gatewayNativeBefore, "Gateway should not accumulate native ETH"); + assertEq(tokenA.balanceOf(address(gatewayTemp)), gatewayERC20Before, "Gateway should not accumulate ERC20"); + } + + /// @notice Test Case 2.3 - Full msg.value to gas route + /// @dev Entire msg.value goes through gas route (no split) + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_FullMsgValueToGasRoute() public { + uint256 msgValue = 0.003 ether; + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: Entire msg.value went to TSS (via gas route) + assertEq(tss.balance, tssBalanceBefore + msgValue, "Entire msg.value should go to gas route"); + } + + /// @notice Test Case 2.3 - Independent amounts + /// @dev Gas and funds amounts are completely independent + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_IndependentAmounts() public { + uint256 msgValue = 0.001 ether; // Small gas + uint256 erc20Amount = 10000 ether; // Large ERC20 + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Assert: No relationship between amounts + assertEq(tss.balance, tssBalanceBefore + msgValue, "Gas amount independent"); + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + erc20Amount, "Funds amount independent"); + } + + /// @notice Test Case 2.3 - Different tokens in sequence + /// @dev Each ERC20 goes to vault correctly + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_DifferentTokensSameTx() public { + uint256 msgValue = 0.001 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + // Send tokenA + UniversalTxRequest memory reqA = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + 50 ether, + encodedPayload + ); + + // Send usdc + UniversalTxRequest memory reqU = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(usdc), + 50e6, + encodedPayload + ); + + uint256 vaultTokenABefore = tokenA.balanceOf(address(this)); + uint256 vaultUsdcBefore = usdc.balanceOf(address(this)); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(reqA); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(reqU); + + // Assert: Both tokens received correctly + assertEq(tokenA.balanceOf(address(this)), vaultTokenABefore + 50 ether, "TokenA to vault"); + assertEq(usdc.balanceOf(address(this)), vaultUsdcBefore + 50e6, "USDC to vault"); + } + + // ========================= + // CATEGORY 6: EDGE CASES & BOUNDARY CONDITIONS + // ========================= + + /// @notice Test Case 2.3 - Maximal gas amount at max cap + /// @dev msg.value = 0.005 ETH (exactly $10 at $2000/ETH) + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_MaximalGasAmount_AtMaxCap() public { + uint256 msgValue = 0.005 ether; // $10 (at max cap) + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + assertEq(tss.balance, tssBalanceBefore + msgValue, "Should succeed at max cap"); + } + + /// @notice Test Case 2.3 - Large payload does not affect gas caps + /// @dev Gas USD caps only check msg.value, not payload size + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_LargePayload_DoesNotAffectGasCaps() public { + uint256 msgValue = 0.002 ether; + uint256 erc20Amount = 100 ether; + + // Create large payload (10KB) + bytes memory largeData = new bytes(10000); + for (uint256 i = 0; i < 10000; i++) { + largeData[i] = bytes1(uint8(i % 256)); + } + + UniversalPayload memory largePayload = UniversalPayload({ + to: address(0xABCD), + value: 0, + data: largeData, + gasLimit: 1000000, + maxFeePerGas: 0, + maxPriorityFeePerGas: 0, + nonce: 1, + deadline: 0, + vType: VerificationType.signedVerification + }); + bytes memory encodedPayload = abi.encode(largePayload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + uint256 tssBalanceBefore = tss.balance; + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + assertEq(tss.balance, tssBalanceBefore + msgValue, "Large payload should not affect gas caps"); + } + + /// @notice Test Case 2.3 - Very large ERC20 amount within rate limit + /// @dev Should handle large ERC20 amounts correctly + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_VeryLargeERC20Amount_WithinRateLimit() public { + uint256 msgValue = 0.001 ether; + uint256 erc20Amount = 500000 ether; // Large but within default threshold + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + uint256 vaultBalanceBefore = tokenA.balanceOf(address(this)); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + assertEq(tokenA.balanceOf(address(this)), vaultBalanceBefore + erc20Amount, "Should handle large ERC20 amounts"); + } + + /// @notice Test Case 2.3 - Multiple calls same block respect gas block cap + /// @dev Cumulative gas amounts checked against block cap + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_MultipleCallsSameBlock_GasBlockCap() public { + // Set block cap to $8 + vm.prank(admin); + gatewayTemp.setBlockUsdCap(8e18); + + uint256 msgValue = 0.002 ether; // $4 per call + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + UniversalTxRequest memory req = buildUniversalTxRequest( + address(0), // FUNDS_AND_PAYLOAD requires recipient == address(0) + address(tokenA), + erc20Amount, + encodedPayload + ); + + // First call: $4 gas (within $8 cap) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Second call: $4 gas (cumulative $8, at cap) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + + // Third call: $4 gas (cumulative $12, exceeds $8 cap) + vm.expectRevert(Errors.BlockCapLimitExceeded.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req); + } + + /// @notice Test Case 2.3 - Different recipients work correctly + /// @dev Both zero and non-zero recipients should work + function test_Case2_3_FUNDS_AND_PAYLOAD_ERC20_DifferentRecipients_Work() public { + uint256 msgValue = 0.001 ether; + uint256 erc20Amount = 100 ether; + + UniversalPayload memory payload = buildDefaultPayload(); + bytes memory encodedPayload = abi.encode(payload); + + // Test with zero recipient + UniversalTxRequest memory req1 = buildUniversalTxRequest( + address(0), // Zero recipient (required) + address(tokenA), + erc20Amount, + encodedPayload + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req1); + + // Test with zero recipient (non-zero not allowed for FUNDS_AND_PAYLOAD) + UniversalTxRequest memory req2 = buildUniversalTxRequest( + address(0), // Zero recipient (required) + address(tokenA), + erc20Amount, + encodedPayload + ); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: msgValue }(req2); + + // Both should succeed + } +} diff --git a/contracts/evm-gateway/test/gateway/9_sendUniversalTxFetchTxType.t.sol b/contracts/evm-gateway/test/gateway/9_sendUniversalTxFetchTxType.t.sol new file mode 100644 index 0000000..00daebe --- /dev/null +++ b/contracts/evm-gateway/test/gateway/9_sendUniversalTxFetchTxType.t.sol @@ -0,0 +1,597 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BaseTest } from "../BaseTest.t.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { TX_TYPE, RevertInstructions, UniversalPayload, UniversalTxRequest } from "../../src/libraries/Types.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/** + * @title _fetchTxType Comprehensive Test Suite + * @notice Tests the core routing logic of _fetchTxType function + * @dev This test suite validates that _fetchTxType correctly classifies transactions + * based on the four decision variables: + * - hasPayload (P): req.payload.length > 0 + * - hasFunds (F): req.amount > 0 + * - fundsIsNative (N): req.token == address(0) + * - hasNativeValue (G): nativeValue > 0 + */ +contract GatewayFetchTxTypeTest is BaseTest { + UniversalGateway public gatewayTemp; + address public erc20A; + address public erc20B; + + // ========================= + // EVENTS + // ========================= + event UniversalTx( + address indexed sender, + address indexed recipient, + address token, + uint256 amount, + bytes payload, + address revertRecipient, + TX_TYPE txType, + bytes signatureData + ); + + // ========================= + // SETUP + // ========================= + function setUp() public override { + super.setUp(); + + _deployGatewayTemp(); + + vm.prank(admin); + gatewayTemp.setEthUsdFeed(address(ethUsdFeedMock)); + + erc20A = address(tokenA); + erc20B = address(usdc); + + // Setup token support + address[] memory tokens = new address[](4); + uint256[] memory thresholds = new uint256[](4); + tokens[0] = address(0); + tokens[1] = erc20A; + tokens[2] = erc20B; + tokens[3] = address(weth); + thresholds[0] = 1000000 ether; + thresholds[1] = 1000000 ether; + thresholds[2] = 1000000e6; + thresholds[3] = 1000000 ether; + + vm.prank(admin); + gatewayTemp.setTokenLimitThresholds(tokens, thresholds); + + // Approve tokens + vm.prank(user1); + tokenA.approve(address(gatewayTemp), type(uint256).max); + vm.prank(user1); + usdc.approve(address(gatewayTemp), type(uint256).max); + } + + function _deployGatewayTemp() internal { + UniversalGateway implementation = new UniversalGateway(); + + bytes memory initData = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, + tss, + address(this), + MIN_CAP_USD, + MAX_CAP_USD, + uniV3Factory, + uniV3Router, + address(weth) + ); + + TransparentUpgradeableProxy tempProxy = + new TransparentUpgradeableProxy(address(implementation), address(proxyAdmin), initData); + + gatewayTemp = UniversalGateway(payable(address(tempProxy))); + vm.label(address(gatewayTemp), "UniversalGateway"); + } + + // ========================= + // HELPER FUNCTIONS + // ========================= + + /// @notice Helper to build UniversalTxRequest + function makeReq(bytes memory payloadBytes, uint256 amount, address token) + internal + pure + returns (UniversalTxRequest memory) + { + return UniversalTxRequest({ + recipient: address(0), // Always address(0) - funds go to caller's UEA on Push Chain + token: token, + amount: amount, + payload: payloadBytes, + revertRecipient: address(0x456), + signatureData: bytes("sig") + }); + } + + /// @notice Helper to get non-empty payload + function nonEmptyPayload() internal view returns (bytes memory) { + return abi.encode(buildDefaultPayload()); + } + + // ========================= + // GROUP 1: TX_TYPE.GAS + // ========================= + + /// @notice Test 1.1.1: Basic GAS with native token + function test_GAS_basic_native() public { + UniversalTxRequest memory req = makeReq(bytes(""), 0, address(0)); + uint256 nativeValue = 0.002 ether; // $4 at $2000/ETH - within $1-$10 cap + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), + token: address(0), + amount: nativeValue, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req); + } + + /// @notice Test 1.1.2: GAS with non-native token (token field ignored) + function test_GAS_basic_nonNativeToken_ignored() public { + UniversalTxRequest memory req = makeReq(bytes(""), 0, erc20A); + uint256 nativeValue = 0.002 ether; // $4 at $2000/ETH - within $1-$10 cap + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS, + sender: user1, + recipient: address(0), + token: address(0), + amount: nativeValue, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req); + } + + /// @notice Test 1.2.1: GAS mutation with funds becomes FUNDS + function test_GAS_mutation_hasFunds_becomes_FUNDS_native() public { + uint256 amount = 100 ether; // FUNDS route doesn't have USD cap restrictions + UniversalTxRequest memory req = makeReq(bytes(""), amount, address(0)); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS, + sender: user1, + recipient: address(0), + token: address(0), + amount: amount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: amount }(req); + } + + /// @notice Test 1.2.2: GAS mutation with payload becomes GAS_AND_PAYLOAD + function test_GAS_mutation_hasPayload_becomes_GAS_AND_PAYLOAD() public { + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), 0, address(0)); + uint256 nativeValue = 0.002 ether; // $4 at $2000/ETH - within $1-$10 cap + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS_AND_PAYLOAD, + sender: user1, + recipient: address(0), + token: address(0), + amount: nativeValue, + payload: nonEmptyPayload(), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req); + } + + /// @notice Test 1.2.3: GAS mutation with no native value reverts + function test_GAS_mutation_noNativeValue_revert() public { + UniversalTxRequest memory req = makeReq(bytes(""), 0, address(0)); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + // ========================= + // GROUP 2: TX_TYPE.GAS_AND_PAYLOAD + // ========================= + + /// @notice Test 2.1.1: Basic GAS_AND_PAYLOAD + function test_GAS_AND_PAYLOAD_basic() public { + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), 0, address(0)); + uint256 nativeValue = 0.003 ether; // $6 at $2000/ETH - within $1-$10 cap + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS_AND_PAYLOAD, + sender: user1, + recipient: address(0), + token: address(0), + amount: nativeValue, + payload: nonEmptyPayload(), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req); + } + + /// @notice Test 2.1.2: GAS_AND_PAYLOAD with non-native token (ignored) + function test_GAS_AND_PAYLOAD_nonNativeToken_ignored() public { + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), 0, erc20A); + uint256 nativeValue = 0.003 ether; // $6 at $2000/ETH - within $1-$10 cap + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS_AND_PAYLOAD, + sender: user1, + recipient: address(0), + token: address(0), + amount: nativeValue, + payload: nonEmptyPayload(), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req); + } + + /// @notice Test 2.2.1: GAS_AND_PAYLOAD mutation with funds becomes FUNDS_AND_PAYLOAD + function test_GAS_AND_PAYLOAD_mutation_addFunds_native_becomes_FUNDS_AND_PAYLOAD() public { + uint256 amount = 100 ether; // FUNDS_AND_PAYLOAD route doesn't have USD cap restrictions + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), amount, address(0)); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // Always address(0) for UEA credit + token: address(0), + amount: amount, + payload: nonEmptyPayload(), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: amount }(req); + } + + /// @notice Test 2.2.2: GAS_AND_PAYLOAD with zero native value (payload-only) + function test_GAS_AND_PAYLOAD_payload_only_native0() public { + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), 0, address(0)); + + uint256 tssBalanceBefore = tss.balance; + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.GAS_AND_PAYLOAD, + sender: user1, + recipient: address(0), + token: address(0), + amount: 0, + payload: nonEmptyPayload(), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + + assertEq(tss.balance, tssBalanceBefore, "TSS balance should remain unchanged"); + } + + // ========================= + // GROUP 3: TX_TYPE.FUNDS (Native) + // ========================= + + /// @notice Test 3.1.1: Basic native FUNDS + function test_FUNDS_native_basic() public { + uint256 amount = 100 ether; // FUNDS route doesn't have USD cap restrictions + UniversalTxRequest memory req = makeReq(bytes(""), amount, address(0)); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS, + sender: user1, + recipient: address(0), + token: address(0), + amount: amount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: amount }(req); + } + + /// @notice Test 3.2.1: Native FUNDS missing native value reverts + function test_FUNDS_native_missingNativeValue_revert() public { + UniversalTxRequest memory req = makeReq(bytes(""), 100 ether, address(0)); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + // ========================= + // GROUP 4: TX_TYPE.FUNDS (ERC-20) + // ========================= + + /// @notice Test 4.1.1: Basic ERC-20 FUNDS + function test_FUNDS_erc20_basic() public { + uint256 amount = 1000 ether; + UniversalTxRequest memory req = makeReq(bytes(""), amount, erc20A); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS, + sender: user1, + recipient: address(0), + token: erc20A, + amount: amount, + payload: bytes(""), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test 4.2.1: ERC-20 FUNDS with native value reverts + function test_FUNDS_erc20_withNativeValue_revert() public { + UniversalTxRequest memory req = makeReq(bytes(""), 1000 ether, erc20A); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 1 ether }(req); + } + + /// @notice Test 4.2.2: ERC-20 with no funds reverts + function test_FUNDS_erc20_noFunds_revert() public { + UniversalTxRequest memory req = makeReq(bytes(""), 0, erc20A); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + // ========================= + // GROUP 5: TX_TYPE.FUNDS_AND_PAYLOAD (No batching - ERC-20 only) + // ========================= + + /// @notice Test 5.1.1: FUNDS_AND_PAYLOAD no batching ERC-20 + function test_FAP_nobatching_erc20_basic() public { + uint256 amount = 500 ether; + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), amount, erc20A); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: req.recipient, + token: erc20A, + amount: amount, + payload: nonEmptyPayload(), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test 5.2.1: FAP no batching with native value becomes ERC-20 + gas batching + function test_FAP_nobatching_addNativeValue_becomes_FAP_erc20_plus_gas() public { + uint256 amount = 500 ether; + uint256 nativeValue = 0.003 ether; // $6 gas within $1-$10 cap + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), amount, erc20A); + + // Should emit TWO events: one for GAS, one for FUNDS_AND_PAYLOAD + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req); + } + + /// @notice Test 5.2.2: FAP no batching missing funds reverts + function test_FAP_nobatching_missingFunds_revert() public { + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), 0, erc20A); + + // This routes to GAS_AND_PAYLOAD (payload-only) + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + // ========================= + // GROUP 6: TX_TYPE.FUNDS_AND_PAYLOAD (Native + Gas batching) + // ========================= + + /// @notice Test 6.1.1: FAP native batching basic + function test_FAP_native_batching_basic() public { + uint256 amount = 100 ether; // Bridge amount + uint256 nativeValue = 100.002 ether; // Bridge + gas (0.002 = $4 gas within cap) + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), amount, address(0)); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req); + } + + /// @notice Test 6.1.2: FAP native batching with zero extra gas + function test_FAP_native_batching_zeroExtraGas_ok() public { + uint256 amount = 100 ether; + uint256 nativeValue = 100 ether; // Exact amount, no extra gas + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), amount, address(0)); + + vm.expectEmit(true, true, false, true, address(gatewayTemp)); + emit UniversalTx({ + txType: TX_TYPE.FUNDS_AND_PAYLOAD, + sender: user1, + recipient: address(0), // Always address(0) for UEA credit + token: address(0), + amount: amount, + payload: nonEmptyPayload(), + revertRecipient: req.revertRecipient, + signatureData: req.signatureData + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req); + } + + /// @notice Test 6.2.1: FAP native batching missing native value reverts + function test_FAP_native_batching_missingNativeValue_revert() public { + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), 10 ether, address(0)); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + // ========================= + // GROUP 7: TX_TYPE.FUNDS_AND_PAYLOAD (ERC-20 + Gas batching) + // ========================= + + /// @notice Test 7.1.1: FAP ERC-20 plus gas basic + function test_FAP_erc20_plus_gas_basic() public { + uint256 amount = 1000 ether; + uint256 nativeValue = 0.003 ether; // $6 gas within $1-$10 cap + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), amount, erc20A); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req); + } + + // ========================= + // GROUP 8: Invalid combinations + // ========================= + + /// @notice Test 8.1: All zero reverts + function test_Invalid_allZero() public { + UniversalTxRequest memory req = makeReq(bytes(""), 0, erc20A); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test 8.2: Payload only with no native (routes to GAS_AND_PAYLOAD) + function test_Invalid_payload_only_noNative() public { + UniversalTxRequest memory req = makeReq(nonEmptyPayload(), 0, address(0)); + + // This is actually valid - routes to GAS_AND_PAYLOAD with zero gas + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test 8.3: Native funds with no native value reverts + function test_Invalid_nativeFunds_noNativeValue() public { + UniversalTxRequest memory req = makeReq(bytes(""), 7 ether, address(0)); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 0 }(req); + } + + /// @notice Test 8.4: ERC-20 funds with native value reverts + function test_Invalid_erc20Funds_withNoPayload_andNativeValue() public { + UniversalTxRequest memory req = makeReq(bytes(""), 1000 ether, erc20A); + + vm.expectRevert(Errors.InvalidInput.selector); + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: 1 ether }(req); + } + + // ========================= + // GROUP 9: Invariance tests + // ========================= + + /// @notice Test 9.2: SignatureData does not affect txType + function test_Invariance_signatureData_ignored() public { + uint256 amount = 500 ether; + uint256 nativeValue = 0.003 ether; // $6 gas within $1-$10 cap + + // Test with empty signatureData + UniversalTxRequest memory req1 = UniversalTxRequest({ + recipient: address(0), + token: erc20A, + amount: amount, + payload: nonEmptyPayload(), + revertRecipient: address(0x456), + signatureData: bytes("") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req1); + + // Test with non-empty signatureData + UniversalTxRequest memory req2 = UniversalTxRequest({ + recipient: address(0), + token: erc20A, + amount: amount, + payload: nonEmptyPayload(), + revertRecipient: address(0x456), + signatureData: bytes("different signature data") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req2); + } + + /// @notice Test 9.3: RevertInstruction context does not affect txType + function test_Invariance_revertInstruction_onlyRecipientMatters() public { + uint256 amount = 500 ether; + uint256 nativeValue = 0.003 ether; // $6 gas within $1-$10 cap + + // Test with different revertMsg + UniversalTxRequest memory req1 = UniversalTxRequest({ + recipient: address(0), + token: erc20A, + amount: amount, + payload: nonEmptyPayload(), + revertRecipient: address(0x456), + signatureData: bytes("sig") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req1); + + UniversalTxRequest memory req2 = UniversalTxRequest({ + recipient: address(0), + token: erc20A, + amount: amount, + payload: nonEmptyPayload(), + revertRecipient: address(0x456), + signatureData: bytes("sig") + }); + + vm.prank(user1); + gatewayTemp.sendUniversalTx{ value: nativeValue }(req2); + } +} diff --git a/contracts/evm-gateway/test/mocks/MockPC20.sol b/contracts/evm-gateway/test/mocks/MockPC20.sol new file mode 100644 index 0000000..be11fbd --- /dev/null +++ b/contracts/evm-gateway/test/mocks/MockPC20.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @title MockPC20 + * @notice A comprehensive mock ERC20 token for testing purposes + * @dev Extends OpenZeppelin's ERC20 with additional testing utilities + */ +contract MockPC20 is ERC20 { + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} \ No newline at end of file diff --git a/contracts/evm-gateway/test/mocks/MockPC721.sol b/contracts/evm-gateway/test/mocks/MockPC721.sol new file mode 100644 index 0000000..18e8e7e --- /dev/null +++ b/contracts/evm-gateway/test/mocks/MockPC721.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +/** + * @title MockPC721 + * @notice A comprehensive mock ERC721 token for testing purposes + * @dev Extends OpenZeppelin's ERC721 with additional testing utilities + */ +contract MockPC721 is ERC721 { + string private _baseTokenURI; + + constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) { + _baseTokenURI = "https://example.com/token/"; + } + + function mint(address to, uint256 tokenId) external { + _mint(to, tokenId); + } + + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + _requireOwned(tokenId); + return string(abi.encodePacked(_baseTokenURI, _toString(tokenId))); + } + + function _toString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } +} \ No newline at end of file diff --git a/contracts/evm-gateway/test/mocks/MockPRC20.sol b/contracts/evm-gateway/test/mocks/MockPRC20.sol index 527e5fa..6619544 100644 --- a/contracts/evm-gateway/test/mocks/MockPRC20.sol +++ b/contracts/evm-gateway/test/mocks/MockPRC20.sol @@ -149,10 +149,7 @@ contract MockPRC20 is IPRC20 { /// @param to Recipient on Push EVM /// @param amount Amount to mint function deposit(address to, uint256 amount) external returns (bool) { - require( - msg.sender == UNIVERSAL_CORE || msg.sender == UNIVERSAL_EXECUTOR_MODULE, - "MockPRC20: Invalid sender" - ); + require(msg.sender == UNIVERSAL_CORE || msg.sender == UNIVERSAL_EXECUTOR_MODULE, "MockPRC20: Invalid sender"); _mint(to, amount); diff --git a/contracts/evm-gateway/test/mocks/MockReentrantContract.sol b/contracts/evm-gateway/test/mocks/MockReentrantContract.sol index 4035e98..01a6c1f 100644 --- a/contracts/evm-gateway/test/mocks/MockReentrantContract.sol +++ b/contracts/evm-gateway/test/mocks/MockReentrantContract.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IUniversalGatewayPC} from "../../src/interfaces/IUniversalGatewayPC.sol"; -import {RevertInstructions} from "../../src/libraries/Types.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IUniversalGatewayPC } from "../../src/interfaces/IUniversalGatewayPC.sol"; +import { RevertInstructions } from "../../src/libraries/Types.sol"; /** * @title MockReentrantContract @@ -14,7 +14,7 @@ contract MockReentrantContract { address public gateway; address public prc20Token; address public gasToken; - + // Vault-specific reentrancy state address public vault; address public vaultPC; @@ -32,13 +32,21 @@ contract MockReentrantContract { // UniversalGatewayPC Reentrancy Functions // ============================================================================ - function attemptReentrancy( - bytes calldata to, - uint256 amount, - uint256 gasLimit, - RevertInstructions calldata revertCfg - ) external { - IUniversalGatewayPC(gateway).withdraw(to, prc20Token, amount, gasLimit, revertCfg); + function attemptReentrancy(bytes calldata to, uint256 amount, uint256 gasLimit, address revertRecipient) external { + RevertInstructions memory revertCfg = RevertInstructions({ + revertRecipient: revertRecipient, + revertMsg: bytes("") + }); + IUniversalGatewayPC(gateway).sendUniversalTxOutbound( + to, + prc20Token, + amount, + 0, // tokenId + gasLimit, + "", // empty payload + "", // chainNamespace will be fetched from PRC20 + revertCfg + ); } function attemptReentrancyWithExecute( @@ -48,12 +56,14 @@ contract MockReentrantContract { uint256 gasLimit, RevertInstructions calldata revertCfg ) external { - IUniversalGatewayPC(gateway).withdrawAndExecute( - target, - prc20Token, - amount, - payload, - gasLimit, + IUniversalGatewayPC(gateway).sendUniversalTxOutbound( + target, + prc20Token, + amount, + 0, // tokenId + gasLimit, + payload, + "", // chainNamespace will be fetched from PRC20 revertCfg ); } @@ -78,21 +88,22 @@ contract MockReentrantContract { function pullTokens(address _token, address from, uint256 amount) external { IERC20(_token).transferFrom(from, address(this), amount); - + if (shouldReenter && vault != address(0)) { shouldReenter = false; // prevent infinite loop - + // Call vault based on reenter type if (reenterType == 0) { // Attempt to reenter withdraw - (bool success,) = vault.call( - abi.encodeWithSignature("withdraw(address,address,uint256)", _token, address(this), 1) - ); + (bool success,) = + vault.call(abi.encodeWithSignature("withdraw(address,address,uint256)", _token, address(this), 1)); require(success, "Reentry failed"); } else if (reenterType == 1) { // Attempt to reenter withdrawAndCall (bool success,) = vault.call( - abi.encodeWithSignature("withdrawAndCall(address,address,uint256,bytes)", _token, address(this), 1, "") + abi.encodeWithSignature( + "withdrawAndCall(address,address,uint256,bytes)", _token, address(this), 1, "" + ) ); require(success, "Reentry failed"); } else if (reenterType == 2) { @@ -111,18 +122,15 @@ contract MockReentrantContract { function attackVaultPCWithdraw(address _token, address to, uint256 amount) external { // First call to VaultPC withdraw - this should trigger transferFrom callback - (bool success,) = vaultPC.call( - abi.encodeWithSignature("withdraw(address,address,uint256)", _token, to, amount) - ); + (bool success,) = vaultPC.call(abi.encodeWithSignature("withdraw(address,address,uint256)", _token, to, amount)); require(!success, "Attack should have failed due to reentrancy guard"); } // Callback from ERC20 transfer that attempts reentrancy function transferFrom(address from, address to, uint256 amount) external returns (bool) { // Attempt to reenter withdraw during the transfer callback - (bool success,) = vaultPC.call( - abi.encodeWithSignature("withdraw(address,address,uint256)", msg.sender, address(this), 1) - ); + (bool success,) = + vaultPC.call(abi.encodeWithSignature("withdraw(address,address,uint256)", msg.sender, address(this), 1)); // The reentrancy should fail, so we just return true for the original transfer return true; } @@ -130,4 +138,4 @@ contract MockReentrantContract { function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { return this.onERC721Received.selector; } -} \ No newline at end of file +} diff --git a/contracts/evm-gateway/test/mocks/MockRevertingTarget.sol b/contracts/evm-gateway/test/mocks/MockRevertingTarget.sol index 6aad63f..f40f1a3 100644 --- a/contracts/evm-gateway/test/mocks/MockRevertingTarget.sol +++ b/contracts/evm-gateway/test/mocks/MockRevertingTarget.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /** * @title MockRevertingTarget diff --git a/contracts/evm-gateway/test/mocks/MockUniswapV3.sol b/contracts/evm-gateway/test/mocks/MockUniswapV3.sol new file mode 100644 index 0000000..95a67bb --- /dev/null +++ b/contracts/evm-gateway/test/mocks/MockUniswapV3.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IWETH } from "../../src/interfaces/IWETH.sol"; + +/** + * @title MockUniswapV3Factory + * @notice Simple mock for Uniswap V3 Factory for testing + */ +contract MockUniswapV3Factory is IUniswapV3Factory { + mapping(address => mapping(address => mapping(uint24 => address))) public pools; + address public factoryOwner; + + constructor() { + factoryOwner = msg.sender; + } + + function setPool(address token0, address token1, uint24 fee, address pool) external { + require(msg.sender == factoryOwner, "MockFactory: not owner"); + pools[token0][token1][fee] = pool; + pools[token1][token0][fee] = pool; // Symmetric + } + + function getPool(address tokenA, address tokenB, uint24 fee) external view override returns (address pool) { + return pools[tokenA][tokenB][fee]; + } + + function owner() external view override returns (address) { + return factoryOwner; + } + + function feeAmountTickSpacing(uint24) external pure override returns (int24) { + return 60; + } + + function createPool(address, address, uint24) external pure override returns (address) { + revert("MockFactory: createPool not implemented"); + } + + function setOwner(address) external pure override { + revert("MockFactory: setOwner not implemented"); + } + + function enableFeeAmount(uint24, int24) external pure override { + revert("MockFactory: enableFeeAmount not implemented"); + } +} + +/** + * @title MockUniswapV3Router + * @notice Simple mock for Uniswap V3 Router for testing + * @dev Simulates swaps by transferring tokens and returning WETH + */ +contract MockUniswapV3Router is ISwapRouter { + address public weth; + mapping(address => uint256) public swapRates; // token -> ETH rate (1e18 = 1:1) + + constructor(address _weth) { + weth = _weth; + } + + /// @notice Set swap rate for a token (amountOut = amountIn * rate / 1e18) + function setSwapRate(address token, uint256 rate) external { + swapRates[token] = rate; + } + + function exactInputSingle(ExactInputSingleParams calldata params) + external + payable + override + returns (uint256 amountOut) + { + require(params.tokenOut == weth, "MockRouter: tokenOut must be WETH"); + + // Calculate amount out based on swap rate (default 1:1 if not set) + uint256 rate = swapRates[params.tokenIn]; + if (rate == 0) { + rate = 1e18; // Default 1:1 + } + amountOut = (params.amountIn * rate) / 1e18; + + // Transfer tokens from gateway to this router + IERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn); + + // Mint WETH to gateway (simulating swap output) + // If router doesn't have enough WETH, mint it by depositing ETH + uint256 wethBalance = IERC20(weth).balanceOf(address(this)); + if (wethBalance < amountOut) { + uint256 ethNeeded = amountOut - wethBalance; + // Mint WETH by depositing ETH (router should have ETH from setUp) + IWETH(weth).deposit{ value: ethNeeded }(); + } + + // Transfer WETH to gateway + IERC20(weth).transfer(msg.sender, amountOut); + + return amountOut; + } + + function exactInput(ExactInputParams calldata) external payable override returns (uint256) { + revert("MockRouter: exactInput not implemented"); + } + + function exactOutputSingle(ExactOutputSingleParams calldata) external payable override returns (uint256) { + revert("MockRouter: exactOutputSingle not implemented"); + } + + function exactOutput(ExactOutputParams calldata) external payable override returns (uint256) { + revert("MockRouter: exactOutput not implemented"); + } + + function uniswapV3SwapCallback(int256, int256, bytes calldata) external pure override { + // Mock router doesn't need to implement callback logic + revert("MockRouter: callback not used"); + } +} diff --git a/contracts/evm-gateway/test/mocks/MockUniversalCoreReal.sol b/contracts/evm-gateway/test/mocks/MockUniversalCoreReal.sol index 083c13b..5eba638 100644 --- a/contracts/evm-gateway/test/mocks/MockUniversalCoreReal.sol +++ b/contracts/evm-gateway/test/mocks/MockUniversalCoreReal.sol @@ -35,6 +35,11 @@ contract MockUniversalCoreReal is IUniversalCore { /// @notice Default deadline in minutes for swaps uint256 public defaultDeadlineMins = 20; + /// @notice Protocol fees for different token types + uint256 private _pc20ProtocolFee = 100e18; // 100 PC default + uint256 private _pc721ProtocolFee = 50e18; // 50 PC default + uint256 private _defaultProtocolFee = 75e18; // 75 PC default + /// @notice Uniswap V3 addresses. address public uniswapV3FactoryAddress; address public uniswapV3SwapRouterAddress; @@ -59,6 +64,10 @@ contract MockUniversalCoreReal is IUniversalCore { // Supported tokens mapping mapping(address => bool) private _supportedTokens; + // Chain support mappings + mapping(string => bool) private _pc20SupportedChains; + mapping(string => bool) private _pc721SupportedChains; + // ========= Events ========= event SetGasPrice(string indexed chainID, uint256 price); event SetGasToken(string indexed chainID, address token); @@ -261,7 +270,11 @@ contract MockUniversalCoreReal is IUniversalCore { gasFee = price * BASE_GAS_LIMIT + IPRC20(_prc20).PC_PROTOCOL_FEE(); } - function withdrawGasFeeWithGasLimit(address _prc20, uint256 gasLimit) public view returns (address gasToken, uint256 gasFee) { + function withdrawGasFeeWithGasLimit(address _prc20, uint256 gasLimit) + public + view + returns (address gasToken, uint256 gasFee) + { string memory chainID = IPRC20(_prc20).SOURCE_CHAIN_ID(); gasToken = gasTokenPRC20ByChainId[chainID]; @@ -281,6 +294,42 @@ contract MockUniversalCoreReal is IUniversalCore { emit BaseGasLimitUpdated(oldLimit, gasLimit); } + // ========= Protocol Fee Functions ========= + function PC20_PROTOCOL_FEES() external view returns (uint256 fee) { + return _pc20ProtocolFee; + } + + function PC721_PROTOCOL_FEES() external view returns (uint256 fee) { + return _pc721ProtocolFee; + } + + function DEFAULT_PROTOCOL_FEES() external view returns (uint256 fee) { + return _defaultProtocolFee; + } + + function setProtocolFees(uint256 pc20Fee, uint256 pc721Fee, uint256 defaultFee) external onlyRole(DEFAULT_ADMIN_ROLE) { + _pc20ProtocolFee = pc20Fee; + _pc721ProtocolFee = pc721Fee; + _defaultProtocolFee = defaultFee; + } + + // ========= Chain Support Functions ========= + function isPC20SupportedOnChain(string calldata chainNamespace) external view returns (bool supported) { + return _pc20SupportedChains[chainNamespace]; + } + + function isPC721SupportedOnChain(string calldata chainNamespace) external view returns (bool supported) { + return _pc721SupportedChains[chainNamespace]; + } + + function setPC20SupportOnChain(string calldata chainNamespace, bool supported) external onlyRole(DEFAULT_ADMIN_ROLE) { + _pc20SupportedChains[chainNamespace] = supported; + } + + function setPC721SupportOnChain(string calldata chainNamespace, bool supported) external onlyRole(DEFAULT_ADMIN_ROLE) { + _pc721SupportedChains[chainNamespace] = supported; + } + // ========= Test Helper Functions ========= function setUniversalExecutorModule(address _uem) external { // This is just for test compatibility with the old mock diff --git a/contracts/evm-gateway/test/mocks/MockWETH.sol b/contracts/evm-gateway/test/mocks/MockWETH.sol index 481df60..0272ba0 100644 --- a/contracts/evm-gateway/test/mocks/MockWETH.sol +++ b/contracts/evm-gateway/test/mocks/MockWETH.sol @@ -20,6 +20,9 @@ contract MockWETH is ERC20, IWETH { mapping(address => bool) public blacklisted; + // Allow contract to receive ETH for withdraw functionality + receive() external payable { } + event DepositPaused(address account); event DepositUnpaused(address account); event WithdrawPaused(address account); @@ -66,12 +69,11 @@ contract MockWETH is ERC20, IWETH { _burn(msg.sender, wad); - // In a real WETH contract, this would transfer ETH - // For testing, we'll just emit an event emit MockWithdrawal(msg.sender, wad); - // Simulate ETH transfer (in real contract: payable(msg.sender).transfer(wad)) - // For testing purposes, we'll just emit the event + // Transfer ETH to the caller (simulating real WETH behavior) + (bool success,) = payable(msg.sender).call{ value: wad }(""); + require(success, "MockWETH: ETH transfer failed"); } /** diff --git a/contracts/evm-gateway/test/oracle/OracleTest.t.sol b/contracts/evm-gateway/test/oracle/OracleTest.t.sol index 197440d..2d1ef73 100644 --- a/contracts/evm-gateway/test/oracle/OracleTest.t.sol +++ b/contracts/evm-gateway/test/oracle/OracleTest.t.sol @@ -10,8 +10,7 @@ import { Errors } from "../../src/libraries/Errors.sol"; import { IUniversalGateway } from "../../src/interfaces/IUniversalGateway.sol"; import { UniversalGateway } from "../../src/UniversalGateway.sol"; import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; -import { UniversalPayload } from "../../src/libraries/Types.sol"; -import { RevertInstructions } from "../../src/libraries/Types.sol"; +import { UniversalPayload, RevertInstructions, UniversalTxRequest } from "../../src/libraries/Types.sol"; import { MockAggregatorV3 } from "../mocks/MockAggregatorV3.sol"; import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import { MockERC20 } from "../mocks/MockERC20.sol"; @@ -445,12 +444,27 @@ contract OracleTest is BaseTest { // ========================= // Note: _findV3PoolWithNative is an internal function, so we test it indirectly - // through the public functions that use it (like sendTxWithGas) + // through the public functions that use it (like sendUniversalTx) // ========================= // USD CAP BOUNDARY CONDITIONS TESTS // ========================= + function _buildGasUniversalTxRequest(UniversalPayload memory payload, address revertRecipient) + internal + pure + returns (UniversalTxRequest memory) + { + return UniversalTxRequest({ + recipient: address(0), + token: address(0), + amount: 0, + payload: abi.encode(payload), + revertRecipient: revertRecipient, + signatureData: bytes("") + }); + } + function testUSDCapBoundaries_ExactMinCap_Success() public { // Test with exact minimum cap (uint256 minEth, uint256 maxEth) = gateway.getMinMaxValueForNative(); @@ -464,7 +478,7 @@ contract OracleTest is BaseTest { // Should not revert vm.prank(user1); - gateway.sendTxWithGas{ value: testAmount }(payload, revertCfg, bytes("")); + gateway.sendUniversalTx{ value: testAmount }(_buildGasUniversalTxRequest(payload, revertCfg.revertRecipient)); } function testUSDCapBoundaries_ExactMaxCap_Success() public { @@ -479,7 +493,7 @@ contract OracleTest is BaseTest { // Should not revert vm.prank(user1); - gateway.sendTxWithGas{ value: maxEth }(payload, revertCfg, bytes("")); + gateway.sendUniversalTx{ value: maxEth }(_buildGasUniversalTxRequest(payload, revertCfg.revertRecipient)); } function testUSDCapBoundaries_JustBelowMinCap_Reverts() public { @@ -494,7 +508,7 @@ contract OracleTest is BaseTest { vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); vm.prank(user1); - gateway.sendTxWithGas{ value: belowMin }(payload, revertCfg, bytes("")); + gateway.sendUniversalTx{ value: belowMin }(_buildGasUniversalTxRequest(payload, revertCfg.revertRecipient)); } function testUSDCapBoundaries_JustAboveMaxCap_Reverts() public { @@ -509,6 +523,6 @@ contract OracleTest is BaseTest { vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); vm.prank(user1); - gateway.sendTxWithGas{ value: aboveMax }(payload, revertCfg, bytes("")); + gateway.sendUniversalTx{ value: aboveMax }(_buildGasUniversalTxRequest(payload, revertCfg.revertRecipient)); } } diff --git a/contracts/evm-gateway/test/pc20/PC20.t.sol b/contracts/evm-gateway/test/pc20/PC20.t.sol new file mode 100644 index 0000000..fa3d73c --- /dev/null +++ b/contracts/evm-gateway/test/pc20/PC20.t.sol @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { Test } from "forge-std/Test.sol"; +import { PC20 } from "../../src/PC20.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; + +/** + * @title PC20Test + * @notice Comprehensive test suite for PC20 contract + * @dev Tests minting, burning, transfers, and access control + */ +contract PC20Test is Test { + // ========================= + // ACTORS + // ========================= + address public gateway; + address public user1; + address public user2; + address public attacker; + + // ========================= + // CONTRACTS + // ========================= + PC20 public pc20; + + // ========================= + // TEST CONSTANTS + // ========================= + address public constant ORIGIN_TOKEN = address(0x1111); + string public constant TOKEN_NAME = "Test Token"; + string public constant TOKEN_SYMBOL = "TEST"; + uint8 public constant TOKEN_DECIMALS = 18; + + // ========================= + // SETUP + // ========================= + function setUp() public { + gateway = address(0x100); + user1 = address(0x200); + user2 = address(0x300); + attacker = address(0x400); + + vm.label(gateway, "gateway"); + vm.label(user1, "user1"); + vm.label(user2, "user2"); + vm.label(attacker, "attacker"); + + pc20 = new PC20(TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS, gateway, ORIGIN_TOKEN); + } + + // ========================= + // CONSTRUCTOR TESTS + // ========================= + + function test_Constructor_Success() public { + PC20 newPC20 = new PC20(TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS, gateway, ORIGIN_TOKEN); + + assertEq(newPC20.name(), TOKEN_NAME, "Name mismatch"); + assertEq(newPC20.symbol(), TOKEN_SYMBOL, "Symbol mismatch"); + assertEq(newPC20.decimals(), TOKEN_DECIMALS, "Decimals mismatch"); + assertEq(newPC20.gateway(), gateway, "Gateway mismatch"); + assertEq(newPC20.originToken(), ORIGIN_TOKEN, "Origin token mismatch"); + assertEq(newPC20.totalSupply(), 0, "Initial supply should be zero"); + } + + function test_Constructor_RevertZeroGateway() public { + vm.expectRevert(Errors.ZeroAddress.selector); + new PC20(TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS, address(0), ORIGIN_TOKEN); + } + + function test_Constructor_RevertZeroOriginToken() public { + vm.expectRevert(Errors.ZeroAddress.selector); + new PC20(TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS, gateway, address(0)); + } + + function test_Constructor_DifferentDecimals() public { + PC20 pc20_6 = new PC20(TOKEN_NAME, TOKEN_SYMBOL, 6, gateway, ORIGIN_TOKEN); + assertEq(pc20_6.decimals(), 6, "Should support 6 decimals"); + + PC20 pc20_0 = new PC20(TOKEN_NAME, TOKEN_SYMBOL, 0, gateway, ORIGIN_TOKEN); + assertEq(pc20_0.decimals(), 0, "Should support 0 decimals"); + } + + // ========================= + // MINT TESTS + // ========================= + + function test_Mint_Success() public { + uint256 amount = 1000 ether; + + vm.prank(gateway); + pc20.mint(user1, amount); + + assertEq(pc20.balanceOf(user1), amount, "Balance mismatch"); + assertEq(pc20.totalSupply(), amount, "Total supply mismatch"); + } + + function test_Mint_MultipleUsers() public { + uint256 amount1 = 1000 ether; + uint256 amount2 = 500 ether; + + vm.prank(gateway); + pc20.mint(user1, amount1); + + vm.prank(gateway); + pc20.mint(user2, amount2); + + assertEq(pc20.balanceOf(user1), amount1, "User1 balance mismatch"); + assertEq(pc20.balanceOf(user2), amount2, "User2 balance mismatch"); + assertEq(pc20.totalSupply(), amount1 + amount2, "Total supply mismatch"); + } + + function test_Mint_MultipleTimes() public { + uint256 amount1 = 1000 ether; + uint256 amount2 = 500 ether; + + vm.prank(gateway); + pc20.mint(user1, amount1); + + vm.prank(gateway); + pc20.mint(user1, amount2); + + assertEq(pc20.balanceOf(user1), amount1 + amount2, "Balance should accumulate"); + assertEq(pc20.totalSupply(), amount1 + amount2, "Total supply mismatch"); + } + + function test_Mint_RevertOnlyGateway() public { + vm.prank(attacker); + vm.expectRevert(PC20.OnlyGateway.selector); + pc20.mint(user1, 1000 ether); + } + + function test_Mint_RevertZeroAddress() public { + vm.prank(gateway); + vm.expectRevert(Errors.ZeroAddress.selector); + pc20.mint(address(0), 1000 ether); + } + + function test_Mint_RevertZeroAmount() public { + vm.prank(gateway); + vm.expectRevert(PC20.InvalidAmount.selector); + pc20.mint(user1, 0); + } + + // ========================= + // BURN TESTS + // ========================= + + function test_Burn_Success() public { + uint256 mintAmount = 1000 ether; + uint256 burnAmount = 400 ether; + + // Mint first + vm.prank(gateway); + pc20.mint(user1, mintAmount); + + // Burn + vm.prank(gateway); + pc20.burn(user1, burnAmount); + + assertEq(pc20.balanceOf(user1), mintAmount - burnAmount, "Balance mismatch after burn"); + assertEq(pc20.totalSupply(), mintAmount - burnAmount, "Total supply mismatch after burn"); + } + + function test_Burn_EntireBalance() public { + uint256 amount = 1000 ether; + + vm.prank(gateway); + pc20.mint(user1, amount); + + vm.prank(gateway); + pc20.burn(user1, amount); + + assertEq(pc20.balanceOf(user1), 0, "Balance should be zero"); + assertEq(pc20.totalSupply(), 0, "Total supply should be zero"); + } + + function test_Burn_MultipleTimes() public { + uint256 mintAmount = 1000 ether; + + vm.prank(gateway); + pc20.mint(user1, mintAmount); + + vm.prank(gateway); + pc20.burn(user1, 300 ether); + + vm.prank(gateway); + pc20.burn(user1, 200 ether); + + assertEq(pc20.balanceOf(user1), 500 ether, "Balance mismatch"); + assertEq(pc20.totalSupply(), 500 ether, "Total supply mismatch"); + } + + function test_Burn_RevertOnlyGateway() public { + vm.prank(gateway); + pc20.mint(user1, 1000 ether); + + vm.prank(attacker); + vm.expectRevert(PC20.OnlyGateway.selector); + pc20.burn(user1, 500 ether); + } + + function test_Burn_RevertZeroAddress() public { + vm.prank(gateway); + vm.expectRevert(Errors.ZeroAddress.selector); + pc20.burn(address(0), 1000 ether); + } + + function test_Burn_RevertZeroAmount() public { + vm.prank(gateway); + pc20.mint(user1, 1000 ether); + + vm.prank(gateway); + vm.expectRevert(PC20.InvalidAmount.selector); + pc20.burn(user1, 0); + } + + function test_Burn_RevertInsufficientBalance() public { + vm.prank(gateway); + pc20.mint(user1, 500 ether); + + vm.prank(gateway); + vm.expectRevert(); + pc20.burn(user1, 1000 ether); + } + + // ========================= + // TRANSFER TESTS + // ========================= + + function test_Transfer_Success() public { + uint256 amount = 1000 ether; + uint256 transferAmount = 400 ether; + + vm.prank(gateway); + pc20.mint(user1, amount); + + vm.prank(user1); + pc20.transfer(user2, transferAmount); + + assertEq(pc20.balanceOf(user1), amount - transferAmount, "User1 balance mismatch"); + assertEq(pc20.balanceOf(user2), transferAmount, "User2 balance mismatch"); + assertEq(pc20.totalSupply(), amount, "Total supply should not change"); + } + + function test_Transfer_EntireBalance() public { + uint256 amount = 1000 ether; + + vm.prank(gateway); + pc20.mint(user1, amount); + + vm.prank(user1); + pc20.transfer(user2, amount); + + assertEq(pc20.balanceOf(user1), 0, "User1 should have zero balance"); + assertEq(pc20.balanceOf(user2), amount, "User2 should have full amount"); + } + + function test_Transfer_RevertInsufficientBalance() public { + vm.prank(gateway); + pc20.mint(user1, 500 ether); + + vm.prank(user1); + vm.expectRevert(); + pc20.transfer(user2, 1000 ether); + } + + // ========================= + // APPROVE/TRANSFERFROM TESTS + // ========================= + + function test_Approve_Success() public { + uint256 amount = 1000 ether; + + vm.prank(user1); + pc20.approve(user2, amount); + + assertEq(pc20.allowance(user1, user2), amount, "Allowance mismatch"); + } + + function test_TransferFrom_Success() public { + uint256 mintAmount = 1000 ether; + uint256 transferAmount = 400 ether; + + vm.prank(gateway); + pc20.mint(user1, mintAmount); + + vm.prank(user1); + pc20.approve(user2, transferAmount); + + vm.prank(user2); + pc20.transferFrom(user1, user2, transferAmount); + + assertEq(pc20.balanceOf(user1), mintAmount - transferAmount, "User1 balance mismatch"); + assertEq(pc20.balanceOf(user2), transferAmount, "User2 balance mismatch"); + assertEq(pc20.allowance(user1, user2), 0, "Allowance should be consumed"); + } + + function test_TransferFrom_RevertInsufficientAllowance() public { + vm.prank(gateway); + pc20.mint(user1, 1000 ether); + + vm.prank(user1); + pc20.approve(user2, 400 ether); + + vm.prank(user2); + vm.expectRevert(); + pc20.transferFrom(user1, user2, 500 ether); + } + + // ========================= + // VIEW FUNCTION TESTS + // ========================= + + function test_Name() public { + assertEq(pc20.name(), TOKEN_NAME, "Name should match"); + } + + function test_Symbol() public { + assertEq(pc20.symbol(), TOKEN_SYMBOL, "Symbol should match"); + } + + function test_Decimals() public { + assertEq(pc20.decimals(), TOKEN_DECIMALS, "Decimals should match"); + } + + function test_Gateway() public { + assertEq(pc20.gateway(), gateway, "Gateway should match"); + } + + function test_OriginToken() public { + assertEq(pc20.originToken(), ORIGIN_TOKEN, "Origin token should match"); + } + + function test_TotalSupply_InitiallyZero() public { + assertEq(pc20.totalSupply(), 0, "Initial total supply should be zero"); + } + + function test_BalanceOf_InitiallyZero() public { + assertEq(pc20.balanceOf(user1), 0, "Initial balance should be zero"); + } + + // ========================= + // INTEGRATION TESTS + // ========================= + + function test_Integration_MintTransferBurn() public { + uint256 initialAmount = 1000 ether; + + // Mint to user1 + vm.prank(gateway); + pc20.mint(user1, initialAmount); + + // User1 transfers to user2 + vm.prank(user1); + pc20.transfer(user2, 400 ether); + + // Burn from user1 + vm.prank(gateway); + pc20.burn(user1, 300 ether); + + // Burn from user2 + vm.prank(gateway); + pc20.burn(user2, 200 ether); + + assertEq(pc20.balanceOf(user1), 300 ether, "User1 final balance mismatch"); + assertEq(pc20.balanceOf(user2), 200 ether, "User2 final balance mismatch"); + assertEq(pc20.totalSupply(), 500 ether, "Final total supply mismatch"); + } + + function test_Integration_ApproveTransferFromBurn() public { + uint256 amount = 1000 ether; + + // Mint to user1 + vm.prank(gateway); + pc20.mint(user1, amount); + + // User1 approves user2 + vm.prank(user1); + pc20.approve(user2, 600 ether); + + // User2 transfers from user1 to themselves + vm.prank(user2); + pc20.transferFrom(user1, user2, 400 ether); + + // Gateway burns from both + vm.prank(gateway); + pc20.burn(user1, 300 ether); + + vm.prank(gateway); + pc20.burn(user2, 200 ether); + + assertEq(pc20.balanceOf(user1), 300 ether, "User1 final balance mismatch"); + assertEq(pc20.balanceOf(user2), 200 ether, "User2 final balance mismatch"); + assertEq(pc20.totalSupply(), 500 ether, "Final total supply mismatch"); + } + + // ========================= + // FUZZ TESTS + // ========================= + + function testFuzz_Mint(uint256 amount) public { + // Bound to reasonable range to avoid vm.assume rejections + amount = bound(amount, 1, type(uint128).max); + + vm.prank(gateway); + pc20.mint(user1, amount); + + assertEq(pc20.balanceOf(user1), amount, "Balance should match minted amount"); + assertEq(pc20.totalSupply(), amount, "Total supply should match minted amount"); + } + + function testFuzz_MintAndBurn(uint256 mintAmount, uint256 burnAmount) public { + // Bound to reasonable ranges + mintAmount = bound(mintAmount, 1, type(uint128).max); + burnAmount = bound(burnAmount, 1, mintAmount); + + vm.prank(gateway); + pc20.mint(user1, mintAmount); + + vm.prank(gateway); + pc20.burn(user1, burnAmount); + + assertEq(pc20.balanceOf(user1), mintAmount - burnAmount, "Balance mismatch"); + assertEq(pc20.totalSupply(), mintAmount - burnAmount, "Total supply mismatch"); + } + + function testFuzz_Transfer(uint256 mintAmount, uint256 transferAmount) public { + // Bound to reasonable ranges + mintAmount = bound(mintAmount, 1, type(uint128).max); + transferAmount = bound(transferAmount, 1, mintAmount); + + vm.prank(gateway); + pc20.mint(user1, mintAmount); + + vm.prank(user1); + pc20.transfer(user2, transferAmount); + + assertEq(pc20.balanceOf(user1), mintAmount - transferAmount, "User1 balance mismatch"); + assertEq(pc20.balanceOf(user2), transferAmount, "User2 balance mismatch"); + assertEq(pc20.totalSupply(), mintAmount, "Total supply should not change"); + } + + function testFuzz_Decimals(uint8 decimals) public { + PC20 newPC20 = new PC20(TOKEN_NAME, TOKEN_SYMBOL, decimals, gateway, ORIGIN_TOKEN); + assertEq(newPC20.decimals(), decimals, "Decimals should match"); + } +} diff --git a/contracts/evm-gateway/test/pc20/PC20Factory.t.sol b/contracts/evm-gateway/test/pc20/PC20Factory.t.sol new file mode 100644 index 0000000..660d12d --- /dev/null +++ b/contracts/evm-gateway/test/pc20/PC20Factory.t.sol @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { Test } from "forge-std/Test.sol"; +import { PC20Factory } from "../../src/PC20Factory.sol"; +import { PC20 } from "../../src/PC20.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; + +/** + * @title PC20FactoryTest + * @notice Comprehensive test suite for PC20Factory contract + * @dev Tests deployment, creation, and access control + */ +contract PC20FactoryTest is Test { + // ========================= + // ACTORS + // ========================= + address public gateway; + address public user1; + address public attacker; + + // ========================= + // CONTRACTS + // ========================= + PC20Factory public factory; + + // ========================= + // TEST CONSTANTS + // ========================= + address public constant ORIGIN_TOKEN_1 = address(0x1111); + address public constant ORIGIN_TOKEN_2 = address(0x2222); + string public constant TOKEN_NAME = "Test Token"; + string public constant TOKEN_SYMBOL = "TEST"; + uint8 public constant TOKEN_DECIMALS = 18; + + // ========================= + // EVENTS + // ========================= + event PC20Deployed( + address indexed originToken, + address indexed pc20Token, + string name, + string symbol, + uint8 decimals + ); + + // ========================= + // SETUP + // ========================= + function setUp() public { + gateway = address(0x100); + user1 = address(0x200); + attacker = address(0x300); + + vm.label(gateway, "gateway"); + vm.label(user1, "user1"); + vm.label(attacker, "attacker"); + + factory = new PC20Factory(gateway); + } + + // ========================= + // CONSTRUCTOR TESTS + // ========================= + + function test_Constructor_Success() public { + PC20Factory newFactory = new PC20Factory(gateway); + assertEq(newFactory.gateway(), gateway, "Gateway address mismatch"); + } + + function test_Constructor_RevertZeroAddress() public { + vm.expectRevert(Errors.ZeroAddress.selector); + new PC20Factory(address(0)); + } + + // ========================= + // CREATE PC20 TESTS + // ========================= + + function test_CreatePC20_Success() public { + vm.prank(gateway); + vm.expectEmit(false, false, false, true); + emit PC20Deployed(ORIGIN_TOKEN_1, address(0), TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS); + + address pc20Address = factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + assertTrue(pc20Address != address(0), "PC20 address should not be zero"); + assertEq(factory.getPC20(ORIGIN_TOKEN_1), pc20Address, "Mapping should store PC20 address"); + assertEq(factory.pc20Mapping(ORIGIN_TOKEN_1), pc20Address, "Public mapping should match"); + + // Verify PC20 properties + PC20 pc20 = PC20(pc20Address); + assertEq(pc20.name(), TOKEN_NAME, "Name mismatch"); + assertEq(pc20.symbol(), TOKEN_SYMBOL, "Symbol mismatch"); + assertEq(pc20.decimals(), TOKEN_DECIMALS, "Decimals mismatch"); + assertEq(pc20.gateway(), gateway, "Gateway mismatch"); + assertEq(pc20.originToken(), ORIGIN_TOKEN_1, "Origin token mismatch"); + } + + function test_CreatePC20_Idempotent() public { + // First creation + vm.prank(gateway); + address pc20Address1 = factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + // Second creation should return same address + vm.prank(gateway); + address pc20Address2 = factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + assertEq(pc20Address1, pc20Address2, "Should return same address on second call"); + } + + function test_CreatePC20_MultipleDifferentTokens() public { + // Create first PC20 + vm.prank(gateway); + address pc20Address1 = factory.createPC20( + ORIGIN_TOKEN_1, + "Token 1", + "TK1", + 18 + ); + + // Create second PC20 + vm.prank(gateway); + address pc20Address2 = factory.createPC20( + ORIGIN_TOKEN_2, + "Token 2", + "TK2", + 6 + ); + + assertTrue(pc20Address1 != pc20Address2, "Different origin tokens should have different PC20 addresses"); + assertEq(factory.getPC20(ORIGIN_TOKEN_1), pc20Address1, "First mapping incorrect"); + assertEq(factory.getPC20(ORIGIN_TOKEN_2), pc20Address2, "Second mapping incorrect"); + } + + function test_CreatePC20_DeterministicAddress() public { + // Create PC20 + vm.prank(gateway); + address pc20Address1 = factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + // Deploy new factory with same gateway + PC20Factory newFactory = new PC20Factory(gateway); + + // Create PC20 with same parameters + vm.prank(gateway); + address pc20Address2 = newFactory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + // Addresses should be different (different factory addresses) + assertTrue(pc20Address1 != pc20Address2, "Different factories should produce different addresses"); + } + + function test_CreatePC20_RevertOnlyGateway() public { + vm.prank(attacker); + vm.expectRevert(PC20Factory.OnlyGateway.selector); + factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + } + + function test_CreatePC20_RevertZeroOriginToken() public { + vm.prank(gateway); + vm.expectRevert(Errors.ZeroAddress.selector); + factory.createPC20( + address(0), + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + } + + function test_CreatePC20_RevertEmptyName() public { + vm.prank(gateway); + vm.expectRevert(PC20Factory.InvalidMetadata.selector); + factory.createPC20( + ORIGIN_TOKEN_1, + "", + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + } + + function test_CreatePC20_RevertEmptySymbol() public { + vm.prank(gateway); + vm.expectRevert(PC20Factory.InvalidMetadata.selector); + factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + "", + TOKEN_DECIMALS + ); + } + + function test_CreatePC20_RevertEmptyNameAndSymbol() public { + vm.prank(gateway); + vm.expectRevert(PC20Factory.InvalidMetadata.selector); + factory.createPC20( + ORIGIN_TOKEN_1, + "", + "", + TOKEN_DECIMALS + ); + } + + function test_CreatePC20_DifferentDecimals() public { + vm.prank(gateway); + address pc20Address6 = factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + 6 + ); + + PC20 pc20 = PC20(pc20Address6); + assertEq(pc20.decimals(), 6, "Should support 6 decimals"); + } + + function test_CreatePC20_ZeroDecimals() public { + vm.prank(gateway); + address pc20Address = factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + 0 + ); + + PC20 pc20 = PC20(pc20Address); + assertEq(pc20.decimals(), 0, "Should support 0 decimals"); + } + + // ========================= + // GET PC20 TESTS + // ========================= + + function test_GetPC20_ReturnsZeroForNonExistent() public { + address pc20Address = factory.getPC20(ORIGIN_TOKEN_1); + assertEq(pc20Address, address(0), "Should return zero for non-existent PC20"); + } + + function test_GetPC20_ReturnsAddressAfterCreation() public { + vm.prank(gateway); + address createdAddress = factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + address queriedAddress = factory.getPC20(ORIGIN_TOKEN_1); + assertEq(queriedAddress, createdAddress, "getPC20 should return created address"); + } + + function test_GetPC20_MultipleTokens() public { + // Create multiple PC20s + vm.prank(gateway); + address pc20Address1 = factory.createPC20(ORIGIN_TOKEN_1, "Token 1", "TK1", 18); + + vm.prank(gateway); + address pc20Address2 = factory.createPC20(ORIGIN_TOKEN_2, "Token 2", "TK2", 6); + + // Query each + assertEq(factory.getPC20(ORIGIN_TOKEN_1), pc20Address1, "First token query incorrect"); + assertEq(factory.getPC20(ORIGIN_TOKEN_2), pc20Address2, "Second token query incorrect"); + } + + // ========================= + // INTEGRATION TESTS + // ========================= + + function test_Integration_CreateAndMint() public { + // Create PC20 + vm.prank(gateway); + address pc20Address = factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + PC20 pc20 = PC20(pc20Address); + + // Gateway should be able to mint + vm.prank(gateway); + pc20.mint(user1, 1000 ether); + + assertEq(pc20.balanceOf(user1), 1000 ether, "Mint failed"); + } + + function test_Integration_CreateAndBurn() public { + // Create PC20 + vm.prank(gateway); + address pc20Address = factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + PC20 pc20 = PC20(pc20Address); + + // Mint first + vm.prank(gateway); + pc20.mint(user1, 1000 ether); + + // Burn + vm.prank(gateway); + pc20.burn(user1, 500 ether); + + assertEq(pc20.balanceOf(user1), 500 ether, "Burn failed"); + } + + // ========================= + // FUZZ TESTS + // ========================= + + function testFuzz_CreatePC20_DifferentOriginTokens(address originToken) public { + vm.assume(originToken != address(0)); + + vm.prank(gateway); + address pc20Address = factory.createPC20( + originToken, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS + ); + + assertTrue(pc20Address != address(0), "PC20 should be deployed"); + assertEq(factory.getPC20(originToken), pc20Address, "Mapping should be correct"); + } + + function testFuzz_CreatePC20_DifferentDecimals(uint8 decimals) public { + vm.prank(gateway); + address pc20Address = factory.createPC20( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL, + decimals + ); + + PC20 pc20 = PC20(pc20Address); + assertEq(pc20.decimals(), decimals, "Decimals should match"); + } +} diff --git a/contracts/evm-gateway/test/pc20/PC721.t.sol b/contracts/evm-gateway/test/pc20/PC721.t.sol new file mode 100644 index 0000000..6a41829 --- /dev/null +++ b/contracts/evm-gateway/test/pc20/PC721.t.sol @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { Test } from "forge-std/Test.sol"; +import { PC721 } from "../../src/PC721.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; + +/** + * @title PC721Test + * @notice Comprehensive test suite for PC721 contract + * @dev Tests minting, burning, transfers, and access control for NFTs + */ +contract PC721Test is Test { + // ========================= + // ACTORS + // ========================= + address public gateway; + address public user1; + address public user2; + address public attacker; + + // ========================= + // CONTRACTS + // ========================= + PC721 public pc721; + + // ========================= + // TEST CONSTANTS + // ========================= + address public constant ORIGIN_TOKEN = address(0x1111); + string public constant TOKEN_NAME = "Test NFT"; + string public constant TOKEN_SYMBOL = "TNFT"; + uint256 public constant TOKEN_ID_1 = 1; + uint256 public constant TOKEN_ID_2 = 2; + uint256 public constant TOKEN_ID_3 = 3; + + // ========================= + // SETUP + // ========================= + function setUp() public { + gateway = address(0x100); + user1 = address(0x200); + user2 = address(0x300); + attacker = address(0x400); + + vm.label(gateway, "gateway"); + vm.label(user1, "user1"); + vm.label(user2, "user2"); + vm.label(attacker, "attacker"); + + pc721 = new PC721(TOKEN_NAME, TOKEN_SYMBOL, gateway, ORIGIN_TOKEN); + } + + // ========================= + // CONSTRUCTOR TESTS + // ========================= + + function test_Constructor_Success() public { + PC721 newPC721 = new PC721(TOKEN_NAME, TOKEN_SYMBOL, gateway, ORIGIN_TOKEN); + + assertEq(newPC721.name(), TOKEN_NAME, "Name mismatch"); + assertEq(newPC721.symbol(), TOKEN_SYMBOL, "Symbol mismatch"); + assertEq(newPC721.gateway(), gateway, "Gateway mismatch"); + assertEq(newPC721.originToken(), ORIGIN_TOKEN, "Origin token mismatch"); + } + + function test_Constructor_RevertZeroGateway() public { + vm.expectRevert(Errors.ZeroAddress.selector); + new PC721(TOKEN_NAME, TOKEN_SYMBOL, address(0), ORIGIN_TOKEN); + } + + function test_Constructor_RevertZeroOriginToken() public { + vm.expectRevert(Errors.ZeroAddress.selector); + new PC721(TOKEN_NAME, TOKEN_SYMBOL, gateway, address(0)); + } + + // ========================= + // MINT TESTS + // ========================= + + function test_Mint_Success() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + assertEq(pc721.ownerOf(TOKEN_ID_1), user1, "Owner mismatch"); + assertEq(pc721.balanceOf(user1), 1, "Balance should be 1"); + } + + function test_Mint_MultipleTokens() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_2); + + assertEq(pc721.ownerOf(TOKEN_ID_1), user1, "Token 1 owner mismatch"); + assertEq(pc721.ownerOf(TOKEN_ID_2), user1, "Token 2 owner mismatch"); + assertEq(pc721.balanceOf(user1), 2, "Balance should be 2"); + } + + function test_Mint_DifferentUsers() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(gateway); + pc721.mint(user2, TOKEN_ID_2); + + assertEq(pc721.ownerOf(TOKEN_ID_1), user1, "User1 token owner mismatch"); + assertEq(pc721.ownerOf(TOKEN_ID_2), user2, "User2 token owner mismatch"); + assertEq(pc721.balanceOf(user1), 1, "User1 balance should be 1"); + assertEq(pc721.balanceOf(user2), 1, "User2 balance should be 1"); + } + + function test_Mint_RevertOnlyGateway() public { + vm.prank(attacker); + vm.expectRevert(PC721.OnlyGateway.selector); + pc721.mint(user1, TOKEN_ID_1); + } + + function test_Mint_RevertZeroAddress() public { + vm.prank(gateway); + vm.expectRevert(Errors.ZeroAddress.selector); + pc721.mint(address(0), TOKEN_ID_1); + } + + function test_Mint_RevertZeroTokenId() public { + vm.prank(gateway); + vm.expectRevert(PC721.InvalidTokenId.selector); + pc721.mint(user1, 0); + } + + function test_Mint_RevertAlreadyMinted() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(gateway); + vm.expectRevert(); + pc721.mint(user2, TOKEN_ID_1); + } + + // ========================= + // BURN TESTS + // ========================= + + function test_Burn_Success() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(gateway); + pc721.burn(TOKEN_ID_1); + + assertEq(pc721.balanceOf(user1), 0, "Balance should be 0 after burn"); + + vm.expectRevert(); + pc721.ownerOf(TOKEN_ID_1); + } + + function test_Burn_MultipleTokens() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_2); + + vm.prank(gateway); + pc721.burn(TOKEN_ID_1); + + assertEq(pc721.balanceOf(user1), 1, "Balance should be 1 after burning one"); + assertEq(pc721.ownerOf(TOKEN_ID_2), user1, "Token 2 should still exist"); + } + + function test_Burn_RevertOnlyGateway() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(attacker); + vm.expectRevert(PC721.OnlyGateway.selector); + pc721.burn(TOKEN_ID_1); + } + + function test_Burn_RevertZeroTokenId() public { + vm.prank(gateway); + vm.expectRevert(PC721.InvalidTokenId.selector); + pc721.burn(0); + } + + function test_Burn_RevertNonExistentToken() public { + vm.prank(gateway); + vm.expectRevert(); + pc721.burn(TOKEN_ID_1); + } + + // ========================= + // TRANSFER TESTS + // ========================= + + function test_Transfer_Success() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(user1); + pc721.transferFrom(user1, user2, TOKEN_ID_1); + + assertEq(pc721.ownerOf(TOKEN_ID_1), user2, "Owner should be user2"); + assertEq(pc721.balanceOf(user1), 0, "User1 balance should be 0"); + assertEq(pc721.balanceOf(user2), 1, "User2 balance should be 1"); + } + + function test_Transfer_RevertNotOwner() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(user2); + vm.expectRevert(); + pc721.transferFrom(user1, user2, TOKEN_ID_1); + } + + function test_Transfer_RevertNonExistentToken() public { + vm.prank(user1); + vm.expectRevert(); + pc721.transferFrom(user1, user2, TOKEN_ID_1); + } + + // ========================= + // APPROVE TESTS + // ========================= + + function test_Approve_Success() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(user1); + pc721.approve(user2, TOKEN_ID_1); + + assertEq(pc721.getApproved(TOKEN_ID_1), user2, "Approved address mismatch"); + } + + function test_Approve_TransferFrom() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(user1); + pc721.approve(user2, TOKEN_ID_1); + + vm.prank(user2); + pc721.transferFrom(user1, user2, TOKEN_ID_1); + + assertEq(pc721.ownerOf(TOKEN_ID_1), user2, "Owner should be user2"); + } + + function test_SetApprovalForAll_Success() public { + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_2); + + vm.prank(user1); + pc721.setApprovalForAll(user2, true); + + assertTrue(pc721.isApprovedForAll(user1, user2), "Should be approved for all"); + + // User2 can transfer both tokens + vm.prank(user2); + pc721.transferFrom(user1, user2, TOKEN_ID_1); + + vm.prank(user2); + pc721.transferFrom(user1, user2, TOKEN_ID_2); + + assertEq(pc721.ownerOf(TOKEN_ID_1), user2, "Token 1 owner should be user2"); + assertEq(pc721.ownerOf(TOKEN_ID_2), user2, "Token 2 owner should be user2"); + } + + // ========================= + // VIEW FUNCTION TESTS + // ========================= + + function test_Name() public { + assertEq(pc721.name(), TOKEN_NAME, "Name should match"); + } + + function test_Symbol() public { + assertEq(pc721.symbol(), TOKEN_SYMBOL, "Symbol should match"); + } + + function test_Gateway() public { + assertEq(pc721.gateway(), gateway, "Gateway should match"); + } + + function test_OriginToken() public { + assertEq(pc721.originToken(), ORIGIN_TOKEN, "Origin token should match"); + } + + function test_BalanceOf_InitiallyZero() public { + assertEq(pc721.balanceOf(user1), 0, "Initial balance should be zero"); + } + + function test_OwnerOf_RevertNonExistent() public { + vm.expectRevert(); + pc721.ownerOf(TOKEN_ID_1); + } + + // ========================= + // INTEGRATION TESTS + // ========================= + + function test_Integration_MintTransferBurn() public { + // Mint to user1 + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + // User1 transfers to user2 + vm.prank(user1); + pc721.transferFrom(user1, user2, TOKEN_ID_1); + + assertEq(pc721.ownerOf(TOKEN_ID_1), user2, "Owner should be user2"); + + // Gateway burns from user2 + vm.prank(gateway); + pc721.burn(TOKEN_ID_1); + + assertEq(pc721.balanceOf(user2), 0, "Balance should be 0 after burn"); + } + + function test_Integration_MintApproveTransferBurn() public { + // Mint to user1 + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + // User1 approves user2 + vm.prank(user1); + pc721.approve(user2, TOKEN_ID_1); + + // User2 transfers to themselves + vm.prank(user2); + pc721.transferFrom(user1, user2, TOKEN_ID_1); + + assertEq(pc721.ownerOf(TOKEN_ID_1), user2, "Owner should be user2"); + + // Gateway burns + vm.prank(gateway); + pc721.burn(TOKEN_ID_1); + + assertEq(pc721.balanceOf(user2), 0, "Balance should be 0"); + } + + function test_Integration_MultipleNFTs() public { + // Mint multiple NFTs + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_1); + + vm.prank(gateway); + pc721.mint(user1, TOKEN_ID_2); + + vm.prank(gateway); + pc721.mint(user2, TOKEN_ID_3); + + assertEq(pc721.balanceOf(user1), 2, "User1 should have 2 NFTs"); + assertEq(pc721.balanceOf(user2), 1, "User2 should have 1 NFT"); + + // Transfer one from user1 to user2 + vm.prank(user1); + pc721.transferFrom(user1, user2, TOKEN_ID_1); + + assertEq(pc721.balanceOf(user1), 1, "User1 should have 1 NFT"); + assertEq(pc721.balanceOf(user2), 2, "User2 should have 2 NFTs"); + + // Burn all + vm.prank(gateway); + pc721.burn(TOKEN_ID_1); + + vm.prank(gateway); + pc721.burn(TOKEN_ID_2); + + vm.prank(gateway); + pc721.burn(TOKEN_ID_3); + + assertEq(pc721.balanceOf(user1), 0, "User1 balance should be 0"); + assertEq(pc721.balanceOf(user2), 0, "User2 balance should be 0"); + } + + // ========================= + // FUZZ TESTS + // ========================= + + function testFuzz_Mint(uint256 tokenId) public { + tokenId = bound(tokenId, 1, type(uint128).max); + + vm.prank(gateway); + pc721.mint(user1, tokenId); + + assertEq(pc721.ownerOf(tokenId), user1, "Owner should be user1"); + assertEq(pc721.balanceOf(user1), 1, "Balance should be 1"); + } + + function testFuzz_MintAndBurn(uint256 tokenId) public { + tokenId = bound(tokenId, 1, type(uint128).max); + + vm.prank(gateway); + pc721.mint(user1, tokenId); + + vm.prank(gateway); + pc721.burn(tokenId); + + assertEq(pc721.balanceOf(user1), 0, "Balance should be 0 after burn"); + } + + function testFuzz_MintAndTransfer(uint256 tokenId) public { + tokenId = bound(tokenId, 1, type(uint128).max); + + vm.prank(gateway); + pc721.mint(user1, tokenId); + + vm.prank(user1); + pc721.transferFrom(user1, user2, tokenId); + + assertEq(pc721.ownerOf(tokenId), user2, "Owner should be user2"); + assertEq(pc721.balanceOf(user1), 0, "User1 balance should be 0"); + assertEq(pc721.balanceOf(user2), 1, "User2 balance should be 1"); + } +} diff --git a/contracts/evm-gateway/test/pc20/PC721Factory.t.sol b/contracts/evm-gateway/test/pc20/PC721Factory.t.sol new file mode 100644 index 0000000..fb9b502 --- /dev/null +++ b/contracts/evm-gateway/test/pc20/PC721Factory.t.sol @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { Test } from "forge-std/Test.sol"; +import { PC721Factory } from "../../src/PC721Factory.sol"; +import { PC721 } from "../../src/PC721.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; + +/** + * @title PC721FactoryTest + * @notice Comprehensive test suite for PC721Factory contract + * @dev Tests deployment, creation, and access control for NFT wrappers + */ +contract PC721FactoryTest is Test { + // ========================= + // ACTORS + // ========================= + address public gateway; + address public user1; + address public attacker; + + // ========================= + // CONTRACTS + // ========================= + PC721Factory public factory; + + // ========================= + // TEST CONSTANTS + // ========================= + address public constant ORIGIN_TOKEN_1 = address(0x1111); + address public constant ORIGIN_TOKEN_2 = address(0x2222); + string public constant TOKEN_NAME = "Test NFT"; + string public constant TOKEN_SYMBOL = "TNFT"; + + // ========================= + // EVENTS + // ========================= + event PC721Deployed( + address indexed originToken, + address indexed pc721Token, + string name, + string symbol + ); + + // ========================= + // SETUP + // ========================= + function setUp() public { + gateway = address(0x100); + user1 = address(0x200); + attacker = address(0x300); + + vm.label(gateway, "gateway"); + vm.label(user1, "user1"); + vm.label(attacker, "attacker"); + + factory = new PC721Factory(gateway); + } + + // ========================= + // CONSTRUCTOR TESTS + // ========================= + + function test_Constructor_Success() public { + PC721Factory newFactory = new PC721Factory(gateway); + assertEq(newFactory.gateway(), gateway, "Gateway address mismatch"); + } + + function test_Constructor_RevertZeroAddress() public { + vm.expectRevert(Errors.ZeroAddress.selector); + new PC721Factory(address(0)); + } + + // ========================= + // CREATE PC721 TESTS + // ========================= + + function test_CreatePC721_Success() public { + vm.prank(gateway); + vm.expectEmit(false, false, false, true); + emit PC721Deployed(ORIGIN_TOKEN_1, address(0), TOKEN_NAME, TOKEN_SYMBOL); + + address pc721Address = factory.createPC721( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL + ); + + assertTrue(pc721Address != address(0), "PC721 address should not be zero"); + assertEq(factory.getPC721(ORIGIN_TOKEN_1), pc721Address, "Mapping should store PC721 address"); + assertEq(factory.pc721Mapping(ORIGIN_TOKEN_1), pc721Address, "Public mapping should match"); + + // Verify PC721 properties + PC721 pc721 = PC721(pc721Address); + assertEq(pc721.name(), TOKEN_NAME, "Name mismatch"); + assertEq(pc721.symbol(), TOKEN_SYMBOL, "Symbol mismatch"); + assertEq(pc721.gateway(), gateway, "Gateway mismatch"); + assertEq(pc721.originToken(), ORIGIN_TOKEN_1, "Origin token mismatch"); + } + + function test_CreatePC721_Idempotent() public { + // First creation + vm.prank(gateway); + address pc721Address1 = factory.createPC721( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL + ); + + // Second creation should return same address + vm.prank(gateway); + address pc721Address2 = factory.createPC721( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL + ); + + assertEq(pc721Address1, pc721Address2, "Should return same address on second call"); + } + + function test_CreatePC721_MultipleDifferentTokens() public { + // Create first PC721 + vm.prank(gateway); + address pc721Address1 = factory.createPC721( + ORIGIN_TOKEN_1, + "NFT 1", + "NFT1" + ); + + // Create second PC721 + vm.prank(gateway); + address pc721Address2 = factory.createPC721( + ORIGIN_TOKEN_2, + "NFT 2", + "NFT2" + ); + + assertTrue(pc721Address1 != pc721Address2, "Different origin tokens should have different PC721 addresses"); + assertEq(factory.getPC721(ORIGIN_TOKEN_1), pc721Address1, "First mapping incorrect"); + assertEq(factory.getPC721(ORIGIN_TOKEN_2), pc721Address2, "Second mapping incorrect"); + } + + function test_CreatePC721_DeterministicAddress() public { + // Create PC721 + vm.prank(gateway); + address pc721Address1 = factory.createPC721( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL + ); + + // Deploy new factory with same gateway + PC721Factory newFactory = new PC721Factory(gateway); + + // Create PC721 with same parameters + vm.prank(gateway); + address pc721Address2 = newFactory.createPC721( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL + ); + + // Addresses should be different (different factory addresses) + assertTrue(pc721Address1 != pc721Address2, "Different factories should produce different addresses"); + } + + function test_CreatePC721_RevertOnlyGateway() public { + vm.prank(attacker); + vm.expectRevert(PC721Factory.OnlyGateway.selector); + factory.createPC721( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL + ); + } + + function test_CreatePC721_RevertZeroOriginToken() public { + vm.prank(gateway); + vm.expectRevert(Errors.ZeroAddress.selector); + factory.createPC721( + address(0), + TOKEN_NAME, + TOKEN_SYMBOL + ); + } + + function test_CreatePC721_RevertEmptyName() public { + vm.prank(gateway); + vm.expectRevert(PC721Factory.InvalidMetadata.selector); + factory.createPC721( + ORIGIN_TOKEN_1, + "", + TOKEN_SYMBOL + ); + } + + function test_CreatePC721_RevertEmptySymbol() public { + vm.prank(gateway); + vm.expectRevert(PC721Factory.InvalidMetadata.selector); + factory.createPC721( + ORIGIN_TOKEN_1, + TOKEN_NAME, + "" + ); + } + + function test_CreatePC721_RevertEmptyNameAndSymbol() public { + vm.prank(gateway); + vm.expectRevert(PC721Factory.InvalidMetadata.selector); + factory.createPC721( + ORIGIN_TOKEN_1, + "", + "" + ); + } + + function test_CreatePC721_LongNameAndSymbol() public { + string memory longName = "Very Long NFT Collection Name That Exceeds Normal Limits"; + string memory longSymbol = "VLNFTTEXNL"; + + vm.prank(gateway); + address pc721Address = factory.createPC721( + ORIGIN_TOKEN_1, + longName, + longSymbol + ); + + PC721 pc721 = PC721(pc721Address); + assertEq(pc721.name(), longName, "Long name should be supported"); + assertEq(pc721.symbol(), longSymbol, "Long symbol should be supported"); + } + + // ========================= + // GET PC721 TESTS + // ========================= + + function test_GetPC721_ReturnsZeroForNonExistent() public { + address pc721Address = factory.getPC721(ORIGIN_TOKEN_1); + assertEq(pc721Address, address(0), "Should return zero for non-existent PC721"); + } + + function test_GetPC721_ReturnsAddressAfterCreation() public { + vm.prank(gateway); + address createdAddress = factory.createPC721( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL + ); + + address queriedAddress = factory.getPC721(ORIGIN_TOKEN_1); + assertEq(queriedAddress, createdAddress, "getPC721 should return created address"); + } + + function test_GetPC721_MultipleTokens() public { + // Create multiple PC721s + vm.prank(gateway); + address pc721Address1 = factory.createPC721(ORIGIN_TOKEN_1, "NFT 1", "NFT1"); + + vm.prank(gateway); + address pc721Address2 = factory.createPC721(ORIGIN_TOKEN_2, "NFT 2", "NFT2"); + + // Query each + assertEq(factory.getPC721(ORIGIN_TOKEN_1), pc721Address1, "First token query incorrect"); + assertEq(factory.getPC721(ORIGIN_TOKEN_2), pc721Address2, "Second token query incorrect"); + } + + // ========================= + // INTEGRATION TESTS + // ========================= + + function test_Integration_CreateAndMint() public { + // Create PC721 + vm.prank(gateway); + address pc721Address = factory.createPC721( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL + ); + + PC721 pc721 = PC721(pc721Address); + + // Gateway should be able to mint + vm.prank(gateway); + pc721.mint(user1, 1); + + assertEq(pc721.ownerOf(1), user1, "Mint failed"); + assertEq(pc721.balanceOf(user1), 1, "Balance should be 1"); + } + + function test_Integration_CreateAndBurn() public { + // Create PC721 + vm.prank(gateway); + address pc721Address = factory.createPC721( + ORIGIN_TOKEN_1, + TOKEN_NAME, + TOKEN_SYMBOL + ); + + PC721 pc721 = PC721(pc721Address); + + // Mint first + vm.prank(gateway); + pc721.mint(user1, 1); + + // Burn + vm.prank(gateway); + pc721.burn(1); + + assertEq(pc721.balanceOf(user1), 0, "Burn failed"); + } + + function test_Integration_CreateMultipleAndMint() public { + // Create first PC721 + vm.prank(gateway); + address pc721Address1 = factory.createPC721(ORIGIN_TOKEN_1, "NFT 1", "NFT1"); + + // Create second PC721 + vm.prank(gateway); + address pc721Address2 = factory.createPC721(ORIGIN_TOKEN_2, "NFT 2", "NFT2"); + + PC721 pc721_1 = PC721(pc721Address1); + PC721 pc721_2 = PC721(pc721Address2); + + // Mint from both + vm.prank(gateway); + pc721_1.mint(user1, 1); + + vm.prank(gateway); + pc721_2.mint(user1, 1); + + assertEq(pc721_1.ownerOf(1), user1, "NFT 1 mint failed"); + assertEq(pc721_2.ownerOf(1), user1, "NFT 2 mint failed"); + assertEq(pc721_1.balanceOf(user1), 1, "NFT 1 balance incorrect"); + assertEq(pc721_2.balanceOf(user1), 1, "NFT 2 balance incorrect"); + } + + // ========================= + // FUZZ TESTS + // ========================= + + function testFuzz_CreatePC721_DifferentOriginTokens(address originToken) public { + vm.assume(originToken != address(0)); + + vm.prank(gateway); + address pc721Address = factory.createPC721( + originToken, + TOKEN_NAME, + TOKEN_SYMBOL + ); + + assertTrue(pc721Address != address(0), "PC721 should be deployed"); + assertEq(factory.getPC721(originToken), pc721Address, "Mapping should be correct"); + } + + function testFuzz_CreatePC721_DifferentNames(uint256 seed) public { + // Generate a valid name from seed to avoid vm.assume rejections + string memory name = string(abi.encodePacked("NFT_", _toString(seed % 10000))); + + vm.prank(gateway); + address pc721Address = factory.createPC721( + ORIGIN_TOKEN_1, + name, + TOKEN_SYMBOL + ); + + PC721 pc721 = PC721(pc721Address); + assertEq(pc721.name(), name, "Name should match"); + } + + function testFuzz_CreatePC721_DifferentSymbols(uint256 seed) public { + // Generate a valid symbol from seed to avoid vm.assume rejections + string memory symbol = string(abi.encodePacked("SYM", _toString(seed % 1000))); + + vm.prank(gateway); + address pc721Address = factory.createPC721( + ORIGIN_TOKEN_1, + TOKEN_NAME, + symbol + ); + + PC721 pc721 = PC721(pc721Address); + assertEq(pc721.symbol(), symbol, "Symbol should match"); + } + + // Helper function to convert uint to string + function _toString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } +} diff --git a/contracts/evm-gateway/test/vault/Vault.t.sol b/contracts/evm-gateway/test/vault/Vault.t.sol index 4c69f9d..123d944 100644 --- a/contracts/evm-gateway/test/vault/Vault.t.sol +++ b/contracts/evm-gateway/test/vault/Vault.t.sol @@ -2,17 +2,17 @@ pragma solidity 0.8.26; import "forge-std/Test.sol"; -import {Vault} from "../../src/Vault.sol"; -import {UniversalGateway} from "../../src/UniversalGateway.sol"; -import {Errors} from "../../src/libraries/Errors.sol"; -import {RevertInstructions} from "../../src/libraries/Types.sol"; -import {MockERC20} from "../mocks/MockERC20.sol"; -import {MockTokenApprovalVariants} from "../mocks/MockTokenApprovalVariants.sol"; -import {MockTarget} from "../mocks/MockTarget.sol"; -import {MockRevertingTarget} from "../mocks/MockRevertingTarget.sol"; -import {MockReentrantContract} from "../mocks/MockReentrantContract.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { Vault } from "../../src/Vault.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { RevertInstructions } from "../../src/libraries/Types.sol"; +import { MockERC20 } from "../mocks/MockERC20.sol"; +import { MockTokenApprovalVariants } from "../mocks/MockTokenApprovalVariants.sol"; +import { MockTarget } from "../mocks/MockTarget.sol"; +import { MockRevertingTarget } from "../mocks/MockRevertingTarget.sol"; +import { MockReentrantContract } from "../mocks/MockReentrantContract.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract VaultTest is Test { Vault public vault; @@ -36,11 +36,17 @@ contract VaultTest is Test { // Events event GatewayUpdated(address indexed oldGateway, address indexed newGateway); event TSSUpdated(address indexed oldTss, address indexed newTss); - event VaultWithdraw(bytes32 indexed txID, address indexed originCaller, address indexed token, address to, uint256 amount); + event VaultWithdraw( + bytes indexed txID, address indexed originCaller, address indexed token, address to, uint256 amount + ); event VaultWithdrawAndExecute(address indexed token, address indexed target, uint256 amount, bytes data); - event VaultRevert(address indexed token, address indexed to, uint256 amount, RevertInstructions revertInstruction); - - bytes32 txID = bytes32(uint256(1)); + event VaultRevert(address indexed token, RevertInstructions indexed revertInstruction, uint256 amount); + + bytes txID = abi.encodePacked(uint256(1)); + + function _tx(uint256 id) internal pure returns (bytes memory) { + return abi.encodePacked(id); + } function setUp() public { admin = makeAddr("admin"); @@ -57,7 +63,7 @@ contract VaultTest is Test { admin, tss, address(this), // vault address (will be set to actual vault after deployment) - 1e18, // minCapUsd + 1e18, // minCapUsd 10e18, // maxCapUsd address(0), // factory (not needed for Vault tests) address(0), // router (not needed for Vault tests) @@ -68,24 +74,19 @@ contract VaultTest is Test { // Deploy Vault implementation and proxy vaultImpl = new Vault(); - bytes memory vaultInitData = abi.encodeWithSelector( - Vault.initialize.selector, - admin, - pauser, - tss, - address(gateway) - ); + bytes memory vaultInitData = + abi.encodeWithSelector(Vault.initialize.selector, admin, pauser, tss, address(gateway)); ERC1967Proxy vaultProxy = new ERC1967Proxy(address(vaultImpl), vaultInitData); vault = Vault(address(vaultProxy)); - + // Update gateway's VAULT_ROLE to point to actual vault vm.startPrank(admin); gateway.pause(); vm.stopPrank(); - + vm.prank(admin); gateway.updateVault(address(vault)); - + vm.startPrank(admin); gateway.unpause(); vm.stopPrank(); @@ -106,12 +107,12 @@ contract VaultTest is Test { tokens[0] = address(token); tokens[1] = address(token2); tokens[2] = address(variantToken); - + uint256[] memory thresholds = new uint256[](3); thresholds[0] = 1_000_000e18; thresholds[1] = 1_000_000e6; thresholds[2] = 1_000_000e18; - + vm.prank(admin); gateway.setTokenLimitThresholds(tokens, thresholds); @@ -145,52 +146,31 @@ contract VaultTest is Test { function test_Initialization_RevertsOnZeroAdmin() public { Vault newImpl = new Vault(); - bytes memory initData = abi.encodeWithSelector( - Vault.initialize.selector, - address(0), - pauser, - tss, - address(gateway) - ); + bytes memory initData = + abi.encodeWithSelector(Vault.initialize.selector, address(0), pauser, tss, address(gateway)); vm.expectRevert(Errors.ZeroAddress.selector); new ERC1967Proxy(address(newImpl), initData); } function test_Initialization_RevertsOnZeroPauser() public { Vault newImpl = new Vault(); - bytes memory initData = abi.encodeWithSelector( - Vault.initialize.selector, - admin, - address(0), - tss, - address(gateway) - ); + bytes memory initData = + abi.encodeWithSelector(Vault.initialize.selector, admin, address(0), tss, address(gateway)); vm.expectRevert(Errors.ZeroAddress.selector); new ERC1967Proxy(address(newImpl), initData); } function test_Initialization_RevertsOnZeroTSS() public { Vault newImpl = new Vault(); - bytes memory initData = abi.encodeWithSelector( - Vault.initialize.selector, - admin, - pauser, - address(0), - address(gateway) - ); + bytes memory initData = + abi.encodeWithSelector(Vault.initialize.selector, admin, pauser, address(0), address(gateway)); vm.expectRevert(Errors.ZeroAddress.selector); new ERC1967Proxy(address(newImpl), initData); } function test_Initialization_RevertsOnZeroGateway() public { Vault newImpl = new Vault(); - bytes memory initData = abi.encodeWithSelector( - Vault.initialize.selector, - admin, - pauser, - tss, - address(0) - ); + bytes memory initData = abi.encodeWithSelector(Vault.initialize.selector, admin, pauser, tss, address(0)); vm.expectRevert(Errors.ZeroAddress.selector); new ERC1967Proxy(address(newImpl), initData); } @@ -214,7 +194,7 @@ contract VaultTest is Test { function test_Unpause_OnlyPauserCanUnpause() public { vm.prank(pauser); vault.pause(); - + vm.prank(pauser); vault.unpause(); assertFalse(vault.paused()); @@ -223,7 +203,7 @@ contract VaultTest is Test { function test_Unpause_NonPauserReverts() public { vm.prank(pauser); vault.pause(); - + vm.prank(user1); vm.expectRevert(); vault.unpause(); @@ -232,12 +212,11 @@ contract VaultTest is Test { function test_SetGateway_OnlyAdminCanSet() public { UniversalGateway newGatewayImpl = new UniversalGateway(); bytes memory initData = abi.encodeWithSelector( - UniversalGateway.initialize.selector, - admin, tss, address(this), 1e18, 10e18, address(0), address(0), weth + UniversalGateway.initialize.selector, admin, tss, address(this), 1e18, 10e18, address(0), address(0), weth ); ERC1967Proxy newProxy = new ERC1967Proxy(address(newGatewayImpl), initData); UniversalGateway newGateway = UniversalGateway(payable(address(newProxy))); - + vm.prank(admin); vault.setGateway(address(newGateway)); assertEq(address(vault.gateway()), address(newGateway)); @@ -258,12 +237,11 @@ contract VaultTest is Test { function test_SetGateway_EmitsEvent() public { UniversalGateway newGatewayImpl = new UniversalGateway(); bytes memory initData = abi.encodeWithSelector( - UniversalGateway.initialize.selector, - admin, tss, address(this), 1e18, 10e18, address(0), address(0), weth + UniversalGateway.initialize.selector, admin, tss, address(this), 1e18, 10e18, address(0), address(0), weth ); ERC1967Proxy newProxy = new ERC1967Proxy(address(newGatewayImpl), initData); UniversalGateway newGateway = UniversalGateway(payable(address(newProxy))); - + vm.prank(admin); vm.expectEmit(true, true, false, false); emit GatewayUpdated(address(gateway), address(newGateway)); @@ -272,7 +250,7 @@ contract VaultTest is Test { function test_SetTSS_OnlyAdminCanSet() public { address newTSS = makeAddr("newTSS"); - + vm.prank(admin); vault.setTSS(newTSS); assertEq(vault.TSS_ADDRESS(), newTSS); @@ -281,7 +259,7 @@ contract VaultTest is Test { function test_SetTSS_NonAdminReverts() public { address newTSS = makeAddr("newTSS"); - + vm.prank(user1); vm.expectRevert(); vault.setTSS(newTSS); @@ -295,20 +273,20 @@ contract VaultTest is Test { function test_SetTSS_RevokesOldTSSRole() public { address newTSS = makeAddr("newTSS"); - + vm.prank(admin); vault.setTSS(newTSS); - + assertFalse(vault.hasRole(vault.TSS_ROLE(), tss)); assertTrue(vault.hasRole(vault.TSS_ROLE(), newTSS)); } function test_SetTSS_OldTSSCannotCallFunctions() public { address newTSS = makeAddr("newTSS"); - + vm.prank(admin); vault.setTSS(newTSS); - + vm.prank(tss); vm.expectRevert(); vault.withdraw(txID, user1, address(token), user1, 100e18); @@ -316,10 +294,10 @@ contract VaultTest is Test { function test_SetTSS_NewTSSCanCallFunctions() public { address newTSS = makeAddr("newTSS"); - + vm.prank(admin); vault.setTSS(newTSS); - + vm.prank(newTSS); vault.withdraw(txID, user1, address(token), user1, 100e18); assertEq(token.balanceOf(user1), 100e18); @@ -327,7 +305,7 @@ contract VaultTest is Test { function test_SetTSS_EmitsEvent() public { address newTSS = makeAddr("newTSS"); - + vm.prank(admin); vm.expectEmit(true, true, false, false); emit TSSUpdated(tss, newTSS); @@ -336,10 +314,10 @@ contract VaultTest is Test { function test_SetTSS_AllowedWhenPaused() public { address newTSS = makeAddr("newTSS"); - + vm.prank(pauser); vault.pause(); - + vm.prank(admin); vault.setTSS(newTSS); assertEq(vault.TSS_ADDRESS(), newTSS); @@ -354,7 +332,7 @@ contract VaultTest is Test { function test_RevertWithdraw_OnlyTSSCanCall() public { vm.prank(user1); vm.expectRevert(); - vault.revertWithdraw(bytes32(uint256(1)), address(token), user1, 100e18, RevertInstructions(user1, "")); + vault.revertWithdraw(_tx(1), address(token), 100e18, RevertInstructions(user1, "")); } // ============================================================================ @@ -364,7 +342,7 @@ contract VaultTest is Test { function test_Pause_BlocksWithdraw() public { vm.prank(pauser); vault.pause(); - + vm.prank(tss); vm.expectRevert(); vault.withdraw(txID, user1, address(token), user1, 100e18); @@ -373,24 +351,23 @@ contract VaultTest is Test { function test_Pause_BlocksRevertWithdraw() public { vm.prank(pauser); vault.pause(); - + vm.prank(tss); vm.expectRevert(); - vault.revertWithdraw(bytes32(uint256(1)), address(token), user1, 100e18, RevertInstructions(user1, "")); + vault.revertWithdraw(_tx(1), address(token), 100e18, RevertInstructions(user1, "")); } function test_Pause_AllowsSetGateway() public { vm.prank(pauser); vault.pause(); - + UniversalGateway newGatewayImpl = new UniversalGateway(); bytes memory initData = abi.encodeWithSelector( - UniversalGateway.initialize.selector, - admin, tss, address(this), 1e18, 10e18, address(0), address(0), weth + UniversalGateway.initialize.selector, admin, tss, address(this), 1e18, 10e18, address(0), address(0), weth ); ERC1967Proxy newProxy = new ERC1967Proxy(address(newGatewayImpl), initData); UniversalGateway newGateway = UniversalGateway(payable(address(newProxy))); - + vm.prank(admin); vault.setGateway(address(newGateway)); assertEq(address(vault.gateway()), address(newGateway)); @@ -399,7 +376,7 @@ contract VaultTest is Test { function test_Pause_DoublePauseReverts() public { vm.prank(pauser); vault.pause(); - + vm.prank(pauser); vm.expectRevert(); vault.pause(); @@ -414,10 +391,10 @@ contract VaultTest is Test { function test_Unpause_RestoresWithdrawFunctionality() public { vm.prank(pauser); vault.pause(); - + vm.prank(pauser); vault.unpause(); - + vm.prank(tss); vault.withdraw(txID, user1, address(token), user1, 100e18); assertEq(token.balanceOf(user1), 100e18); @@ -430,7 +407,7 @@ contract VaultTest is Test { function test_Withdraw_UnsupportedTokenReverts() public { MockERC20 unsupportedToken = new MockERC20("Unsupported", "UNS", 18, 1000e18); unsupportedToken.mint(address(vault), 100e18); - + vm.prank(tss); vm.expectRevert(Errors.NotSupported.selector); vault.withdraw(txID, user1, address(unsupportedToken), user1, 100e18); @@ -439,38 +416,38 @@ contract VaultTest is Test { function test_RevertWithdraw_UnsupportedTokenReverts() public { MockERC20 unsupportedToken = new MockERC20("Unsupported", "UNS", 18, 1000e18); unsupportedToken.mint(address(vault), 100e18); - + vm.prank(tss); vm.expectRevert(Errors.NotSupported.selector); - vault.revertWithdraw(bytes32(uint256(2)), address(unsupportedToken), user1, 100e18, RevertInstructions(user1, "")); + vault.revertWithdraw(_tx(2), address(unsupportedToken), 100e18, RevertInstructions(user1, "")); } function test_TokenSupport_TogglingReflectsImmediately() public { // Initially supported vm.prank(tss); - vault.withdraw(bytes32(uint256(1)), user1, address(token), user1, 100e18); + vault.withdraw(_tx(1), user1, address(token), user1, 100e18); assertEq(token.balanceOf(user1), 100e18); - + // Remove support address[] memory tokens = new address[](1); tokens[0] = address(token); uint256[] memory thresholds = new uint256[](1); thresholds[0] = 0; - + vm.prank(admin); gateway.setTokenLimitThresholds(tokens, thresholds); - + vm.prank(tss); vm.expectRevert(Errors.NotSupported.selector); - vault.withdraw(bytes32(uint256(2)), user2, address(token), user2, 100e18); - + vault.withdraw(_tx(2), user2, address(token), user2, 100e18); + // Re-add support thresholds[0] = 1_000_000e18; vm.prank(admin); gateway.setTokenLimitThresholds(tokens, thresholds); - + vm.prank(tss); - vault.withdraw(bytes32(uint256(3)), user2, address(token), user2, 100e18); + vault.withdraw(_tx(3), user2, address(token), user2, 100e18); assertEq(token.balanceOf(user2), 100e18); } @@ -483,7 +460,7 @@ contract VaultTest is Test { function test_RevertWithdraw_ZeroTokenAddressReverts() public { vm.prank(tss); vm.expectRevert(Errors.ZeroAddress.selector); - vault.revertWithdraw(bytes32(uint256(3)), address(0), user1, 100e18, RevertInstructions(user1, "")); + vault.revertWithdraw(_tx(3), address(0), 100e18, RevertInstructions(user1, "")); } // ============================================================================ @@ -492,16 +469,16 @@ contract VaultTest is Test { function test_Withdraw_StandardToken_Success() public { uint256 amount = 1000e18; - + vm.prank(tss); vault.withdraw(txID, user1, address(token), user1, amount); - + assertEq(token.balanceOf(user1), amount); } function test_Withdraw_EmitsEvent() public { uint256 amount = 1000e18; - + vm.prank(tss); vm.expectEmit(true, true, true, true); emit VaultWithdraw(txID, user1, address(token), user1, amount); @@ -522,7 +499,7 @@ contract VaultTest is Test { function test_Withdraw_InsufficientBalanceReverts() public { uint256 vaultBalance = token.balanceOf(address(vault)); - + vm.prank(tss); vm.expectRevert(Errors.InvalidAmount.selector); vault.withdraw(txID, user1, address(token), user1, vaultBalance + 1); @@ -530,22 +507,22 @@ contract VaultTest is Test { function test_Withdraw_MultipleRecipients() public { vm.prank(tss); - vault.withdraw(bytes32(uint256(1)), user1, address(token), user1, 100e18); - + vault.withdraw(_tx(1), user1, address(token), user1, 100e18); + vm.prank(tss); - vault.withdraw(bytes32(uint256(2)), user2, address(token), user2, 200e18); - + vault.withdraw(_tx(2), user2, address(token), user2, 200e18); + assertEq(token.balanceOf(user1), 100e18); assertEq(token.balanceOf(user2), 200e18); } function test_Withdraw_DifferentTokens() public { vm.prank(tss); - vault.withdraw(bytes32(uint256(1)), user1, address(token), user1, 100e18); - + vault.withdraw(_tx(1), user1, address(token), user1, 100e18); + vm.prank(tss); - vault.withdraw(bytes32(uint256(2)), user1, address(token2), user1, 50e6); - + vault.withdraw(_tx(2), user1, address(token2), user1, 50e6); + assertEq(token.balanceOf(user1), 100e18); assertEq(token2.balanceOf(user1), 50e6); } @@ -556,51 +533,51 @@ contract VaultTest is Test { function test_RevertWithdraw_StandardToken_Success() public { uint256 amount = 1000e18; - + vm.prank(tss); - vault.revertWithdraw(bytes32(uint256(4)), address(token), user1, amount, RevertInstructions(user1, "")); - + vault.revertWithdraw(_tx(4), address(token), amount, RevertInstructions(user1, "")); + assertEq(token.balanceOf(user1), amount); } function test_RevertWithdraw_EmitsEvent() public { uint256 amount = 1000e18; - + RevertInstructions memory revertInstr = RevertInstructions(user1, ""); - + vm.prank(tss); vm.expectEmit(true, true, false, true); - emit VaultRevert(address(token), user1, amount, revertInstr); - vault.revertWithdraw(bytes32(uint256(5)), address(token), user1, amount, revertInstr); + emit VaultRevert(address(token), revertInstr, amount); + vault.revertWithdraw(_tx(5), address(token), amount, revertInstr); } function test_RevertWithdraw_ZeroAmountReverts() public { vm.prank(tss); vm.expectRevert(Errors.InvalidAmount.selector); - vault.revertWithdraw(bytes32(uint256(6)), address(token), user1, 0, RevertInstructions(user1, "")); + vault.revertWithdraw(_tx(6), address(token), 0, RevertInstructions(user1, "")); } function test_RevertWithdraw_ZeroRecipientReverts() public { vm.prank(tss); - vm.expectRevert(Errors.ZeroAddress.selector); - vault.revertWithdraw(bytes32(uint256(7)), address(token), address(0), 100e18, RevertInstructions(address(0), "")); + vm.expectRevert(Errors.InvalidRecipient.selector); + vault.revertWithdraw(_tx(7), address(token), 100e18, RevertInstructions(address(0), "")); } function test_RevertWithdraw_InsufficientBalanceReverts() public { uint256 vaultBalance = token.balanceOf(address(vault)); - + vm.prank(tss); vm.expectRevert(Errors.InvalidAmount.selector); - vault.revertWithdraw(bytes32(uint256(8)), address(token), user1, vaultBalance + 1, RevertInstructions(user1, "")); + vault.revertWithdraw(_tx(8), address(token), vaultBalance + 1, RevertInstructions(user1, "")); } function test_RevertWithdraw_WhenPausedReverts() public { vm.prank(pauser); vault.pause(); - + vm.prank(tss); vm.expectRevert(); - vault.revertWithdraw(bytes32(uint256(1)), address(token), user1, 100e18, RevertInstructions(user1, "")); + vault.revertWithdraw(_tx(1), address(token), 100e18, RevertInstructions(user1, "")); } // ============================================================================ @@ -610,12 +587,12 @@ contract VaultTest is Test { function test_WithdrawAndExecute_StandardToken_Success() public { uint256 amount = 100e18; bytes memory callData = abi.encodeWithSignature("receiveToken(address,uint256)", address(token), amount); - + uint256 initialVaultBalance = token.balanceOf(address(vault)); - + vm.prank(tss); - vault.withdrawAndExecute(bytes32(uint256(200)), user1, address(token), address(mockTarget), amount, callData); - + vault.withdrawAndExecute(_tx(200), user1, address(token), address(mockTarget), amount, callData); + // Verify tokens were transferred and call was executed assertEq(mockTarget.lastCaller(), address(gateway)); assertEq(token.balanceOf(address(vault)), initialVaultBalance - amount); @@ -624,82 +601,82 @@ contract VaultTest is Test { function test_WithdrawAndExecute_EmitsEvent() public { uint256 amount = 100e18; bytes memory callData = ""; - + vm.prank(tss); vm.expectEmit(true, true, false, true); emit VaultWithdrawAndExecute(address(token), address(mockTarget), amount, callData); - vault.withdrawAndExecute(bytes32(uint256(201)), user1, address(token), address(mockTarget), amount, callData); + vault.withdrawAndExecute(_tx(201), user1, address(token), address(mockTarget), amount, callData); } function test_WithdrawAndExecute_OnlyTSSCanCall() public { bytes memory callData = ""; - + vm.prank(user1); vm.expectRevert(); - vault.withdrawAndExecute(bytes32(uint256(202)), user1, address(token), address(mockTarget), 100e18, callData); + vault.withdrawAndExecute(_tx(202), user1, address(token), address(mockTarget), 100e18, callData); } function test_WithdrawAndExecute_WhenPausedReverts() public { vm.prank(pauser); vault.pause(); - + bytes memory callData = ""; - + vm.prank(tss); vm.expectRevert(); - vault.withdrawAndExecute(bytes32(uint256(203)), user1, address(token), address(mockTarget), 100e18, callData); + vault.withdrawAndExecute(_tx(203), user1, address(token), address(mockTarget), 100e18, callData); } function test_WithdrawAndExecute_ZeroTokenReverts() public { bytes memory callData = ""; - + vm.prank(tss); vm.expectRevert(Errors.ZeroAddress.selector); - vault.withdrawAndExecute(bytes32(uint256(204)), user1, address(0), address(mockTarget), 100e18, callData); + vault.withdrawAndExecute(_tx(204), user1, address(0), address(mockTarget), 100e18, callData); } function test_WithdrawAndExecute_ZeroTargetReverts() public { bytes memory callData = ""; - + vm.prank(tss); vm.expectRevert(Errors.ZeroAddress.selector); - vault.withdrawAndExecute(bytes32(uint256(205)), user1, address(token), address(0), 100e18, callData); + vault.withdrawAndExecute(_tx(205), user1, address(token), address(0), 100e18, callData); } function test_WithdrawAndExecute_ZeroAmountReverts() public { bytes memory callData = ""; - + vm.prank(tss); vm.expectRevert(Errors.InvalidAmount.selector); - vault.withdrawAndExecute(bytes32(uint256(206)), user1, address(token), address(mockTarget), 0, callData); + vault.withdrawAndExecute(_tx(206), user1, address(token), address(mockTarget), 0, callData); } function test_WithdrawAndExecute_InsufficientBalanceReverts() public { uint256 vaultBalance = token.balanceOf(address(vault)); bytes memory callData = ""; - + vm.prank(tss); vm.expectRevert(Errors.InvalidAmount.selector); - vault.withdrawAndExecute(bytes32(uint256(207)), user1, address(token), address(mockTarget), vaultBalance + 1, callData); + vault.withdrawAndExecute(_tx(207), user1, address(token), address(mockTarget), vaultBalance + 1, callData); } function test_WithdrawAndExecute_UnsupportedTokenReverts() public { MockERC20 unsupportedToken = new MockERC20("Unsupported", "UNS", 18, 1000e18); unsupportedToken.mint(address(vault), 100e18); bytes memory callData = ""; - + vm.prank(tss); vm.expectRevert(Errors.NotSupported.selector); - vault.withdrawAndExecute(bytes32(uint256(208)), user1, address(unsupportedToken), address(mockTarget), 100e18, callData); + vault.withdrawAndExecute(_tx(208), user1, address(unsupportedToken), address(mockTarget), 100e18, callData); } function test_WithdrawAndExecute_WithPayload_VerifiesExecution() public { uint256 amount = 100e18; bytes memory callData = abi.encodeWithSignature("receiveToken(address,uint256)", address(token), amount); - + vm.prank(tss); - vault.withdrawAndExecute(bytes32(uint256(209)), user1, address(token), address(mockTarget), amount, callData); - + vault.withdrawAndExecute(_tx(209), user1, address(token), address(mockTarget), amount, callData); + // Verify the call was executed (MockTarget stores lastCaller) assertEq(mockTarget.lastCaller(), address(gateway)); assertEq(mockTarget.lastToken(), address(token)); @@ -708,29 +685,29 @@ contract VaultTest is Test { function test_WithdrawAndExecute_EmptyPayload_Success() public { uint256 amount = 100e18; bytes memory callData = ""; - + // With empty payload, tokens are approved to target but not consumed // Gateway will return them back to vault, so balance should remain same uint256 initialVaultBalance = token.balanceOf(address(vault)); - + vm.prank(tss); - vault.withdrawAndExecute(bytes32(uint256(210)), user1, address(token), address(mockTarget), amount, callData); - + vault.withdrawAndExecute(_tx(210), user1, address(token), address(mockTarget), amount, callData); + // Tokens returned to vault after empty call assertEq(token.balanceOf(address(vault)), initialVaultBalance); } function test_WithdrawAndExecute_DifferentTokens() public { bytes memory callData = abi.encodeWithSignature("receiveToken(address,uint256)", address(token), 50e18); - + vm.prank(tss); - vault.withdrawAndExecute(bytes32(uint256(211)), user1, address(token), address(mockTarget), 50e18, callData); - + vault.withdrawAndExecute(_tx(211), user1, address(token), address(mockTarget), 50e18, callData); + // Token 2 with different decimals bytes memory callData2 = abi.encodeWithSignature("receiveToken(address,uint256)", address(token2), 25e6); vm.prank(tss); - vault.withdrawAndExecute(bytes32(uint256(212)), user1, address(token2), address(mockTarget), 25e6, callData2); - + vault.withdrawAndExecute(_tx(212), user1, address(token2), address(mockTarget), 25e6, callData2); + // Verify both calls executed assertEq(mockTarget.lastCaller(), address(gateway)); } @@ -765,18 +742,18 @@ contract VaultTest is Test { function test_Sweep_StandardToken_Success() public { uint256 amount = 500e18; - + vm.prank(admin); vault.sweep(address(token), user1, amount); - + assertEq(token.balanceOf(user1), amount); } function test_Sweep_NoReturnToken_Success() public { variantToken.setApprovalBehavior(MockTokenApprovalVariants.ApprovalBehavior.NO_RETURN_DATA); - + uint256 amount = 500e18; - + vm.prank(admin); vault.sweep(address(variantToken), user1, amount); assertEq(variantToken.balanceOf(user1), amount); @@ -788,15 +765,15 @@ contract VaultTest is Test { function test_NoNative_DirectETHSendReverts() public { vm.deal(user1, 1 ether); - + vm.prank(user1); - (bool success,) = address(vault).call{value: 1 ether}(""); + (bool success,) = address(vault).call{ value: 1 ether }(""); assertFalse(success); } function test_NoNative_NoReceiveFunction() public { vm.deal(user1, 1 ether); - + vm.prank(user1); vm.expectRevert(); payable(address(vault)).transfer(1 ether); @@ -804,9 +781,9 @@ contract VaultTest is Test { function test_NoNative_FunctionsDoNotAcceptValue() public { vm.deal(tss, 1 ether); - + vm.prank(tss); - (bool success,) = address(vault).call{value: 1 ether}( + (bool success,) = address(vault).call{ value: 1 ether }( abi.encodeWithSelector(vault.withdraw.selector, txID, user1, address(token), user1, 100e18) ); assertFalse(success); @@ -818,23 +795,22 @@ contract VaultTest is Test { function test_GatewayChange_LiveSupport() public { assertTrue(gateway.isSupportedToken(address(token))); - + vm.prank(tss); vault.withdraw(txID, user1, address(token), user1, 100e18); assertEq(token.balanceOf(user1), 100e18); - + // Create new gateway that doesn't support token UniversalGateway newGatewayImpl = new UniversalGateway(); bytes memory initData = abi.encodeWithSelector( - UniversalGateway.initialize.selector, - admin, tss, address(this), 1e18, 10e18, address(0), address(0), weth + UniversalGateway.initialize.selector, admin, tss, address(this), 1e18, 10e18, address(0), address(0), weth ); ERC1967Proxy newProxy = new ERC1967Proxy(address(newGatewayImpl), initData); UniversalGateway newGateway = UniversalGateway(payable(address(newProxy))); - + vm.prank(admin); vault.setGateway(address(newGateway)); - + vm.prank(tss); vm.expectRevert(Errors.NotSupported.selector); vault.withdraw(txID, user1, address(token), user2, 100e18); @@ -844,30 +820,29 @@ contract VaultTest is Test { // Create new gateway without support UniversalGateway newGatewayImpl = new UniversalGateway(); bytes memory initData = abi.encodeWithSelector( - UniversalGateway.initialize.selector, - admin, tss, address(vault), 1e18, 10e18, address(0), address(0), weth + UniversalGateway.initialize.selector, admin, tss, address(vault), 1e18, 10e18, address(0), address(0), weth ); ERC1967Proxy newProxy = new ERC1967Proxy(address(newGatewayImpl), initData); UniversalGateway newGateway = UniversalGateway(payable(address(newProxy))); - + vm.prank(admin); vault.setGateway(address(newGateway)); - + vm.prank(tss); vm.expectRevert(Errors.NotSupported.selector); - vault.withdraw(bytes32(uint256(100)), user1, address(token), user1, 100e18); - + vault.withdraw(_tx(100), user1, address(token), user1, 100e18); + // Re-enable support in new gateway address[] memory tokens = new address[](1); tokens[0] = address(token); uint256[] memory thresholds = new uint256[](1); thresholds[0] = 1_000_000e18; - + vm.prank(admin); newGateway.setTokenLimitThresholds(tokens, thresholds); - + vm.prank(tss); - vault.withdraw(bytes32(uint256(101)), user1, address(token), user1, 100e18); + vault.withdraw(_tx(101), user1, address(token), user1, 100e18); assertEq(token.balanceOf(user1), 100e18); } @@ -878,12 +853,11 @@ contract VaultTest is Test { function test_Events_GatewayUpdated() public { UniversalGateway newGatewayImpl = new UniversalGateway(); bytes memory initData = abi.encodeWithSelector( - UniversalGateway.initialize.selector, - admin, tss, address(this), 1e18, 10e18, address(0), address(0), weth + UniversalGateway.initialize.selector, admin, tss, address(this), 1e18, 10e18, address(0), address(0), weth ); ERC1967Proxy newProxy = new ERC1967Proxy(address(newGatewayImpl), initData); UniversalGateway newGateway = UniversalGateway(payable(address(newProxy))); - + vm.prank(admin); vm.expectEmit(true, true, false, false); emit GatewayUpdated(address(gateway), address(newGateway)); @@ -892,7 +866,7 @@ contract VaultTest is Test { function test_Events_TSSUpdated() public { address newTSS = makeAddr("newTSS"); - + vm.prank(admin); vm.expectEmit(true, true, false, false); emit TSSUpdated(tss, newTSS); @@ -901,7 +875,7 @@ contract VaultTest is Test { function test_Events_VaultWithdraw() public { uint256 amount = 1000e18; - + vm.prank(tss); vm.expectEmit(true, true, true, true); emit VaultWithdraw(txID, user1, address(token), user1, amount); @@ -910,31 +884,25 @@ contract VaultTest is Test { function test_Events_VaultRefund() public { uint256 amount = 1000e18; - + RevertInstructions memory revertInstr = RevertInstructions(user1, ""); - + vm.prank(tss); vm.expectEmit(true, true, false, true); - emit VaultRevert(address(token), user1, amount, revertInstr); - vault.revertWithdraw(bytes32(uint256(5)), address(token), user1, amount, revertInstr); + emit VaultRevert(address(token), revertInstr, amount); + vault.revertWithdraw(_tx(5), address(token), amount, revertInstr); } function test_Events_InitializationEvents() public { Vault newImpl = new Vault(); - + vm.expectEmit(true, true, false, false); emit GatewayUpdated(address(0), address(gateway)); - + vm.expectEmit(true, true, false, false); emit TSSUpdated(address(0), tss); - - bytes memory initData = abi.encodeWithSelector( - Vault.initialize.selector, - admin, - pauser, - tss, - address(gateway) - ); + + bytes memory initData = abi.encodeWithSelector(Vault.initialize.selector, admin, pauser, tss, address(gateway)); new ERC1967Proxy(address(newImpl), initData); } } diff --git a/contracts/evm-gateway/test/vault/VaultPC.t.sol b/contracts/evm-gateway/test/vault/VaultPC.t.sol index 46d66d6..6663a55 100644 --- a/contracts/evm-gateway/test/vault/VaultPC.t.sol +++ b/contracts/evm-gateway/test/vault/VaultPC.t.sol @@ -2,13 +2,13 @@ pragma solidity 0.8.26; import "forge-std/Test.sol"; -import {VaultPC} from "../../src/VaultPC.sol"; -import {Errors} from "../../src/libraries/Errors.sol"; -import {MockPRC20} from "../mocks/MockPRC20.sol"; -import {MockUniversalCoreReal} from "../mocks/MockUniversalCoreReal.sol"; -import {MockReentrantContract} from "../mocks/MockReentrantContract.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { VaultPC } from "../../src/VaultPC.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { MockPRC20 } from "../mocks/MockPRC20.sol"; +import { MockUniversalCoreReal } from "../mocks/MockUniversalCoreReal.sol"; +import { MockReentrantContract } from "../mocks/MockReentrantContract.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract VaultPCTest is Test { VaultPC public vault; @@ -42,12 +42,7 @@ contract VaultPCTest is Test { // Deploy VaultPC implementation and proxy vaultImpl = new VaultPC(); - bytes memory vaultInitData = abi.encodeWithSelector( - VaultPC.initialize.selector, - admin, - pauser, - fundManager - ); + bytes memory vaultInitData = abi.encodeWithSelector(VaultPC.initialize.selector, admin, pauser, fundManager); ERC1967Proxy vaultProxy = new ERC1967Proxy(address(vaultImpl), vaultInitData); vault = VaultPC(payable(address(vaultProxy))); @@ -62,7 +57,7 @@ contract VaultPCTest is Test { address(universalCore), "0x0000000000000000000000000000000000000000" ); - + prc20Token2 = new MockPRC20( "Push BNB", "pBNB", @@ -99,36 +94,21 @@ contract VaultPCTest is Test { function test_Initialization_RevertsOnZeroAdmin() public { VaultPC newImpl = new VaultPC(); - bytes memory initData = abi.encodeWithSelector( - VaultPC.initialize.selector, - address(0), - pauser, - fundManager - ); + bytes memory initData = abi.encodeWithSelector(VaultPC.initialize.selector, address(0), pauser, fundManager); vm.expectRevert(Errors.ZeroAddress.selector); new ERC1967Proxy(address(newImpl), initData); } function test_Initialization_RevertsOnZeroPauser() public { VaultPC newImpl = new VaultPC(); - bytes memory initData = abi.encodeWithSelector( - VaultPC.initialize.selector, - admin, - address(0), - fundManager - ); + bytes memory initData = abi.encodeWithSelector(VaultPC.initialize.selector, admin, address(0), fundManager); vm.expectRevert(Errors.ZeroAddress.selector); new ERC1967Proxy(address(newImpl), initData); } function test_Initialization_RevertsOnZeroFundManager() public { VaultPC newImpl = new VaultPC(); - bytes memory initData = abi.encodeWithSelector( - VaultPC.initialize.selector, - admin, - pauser, - address(0) - ); + bytes memory initData = abi.encodeWithSelector(VaultPC.initialize.selector, admin, pauser, address(0)); vm.expectRevert(Errors.ZeroAddress.selector); new ERC1967Proxy(address(newImpl), initData); } @@ -152,7 +132,7 @@ contract VaultPCTest is Test { function test_Unpause_OnlyPauserCanUnpause() public { vm.prank(pauser); vault.pause(); - + vm.prank(pauser); vault.unpause(); assertFalse(vault.paused()); @@ -161,7 +141,7 @@ contract VaultPCTest is Test { function test_Unpause_NonPauserReverts() public { vm.prank(pauser); vault.pause(); - + vm.prank(user1); vm.expectRevert(); vault.unpause(); @@ -186,7 +166,7 @@ contract VaultPCTest is Test { function test_Pause_BlocksWithdrawToken() public { vm.prank(pauser); vault.pause(); - + vm.prank(fundManager); vm.expectRevert(); vault.withdrawToken(address(prc20Token), user1, 100e18); @@ -195,7 +175,7 @@ contract VaultPCTest is Test { function test_Pause_DoublePauseReverts() public { vm.prank(pauser); vault.pause(); - + vm.prank(pauser); vm.expectRevert(); vault.pause(); @@ -210,10 +190,10 @@ contract VaultPCTest is Test { function test_Unpause_RestoresWithdrawTokenFunctionality() public { vm.prank(pauser); vault.pause(); - + vm.prank(pauser); vault.unpause(); - + vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, 100e18); assertEq(prc20Token.balanceOf(user1), 100e18); @@ -225,16 +205,16 @@ contract VaultPCTest is Test { function test_WithdrawToken_StandardToken_Success() public { uint256 amount = 1000e18; - + vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, amount); - + assertEq(prc20Token.balanceOf(user1), amount); } function test_WithdrawToken_EmitsFeesWithdrawnEvent() public { uint256 amount = 1000e18; - + vm.prank(fundManager); vm.expectEmit(true, true, false, true); emit FeesWithdrawn(fundManager, address(prc20Token), amount); @@ -261,7 +241,7 @@ contract VaultPCTest is Test { function test_WithdrawToken_InsufficientBalanceReverts() public { uint256 vaultBalance = prc20Token.balanceOf(address(vault)); - + vm.prank(fundManager); vm.expectRevert(Errors.InvalidAmount.selector); vault.withdrawToken(address(prc20Token), user1, vaultBalance + 1); @@ -270,10 +250,10 @@ contract VaultPCTest is Test { function test_WithdrawToken_MultipleRecipients() public { vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, 100e18); - + vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user2, 200e18); - + assertEq(prc20Token.balanceOf(user1), 100e18); assertEq(prc20Token.balanceOf(user2), 200e18); } @@ -281,10 +261,10 @@ contract VaultPCTest is Test { function test_WithdrawToken_DifferentTokens() public { vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, 100e18); - + vm.prank(fundManager); vault.withdrawToken(address(prc20Token2), user1, 50e18); - + assertEq(prc20Token.balanceOf(user1), 100e18); assertEq(prc20Token2.balanceOf(user1), 50e18); } @@ -293,10 +273,10 @@ contract VaultPCTest is Test { // Multiple sequential withdrawals should work fine vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, 100e18); - + vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, 200e18); - + assertEq(prc20Token.balanceOf(user1), 300e18); } @@ -307,12 +287,12 @@ contract VaultPCTest is Test { function test_Withdraw_Native_Success() public { uint256 amount = 10 ether; vm.deal(address(vault), amount); - + uint256 userBalanceBefore = user1.balance; - + vm.prank(fundManager); vault.withdraw(user1, amount); - + assertEq(user1.balance, userBalanceBefore + amount); assertEq(address(vault).balance, 0); } @@ -320,7 +300,7 @@ contract VaultPCTest is Test { function test_Withdraw_Native_EmitsFeesWithdrawnEvent() public { uint256 amount = 5 ether; vm.deal(address(vault), amount); - + vm.prank(fundManager); vm.expectEmit(true, true, false, true); emit FeesWithdrawn(fundManager, address(0), amount); @@ -329,7 +309,7 @@ contract VaultPCTest is Test { function test_Withdraw_Native_ZeroAmountReverts() public { vm.deal(address(vault), 10 ether); - + vm.prank(fundManager); vm.expectRevert(Errors.InvalidAmount.selector); vault.withdraw(user1, 0); @@ -337,7 +317,7 @@ contract VaultPCTest is Test { function test_Withdraw_Native_ZeroRecipientReverts() public { vm.deal(address(vault), 10 ether); - + vm.prank(fundManager); vm.expectRevert(Errors.ZeroAddress.selector); vault.withdraw(address(0), 1 ether); @@ -345,7 +325,7 @@ contract VaultPCTest is Test { function test_Withdraw_Native_InsufficientBalanceReverts() public { vm.deal(address(vault), 5 ether); - + vm.prank(fundManager); vm.expectRevert(Errors.InvalidAmount.selector); vault.withdraw(user1, 10 ether); @@ -353,7 +333,7 @@ contract VaultPCTest is Test { function test_Withdraw_Native_OnlyFundManagerCanCall() public { vm.deal(address(vault), 10 ether); - + vm.prank(user1); vm.expectRevert(); vault.withdraw(user1, 1 ether); @@ -361,10 +341,10 @@ contract VaultPCTest is Test { function test_Withdraw_Native_BlockedWhenPaused() public { vm.deal(address(vault), 10 ether); - + vm.prank(pauser); vault.pause(); - + vm.prank(fundManager); vm.expectRevert(); vault.withdraw(user1, 1 ether); @@ -372,13 +352,13 @@ contract VaultPCTest is Test { function test_Withdraw_Native_SequentialCalls() public { vm.deal(address(vault), 30 ether); - + vm.prank(fundManager); vault.withdraw(user1, 10 ether); - + vm.prank(fundManager); vault.withdraw(user2, 15 ether); - + assertEq(user1.balance, 10 ether); assertEq(user2.balance, 15 ether); assertEq(address(vault).balance, 5 ether); @@ -387,10 +367,10 @@ contract VaultPCTest is Test { function test_Withdraw_Native_ExactBalance() public { uint256 vaultBalance = 25 ether; vm.deal(address(vault), vaultBalance); - + vm.prank(fundManager); vault.withdraw(user1, vaultBalance); - + assertEq(user1.balance, vaultBalance); assertEq(address(vault).balance, 0); } @@ -398,10 +378,10 @@ contract VaultPCTest is Test { function test_ReceiveNative_ContractCanReceiveETH() public { uint256 amount = 10 ether; vm.deal(user1, amount); - + vm.prank(user1); - (bool success, ) = address(vault).call{value: amount}(""); - + (bool success,) = address(vault).call{ value: amount }(""); + assertTrue(success); assertEq(address(vault).balance, amount); } @@ -412,23 +392,23 @@ contract VaultPCTest is Test { function test_MultipleWithdrawalsToken_ReducesBalance() public { uint256 initialBalance = prc20Token.balanceOf(address(vault)); - + vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, 100e18); - + vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user2, 200e18); - + uint256 finalBalance = prc20Token.balanceOf(address(vault)); assertEq(finalBalance, initialBalance - 300e18); } function test_WithdrawToken_ExactBalance_Success() public { uint256 vaultBalance = prc20Token.balanceOf(address(vault)); - + vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, vaultBalance); - + assertEq(prc20Token.balanceOf(user1), vaultBalance); assertEq(prc20Token.balanceOf(address(vault)), 0); } @@ -440,10 +420,10 @@ contract VaultPCTest is Test { function test_WithdrawToken_AfterPauseUnpause_WorksNormally() public { vm.prank(pauser); vault.pause(); - + vm.prank(pauser); vault.unpause(); - + vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, 100e18); assertEq(prc20Token.balanceOf(user1), 100e18); @@ -452,15 +432,13 @@ contract VaultPCTest is Test { function test_WithdrawToken_MultipleSmallAmounts() public { vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, 1e18); - + vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, 2e18); - + vm.prank(fundManager); vault.withdrawToken(address(prc20Token), user1, 3e18); - + assertEq(prc20Token.balanceOf(user1), 6e18); } - } - diff --git a/contracts/svm-gateway/Anchor.toml b/contracts/svm-gateway/Anchor.toml index 3a2caaf..dafb6c9 100644 --- a/contracts/svm-gateway/Anchor.toml +++ b/contracts/svm-gateway/Anchor.toml @@ -5,14 +5,32 @@ resolution = true skip-lint = false [programs.devnet] -UniversalGateway = "CFVSincHYbETh2k7w6u1ENEkjbSLtveRCEBupKidw2VS" +# Use DJoFYDpgbTfxbXBv1QYhYGc9FK4J5FUKpYXAfSkHryXp for local testing +universal_gateway = "CFVSincHYbETh2k7w6u1ENEkjbSLtveRCEBupKidw2VS" [registry] url = "https://api.apr.dev" [provider] +# cluster = "localnet" cluster = "https://api.devnet.solana.com" wallet = "./upgrade-keypair.json" [scripts] -test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/*.test.ts" + +[test] +startup_wait = 10000 +shutdown_wait = 5000 +upgradeable = false + +[test.validator] +bind_address = "0.0.0.0" +rpc_port = 8899 +ledger = ".anchor/test-ledger" +reset = true + +[[test.genesis]] +address = "rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ" +program = "tests/fixtures/pyth_pull.so" +upgradeable = true diff --git a/contracts/svm-gateway/Cargo.lock b/contracts/svm-gateway/Cargo.lock index e36ce40..ed7a1d6 100644 --- a/contracts/svm-gateway/Cargo.lock +++ b/contracts/svm-gateway/Cargo.lock @@ -3113,4 +3113,4 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.101", -] +] \ No newline at end of file diff --git a/contracts/svm-gateway/README.md b/contracts/svm-gateway/README.md index d8078a2..5ccdf0a 100644 --- a/contracts/svm-gateway/README.md +++ b/contracts/svm-gateway/README.md @@ -11,7 +11,7 @@ Production-ready Solana program for cross-chain asset bridging to Push Chain wit ## Core Functions ### Deposit Functions -- **`send_tx_with_gas`** - Native SOL gas deposits with USD caps ($1-$10) +- **`send_tx_with_gas`** - Native SOL gas deposits with USD caps ($1-$10) and signature data - **`send_funds`** - SPL token bridging (whitelisted tokens only) - **`send_funds_native`** - Native SOL bridging (high value, no caps) - **`send_tx_with_funds`** - Combined SPL tokens + gas with payload execution diff --git a/contracts/svm-gateway/convert.mjs b/contracts/svm-gateway/app/convert.mjs similarity index 100% rename from contracts/svm-gateway/convert.mjs rename to contracts/svm-gateway/app/convert.mjs diff --git a/contracts/svm-gateway/app/create-universal-alt.ts b/contracts/svm-gateway/app/create-universal-alt.ts new file mode 100644 index 0000000..0f7a02b --- /dev/null +++ b/contracts/svm-gateway/app/create-universal-alt.ts @@ -0,0 +1,121 @@ +#!/usr/bin/env ts-node + +import { + AddressLookupTableProgram, + Connection, + Keypair, + PublicKey, + SystemProgram, + Transaction, +} from "@solana/web3.js"; +import fs from "fs"; +import * as spl from "@solana/spl-token"; + +// This script: +// 1) Creates an Address Lookup Table (ALT) owned by the upgrade/admin key. +// 2) Extends it with the static accounts used by send_universal_tx so that +// user transactions can spend more of the 1232‑byte budget on instruction +// data (payload / revertInstruction / signatureData). +// +// It writes the created ALT address to ./universal-alt.json so other scripts +// (like the ALT send_universal_tx test) can load and use it. + +const PROGRAM_ID = new PublicKey("CFVSincHYbETh2k7w6u1ENEkjbSLtveRCEBupKidw2VS"); +const CONFIG_SEED = "config"; +const VAULT_SEED = "vault"; +const RATE_LIMIT_CONFIG_SEED = "rate_limit_config"; +const PRICE_ACCOUNT = new PublicKey("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE"); // Pyth SOL/USD + +function loadKeypair(path: string): Keypair { + const secret = JSON.parse(fs.readFileSync(path, "utf8")); + return Keypair.fromSecretKey(Uint8Array.from(secret)); +} + +async function main() { + const adminKeypair = loadKeypair("./upgrade-keypair.json"); + + const connection = new Connection("https://api.devnet.solana.com", "confirmed"); + + console.log("Admin:", adminKeypair.publicKey.toBase58()); + + // Derive PDAs we want to pack into the ALT + const [configPda] = PublicKey.findProgramAddressSync( + [Buffer.from(CONFIG_SEED)], + PROGRAM_ID, + ); + const [vaultPda] = PublicKey.findProgramAddressSync( + [Buffer.from(VAULT_SEED)], + PROGRAM_ID, + ); + const [rateLimitConfigPda] = PublicKey.findProgramAddressSync( + [Buffer.from(RATE_LIMIT_CONFIG_SEED)], + PROGRAM_ID, + ); + + console.log("Config PDA:", configPda.toBase58()); + console.log("Vault PDA:", vaultPda.toBase58()); + console.log("RateLimitConfig PDA:", rateLimitConfigPda.toBase58()); + console.log("Price account:", PRICE_ACCOUNT.toBase58()); + + // 1) Create the lookup table + const slot = await connection.getSlot("finalized"); + const [createIx, altAddress] = AddressLookupTableProgram.createLookupTable({ + authority: adminKeypair.publicKey, + payer: adminKeypair.publicKey, + recentSlot: slot, + }); + + console.log("Creating ALT at address:", altAddress.toBase58()); + + let tx = new Transaction().add(createIx); + tx.feePayer = adminKeypair.publicKey; + tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + + const createSig = await connection.sendTransaction(tx, [adminKeypair]); + console.log(" ALT create tx:", createSig); + await connection.confirmTransaction(createSig, "confirmed"); + + // 2) Extend the table with static accounts used by send_universal_tx + // We intentionally do NOT include token_rate_limit here to avoid having to + // manage many per‑mint PDAs. We focus on global/static addresses instead. + const addressesToAdd: PublicKey[] = [ + configPda, + vaultPda, + rateLimitConfigPda, + PRICE_ACCOUNT, + spl.TOKEN_PROGRAM_ID, + SystemProgram.programId, + ]; + + const extendIx = AddressLookupTableProgram.extendLookupTable({ + payer: adminKeypair.publicKey, + authority: adminKeypair.publicKey, + lookupTable: altAddress, + addresses: addressesToAdd, + }); + + tx = new Transaction().add(extendIx); + tx.feePayer = adminKeypair.publicKey; + tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + + const extendSig = await connection.sendTransaction(tx, [adminKeypair]); + console.log(" ALT extend tx:", extendSig); + await connection.confirmTransaction(extendSig, "confirmed"); + + // Persist ALT address so the test script can load it. + const out = { + altAddress: altAddress.toBase58(), + entries: addressesToAdd.map((a) => a.toBase58()), + }; + fs.writeFileSync("./universal-alt.json", JSON.stringify(out, null, 2)); + console.log("Saved ALT metadata to ./universal-alt.json"); + + console.log("\n✅ ALT created and extended successfully."); +} + +main().catch((err) => { + console.error("create-universal-alt failed:", err); + process.exit(1); +}); + + diff --git a/contracts/svm-gateway/app/decode-tx.js b/contracts/svm-gateway/app/decode-tx.js new file mode 100644 index 0000000..4a196b8 --- /dev/null +++ b/contracts/svm-gateway/app/decode-tx.js @@ -0,0 +1,243 @@ +const anchor = require("@coral-xyz/anchor"); +const { Connection, PublicKey } = require("@solana/web3.js"); +const fs = require("fs"); + +async function decodeTx(sig, label) { + try { + const PROGRAM_ID = new PublicKey("CFVSincHYbETh2k7w6u1ENEkjbSLtveRCEBupKidw2VS"); + const connection = new Connection("https://api.devnet.solana.com", { commitment: "confirmed" }); + const idl = JSON.parse(fs.readFileSync("./target/idl/universal_gateway.json", "utf8")); + const coder = new anchor.BorshEventCoder(idl); + + const tx = await connection.getTransaction(sig, { commitment: "confirmed", maxSupportedTransactionVersion: 0 }); + if (!tx) { + console.log(`${label}: Transaction not found. Check if signature is correct and network matches.`); + return; + } + if (!tx.meta || !tx.meta.logMessages) { + console.log(`${label}: No logs found in transaction`); + return; + } + if (tx.meta.err) { + console.log(`${label}: Transaction failed with error:`, tx.meta.err); + return; + } + const dataLogs = tx.meta.logMessages.filter(l => l.startsWith("Program data: ")); + if (dataLogs.length === 0) { + console.log(`${label}: No Anchor event logs in tx`); + return; + } + + // Debug: Show all program logs and try to manually decode UniversalTx + console.log(`${label}: Found ${dataLogs.length} event log(s)`); + const UNIVERSAL_TX_DISCRIMINATOR = Buffer.from([0x6c, 0x9a, 0xd8, 0x29, 0xb5, 0xea, 0x1d, 0x7c]); // From the output + dataLogs.forEach((log, idx) => { + const b64 = log.replace("Program data: ", ""); + const buf = Buffer.from(b64, "base64"); + const disc = buf.slice(0, 8); + const isUniversalTx = disc.equals(UNIVERSAL_TX_DISCRIMINATOR); + console.log(` [${idx}] discriminator=${disc.toString('hex')} data_len=${buf.length - 8} ${isUniversalTx ? '(UniversalTx)' : ''}`); + if (isUniversalTx) { + // Try to manually parse the event to see payload + const eventData = buf.slice(8); + console.log(` Raw event data (first 100 bytes): ${eventData.slice(0, 100).toString('hex')}`); + } + }); + + function readU64LE(buf, off) { + if (off + 8 > buf.length) throw new Error(`Buffer overflow: trying to read u64 at offset ${off}, buffer length ${buf.length}`); + return Number(buf.readBigUInt64LE(off)); + } + function readI64LE(buf, off) { + if (off + 8 > buf.length) throw new Error(`Buffer overflow: trying to read i64 at offset ${off}, buffer length ${buf.length}`); + return Number(buf.readBigInt64LE(off)); + } + function readVecU8(buf, off) { + if (off + 4 > buf.length) throw new Error(`Buffer overflow: trying to read vec length at offset ${off}, buffer length ${buf.length}`); + const len = buf.readUInt32LE(off); + const start = off + 4; + const end = start + len; + if (end > buf.length) throw new Error(`Buffer overflow: trying to read vec of length ${len} at offset ${start}, buffer length ${buf.length}`); + return { bytes: buf.slice(start, end), next: end }; + } + function decodeVerificationType(buf, off) { + if (off >= buf.length) throw new Error(`Buffer overflow: trying to read u8 at offset ${off}, buffer length ${buf.length}`); + const d = buf.readUInt8(off); + if (d === 0) return { value: "SignedVerification", next: off + 1 }; + if (d === 1) return { value: "UniversalTxVerification", next: off + 1 }; + return { value: "Unknown", next: off + 1 }; + } + function decodeUniversalPayload(buf) { + try { + let off = 0; + if (buf.length < 20) throw new Error(`Buffer too small for 'to' field: ${buf.length} bytes`); + const to = buf.slice(off, off + 20); + off += 20; + + const value = readU64LE(buf, off); + off += 8; + + const dataRes = readVecU8(buf, off); + const data = dataRes.bytes; + off = dataRes.next; + + const gasLimit = readU64LE(buf, off); + off += 8; + + const maxFeePerGas = readU64LE(buf, off); + off += 8; + + const maxPriorityFeePerGas = readU64LE(buf, off); + off += 8; + + const nonce = readU64LE(buf, off); + off += 8; + + const deadline = readI64LE(buf, off); + off += 8; + + const vTypeRes = decodeVerificationType(buf, off); + const vType = vTypeRes.value; + off = vTypeRes.next; + + return { + to: "0x" + to.toString("hex"), + value, + data: "0x" + data.toString("hex"), + gasLimit, + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + deadline, + vType + }; + } catch (err) { + console.error(`Error decoding payload (buffer length: ${buf.length}):`, err.message); + throw err; + } + } + + const decoded = []; + for (const log of dataLogs) { + const b64 = log.replace("Program data: ", ""); + const ev = coder.decode(b64); + if (ev) decoded.push(ev); + } + + // Find ALL UniversalTx events (there might be multiple) + const uniEvents = decoded.filter(e => e && e.name === "UniversalTx"); + if (uniEvents.length === 0) { + console.log(`${label}: UniversalTx event not found. Events found:`, decoded.map(d => d ? d.name : 'null')); + return; + } + + console.log(`${label}: Found ${uniEvents.length} UniversalTx event(s), processing the one with payload...`); + + // Find the event with a non-empty payload + let uni = uniEvents.find(e => e.data.payload && (Array.isArray(e.data.payload) ? e.data.payload.length > 0 : Buffer.isBuffer(e.data.payload) ? e.data.payload.length > 0 : true)); + if (!uni) { + // If none have payload, use the largest one (likely has the payload) + uni = uniEvents.reduce((prev, curr) => { + const prevSize = prev.data.payload ? (Array.isArray(prev.data.payload) ? prev.data.payload.length : Buffer.isBuffer(prev.data.payload) ? prev.data.payload.length : 0) : 0; + const currSize = curr.data.payload ? (Array.isArray(curr.data.payload) ? curr.data.payload.length : Buffer.isBuffer(curr.data.payload) ? curr.data.payload.length : 0) : 0; + return currSize > prevSize ? curr : prev; + }); + } + + const e = uni.data; + console.log(`${label}: Found UniversalTx event`); + console.log(` - sender: ${e.sender}`); + console.log(` - recipient: 0x${Buffer.from(e.recipient).toString('hex')}`); + console.log(` - token: ${e.token}`); + console.log(` - amount: ${e.amount}`); + + // Try both snake_case and camelCase for tx_type + const txType = e.tx_type !== undefined ? e.tx_type : e.txType; + if (txType !== undefined) { + // Anchor deserializes enums as objects: { gas: {} }, { gasAndPayload: {} }, etc. + let txTypeName = 'Unknown'; + if (typeof txType === 'object' && txType !== null) { + // Get the first key (enum variant name) + const keys = Object.keys(txType); + if (keys.length > 0) { + // Convert camelCase to PascalCase for display + const variant = keys[0]; + txTypeName = variant.charAt(0).toUpperCase() + variant.slice(1); + } + } else if (typeof txType === 'number') { + // Fallback: if it's a number, map it + const txTypeNames = ['Gas', 'GasAndPayload', 'Funds', 'FundsAndPayload']; + txTypeName = txTypeNames[txType] || `Unknown(${txType})`; + } else { + txTypeName = String(txType); + } + console.log(` - tx_type: ${txTypeName}`); + } else { + console.log(` - tx_type: undefined (field not found in event data)`); + console.log(` - Available fields: ${Object.keys(e).join(', ')}`); + } + + // Handle payload - it might be an array, buffer, or already deserialized + let payloadBytes; + if (Array.isArray(e.payload)) { + payloadBytes = Buffer.from(e.payload); + } else if (Buffer.isBuffer(e.payload)) { + payloadBytes = e.payload; + } else if (e.payload && typeof e.payload === 'object' && e.payload.data) { + // Anchor might have already deserialized it + payloadBytes = Buffer.from(e.payload.data); + } else { + payloadBytes = Buffer.from(e.payload || []); + } + + console.log(` - payload length: ${payloadBytes.length} bytes`); + console.log(` - payload (hex): ${payloadBytes.length > 0 ? payloadBytes.toString('hex').substring(0, 100) + '...' : 'empty'}`); + + if (payloadBytes.length === 0) { + console.log(`${label}: No payload in event (empty payload)`); + console.log(` This might be a GAS-only route (TxType::Gas) or payload was not included.`); + return; + } + + // Check if payload is JSON (starts with '{') or Borsh + const isJson = payloadBytes[0] === 0x7b; // '{' in ASCII + if (isJson) { + console.log(` - Payload appears to be JSON (not Borsh). This is incorrect - should use Borsh serialization.`); + try { + const jsonPayload = JSON.parse(payloadBytes.toString('utf8')); + console.log(`\n${label}: (JSON Payload - INCORRECT FORMAT):`); + console.log(JSON.stringify({ payload: jsonPayload }, null, 2)); + console.log(`\n⚠️ WARNING: Payload is JSON but should be Borsh-serialized UniversalPayload!`); + return; + } catch (err) { + console.log(` - Failed to parse as JSON: ${err.message}`); + } + } + + console.log(` - Attempting to decode payload as Borsh (${payloadBytes.length} bytes)...`); + const payload = decodeUniversalPayload(payloadBytes); + + console.log(`\n${label}:`); + console.log(JSON.stringify({ + payload + }, null, 2)); + } catch (err) { + console.error(`${label} Error:`, err?.message || String(err)); + } +} + +const [, , ...sigs] = process.argv; +if (sigs.length === 0) { + console.log("Usage: node app/decode-tx.js [signature2] ..."); + console.log("\nExample:"); + console.log(" node app/decode-tx.js xuV3B2KRBdUSPrP76uLp7dDPXjf4seiyW9Dqq5WARGghJUhvVirJqyYeUmJz8PaAFUhjaJhcp6wzzoNzCTLNnHW"); + console.log("\nThis script decodes UniversalTx events from Solana transactions."); + process.exit(1); +} else { + (async () => { + for (const sig of sigs) { + await decodeTx(sig, sig); + } + })(); +} + diff --git a/contracts/svm-gateway/app/gateway-test.ts b/contracts/svm-gateway/app/gateway-test.ts index 43cf192..eea49a8 100644 --- a/contracts/svm-gateway/app/gateway-test.ts +++ b/contracts/svm-gateway/app/gateway-test.ts @@ -5,12 +5,15 @@ import { LAMPORTS_PER_SOL, Keypair, SystemProgram, + TransactionMessage, + VersionedTransaction, } from "@solana/web3.js"; import fs from "fs"; import { Program } from "@coral-xyz/anchor"; -import type { Pushsolanagateway } from "../target/types/pushsolanagateway"; +import type { UniversalGateway } from "../target/types/universal_gateway"; import * as spl from "@solana/spl-token"; -import { keccak_256 } from "js-sha3"; +import pkg from 'js-sha3'; +const { keccak_256 } = pkg; import * as secp from "@noble/secp256k1"; import { assert } from "chai"; @@ -19,6 +22,7 @@ const CONFIG_SEED = "config"; const VAULT_SEED = "vault"; const WHITELIST_SEED = "whitelist"; const PRICE_ACCOUNT = new PublicKey("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE"); // Pyth SOL/USD price feed +const ALT_ADDRESS = new PublicKey("EWXJ1ERkMwizmSovjtQ2qBTDpm1vxrZZ4Y2RjEujbqBo"); // Universal Gateway ALT // Load keypairs const adminKeypair = Keypair.fromSecretKey( @@ -40,9 +44,9 @@ const userProvider = new anchor.AnchorProvider(connection, new anchor.Wallet(use anchor.setProvider(adminProvider); // Load IDL -const idl = JSON.parse(fs.readFileSync("./target/idl/pushsolanagateway.json", "utf8")); -const program = new Program(idl as Pushsolanagateway, adminProvider); -const userProgram = new Program(idl as Pushsolanagateway, userProvider); +const idl = JSON.parse(fs.readFileSync("./target/idl/universal_gateway.json", "utf8")); +const program = new Program(idl, adminProvider); +const userProgram = new Program(idl, userProvider); // Helper: Get dynamic gas amount based on current SOL price async function getDynamicGasAmount(targetUsd: number, fallbackSol: number = 0.01): Promise { @@ -127,10 +131,111 @@ async function run() { [Buffer.from("tss")], PROGRAM_ID ); + const [rateLimitConfigPda] = PublicKey.findProgramAddressSync( + [Buffer.from("rate_limit_config")], + PROGRAM_ID + ); + const [whitelistPda] = PublicKey.findProgramAddressSync( + [Buffer.from(WHITELIST_SEED)], + PROGRAM_ID + ); const admin = adminKeypair.publicKey; const user = userKeypair.publicKey; + // Helper to get token rate limit PDA + const getTokenRateLimitPda = (tokenMint: PublicKey): PublicKey => { + const [pda] = PublicKey.findProgramAddressSync( + [Buffer.from("rate_limit"), tokenMint.toBuffer()], + PROGRAM_ID + ); + return pda; + }; + + // Helper to create payload + const createPayload = (to: number, vType: any = { signedVerification: {} }) => ({ + to: Array.from(Buffer.alloc(20, to)), + value: new anchor.BN(0), + data: Buffer.from([]), + gasLimit: new anchor.BN(21000), + maxFeePerGas: new anchor.BN(20000000000), + maxPriorityFeePerGas: new anchor.BN(1000000000), + nonce: new anchor.BN(0), + deadline: new anchor.BN(Math.floor(Date.now() / 1000) + 3600), + vType, + }); + + // Helper to serialize payload to bytes using Borsh (matching Rust's try_to_vec) + // UniversalPayload structure: to: [u8; 20], value: u64, data: Vec, gasLimit: u64, + // maxFeePerGas: u64, maxPriorityFeePerGas: u64, nonce: u64, deadline: i64, vType: VerificationType + const serializePayload = (payload: any): Buffer => { + // Helper to write u64 (little-endian) + const writeU64 = (val: anchor.BN): Buffer => { + const b = Buffer.alloc(8); + b.writeBigUInt64LE(BigInt(val.toString()), 0); + return b; + }; + + // Helper to write i64 (little-endian) + const writeI64 = (val: anchor.BN): Buffer => { + const b = Buffer.alloc(8); + b.writeBigInt64LE(BigInt(val.toString()), 0); + return b; + }; + + // Helper to write Vec (length as u32 LE, then bytes) + const writeVecU8 = (val: Buffer | number[]): Buffer => { + const bytes = Buffer.isBuffer(val) ? val : Buffer.from(val); + const len = Buffer.alloc(4); + len.writeUInt32LE(bytes.length, 0); + return Buffer.concat([len, bytes]); + }; + + // Helper to write u8 + const writeU8 = (val: number): Buffer => { + return Buffer.from([val]); + }; + + // Serialize UniversalPayload in Borsh format: + // 1. to: [u8; 20] - 20 bytes + const toBytes = Buffer.from(payload.to); + // 2. value: u64 - 8 bytes + const valueBytes = writeU64(payload.value); + // 3. data: Vec - 4 bytes (length) + data + const dataBytes = writeVecU8(payload.data); + // 4. gasLimit: u64 - 8 bytes + const gasLimitBytes = writeU64(payload.gasLimit); + // 5. maxFeePerGas: u64 - 8 bytes + const maxFeePerGasBytes = writeU64(payload.maxFeePerGas); + // 6. maxPriorityFeePerGas: u64 - 8 bytes + const maxPriorityFeePerGasBytes = writeU64(payload.maxPriorityFeePerGas); + // 7. nonce: u64 - 8 bytes + const nonceBytes = writeU64(payload.nonce); + // 8. deadline: i64 - 8 bytes + const deadlineBytes = writeI64(payload.deadline); + // 9. vType: VerificationType enum - 1 byte (0 = SignedVerification, 1 = UniversalTxVerification) + const vTypeVal = payload.vType.signedVerification !== undefined ? 0 : 1; + const vTypeBytes = writeU8(vTypeVal); + + return Buffer.concat([ + toBytes, + valueBytes, + dataBytes, + gasLimitBytes, + maxFeePerGasBytes, + maxPriorityFeePerGasBytes, + nonceBytes, + deadlineBytes, + vTypeBytes, + ]); + }; + + // Helper to create revert instruction + const createRevertInstruction = (recipient: PublicKey, msg: string = "test") => ({ + fundRecipient: recipient, + revertMsg: Buffer.from(msg), + }); + console.log(`Program ID: ${PROGRAM_ID.toString()}`); console.log(`Admin: ${admin.toString()}`); console.log(`User: ${user.toString()}`); @@ -163,6 +268,50 @@ async function run() { console.log("Gateway already initialized\n"); } + // Step 1.5: Initialize Rate Limit Config and Token Rate Limits + console.log("1.5. Setting up Rate Limits..."); + const veryLargeThreshold = new anchor.BN("1000000000000000000000"); // Effectively unlimited + + // Initialize rate limit config by calling setBlockUsdCap (uses init_if_needed) + try { + await (program.account as any).rateLimitConfig.fetch(rateLimitConfigPda); + console.log("Rate limit config already initialized"); + } catch { + // Initialize by setting block USD cap to 0 (disabled, but creates the account) + await program.methods + .setBlockUsdCap(new anchor.BN(0)) + .accounts({ + admin: admin, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([adminKeypair]) + .rpc(); + console.log("✅ Rate limit config initialized"); + } + + // Initialize native SOL rate limit + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + try { + await (program.account as any).tokenRateLimit.fetch(nativeSolTokenRateLimitPda); + console.log("Native SOL rate limit already initialized"); + } catch { + await program.methods + .setTokenRateLimit(veryLargeThreshold) + .accounts({ + admin: admin, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenMint: PublicKey.default, + systemProgram: SystemProgram.programId, + }) + .signers([adminKeypair]) + .rpc(); + console.log("✅ Native SOL rate limit initialized"); + } + // Step 2: Test Admin Functions console.log("2. Testing Admin Functions..."); @@ -245,10 +394,6 @@ async function run() { // Step 4: Whitelist SPL Token console.log("4. Whitelisting SPL Token..."); - const [whitelistPda] = PublicKey.findProgramAddressSync( - [Buffer.from(WHITELIST_SEED)], - PROGRAM_ID - ); try { const whitelistTx = await program.methods @@ -269,7 +414,577 @@ async function run() { } } - // Step 5: Test send_tx_with_gas (SOL deposit with payload) + // Initialize SPL token rate limit if needed + if (tokenLoaded) { + const splTokenRateLimitPda = getTokenRateLimitPda(mint); + try { + await (program.account as any).tokenRateLimit.fetch(splTokenRateLimitPda); + console.log("SPL token rate limit already initialized"); + } catch { + await program.methods + .setTokenRateLimit(veryLargeThreshold) + .accounts({ + admin: admin, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: splTokenRateLimitPda, + tokenMint: mint, + systemProgram: SystemProgram.programId, + }) + .signers([adminKeypair]) + .rpc(); + console.log("✅ SPL token rate limit initialized"); + } + } + + // ========================= + // NEW: sendUniversalTx Tests + // ========================= + console.log("\n=== 4.5. Testing sendUniversalTx (New Universal Entrypoint) ===\n"); + + // Test 4.5.1: GAS Route (TxType.GAS) + console.log("4.5.1. Testing GAS Route (TxType.GAS)..."); + const universalGasAmount = await getDynamicGasAmount(2.5, 0.01); + const initialVaultBalanceGas = await connection.getBalance(vaultPda); + + const gasReq = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user), + signatureData: Buffer.from("gas_sig"), + }; + + const gasUniversalTx = await userProgram.methods + .sendUniversalTx(gasReq, universalGasAmount) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([userKeypair]) + .rpc(); + + console.log(`✅ GAS route transaction: ${gasUniversalTx}`); + await parseAndPrintEvents(gasUniversalTx, "GAS route events"); + const finalVaultBalanceGas = await connection.getBalance(vaultPda); + const balanceIncrease = finalVaultBalanceGas - initialVaultBalanceGas; + assert.equal(balanceIncrease, universalGasAmount.toNumber(), "Vault should receive exact gas amount"); + console.log(`💰 Vault balance increased by: ${balanceIncrease / LAMPORTS_PER_SOL} SOL (verified)\n`); + + // Test 4.5.2: GAS_AND_PAYLOAD Route + console.log("4.5.2. Testing GAS_AND_PAYLOAD Route..."); + const universalGasPayloadAmount = await getDynamicGasAmount(2.5, 0.01); + const gasPayloadReq = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: serializePayload(createPayload(1)), + revertInstruction: createRevertInstruction(user), + signatureData: Buffer.from("gas_payload_sig"), + }; + + const initialVaultBalanceGasPayload = await connection.getBalance(vaultPda); + const gasPayloadTx = await userProgram.methods + .sendUniversalTx(gasPayloadReq, universalGasPayloadAmount) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([userKeypair]) + .rpc(); + + console.log(`✅ GAS_AND_PAYLOAD route transaction: ${gasPayloadTx}`); + await parseAndPrintEvents(gasPayloadTx, "GAS_AND_PAYLOAD route events"); + const finalVaultBalanceGasPayload = await connection.getBalance(vaultPda); + const balanceIncreaseGasPayload = finalVaultBalanceGasPayload - initialVaultBalanceGasPayload; + assert.equal(balanceIncreaseGasPayload, universalGasPayloadAmount.toNumber(), "Vault should receive exact gas amount"); + console.log(`💰 Vault balance increased by: ${balanceIncreaseGasPayload / LAMPORTS_PER_SOL} SOL (verified)\n`); + + // Test 4.5.3: FUNDS Route (Native SOL) + console.log("4.5.3. Testing FUNDS Route (Native SOL)..."); + const fundsAmount = new anchor.BN(0.005 * LAMPORTS_PER_SOL); + const fundsRecipient = Array.from(Buffer.from("1111111111111111111111111111111111111111", "hex").subarray(0, 20)); + const initialVaultBalanceFunds = await connection.getBalance(vaultPda); + + const fundsReq = { + recipient: fundsRecipient, + token: PublicKey.default, + amount: fundsAmount, + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user), + signatureData: Buffer.from("funds_sig"), + }; + + const fundsTx = await userProgram.methods + .sendUniversalTx(fundsReq, fundsAmount) // native_amount == amount for native FUNDS + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([userKeypair]) + .rpc(); + + console.log(`✅ FUNDS route (native SOL) transaction: ${fundsTx}`); + await parseAndPrintEvents(fundsTx, "FUNDS route events"); + const finalVaultBalanceFunds = await connection.getBalance(vaultPda); + const balanceIncreaseFunds = finalVaultBalanceFunds - initialVaultBalanceFunds; + assert.equal(balanceIncreaseFunds, fundsAmount.toNumber(), "Vault should receive exact funds amount"); + console.log(`💰 Vault balance increased by: ${balanceIncreaseFunds / LAMPORTS_PER_SOL} SOL (verified)\n`); + + // Test 4.5.4: FUNDS Route (SPL Token) - if token is loaded + if (tokenLoaded) { + console.log("4.5.4. Testing FUNDS Route (SPL Token)..."); + // Create vault ATA if needed + const vaultAta = await spl.getOrCreateAssociatedTokenAccount( + adminProvider.connection as any, + adminKeypair, + mint, + vaultPda, + true + ); + + const splFundsAmount = new anchor.BN(1000 * Math.pow(10, tokenInfo.decimals)); + const splFundsRecipient = Array.from(Buffer.from("2222222222222222222222222222222222222222", "hex").subarray(0, 20)); + const userTokenBalanceBeforeSplFunds = (await spl.getAccount(userProvider.connection as any, tokenAccount)).amount; + const vaultTokenBalanceBeforeSplFunds = (await spl.getAccount(userProvider.connection as any, vaultAta.address)).amount; + + const splFundsReq = { + recipient: splFundsRecipient, + token: mint, + amount: splFundsAmount, + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user), + signatureData: Buffer.from("spl_funds_sig"), + }; + + const splTokenRateLimitPda = getTokenRateLimitPda(mint); + const splFundsTx = await userProgram.methods + .sendUniversalTx(splFundsReq, new anchor.BN(0)) // No native SOL for SPL funds + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: tokenAccount, + gatewayTokenAccount: vaultAta.address, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: splTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([userKeypair]) + .rpc(); + + console.log(`✅ FUNDS route (SPL token) transaction: ${splFundsTx}`); + await parseAndPrintEvents(splFundsTx, "FUNDS route (SPL) events"); + const userTokenBalanceAfterSplFunds = (await spl.getAccount(userProvider.connection as any, tokenAccount)).amount; + const vaultTokenBalanceAfterSplFunds = (await spl.getAccount(userProvider.connection as any, vaultAta.address)).amount; + const userBalanceChange = Number(userTokenBalanceAfterSplFunds) - Number(userTokenBalanceBeforeSplFunds); + const vaultBalanceChange = Number(vaultTokenBalanceAfterSplFunds) - Number(vaultTokenBalanceBeforeSplFunds); + assert.equal(userBalanceChange, -splFundsAmount.toNumber(), "User should lose exact SPL amount"); + assert.equal(vaultBalanceChange, splFundsAmount.toNumber(), "Vault should gain exact SPL amount"); + console.log(`📊 User SPL balance: ${userTokenBalanceBeforeSplFunds.toString()} → ${userTokenBalanceAfterSplFunds.toString()} (verified)`); + console.log(`📊 Vault SPL balance: ${vaultTokenBalanceBeforeSplFunds.toString()} → ${vaultTokenBalanceAfterSplFunds.toString()} (verified)\n`); + } + + // Test 4.5.5: FUNDS_AND_PAYLOAD Route (Native SOL) + console.log("4.5.5. Testing FUNDS_AND_PAYLOAD Route (Native SOL)..."); + const fundsPayloadBridgeAmount = new anchor.BN(0.01 * LAMPORTS_PER_SOL); + const fundsPayloadGasAmount = await getDynamicGasAmount(1.5, 0.01); + const totalNativeAmount = fundsPayloadBridgeAmount.add(fundsPayloadGasAmount); + const fundsPayloadRecipient = Array.from(Buffer.from("3333333333333333333333333333333333333333", "hex").subarray(0, 20)); + + const fundsPayloadReq = { + recipient: fundsPayloadRecipient, + token: PublicKey.default, + amount: fundsPayloadBridgeAmount, + payload: serializePayload(createPayload(2)), + revertInstruction: createRevertInstruction(user), + signatureData: Buffer.from("funds_payload_sig"), + }; + + const initialVaultBalanceFundsPayload = await connection.getBalance(vaultPda); + const fundsPayloadTx = await userProgram.methods + .sendUniversalTx(fundsPayloadReq, totalNativeAmount) // native_amount = bridge + gas + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([userKeypair]) + .rpc(); + + console.log(`✅ FUNDS_AND_PAYLOAD route (native SOL) transaction: ${fundsPayloadTx}`); + await parseAndPrintEvents(fundsPayloadTx, "FUNDS_AND_PAYLOAD route events"); + const finalVaultBalanceFundsPayload = await connection.getBalance(vaultPda); + const balanceIncreaseFundsPayload = finalVaultBalanceFundsPayload - initialVaultBalanceFundsPayload; + assert.equal(balanceIncreaseFundsPayload, totalNativeAmount.toNumber(), "Vault should receive bridge + gas amount"); + console.log(`💰 Vault balance increased by: ${balanceIncreaseFundsPayload / LAMPORTS_PER_SOL} SOL (verified)\n`); + + // Test 4.5.6: FUNDS_AND_PAYLOAD Route (SPL Token) - if token is loaded + if (tokenLoaded) { + console.log("4.5.6. Testing FUNDS_AND_PAYLOAD Route (SPL Token)..."); + // Ensure vault ATA exists + const vaultAta = await spl.getOrCreateAssociatedTokenAccount( + adminProvider.connection as any, + adminKeypair, + mint, + vaultPda, + true + ); + + const splFundsPayloadBridgeAmount = new anchor.BN(500 * Math.pow(10, tokenInfo.decimals)); + const splFundsPayloadGasAmount = await getDynamicGasAmount(1.5, 0.01); + const splFundsPayloadRecipient = Array.from(Buffer.from("4444444444444444444444444444444444444444", "hex").subarray(0, 20)); + + // Use a very large payload to intentionally stress Solana's 1232-byte tx limit + // so we can observe the legacy (no ALT) transaction failing with "transaction too large". + const largePayloadData = Buffer.alloc(600, 1); // 600 bytes of non-zero data + const largePayloadStruct = { + ...createPayload(3), + data: largePayloadData, + }; + + const splFundsPayloadReq = { + recipient: splFundsPayloadRecipient, + token: mint, + amount: splFundsPayloadBridgeAmount, + payload: serializePayload(largePayloadStruct), + revertInstruction: createRevertInstruction(user), + signatureData: Buffer.from("spl_funds_payload_sig"), + }; + + const initialVaultBalanceSplFundsPayload = await connection.getBalance(vaultPda); + const userTokenBalanceBeforeSplFundsPayload = (await spl.getAccount(userProvider.connection as any, tokenAccount)).amount; + const vaultTokenBalanceBeforeSplFundsPayload = (await spl.getAccount( + userProvider.connection as any, + vaultAta.address, + )).amount; + const splTokenRateLimitPda = getTokenRateLimitPda(mint); + + // --- Legacy path (without ALT) would now fail with Transaction too large. + // Instead, build a v0 tx using the ALT created by create-universal-alt.ts. + const { value: alt } = await connection.getAddressLookupTable(ALT_ADDRESS); + if (!alt) { + throw new Error(`Lookup table ${ALT_ADDRESS.toBase58()} not found on chain`); + } + + const ix = await userProgram.methods + .sendUniversalTx(splFundsPayloadReq, splFundsPayloadGasAmount) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: tokenAccount, + gatewayTokenAccount: vaultAta.address, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: splTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .instruction(); + + const recent = await connection.getLatestBlockhash(); + const message = new TransactionMessage({ + payerKey: user, + recentBlockhash: recent.blockhash, + instructions: [ix], + }).compileToV0Message([alt]); + + const vtx = new VersionedTransaction(message); + vtx.sign([userKeypair]); + const vtxBytes = vtx.serialize().length; + console.log(` ALT v0 tx size: ${vtxBytes} bytes`); + + const splFundsPayloadTx = await connection.sendTransaction(vtx, { + maxRetries: 3, + }); + // Wait for confirmation so balances / events reflect this tx (sendTransaction alone is fire-and-forget). + await connection.confirmTransaction(splFundsPayloadTx, "confirmed"); + + console.log(`✅ FUNDS_AND_PAYLOAD route (SPL token, ALT v0) transaction: ${splFundsPayloadTx}`); + await parseAndPrintEvents(splFundsPayloadTx, "FUNDS_AND_PAYLOAD route (SPL, ALT) events"); + const finalVaultBalanceSplFundsPayload = await connection.getBalance(vaultPda); + const userTokenBalanceAfterSplFundsPayload = (await spl.getAccount(userProvider.connection as any, tokenAccount)).amount; + const vaultTokenBalanceAfterSplFundsPayload = (await spl.getAccount(userProvider.connection as any, vaultAta.address)).amount; + const vaultSolIncrease = finalVaultBalanceSplFundsPayload - initialVaultBalanceSplFundsPayload; + const userTokenChange = Number(userTokenBalanceAfterSplFundsPayload) - Number(userTokenBalanceBeforeSplFundsPayload); + const vaultTokenChange = Number(vaultTokenBalanceAfterSplFundsPayload) - Number(vaultTokenBalanceBeforeSplFundsPayload); + assert.equal(vaultSolIncrease, splFundsPayloadGasAmount.toNumber(), "Vault should receive exact gas amount"); + assert.equal(userTokenChange, -splFundsPayloadBridgeAmount.toNumber(), "User should lose exact SPL bridge amount"); + assert.equal(vaultTokenChange, splFundsPayloadBridgeAmount.toNumber(), "Vault should gain exact SPL bridge amount"); + console.log(`💰 Vault SOL increased by: ${vaultSolIncrease / LAMPORTS_PER_SOL} SOL (verified)`); + console.log(`📊 User SPL: ${userTokenBalanceBeforeSplFundsPayload.toString()} → ${userTokenBalanceAfterSplFundsPayload.toString()} (verified)`); + console.log(`📊 Vault SPL: ${vaultTokenBalanceBeforeSplFundsPayload.toString()} → ${vaultTokenBalanceAfterSplFundsPayload.toString()} (verified)\n`); + } + + // Test 4.5.7: Edge Cases and Negative Tests + console.log("4.5.7. Testing Edge Cases and Negative Tests..."); + + // Edge Case: Payload-only execution (GAS_AND_PAYLOAD with native_amount == 0) + console.log(" - Testing payload-only execution (GAS_AND_PAYLOAD with 0 gas)..."); + const payloadOnlyReq = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: serializePayload(createPayload(5)), + revertInstruction: createRevertInstruction(user), + signatureData: Buffer.from("payload_only_sig"), + }; + + const initialVaultBalancePayloadOnly = await connection.getBalance(vaultPda); + try { + const payloadOnlyTx = await userProgram.methods + .sendUniversalTx(payloadOnlyReq, new anchor.BN(0)) // 0 native amount + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([userKeypair]) + .rpc(); + const finalVaultBalancePayloadOnly = await connection.getBalance(vaultPda); + assert.equal(finalVaultBalancePayloadOnly, initialVaultBalancePayloadOnly, "Vault balance should not change for payload-only execution"); + console.log(` ✅ Payload-only execution succeeded: ${payloadOnlyTx} (no balance change verified)`); + } catch (error: any) { + console.log(` ⚠️ Payload-only execution failed: ${error.message}`); + throw error; // Re-throw to fail the script if this should succeed + } + + + // Negative Test: FUNDS native with mismatched amounts (should fail) + console.log(" - Testing negative case: FUNDS native with mismatched amounts (should fail)..."); + const mismatchedFundsReq = { + recipient: Array.from(Buffer.from("5555555555555555555555555555555555555555", "hex").subarray(0, 20)), + token: PublicKey.default, + amount: new anchor.BN(0.01 * LAMPORTS_PER_SOL), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user), + signatureData: Buffer.from("mismatched_sig"), + }; + + try { + await userProgram.methods + .sendUniversalTx(mismatchedFundsReq, new anchor.BN(0.005 * LAMPORTS_PER_SOL)) // Different amount + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([userKeypair]) + .rpc(); + assert.fail("Should have rejected mismatched amounts"); + } catch (error: any) { + const errorCode = error.error?.errorCode?.code || error.error?.errorCode || error.code; + assert.equal(errorCode, "InvalidAmount", `Expected InvalidAmount but got: ${errorCode}`); + console.log(` ✅ Correctly rejected mismatched amounts with InvalidAmount error`); + } + + // Negative Test: FUNDS SPL with native SOL provided (should fail) + if (tokenLoaded) { + console.log(" - Testing negative case: FUNDS SPL with native SOL (should fail)..."); + // Ensure vault ATA exists for the test + const testVaultAta = await spl.getOrCreateAssociatedTokenAccount( + adminProvider.connection as any, + adminKeypair, + mint, + vaultPda, + true + ); + + const invalidSplFundsReq = { + recipient: Array.from(Buffer.from("6666666666666666666666666666666666666666", "hex").subarray(0, 20)), + token: mint, + amount: new anchor.BN(1000 * Math.pow(10, tokenInfo.decimals)), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user), + signatureData: Buffer.from("invalid_spl_sig"), + }; + + try { + await userProgram.methods + .sendUniversalTx(invalidSplFundsReq, new anchor.BN(0.001 * LAMPORTS_PER_SOL)) // Native SOL provided + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: tokenAccount, + gatewayTokenAccount: testVaultAta.address, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: getTokenRateLimitPda(mint), + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([userKeypair]) + .rpc(); + assert.fail("Should have rejected SPL FUNDS with native SOL"); + } catch (error: any) { + const errorCode = error.error?.errorCode?.code || error.error?.errorCode || error.code; + assert.equal(errorCode, "InvalidAmount", `Expected InvalidAmount but got: ${errorCode}`); + console.log(` ✅ Correctly rejected SPL FUNDS with native SOL (InvalidAmount error)`); + } + } + + // Negative Test: Invalid revert recipient (should fail with InvalidRecipient) + console.log(" - Testing negative case: Invalid revert recipient (should fail)..."); + const invalidRecipientReq = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(PublicKey.default), // Invalid: default pubkey + signatureData: Buffer.from("invalid_recipient_sig"), + }; + + try { + await userProgram.methods + .sendUniversalTx(invalidRecipientReq, await getDynamicGasAmount(2.5, 0.01)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([userKeypair]) + .rpc(); + assert.fail("Should have rejected invalid revert recipient"); + } catch (error: any) { + const errorCode = error.error?.errorCode?.code || error.error?.errorCode || error.code; + assert.equal(errorCode, "InvalidRecipient", `Expected InvalidRecipient but got: ${errorCode}`); + console.log(` ✅ Correctly rejected invalid revert recipient (InvalidRecipient error)`); + } + + // Negative Test: FUNDS with mismatched native amount (should fail with InvalidAmount) + // NOTE: When payload is empty and amount > 0, fetchTxType routes to TxType::Funds (not FundsAndPayload) + // The validation happens in fetchTxType before routing, so we get InvalidAmount, not InvalidInput + console.log(" - Testing negative case: FUNDS with mismatched native amount (should fail)..."); + const mismatchedNativeReq = { + recipient: Array.from(Buffer.from("7777777777777777777777777777777777777777", "hex").subarray(0, 20)), + token: PublicKey.default, + amount: new anchor.BN(0.01 * LAMPORTS_PER_SOL), + payload: Buffer.from([]), // Empty payload routes to FUNDS, not FUNDS_AND_PAYLOAD + revertInstruction: createRevertInstruction(user), + signatureData: Buffer.from("mismatched_native_sig"), + }; + + try { + await userProgram.methods + .sendUniversalTx(mismatchedNativeReq, new anchor.BN(0.02 * LAMPORTS_PER_SOL)) // native != amount + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([userKeypair]) + .rpc(); + assert.fail("Should have rejected FUNDS with mismatched native amount"); + } catch (error: any) { + const errorCode = error.error?.errorCode?.code || error.error?.errorCode || error.code; + assert.equal(errorCode, "InvalidAmount", `Expected InvalidAmount but got: ${errorCode}`); + console.log(` ✅ Correctly rejected FUNDS with mismatched native amount (InvalidAmount error)`); + } + + // Negative Test: FUNDS_AND_PAYLOAD native with insufficient native amount (should fail) + console.log(" - Testing negative case: FUNDS_AND_PAYLOAD native with insufficient amount (should fail)..."); + const insufficientNativeReq = { + recipient: Array.from(Buffer.from("8888888888888888888888888888888888888888", "hex").subarray(0, 20)), + token: PublicKey.default, + amount: new anchor.BN(0.01 * LAMPORTS_PER_SOL), + payload: serializePayload(createPayload(4)), + revertInstruction: createRevertInstruction(user), + signatureData: Buffer.from("insufficient_native_sig"), + }; + + try { + await userProgram.methods + .sendUniversalTx(insufficientNativeReq, new anchor.BN(0.005 * LAMPORTS_PER_SOL)) // native < amount + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user, + priceUpdate: PRICE_ACCOUNT, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([userKeypair]) + .rpc(); + assert.fail("Should have rejected insufficient native amount"); + } catch (error: any) { + const errorCode = error.error?.errorCode?.code || error.error?.errorCode || error.code; + assert.equal(errorCode, "InvalidAmount", `Expected InvalidAmount but got: ${errorCode}`); + console.log(` ✅ Correctly rejected insufficient native amount (InvalidAmount error)`); + } + + console.log("✅ Edge cases and negative tests completed!\n"); + + console.log("✅ All sendUniversalTx tests completed!\n"); + + // Step 5: Test send_tx_with_gas (SOL deposit with payload) - LEGACY FUNCTION console.log("5. Testing send_tx_with_gas..."); const userBalanceBefore = await connection.getBalance(user); const vaultBalanceBefore = await connection.getBalance(vaultPda); @@ -282,12 +997,12 @@ async function run() { to: Array.from(Buffer.from("1234567890123456789012345678901234567890", "hex").subarray(0, 20)), // Ethereum address (20 bytes) value: new anchor.BN(0), // Value to send data: Buffer.from("test payload data"), - gas_limit: new anchor.BN(100000), - max_fee_per_gas: new anchor.BN(20000000000), // 20 gwei - max_priority_fee_per_gas: new anchor.BN(1000000000), // 1 gwei + gasLimit: new anchor.BN(100000), + maxFeePerGas: new anchor.BN(20000000000), // 20 gwei + maxPriorityFeePerGas: new anchor.BN(1000000000), // 1 gwei nonce: new anchor.BN(0), deadline: new anchor.BN(Date.now() + 3600000), // 1 hour from now - v_type: { signedVerification: {} }, // VerificationType enum + vType: { signedVerification: {} }, // VerificationType enum }; const revertInstructions = { @@ -299,8 +1014,11 @@ async function run() { // Get dynamic gas amount for USD cap compliance const gasAmount = await getDynamicGasAmount(1.20, 0.01); // Target $1.20, fallback 0.01 SOL + // Generate signature data for send_tx_with_gas + const gasSignatureData = Buffer.from("test_signature_data_for_send_tx_with_gas", "utf8"); + const gasTx = await userProgram.methods - .sendTxWithGas(payload, revertInstructions, gasAmount) + .sendTxWithGas(payload, revertInstructions, gasAmount, gasSignatureData) .accounts({ config: configPda, vault: vaultPda, @@ -315,8 +1033,10 @@ async function run() { const userBalanceAfter = await connection.getBalance(user); const vaultBalanceAfter = await connection.getBalance(vaultPda); + const vaultBalanceIncrease = vaultBalanceAfter - vaultBalanceBefore; + assert.equal(vaultBalanceIncrease, gasAmount.toNumber(), "Vault should receive exact gas amount"); console.log(`User balance AFTER: ${userBalanceAfter / LAMPORTS_PER_SOL} SOL`); - console.log(`Vault balance AFTER: ${vaultBalanceAfter / LAMPORTS_PER_SOL} SOL\n`); + console.log(`Vault balance AFTER: ${vaultBalanceAfter / LAMPORTS_PER_SOL} SOL (verified: +${vaultBalanceIncrease / LAMPORTS_PER_SOL} SOL)\n`); // Step 5a: Legacy add_funds (locker-compatible) console.log("5a. Legacy add_funds (locker-compatible)..."); @@ -371,8 +1091,10 @@ async function run() { const userBalanceAfterFunds = await connection.getBalance(user); const vaultBalanceAfterFunds = await connection.getBalance(vaultPda); + const vaultBalanceIncreaseFunds = vaultBalanceAfterFunds - vaultBalanceBeforeFunds; + assert.equal(vaultBalanceIncreaseFunds, fundAmount.toNumber(), "Vault should receive exact funds amount"); console.log(`💳 User balance AFTER send_funds (native): ${userBalanceAfterFunds / LAMPORTS_PER_SOL} SOL`); - console.log(`🏦 Vault balance AFTER send_funds (native): ${vaultBalanceAfterFunds / LAMPORTS_PER_SOL} SOL\n`); + console.log(`🏦 Vault balance AFTER send_funds (native): ${vaultBalanceAfterFunds / LAMPORTS_PER_SOL} SOL (verified: +${vaultBalanceIncreaseFunds / LAMPORTS_PER_SOL} SOL)\n`); // Step 7: Test SPL token functions console.log("7. Testing SPL Token Functions..."); @@ -427,9 +1149,12 @@ async function run() { // Get SPL balances after const userTokenBalanceAfter = (await spl.getAccount(userProvider.connection as any, tokenAccount)).amount; const vaultTokenBalanceAfter = (await spl.getAccount(userProvider.connection as any, vaultAta.address)).amount; - - console.log(`📊 User SPL balance AFTER: ${userTokenBalanceAfter.toString()} tokens`); - console.log(`📊 Vault SPL balance AFTER: ${vaultTokenBalanceAfter.toString()} tokens\n`); + const userTokenChange = Number(userTokenBalanceAfter) - Number(userTokenBalanceBefore); + const vaultTokenChange = Number(vaultTokenBalanceAfter) - Number(vaultTokenBalanceBefore); + assert.equal(userTokenChange, -splAmount.toNumber(), "User should lose exact SPL amount"); + assert.equal(vaultTokenChange, splAmount.toNumber(), "Vault should gain exact SPL amount"); + console.log(`📊 User SPL balance AFTER: ${userTokenBalanceAfter.toString()} tokens (verified: ${userTokenChange < 0 ? '-' : '+'}${Math.abs(userTokenChange)})`); + console.log(`📊 Vault SPL balance AFTER: ${vaultTokenBalanceAfter.toString()} tokens (verified: +${vaultTokenChange})\n`); // Step 8: Test send_tx_with_funds (SPL + payload + gas) console.log("8. Testing send_tx_with_funds (SPL + payload + gas)..."); @@ -445,12 +1170,12 @@ async function run() { to: Array.from(Buffer.from("abcdefabcdefabcdefabcdefabcdefabcdefabcd", "hex").subarray(0, 20)), // Ethereum address (20 bytes) value: new anchor.BN(0), // Value to send data: Buffer.from("test payload for funds+gas"), - gas_limit: new anchor.BN(120000), - max_fee_per_gas: new anchor.BN(20000000000), // 20 gwei - max_priority_fee_per_gas: new anchor.BN(1000000000), // 1 gwei + gasLimit: new anchor.BN(120000), + maxFeePerGas: new anchor.BN(20000000000), // 20 gwei + maxPriorityFeePerGas: new anchor.BN(1000000000), // 1 gwei nonce: new anchor.BN(1), deadline: new anchor.BN(Date.now() + 3600000), // 1 hour from now - v_type: { signedVerification: {} }, // VerificationType enum + vType: { signedVerification: {} }, // VerificationType enum }; console.log(`🚀 Testing combined SPL + Gas transaction...`); @@ -503,11 +1228,16 @@ async function run() { const vaultBalanceAfterTxWithFunds = await connection.getBalance(vaultPda); const userTokenBalanceAfterTx = (await spl.getAccount(userProvider.connection as any, tokenAccount)).amount; const vaultTokenBalanceAfterTx = (await spl.getAccount(userProvider.connection as any, vaultAta.address)).amount; - + const vaultSolIncreaseTx = vaultBalanceAfterTxWithFunds - vaultBalanceBeforeTxWithFunds; + const userTokenChangeTx = Number(userTokenBalanceAfterTx) - Number(userTokenBalanceBeforeTx); + const vaultTokenChangeTx = Number(vaultTokenBalanceAfterTx) - Number(vaultTokenBalanceBeforeTx); + assert.equal(vaultSolIncreaseTx, txWithFundsGasAmount.toNumber(), "Vault should receive exact gas amount"); + assert.equal(userTokenChangeTx, -txWithFundsSplAmount.toNumber(), "User should lose exact SPL amount"); + assert.equal(vaultTokenChangeTx, txWithFundsSplAmount.toNumber(), "Vault should gain exact SPL amount"); console.log(`💳 User SOL balance AFTER: ${userBalanceAfterTxWithFunds / LAMPORTS_PER_SOL} SOL`); - console.log(`🏦 Vault SOL balance AFTER: ${vaultBalanceAfterTxWithFunds / LAMPORTS_PER_SOL} SOL`); - console.log(`📊 User SPL balance AFTER: ${userTokenBalanceAfterTx.toString()} tokens`); - console.log(`📊 Vault SPL balance AFTER: ${vaultTokenBalanceAfterTx.toString()} tokens\n`); + console.log(`🏦 Vault SOL balance AFTER: ${vaultBalanceAfterTxWithFunds / LAMPORTS_PER_SOL} SOL (verified: +${vaultSolIncreaseTx / LAMPORTS_PER_SOL} SOL)`); + console.log(`📊 User SPL balance AFTER: ${userTokenBalanceAfterTx.toString()} tokens (verified: ${userTokenChangeTx < 0 ? '-' : '+'}${Math.abs(userTokenChangeTx)})`); + console.log(`📊 Vault SPL balance AFTER: ${vaultTokenBalanceAfterTx.toString()} tokens (verified: +${vaultTokenChangeTx})\n`); // Step 9.5: Test send_tx_with_funds with native SOL as both bridge token and gas console.log("9.5. Testing send_tx_with_funds with native SOL (bridge + gas)..."); @@ -608,9 +1338,10 @@ async function run() { } // Try to send funds while paused (should fail) + const signatureNew = Buffer.from("test_signature_data_for_gas", "utf8"); try { await userProgram.methods - .sendTxWithGas(payload, revertInstructions, gasAmount) + .sendTxWithGas(payload, revertInstructions, gasAmount, signatureNew) .accounts({ config: configPda, vault: vaultPda, @@ -619,9 +1350,11 @@ async function run() { systemProgram: SystemProgram.programId, }) .rpc(); - console.log("❌ Transaction should have failed while paused!"); - } catch (error) { - console.log("✅ Transaction correctly failed while paused"); + assert.fail("Transaction should have failed while paused"); + } catch (error: any) { + const errorCode = error.error?.errorCode?.code || error.error?.errorCode || error.code; + assert.equal(errorCode, "Paused", `Expected Paused but got: ${errorCode}`); + console.log("✅ Transaction correctly failed while paused (Paused error verified)"); } try { @@ -660,6 +1393,7 @@ async function run() { .accounts({ tssPda: tssPda, authority: admin, + config: configPda, systemProgram: SystemProgram.programId, }) .signers([adminKeypair]) @@ -675,6 +1409,7 @@ async function run() { .accounts({ tssPda: tssPda, authority: admin, + config: configPda, systemProgram: SystemProgram.programId, }) .signers([adminKeypair]) @@ -774,16 +1509,16 @@ async function run() { nonceBE_SPL.writeBigUInt64BE(BigInt(nonce + 1)); // Increment nonce for SPL withdraw const amountBE_SPL = Buffer.alloc(8); amountBE_SPL.writeBigUInt64BE(BigInt(splWithdrawAmount)); - const recipientBytesSPL = admin.toBuffer(); const mintBytes = mint.toBuffer(); // 32 bytes for mint address + // Include both mint AND recipient in message hash (ZetaChain pattern - security fix) const concatSPL = Buffer.concat([ PREFIX_SPL, instructionIdSPL, chainIdBE_SPL, nonceBE_SPL, amountBE_SPL, - mintBytes, // Additional data for SPL withdraw (only mint, not recipient) + mintBytes // Token mint (32 bytes) ]); const messageHashHexSPL = keccak_256(concatSPL); const messageHashSPL = Buffer.from(messageHashHexSPL, "hex"); diff --git a/contracts/svm-gateway/app/payload-test.ts b/contracts/svm-gateway/app/payload-test.ts new file mode 100644 index 0000000..5fea7e0 --- /dev/null +++ b/contracts/svm-gateway/app/payload-test.ts @@ -0,0 +1,128 @@ +import * as anchor from "@coral-xyz/anchor"; +import { + Connection, + Keypair, + PublicKey, + SystemProgram, + LAMPORTS_PER_SOL, +} from "@solana/web3.js"; +import { Program } from "@coral-xyz/anchor"; +import fs from "fs"; +import * as spl from "@solana/spl-token"; + +type PayloadStruct = { + to: number[]; + value: anchor.BN; + data: Buffer; + gasLimit: anchor.BN; + maxFeePerGas: anchor.BN; + maxPriorityFeePerGas: anchor.BN; + nonce: anchor.BN; + deadline: anchor.BN; + vType: { signedVerification: Record }; +}; + +const PROGRAM_ID = new PublicKey("CFVSincHYbETh2k7w6u1ENEkjbSLtveRCEBupKidw2VS"); +const CONFIG_SEED = "config"; +const VAULT_SEED = "vault"; +const WHITELIST_SEED = "whitelist"; +const PRICE_ACCOUNT = new PublicKey("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE"); + +function loadKeypair(path: string): Keypair { + const secret = JSON.parse(fs.readFileSync(path, "utf8")); + return Keypair.fromSecretKey(Uint8Array.from(secret)); +} + +async function main() { + const adminKeypair = loadKeypair("./upgrade-keypair.json"); + const userKeypair = loadKeypair("./clean-user-keypair.json"); + + const connection = new Connection("https://api.devnet.solana.com", "confirmed"); + const adminProvider = new anchor.AnchorProvider( + connection, + new anchor.Wallet(adminKeypair), + { commitment: "confirmed" }, + ); + anchor.setProvider(adminProvider); + + const userProvider = new anchor.AnchorProvider( + connection, + new anchor.Wallet(userKeypair), + { commitment: "confirmed" }, + ); + + const idl = JSON.parse(fs.readFileSync("./target/idl/universal_gateway.json", "utf8")); + const userProgram = new Program(idl, userProvider); + + const [configPda] = PublicKey.findProgramAddressSync([Buffer.from(CONFIG_SEED)], PROGRAM_ID); + const [vaultPda] = PublicKey.findProgramAddressSync([Buffer.from(VAULT_SEED)], PROGRAM_ID); + const [whitelistPda] = PublicKey.findProgramAddressSync([Buffer.from(WHITELIST_SEED)], PROGRAM_ID); + + const commonPayload: PayloadStruct = { + to: Array.from(Buffer.from("1234567890123456789012345678901234567890", "hex").subarray(0, 20)), + value: new anchor.BN(0), + data: Buffer.from("test payload data"), + gasLimit: new anchor.BN(12230_000), + maxFeePerGas: new anchor.BN(223230_000_000_000), + maxPriorityFeePerGas: new anchor.BN(1_0677_000_000), + nonce: new anchor.BN(4982), + deadline: new anchor.BN(Date.now() + 60 * 60 * 1000), + vType: { signedVerification: {} }, + }; + + const revertInstructions = { + fundRecipient: userKeypair.publicKey, + revertMsg: Buffer.from("payload revert test"), + }; + + const gasAmount = new anchor.BN(Math.floor(0.01 * LAMPORTS_PER_SOL)); + const bridgeAmount = new anchor.BN(Math.floor(0.015 * LAMPORTS_PER_SOL)); + const gasSignatureData = Buffer.from("sig-sendTxWithGas"); + const fundsSignatureData = Buffer.from("sig-sendTxWithFunds"); + + console.log("-> sendTxWithGas"); + const gasTx = await userProgram.methods + .sendTxWithGas(commonPayload as any, revertInstructions, gasAmount, gasSignatureData) + .accounts({ + config: configPda, + vault: vaultPda, + user: userKeypair.publicKey, + priceUpdate: PRICE_ACCOUNT, + systemProgram: SystemProgram.programId, + }) + .rpc(); + + console.log(` tx: ${gasTx}`); + + console.log("-> sendTxWithFunds (native)"); + const fundsTx = await userProgram.methods + .sendTxWithFunds( + PublicKey.default, + bridgeAmount, + commonPayload as any, + revertInstructions, + gasAmount, + fundsSignatureData, + ) + .accounts({ + config: configPda, + vault: vaultPda, + user: userKeypair.publicKey, + tokenWhitelist: whitelistPda, + userTokenAccount: userKeypair.publicKey, + gatewayTokenAccount: vaultPda, + priceUpdate: PRICE_ACCOUNT, + bridgeToken: PublicKey.default, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .rpc(); + + console.log(` tx: ${fundsTx}`); +} + +main().catch((err) => { + console.error("Script failed:", err); + process.exit(1); +}); + diff --git a/contracts/svm-gateway/app/token-cli.ts b/contracts/svm-gateway/app/token-cli.ts index 4219386..16e5633 100755 --- a/contracts/svm-gateway/app/token-cli.ts +++ b/contracts/svm-gateway/app/token-cli.ts @@ -10,7 +10,7 @@ import { } from "@solana/web3.js"; import fs from "fs"; import { Program } from "@coral-xyz/anchor"; -import type { Pushsolanagateway } from "../target/types/pushsolanagateway"; +import type { UniversalGateway } from "../target/types/universal_gateway"; import * as spl from "@solana/spl-token"; import { createCreateMetadataAccountV3Instruction, @@ -22,6 +22,7 @@ const PROGRAM_ID = new PublicKey("CFVSincHYbETh2k7w6u1ENEkjbSLtveRCEBupKidw2VS") const CONFIG_SEED = "config"; const VAULT_SEED = "vault"; const WHITELIST_SEED = "whitelist"; +const RATE_LIMIT_SEED = "rate_limit"; // Load keypairs const adminKeypair = Keypair.fromSecretKey( @@ -43,9 +44,9 @@ const userProvider = new anchor.AnchorProvider(connection, new anchor.Wallet(use anchor.setProvider(adminProvider); // Load IDL -const idl = JSON.parse(fs.readFileSync("./target/idl/pushsolanagateway.json", "utf8")); -const program = new Program(idl as Pushsolanagateway, adminProvider); -const userProgram = new Program(idl as Pushsolanagateway, userProvider); +const idl = JSON.parse(fs.readFileSync("./target/idl/universal_gateway.json", "utf8")); +const program = new Program(idl as UniversalGateway, adminProvider); +const userProgram = new Program(idl as UniversalGateway, userProvider); // Helper function to create SPL token with metadata async function createSPLToken( @@ -308,6 +309,40 @@ async function whitelistToken(mintAddress: string): Promise { } } + // Set token rate limit with a very large threshold to enable token (bypass rate limiting) + // Since epoch_duration is 0, rate limiting is disabled, but threshold > 0 makes token valid + console.log(`⚙️ Setting token rate limit (large threshold to enable token)...`); + const [tokenRateLimitPda] = PublicKey.findProgramAddressSync( + [Buffer.from(RATE_LIMIT_SEED), mint.toBuffer()], + PROGRAM_ID + ); + + // Use a very large threshold (effectively unlimited) - u128 max is 2^128 - 1 + // Using a large but reasonable number: 10^30 (1 followed by 30 zeros) + const largeThreshold = new anchor.BN("1000000000000000000000000000000"); // 10^30 + + try { + const rateLimitTx = await program.methods + .setTokenRateLimit(largeThreshold) + .accounts({ + config: configPda, + tokenRateLimit: tokenRateLimitPda, + tokenMint: mint, + admin: admin, + systemProgram: SystemProgram.programId, + }) + .rpc(); + console.log(`✅ Token rate limit set successfully: ${rateLimitTx}`); + } catch (error) { + // If it already exists, that's fine - it might have been set before + if (error.message.includes("already in use") || error.message.includes("already exists")) { + console.log(`✅ Token rate limit already set (skipping)`); + } else { + console.log(`⚠️ Warning: Could not set token rate limit: ${error.message}`); + // Don't throw - whitelisting might still work if rate limit was set manually + } + } + // Create vault ATA for the token console.log(`🏦 Creating vault ATA for token...`); try { diff --git a/contracts/svm-gateway/mock-pyth b/contracts/svm-gateway/mock-pyth new file mode 160000 index 0000000..2b1488d --- /dev/null +++ b/contracts/svm-gateway/mock-pyth @@ -0,0 +1 @@ +Subproject commit 2b1488d864101ca105592deef4ebc724e2a6caf2 diff --git a/contracts/svm-gateway/package.json b/contracts/svm-gateway/package.json index ec9b9d0..692e132 100644 --- a/contracts/svm-gateway/package.json +++ b/contracts/svm-gateway/package.json @@ -8,20 +8,21 @@ "token:mint": " ts-node app/token-cli.ts mint", "token:whitelist": " ts-node app/token-cli.ts whitelist", "token:remove-whitelist": " ts-node app/token-cli.ts remove-whitelist", - "token:list": " ts-node app/token-cli.ts list", - "test": " ts-node app/simple-spl-test.ts", - "test:whitelist": " ts-node app/simple-spl-test.ts whitelist", - "test:deposit": " ts-node app/simple-spl-test.ts test-deposit", - "test:full": " ts-node app/simple-spl-test.ts full-test" + "token:list": " ts-node app/token-cli.ts list" }, "dependencies": { "@coral-xyz/anchor": "^0.31.1", "@jup-ag/api": "^6.0.41", "@metaplex-foundation/mpl-token-metadata": "^2.1.4", + "@noble/hashes": "^1.3.0", + "@noble/secp256k1": "^1.7.1", "@pythnetwork/hermes-client": "^2.0.0", "@pythnetwork/pyth-solana-receiver": "^0.10.1", + "@solana/spl-token": "^0.4.8", + "@tkkinn/mock-pyth-sdk": "^2.0.1", "bs58": "^6.0.0", - "commander": "^11.1.0" + "commander": "^11.1.0", + "js-sha3": "^0.9.2" }, "devDependencies": { "@types/bn.js": "^5.1.0", diff --git a/contracts/svm-gateway/programs/universal-gateway/Cargo.toml b/contracts/svm-gateway/programs/universal-gateway/Cargo.toml index c127ebf..6fd38b7 100644 --- a/contracts/svm-gateway/programs/universal-gateway/Cargo.toml +++ b/contracts/svm-gateway/programs/universal-gateway/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib", "lib"] name = "universal_gateway" [features] -default = ["idl-build"] +default = [] cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] diff --git a/contracts/svm-gateway/programs/universal-gateway/src/errors.rs b/contracts/svm-gateway/programs/universal-gateway/src/errors.rs index 88dcc80..791ed9b 100644 --- a/contracts/svm-gateway/programs/universal-gateway/src/errors.rs +++ b/contracts/svm-gateway/programs/universal-gateway/src/errors.rs @@ -59,6 +59,12 @@ pub enum GatewayError { #[msg("Invalid input")] InvalidInput, + #[msg("Invalid transaction type")] + InvalidTxType, + + #[msg("Invalid data")] + InvalidData, + #[msg("Invalid mint")] InvalidMint, @@ -77,4 +83,7 @@ pub enum GatewayError { #[msg("Invalid account")] InvalidAccount, + + #[msg("Token not supported")] + NotSupported, } diff --git a/contracts/svm-gateway/programs/universal-gateway/src/instructions/admin.rs b/contracts/svm-gateway/programs/universal-gateway/src/instructions/admin.rs index ad98e20..2465c6e 100644 --- a/contracts/svm-gateway/programs/universal-gateway/src/instructions/admin.rs +++ b/contracts/svm-gateway/programs/universal-gateway/src/instructions/admin.rs @@ -97,11 +97,6 @@ pub fn whitelist_token(ctx: Context, token: Pubkey) -> Result<( // Add token to whitelist whitelist.tokens.push(token); - // Emit event - emit!(TokenWhitelisted { - token_address: token, - }); - Ok(()) } @@ -113,11 +108,6 @@ pub fn remove_whitelist_token(ctx: Context, token: Pubkey) -> R // Find and remove token from whitelist if let Some(pos) = whitelist.tokens.iter().position(|&x| x == token) { whitelist.tokens.remove(pos); - - // Emit event - emit!(TokenRemovedFromWhitelist { - token_address: token, - }); } else { return Err(GatewayError::TokenNotWhitelisted.into()); } @@ -180,8 +170,12 @@ pub fn set_block_usd_cap(ctx: Context, block_usd_cap: u12 } /// Update epoch duration for rate limiting (matching EVM updateEpochDuration) -pub fn update_epoch_duration(ctx: Context, epoch_duration_sec: u64) -> Result<()> { - require!(epoch_duration_sec > 0, GatewayError::InvalidAmount); +/// @param epoch_duration_sec Epoch duration in seconds. Set to 0 to disable epoch-based rate limiting. +pub fn update_epoch_duration( + ctx: Context, + epoch_duration_sec: u64, +) -> Result<()> { + // Allow 0 to disable epoch-based rate limiting let rate_limit_config = &mut ctx.accounts.rate_limit_config; rate_limit_config.epoch_duration_sec = epoch_duration_sec; rate_limit_config.bump = ctx.bumps.rate_limit_config; @@ -221,12 +215,13 @@ pub struct TokenRateLimitAction<'info> { pub system_program: Program<'info, System>, } +/// Set token-specific rate limit threshold (matching EVM setTokenToLimitThreshold) +/// @param limit_threshold Max amount per epoch (token's natural units). Set to 0 to disable rate limiting for this token. pub fn set_token_rate_limit( ctx: Context, limit_threshold: u128, ) -> Result<()> { - require!(limit_threshold > 0, GatewayError::InvalidAmount); - + // Allow limit_threshold = 0 to disable rate limiting (matching EVM behavior) let token_rate_limit = &mut ctx.accounts.token_rate_limit; token_rate_limit.token_mint = ctx.accounts.token_mint.key(); token_rate_limit.limit_threshold = limit_threshold; diff --git a/contracts/svm-gateway/programs/universal-gateway/src/instructions/deposit.rs b/contracts/svm-gateway/programs/universal-gateway/src/instructions/deposit.rs index d80bd28..ec5ef62 100644 --- a/contracts/svm-gateway/programs/universal-gateway/src/instructions/deposit.rs +++ b/contracts/svm-gateway/programs/universal-gateway/src/instructions/deposit.rs @@ -11,6 +11,26 @@ use pyth_solana_receiver_sdk::price_update::PriceUpdateV2; // DEPOSITS // ========================= +/// @notice Universal entrypoint (EVM parity): routes native/SPL deposits based on `TxType`. +/// @dev Single entrypoint for all deposit types with internal routing mechanism. +/// `native_amount` mirrors `msg.value` on EVM chains - represents total native SOL sent. +/// Routes to GAS (instant) or FUNDS (standard) handlers based on derived tx type. +pub fn send_universal_tx( + mut ctx: Context, + req: UniversalTxRequest, + native_amount: u64, +) -> Result<()> { + let config = &ctx.accounts.config; + require!(!config.paused, GatewayError::Paused); + require!( + ctx.accounts.user.lamports() >= native_amount, + GatewayError::InsufficientBalance + ); + + let tx_type = fetchTxType(&req, native_amount)?; + route_universal_tx(&mut ctx, req, native_amount, tx_type) +} + /// GAS route (Instant): fund UEA on Push Chain with native SOL; optional payload. /// Enforces USD caps via Pyth (8 decimals). Emits `TxWithGas`. pub fn send_tx_with_gas( @@ -18,6 +38,7 @@ pub fn send_tx_with_gas( payload: UniversalPayload, revert_instruction: RevertInstructions, amount: u64, + signature_data: Vec, ) -> Result<()> { let config = &ctx.accounts.config; let user = &ctx.accounts.user; @@ -71,12 +92,371 @@ pub fn send_tx_with_gas( payload: payload_to_bytes(&payload), revert_instruction, tx_type: TxType::GasAndPayload, - signature_data: vec![], // Empty for gas-only route + signature_data, // Use the provided signature data + }); + + Ok(()) +} + +/// @notice Internal router: dispatches to GAS or FUNDS handlers based on derived tx_type. +/// @dev Route 1: GAS | GAS_AND_PAYLOAD → Instant route (fee abstraction) +/// Route 2: FUNDS | FUNDS_AND_PAYLOAD → Standard route (bridge deposits) +/// @dev GAS routes require req.amount == 0 (funds leg disabled). native_amount represents gas. +/// FUNDS routes require req.amount > 0 (funds leg enabled); native_amount may batch gas. +fn route_universal_tx( + ctx: &mut Context, + req: UniversalTxRequest, + native_amount: u64, + tx_type: TxType, +) -> Result<()> { + match tx_type { + TxType::Gas | TxType::GasAndPayload => send_tx_with_gas_route( + ctx, + tx_type, + native_amount, + &req.payload, + &req.revert_instruction, + &req.signature_data, + ), + TxType::Funds | TxType::FundsAndPayload => { + send_tx_with_funds_route(ctx, req, native_amount, tx_type) + } + _ => Err(error!(GatewayError::InvalidTxType)), + } +} + +#[allow(non_snake_case)] +fn fetchTxType(req: &UniversalTxRequest, native_amount: u64) -> Result { + let has_payload = !req.payload.is_empty(); + let has_funds = req.amount > 0; + let funds_is_native = req.token == Pubkey::default(); + let has_native_value = native_amount > 0; + + if !has_funds { + if has_payload { + return Ok(TxType::GasAndPayload); + } + require!(has_native_value, GatewayError::InvalidInput); + return Ok(TxType::Gas); + } + + if has_payload { + if funds_is_native { + require!(native_amount >= req.amount, GatewayError::InvalidAmount); + } + + return Ok(TxType::FundsAndPayload); + } + + // FUNDS with no payload + if funds_is_native { + require!(native_amount == req.amount, GatewayError::InvalidAmount); + } else { + require!(!has_native_value, GatewayError::InvalidAmount); + } + + Ok(TxType::Funds) +} + +/// @notice Internal helper function to deposit for Instant TX (GAS route). +/// @dev Handles rate-limit checks for Fee Abstraction Tx Route. +/// - Validates revert instruction recipient +/// - Validates payload: GAS must have empty payload, GAS_AND_PAYLOAD must have non-empty payload +/// - Supports payload-only execution (gas_amount == 0) for EVM V0 parity +/// - Enforces USD caps ($1-$10) and block-based USD cap via Pyth oracle +/// - Transfers native SOL to vault (recipient as Pubkey::default() → UEA) +fn send_tx_with_gas_route( + ctx: &mut Context, + tx_type: TxType, + gas_amount: u64, + payload: &[u8], + revert_instruction: &RevertInstructions, + signature_data: &[u8], +) -> Result<()> { + // Validate tx_type + require!( + matches!(tx_type, TxType::Gas | TxType::GasAndPayload), + GatewayError::InvalidTxType + ); + + // NOTE: Payload validation removed for testnet (matching EVM V0) + // V0 has these validations commented out (lines 1271-1277) + // if tx_type == TxType::GasAndPayload { + // require!(!payload.is_empty(), GatewayError::InvalidInput); + // } + // if tx_type == TxType::Gas { + // require!(payload.is_empty(), GatewayError::InvalidInput); + // } + + require!( + revert_instruction.fund_recipient != Pubkey::default(), + GatewayError::InvalidRecipient + ); + + // Payload-only execution (gas_amount == 0) - EVM V0 parity + // User already has UEA with gas on Push Chain, just execute payload + if gas_amount == 0 { + require!( + matches!(tx_type, TxType::GasAndPayload | TxType::FundsAndPayload), + GatewayError::InvalidAmount + ); + + emit!(UniversalTx { + sender: ctx.accounts.user.key(), + recipient: [0u8; 20], + token: Pubkey::default(), + amount: 0, + payload: payload.to_vec(), + revert_instruction: revert_instruction.clone(), + tx_type, + signature_data: signature_data.to_vec(), + }); + + return Ok(()); + } + + require!( + ctx.accounts.user.lamports() >= gas_amount, + GatewayError::InsufficientBalance + ); + + // Performs rate-limit checks and handle deposit + // USD caps: min $1, max $10 (enforced via Pyth oracle) + check_usd_caps(&ctx.accounts.config, gas_amount, &ctx.accounts.price_update)?; + let price_data = calculate_sol_price(&ctx.accounts.price_update)?; + let usd_amount = calculate_usd_amount(gas_amount, &price_data)?; + // Block-based USD cap: per-slot limit (disabled if block_usd_cap == 0) + check_block_usd_cap(&mut ctx.accounts.rate_limit_config, usd_amount)?; + + // Transfer native SOL to vault (like _handleNativeDeposit in ETH) + let cpi_ctx = CpiContext::new( + ctx.accounts.system_program.to_account_info(), + system_program::Transfer { + from: ctx.accounts.user.to_account_info(), + to: ctx.accounts.vault.to_account_info(), + }, + ); + system_program::transfer(cpi_ctx, gas_amount)?; + + // Emit UniversalTx event (recipient as Pubkey::default() → UEA) + emit!(UniversalTx { + sender: ctx.accounts.user.key(), + recipient: [0u8; 20], + token: Pubkey::default(), + amount: gas_amount, + payload: payload.to_vec(), + revert_instruction: revert_instruction.clone(), + tx_type, + signature_data: signature_data.to_vec(), }); Ok(()) } +/// @notice Internal helper function to deposit for Standard TX (FUNDS route). +/// @dev Handles bridge deposits with optional gas batching. +/// Case 1: TX_TYPE = FUNDS +/// - Case 1.1: Native SOL funds → req.token == Pubkey::default() +/// - Case 1.2: SPL token funds → req.token != Pubkey::default() +/// Case 2: TX_TYPE = FUNDS_AND_PAYLOAD +/// - Case 2.1: No batching (native_amount == 0) → user already has UEA with gas +/// - Case 2.2: Batching with native SOL → split: gasAmount = native_amount - req.amount +/// - Case 2.3: Batching with SPL + native gas → gasAmount = native_amount, bridgeAmount = req.amount +fn send_tx_with_funds_route( + ctx: &mut Context, + req: UniversalTxRequest, + native_amount: u64, + tx_type: TxType, +) -> Result<()> { + require!( + req.revert_instruction.fund_recipient != Pubkey::default(), + GatewayError::InvalidRecipient + ); + require!(req.amount > 0, GatewayError::InvalidAmount); + + // Payload validation (matching EVM Temp lines 978-984) + if tx_type == TxType::Funds { + // FUNDS-only must not carry a payload + require!(req.payload.is_empty(), GatewayError::InvalidInput); + } + if tx_type == TxType::FundsAndPayload { + // FUNDS_AND_PAYLOAD must have non-empty payload + require!(!req.payload.is_empty(), GatewayError::InvalidInput); + } + + match tx_type { + TxType::Funds => { + if req.token == Pubkey::default() { + // Case 1.1: Token to bridge is Native SOL → Pubkey::default() + require!(native_amount == req.amount, GatewayError::InvalidAmount); + + // Validate token support and consume rate limit if enabled + validate_token_and_consume_rate_limit( + &mut ctx.accounts.token_rate_limit, + Pubkey::default(), + req.amount as u128, + &ctx.accounts.rate_limit_config, + )?; + + // Transfer SOL + let cpi_ctx = CpiContext::new( + ctx.accounts.system_program.to_account_info(), + system_program::Transfer { + from: ctx.accounts.user.to_account_info(), + to: ctx.accounts.vault.to_account_info(), + }, + ); + system_program::transfer(cpi_ctx, req.amount)?; + } else { + // Case 1.2: Token to bridge is SPL Token → req.token + require!(native_amount == 0, GatewayError::InvalidAmount); + + // Validate token support and consume rate limit if enabled + validate_token_and_consume_rate_limit( + &mut ctx.accounts.token_rate_limit, + req.token, + req.amount as u128, + &ctx.accounts.rate_limit_config, + )?; + + // Transfer SPL + let user_token_info = ctx.accounts.user_token_account.to_account_info(); + let gateway_token_info = ctx.accounts.gateway_token_account.to_account_info(); + require!( + user_token_info.owner == &spl_token::ID, + GatewayError::InvalidOwner + ); + require!( + gateway_token_info.owner == &spl_token::ID, + GatewayError::InvalidOwner + ); + + let cpi_ctx = CpiContext::new( + ctx.accounts.token_program.to_account_info(), + Transfer { + from: user_token_info, + to: gateway_token_info, + authority: ctx.accounts.user.to_account_info(), + }, + ); + token::transfer(cpi_ctx, req.amount)?; + } + // Emit event + emit!(UniversalTx { + sender: ctx.accounts.user.key(), + recipient: req.recipient, + token: req.token, + amount: req.amount, + payload: req.payload, + revert_instruction: req.revert_instruction, + tx_type, + signature_data: req.signature_data, + }); + } + TxType::FundsAndPayload => { + if req.token == Pubkey::default() { + // Case 2.2: Batching of Gas + Funds_and_Payload (native_amount > 0): with token == native_token + // User refills UEA's gas and also bridges native token. + // Split Needed: Native token is split between gasAmount and bridge amount (native_amount >= req.amount) + // Note: If native_amount == 0, this will revert via the require below (Case 2.1 requires SPL token) + require!(native_amount >= req.amount, GatewayError::InvalidAmount); + let gas_amount = native_amount.saturating_sub(req.amount); + + // Send Gas to caller's UEA via instant route (if gas_amount > 0) + if gas_amount > 0 { + send_tx_with_gas_route( + ctx, + TxType::Gas, + gas_amount, + &[], + &req.revert_instruction, + &req.signature_data, + )?; + } + + // Validate token support and consume rate limit if enabled + validate_token_and_consume_rate_limit( + &mut ctx.accounts.token_rate_limit, + Pubkey::default(), + req.amount as u128, + &ctx.accounts.rate_limit_config, + )?; + + // Transfer funds + let cpi_ctx = CpiContext::new( + ctx.accounts.system_program.to_account_info(), + system_program::Transfer { + from: ctx.accounts.user.to_account_info(), + to: ctx.accounts.vault.to_account_info(), + }, + ); + system_program::transfer(cpi_ctx, req.amount)?; + } else { + // Case 2.1: No Batching (native_amount == 0): user already has UEA with gas on Push Chain + // User can directly move req.amount for req.token to Push Chain (SPL token only for Case 2.1) + // Case 2.3: Batching of Gas + Funds_and_Payload (native_amount > 0): with token != native_token + // User refills UEA's gas and also bridges SPL token. + // No Split Needed: gasAmount is used via native_token, and bridgeAmount is used via SPL token. + if native_amount > 0 { + // Send Gas to caller's UEA via instant route + send_tx_with_gas_route( + ctx, + TxType::Gas, + native_amount, + &[], + &req.revert_instruction, + &req.signature_data, + )?; + } + + // Validate token support and consume rate limit if enabled + validate_token_and_consume_rate_limit( + &mut ctx.accounts.token_rate_limit, + req.token, + req.amount as u128, + &ctx.accounts.rate_limit_config, + )?; + + // Transfer SPL + let user_token_info = ctx.accounts.user_token_account.to_account_info(); + let gateway_token_info = ctx.accounts.gateway_token_account.to_account_info(); + require!( + user_token_info.owner == &spl_token::ID, + GatewayError::InvalidOwner + ); + require!( + gateway_token_info.owner == &spl_token::ID, + GatewayError::InvalidOwner + ); + + let cpi_ctx = CpiContext::new( + ctx.accounts.token_program.to_account_info(), + Transfer { + from: user_token_info, + to: gateway_token_info, + authority: ctx.accounts.user.to_account_info(), + }, + ); + token::transfer(cpi_ctx, req.amount)?; + } + // Emit event + emit!(UniversalTx { + sender: ctx.accounts.user.key(), + recipient: [0u8; 20], + token: req.token, + amount: req.amount, + payload: req.payload, + revert_instruction: req.revert_instruction, + tx_type, + signature_data: req.signature_data, + }); + } + _ => return Err(error!(GatewayError::InvalidTxType)), + } + + Ok(()) +} + /// FUNDS route (Universal): move funds to Push Chain (no payload). /// Supports both native SOL and SPL tokens (like ETH Gateway). Emits `TxWithFunds`. pub fn send_funds( @@ -314,6 +694,55 @@ pub fn send_tx_with_funds( // ACCOUNT STRUCTS // ========================= +#[derive(Accounts)] +pub struct SendUniversalTx<'info> { + #[account( + mut, + seeds = [CONFIG_SEED], + bump = config.bump, + )] + pub config: Account<'info, Config>, + + #[account( + mut, + seeds = [VAULT_SEED], + bump = config.vault_bump, + )] + pub vault: SystemAccount<'info>, + + /// CHECK: Only required for SPL token routes; validated at runtime. + /// For native SOL routes, pass vault account as dummy (not used). + #[account(mut)] + pub user_token_account: UncheckedAccount<'info>, + + /// CHECK: Only required for SPL token routes; validated at runtime. + /// For native SOL routes, pass vault account as dummy (not used). + #[account(mut)] + pub gateway_token_account: UncheckedAccount<'info>, + + #[account(mut)] + pub user: Signer<'info>, + + pub price_update: Account<'info, PriceUpdateV2>, + + /// Rate limit config - REQUIRED for universal entrypoint + #[account( + mut, + seeds = [RATE_LIMIT_CONFIG_SEED], + bump, + )] + pub rate_limit_config: Account<'info, RateLimitConfig>, + + /// Token rate limit - REQUIRED for universal entrypoint + /// NOTE: For native SOL, use Pubkey::default() as the token_mint when deriving this PDA + #[account(mut)] + pub token_rate_limit: Account<'info, TokenRateLimit>, + + pub token_program: Program<'info, Token>, + + pub system_program: Program<'info, System>, +} + #[derive(Accounts)] pub struct SendTxWithGas<'info> { #[account( diff --git a/contracts/svm-gateway/programs/universal-gateway/src/instructions/tss.rs b/contracts/svm-gateway/programs/universal-gateway/src/instructions/tss.rs index 3574075..1deb99d 100644 --- a/contracts/svm-gateway/programs/universal-gateway/src/instructions/tss.rs +++ b/contracts/svm-gateway/programs/universal-gateway/src/instructions/tss.rs @@ -1,3 +1,4 @@ +use crate::errors::GatewayError; use crate::state::*; use anchor_lang::prelude::*; use anchor_lang::solana_program::{keccak::hash, secp256k1_recover::secp256k1_recover}; @@ -14,6 +15,13 @@ pub struct InitTss<'info> { )] pub tss_pda: Account<'info, TssPda>, + #[account( + seeds = [CONFIG_SEED], + bump = config.bump, + constraint = config.admin == authority.key() @ GatewayError::Unauthorized + )] + pub config: Account<'info, Config>, + #[account(mut)] pub authority: Signer<'info>, @@ -46,8 +54,18 @@ pub struct UpdateTss<'info> { pub fn update_tss(ctx: Context, tss_eth_address: [u8; 20], chain_id: u64) -> Result<()> { let tss = &mut ctx.accounts.tss_pda; + + // If TSS address changes, reset nonce to 0 for clarity and security + // (Old signatures won't work anyway due to address mismatch, but resetting is cleaner) + let address_changed = tss.tss_eth_address != tss_eth_address; + tss.tss_eth_address = tss_eth_address; tss.chain_id = chain_id; + + if address_changed { + tss.nonce = 0; + } + Ok(()) } diff --git a/contracts/svm-gateway/programs/universal-gateway/src/lib.rs b/contracts/svm-gateway/programs/universal-gateway/src/lib.rs index d970e9a..879f9e5 100644 --- a/contracts/svm-gateway/programs/universal-gateway/src/lib.rs +++ b/contracts/svm-gateway/programs/universal-gateway/src/lib.rs @@ -17,6 +17,17 @@ pub mod universal_gateway { // DEPOSITS // ========================= + /// @notice Universal transaction entrypoint with internal routing (EVM parity). + /// @dev Native amount parameter mirrors `msg.value` on EVM chains. + /// All routing (gas / funds / batching) is handled inside the deposit module. + pub fn send_universal_tx( + ctx: Context, + req: UniversalTxRequest, + native_amount: u64, + ) -> Result<()> { + instructions::deposit::send_universal_tx(ctx, req, native_amount) + } + /// @notice Allows initiating a TX for funding UEA with gas deposits from source chain. /// @dev Supports only native SOL deposits for gas funding. /// The route emits UniversalTx event - important for Instant TX Route. @@ -25,8 +36,15 @@ pub mod universal_gateway { payload: UniversalPayload, revert_instruction: RevertInstructions, amount: u64, + signature_data: Vec, ) -> Result<()> { - instructions::deposit::send_tx_with_gas(ctx, payload, revert_instruction, amount) + instructions::deposit::send_tx_with_gas( + ctx, + payload, + revert_instruction, + amount, + signature_data, + ) } /// @notice Allows initiating a TX for movement of funds from source chain to Push Chain. @@ -149,16 +167,24 @@ pub mod universal_gateway { // ========================= /// @notice Set block-based USD cap for rate limiting - pub fn set_block_usd_cap(ctx: Context, block_usd_cap: u128) -> Result<()> { + pub fn set_block_usd_cap( + ctx: Context, + block_usd_cap: u128, + ) -> Result<()> { instructions::admin::set_block_usd_cap(ctx, block_usd_cap) } /// @notice Update epoch duration for rate limiting - pub fn update_epoch_duration(ctx: Context, epoch_duration_sec: u64) -> Result<()> { + pub fn update_epoch_duration( + ctx: Context, + epoch_duration_sec: u64, + ) -> Result<()> { instructions::admin::update_epoch_duration(ctx, epoch_duration_sec) } /// @notice Set token-specific rate limit threshold + /// @dev For batch operations, call this function multiple times in a single transaction. + /// This is the Solana-idiomatic approach and provides better type safety than using remaining_accounts. pub fn set_token_rate_limit( ctx: Context, limit_threshold: u128, @@ -288,8 +314,10 @@ pub mod universal_gateway { } // Re-export account structs and types -pub use instructions::admin::{AdminAction, PauseAction, WhitelistAction, TokenRateLimitAction, RateLimitConfigAction}; -pub use instructions::deposit::{SendFunds, SendTxWithFunds, SendTxWithGas}; +pub use instructions::admin::{ + AdminAction, PauseAction, RateLimitConfigAction, TokenRateLimitAction, WhitelistAction, +}; +pub use instructions::deposit::{SendFunds, SendTxWithFunds, SendTxWithGas, SendUniversalTx}; pub use instructions::initialize::Initialize; pub use instructions::legacy::{AddFunds, FundsAddedEvent, GetSolPrice}; pub use instructions::withdraw::{RevertWithdraw, RevertWithdrawSplToken}; @@ -301,12 +329,11 @@ pub use state::{ Config, RevertInstructions, TSSAddressUpdated, - TokenRemovedFromWhitelist, TokenWhitelist, - TokenWhitelisted, TxType, UniversalPayload, UniversalTx, + UniversalTxRequest, VerificationType, WithdrawFunds, CONFIG_SEED, diff --git a/contracts/svm-gateway/programs/universal-gateway/src/state.rs b/contracts/svm-gateway/programs/universal-gateway/src/state.rs index 8454cc3..1b646a1 100644 --- a/contracts/svm-gateway/programs/universal-gateway/src/state.rs +++ b/contracts/svm-gateway/programs/universal-gateway/src/state.rs @@ -61,12 +61,23 @@ pub struct RevertInstructions { pub revert_msg: Vec, } +/// Universal transaction request (parity with EVM `UniversalTxRequest`). +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct UniversalTxRequest { + pub recipient: [u8; 20], // [0u8; 20] => credit to UEA on Push + pub token: Pubkey, // Pubkey::default() => native SOL + pub amount: u64, // native or SPL amount for bridging - Funds + pub payload: Vec, // serialized payload (may be empty) + pub revert_instruction: RevertInstructions, + pub signature_data: Vec, +} + /// Gateway configuration state (authorities, caps, oracle). /// PDA: `[b"config"]`. Holds USD caps (8 decimals) for gas-route deposits and oracle config. #[account] pub struct Config { pub admin: Pubkey, - pub tss_address: Pubkey, + pub tss_address: Pubkey, // Not used - TODO: Remove pub pauser: Pubkey, pub min_cap_universal_tx_usd: u128, // 1e8 = $1 (Pyth format) pub max_cap_universal_tx_usd: u128, // 1e8 = $10 (Pyth format) @@ -168,16 +179,6 @@ pub struct TSSAddressUpdated { pub new_tss: Pubkey, } -#[event] -pub struct TokenWhitelisted { - pub token_address: Pubkey, -} - -#[event] -pub struct TokenRemovedFromWhitelist { - pub token_address: Pubkey, -} - #[event] pub struct CapsUpdated { pub min_cap_usd: u128, diff --git a/contracts/svm-gateway/programs/universal-gateway/src/utils.rs b/contracts/svm-gateway/programs/universal-gateway/src/utils.rs index d59ec0a..cbb3df6 100644 --- a/contracts/svm-gateway/programs/universal-gateway/src/utils.rs +++ b/contracts/svm-gateway/programs/universal-gateway/src/utils.rs @@ -85,21 +85,35 @@ pub fn check_usd_caps( } /// Calculate USD amount from SOL amount using price data (matching EVM implementation) +/// @dev Pyth price format: actual_price = price * 10^exponent +/// For SOL/USD: price = 15025000000, exponent = -8 → actual_price = 150.25 USD +/// Result is in 8 decimals (matching EVM's 18 decimals but scaled to 8 for consistency) +/// Formula: USD_8dec = (lamports * price * 10^(exponent + 8)) / 1e9 pub fn calculate_usd_amount(lamports: u64, price_data: &PriceData) -> Result { - // Convert lamports to SOL (1 SOL = 1e9 lamports) - let sol_amount = lamports as u128; - - // Apply price with exponent - let price = if price_data.exponent >= 0 { - price_data.price as u128 * 10u128.pow(price_data.exponent as u32) + let lamports_u128 = lamports as u128; + let price_u128 = price_data.price as u128; + + // Multiply first to preserve precision, then apply exponent adjustment + // For exponent = -8: we need to multiply by 10^(exponent + 8) = 10^0 = 1 + let product = lamports_u128 + .checked_mul(price_u128) + .ok_or(GatewayError::InvalidAmount)?; + + // Apply exponent: multiply by 10^(exponent + 8) to get result in 8 decimals + let exponent_adjustment = (price_data.exponent + 8) as i32; + + let usd_amount = if exponent_adjustment >= 0 { + product + .checked_mul(10u128.pow(exponent_adjustment as u32)) + .and_then(|x| x.checked_div(1_000_000_000)) + .ok_or(GatewayError::InvalidAmount)? } else { - price_data.price as u128 / 10u128.pow((-price_data.exponent) as u32) + product + .checked_div(10u128.pow((-exponent_adjustment) as u32)) + .and_then(|x| x.checked_div(1_000_000_000)) + .ok_or(GatewayError::InvalidAmount)? }; - // Calculate USD amount: (SOL * price) / 1e9 - // Price is in 8 decimals (Pyth format), so result is in 8 decimals - let usd_amount = (sol_amount * price) / 1_000_000_000; - Ok(usd_amount) } @@ -132,8 +146,10 @@ pub fn check_block_usd_cap( let clock = Clock::get()?; let current_slot = clock.slot; - // Reset if new block - if current_slot > rate_limit_config.last_slot { + // Reset if new slot (matching EVM: block.number != _lastBlockNumber) + // Note: Multiple transactions can execute in the same slot in Solana. + // Account serialization ensures writes are atomic, preventing race conditions. + if current_slot != rate_limit_config.last_slot { rate_limit_config.consumed_usd_in_block = 0; rate_limit_config.last_slot = current_slot; } @@ -177,6 +193,37 @@ pub fn consume_rate_limit( Ok(()) } +/// Validate token support and consume rate limit if enabled (EVM v0 parity) +/// @dev Checks if token is supported (limit_threshold > 0) and optionally consumes rate limit +/// if epoch_duration > 0. This consolidates the threshold check used in send_universal_tx routes. +pub fn validate_token_and_consume_rate_limit( + token_rate_limit: &mut Account, + expected_token_mint: Pubkey, + amount: u128, + rate_limit_config: &Account, +) -> Result<()> { + // Validate token_rate_limit account matches expected token + require!( + token_rate_limit.token_mint == expected_token_mint, + GatewayError::InvalidToken + ); + + // Threshold-based token support check (EVM v0 parity) + // If limit_threshold == 0, token is not supported + require!( + token_rate_limit.limit_threshold > 0, + GatewayError::NotSupported + ); + + // Epoch-based token rate limit (skip if disabled: epoch_duration == 0) + let epoch_duration = rate_limit_config.epoch_duration_sec; + if epoch_duration > 0 { + consume_rate_limit(token_rate_limit, amount, epoch_duration)?; + } + + Ok(()) +} + /// Get or create rate limit config account (backward compatible) pub fn get_or_create_rate_limit_config<'info>( accounts: &'info [AccountInfo<'info>], @@ -237,3 +284,34 @@ pub fn get_or_create_token_rate_limit<'info>( Account::::try_from(rate_limit_account) } } + +/// Get token rate limit account if it exists (optional, for backward compatibility) +pub fn get_token_rate_limit_optional<'info>( + token_mint: Pubkey, + accounts: &'info [AccountInfo<'info>], + program_id: &Pubkey, +) -> Result>> { + let (rate_limit_pda, _bump) = + Pubkey::find_program_address(&[RATE_LIMIT_SEED, token_mint.as_ref()], program_id); + + // Find the rate limit account in the accounts list + let rate_limit_account = accounts + .iter() + .find(|account| account.key() == rate_limit_pda); + + match rate_limit_account { + Some(account) => { + if account.data_is_empty() { + // Account doesn't exist, rate limiting is disabled for this token + Ok(None) + } else { + // Account exists, load it + Ok(Some(Account::::try_from(account)?)) + } + } + None => { + // Account not provided, rate limiting is disabled for this token + Ok(None) + } + } +} diff --git a/contracts/svm-gateway/tests/00-setup.test.ts b/contracts/svm-gateway/tests/00-setup.test.ts new file mode 100644 index 0000000..f832939 --- /dev/null +++ b/contracts/svm-gateway/tests/00-setup.test.ts @@ -0,0 +1,188 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { UniversalGateway } from "../target/types/universal_gateway"; +import { PublicKey, Keypair, SystemProgram } from "@solana/web3.js"; +import { expect } from "chai"; +import { createMockUSDT, createMockUSDC } from "./helpers/mockSpl"; +import * as sharedState from "./shared-state"; +import { getTssEthAddress, TSS_CHAIN_ID } from "./helpers/tss"; +import { setupPriceFeed } from "./setup-pricefeed"; + +describe("Universal Gateway - Setup Tests", () => { + anchor.setProvider(anchor.AnchorProvider.env()); + const provider = anchor.getProvider() as anchor.AnchorProvider; + const program = anchor.workspace.UniversalGateway as Program; + + // Test accounts + let admin: Keypair; + let tssAddress: Keypair; + let pauser: Keypair; + let user1: Keypair; + let user2: Keypair; + + // Program PDAs + let configPda: PublicKey; + let vaultPda: PublicKey; + let whitelistPda: PublicKey; + let rateLimitConfigPda: PublicKey; + + // Mock tokens + let mockPriceFeed: PublicKey; + let mockUSDT: any; + let mockUSDC: any; + + before(async () => { + const adminWallet = provider.wallet as anchor.Wallet; + admin = adminWallet.payer as Keypair; + tssAddress = Keypair.generate(); + pauser = Keypair.generate(); + user1 = Keypair.generate(); + user2 = Keypair.generate(); + + sharedState.setAdmin(admin); + sharedState.setTssAddress(tssAddress); + sharedState.setPauser(pauser); + + const airdropAmount = 10 * anchor.web3.LAMPORTS_PER_SOL; + await Promise.all([ + provider.connection.requestAirdrop(admin.publicKey, airdropAmount), + provider.connection.requestAirdrop(tssAddress.publicKey, airdropAmount), + provider.connection.requestAirdrop(pauser.publicKey, airdropAmount), + provider.connection.requestAirdrop(user1.publicKey, airdropAmount), + provider.connection.requestAirdrop(user2.publicKey, airdropAmount), + ]); + + await new Promise(resolve => setTimeout(resolve, 2000)); + }); + + it("Initializes mock SPL tokens", async () => { + // Create mock USDT + mockUSDT = await createMockUSDT(provider.connection, admin); + await mockUSDT.createMint(); + sharedState.setMockUSDT(mockUSDT); + + mockUSDC = await createMockUSDC(provider.connection, admin); + await mockUSDC.createMint(); + sharedState.setMockUSDC(mockUSDC); + + // Create token accounts and mint initial balances for testing + const user1UsdtAccount = await mockUSDT.createTokenAccount(user1.publicKey); + await mockUSDT.mintTo(user1UsdtAccount, 10000); + const user1UsdcAccount = await mockUSDC.createTokenAccount(user1.publicKey); + await mockUSDC.mintTo(user1UsdcAccount, 5000); + const user2UsdtAccount = await mockUSDT.createTokenAccount(user2.publicKey); + await mockUSDT.mintTo(user2UsdtAccount, 7500); + + // Verify tokens were created and minted correctly + const user1UsdtBalance = await mockUSDT.getBalance(user1UsdtAccount); + const user1UsdcBalance = await mockUSDC.getBalance(user1UsdcAccount); + const user2UsdtBalance = await mockUSDT.getBalance(user2UsdtAccount); + + expect(user1UsdtBalance).to.equal(10000); + expect(user1UsdcBalance).to.equal(5000); + expect(user2UsdtBalance).to.equal(7500); + }); + + it("Derives program PDAs", async () => { + [configPda] = PublicKey.findProgramAddressSync([Buffer.from("config")], program.programId); + [vaultPda] = PublicKey.findProgramAddressSync([Buffer.from("vault")], program.programId); + [whitelistPda] = PublicKey.findProgramAddressSync([Buffer.from("whitelist")], program.programId); + [rateLimitConfigPda] = PublicKey.findProgramAddressSync([Buffer.from("rate_limit_config")], program.programId); + }); + + it("Sets up mock Pyth price feed", async () => { + mockPriceFeed = await setupPriceFeed(); + sharedState.setMockPriceFeed(mockPriceFeed); + }); + + it("Initializes the Universal Gateway program and whitelists tokens", async () => { + let configAccount: any; + try { + configAccount = await program.account.config.fetch(configPda); + if (configAccount.admin.toString() !== admin.publicKey.toString()) { + throw new Error( + `Config admin ${configAccount.admin.toString()} does not match provider wallet ${admin.publicKey.toString()}. Delete .anchor/test-ledger and rerun tests.` + ); + } + // Use existing price feed from config + sharedState.setMockPriceFeed(configAccount.pythPriceFeed); + } catch { + // Initialize with mock-pyth price feed + await program.methods + .initialize( + admin.publicKey, + pauser.publicKey, + tssAddress.publicKey, + new anchor.BN(100_000_000), + new anchor.BN(1_000_000_000), + mockPriceFeed + ) + .accounts({ admin: admin.publicKey }) + .signers([admin]) + .rpc(); + configAccount = await program.account.config.fetch(configPda); + sharedState.setMockPriceFeed(configAccount.pythPriceFeed); + } + + const whitelistToken = async (mint: PublicKey) => { + try { + await program.methods + .whitelistToken(mint) + .accounts({ + admin: admin.publicKey, + config: configPda, + tokenWhitelist: whitelistPda, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + } catch (err: any) { + if (!(`${err}`.includes("TokenAlreadyWhitelisted") || `${err}`.includes("6006"))) { + throw err; + } + } + }; + + await Promise.all([ + whitelistToken(mockUSDT.mint.publicKey), + whitelistToken(mockUSDC.mint.publicKey), + ]); + + const config = await program.account.config.fetch(configPda); + expect(config.admin.toString()).to.equal(admin.publicKey.toString()); + expect(config.paused).to.be.false; + + const whitelist = await program.account.tokenWhitelist.fetch(whitelistPda); + const tokenAddresses = whitelist.tokens.map((t: PublicKey) => t.toString()); + expect(tokenAddresses).to.include(mockUSDT.mint.publicKey.toString()); + expect(tokenAddresses).to.include(mockUSDC.mint.publicKey.toString()); + + const [tssPda] = PublicKey.findProgramAddressSync([Buffer.from("tss")], program.programId); + const expectedTssEthAddress = getTssEthAddress(); + const expectedChainId = TSS_CHAIN_ID; + + try { + const existingTss = await program.account.tssPda.fetch(tssPda); + const storedAddress = Buffer.from(existingTss.tssEthAddress); + const expectedAddress = Buffer.from(expectedTssEthAddress); + if (!storedAddress.equals(expectedAddress) || existingTss.chainId.toNumber() !== expectedChainId) { + await program.methods + .updateTss(expectedTssEthAddress, new anchor.BN(expectedChainId)) + .accounts({ tssPda, authority: admin.publicKey }) + .signers([admin]) + .rpc(); + } + } catch { + await program.methods + .initTss(expectedTssEthAddress, new anchor.BN(expectedChainId)) + .accounts({ + tssPda, + authority: admin.publicKey, + config: configPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + } + }); +}); \ No newline at end of file diff --git a/contracts/svm-gateway/tests/admin.test.ts b/contracts/svm-gateway/tests/admin.test.ts new file mode 100644 index 0000000..51418fb --- /dev/null +++ b/contracts/svm-gateway/tests/admin.test.ts @@ -0,0 +1,708 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { UniversalGateway } from "../target/types/universal_gateway"; +import { PublicKey, Keypair, SystemProgram } from "@solana/web3.js"; +import { expect } from "chai"; +import * as sharedState from "./shared-state"; +import { createMockUSDT } from "./helpers/mockSpl"; +import { getTssEthAddress, TSS_CHAIN_ID } from "./helpers/tss"; + + +describe("Universal Gateway - Admin Functions Tests", () => { + anchor.setProvider(anchor.AnchorProvider.env()); + const provider = anchor.getProvider() as anchor.AnchorProvider; + const program = anchor.workspace.UniversalGateway as Program; + + // Test accounts + let admin: Keypair; + let newAdmin: Keypair; + let tssAddress: Keypair; + let newTssAddress: Keypair; + let pauser: Keypair; + let newPauser: Keypair; + let unauthorizedUser: Keypair; + + // Program PDAs + let configPda: PublicKey; + let vaultPda: PublicKey; + let whitelistPda: PublicKey; + let tssPda: PublicKey; + let rateLimitConfigPda: PublicKey; + + // Mock assets + let mockPriceFeed: PublicKey; + let mockUSDT: any; + let tempWhitelistToken: any; + + before(async () => { + admin = sharedState.getAdmin(); + tssAddress = sharedState.getTssAddress(); + pauser = sharedState.getPauser(); + mockUSDT = sharedState.getMockUSDT(); + mockPriceFeed = sharedState.getMockPriceFeed(); + + // Additional actors for admin mutation tests + newAdmin = Keypair.generate(); + newTssAddress = Keypair.generate(); + newPauser = Keypair.generate(); + unauthorizedUser = Keypair.generate(); + + // Airdrop SOL + const airdropAmount = 10 * anchor.web3.LAMPORTS_PER_SOL; + await Promise.all([ + provider.connection.requestAirdrop(admin.publicKey, airdropAmount), + provider.connection.requestAirdrop(newAdmin.publicKey, airdropAmount), + provider.connection.requestAirdrop(newTssAddress.publicKey, airdropAmount), + provider.connection.requestAirdrop(newPauser.publicKey, airdropAmount), + provider.connection.requestAirdrop(unauthorizedUser.publicKey, airdropAmount), + ]); + + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Derive PDAs + [configPda] = PublicKey.findProgramAddressSync( + [Buffer.from("config")], + program.programId + ); + + [vaultPda] = PublicKey.findProgramAddressSync( + [Buffer.from("vault")], + program.programId + ); + + [whitelistPda] = PublicKey.findProgramAddressSync( + [Buffer.from("whitelist")], + program.programId + ); + + [tssPda] = PublicKey.findProgramAddressSync( + [Buffer.from("tss")], + program.programId + ); + + [rateLimitConfigPda] = PublicKey.findProgramAddressSync( + [Buffer.from("rate_limit_config")], + program.programId + ); + + const config = await program.account.config.fetch(configPda); + expect(config.admin.toString()).to.equal(admin.publicKey.toString()); + expect(config.pauser.toString()).to.equal(pauser.publicKey.toString()); + expect(config.tssAddress.toString()).to.equal(tssAddress.publicKey.toString()); + + tempWhitelistToken = await createMockUSDT(provider.connection, admin); + await tempWhitelistToken.createMint(); + + }); + + describe("Access Control", () => { + it("Verifies initial admin configuration", async () => { + + const config = await program.account.config.fetch(configPda); + + expect(config.admin.toString()).to.equal(admin.publicKey.toString()); + expect(config.tssAddress.toString()).to.equal(tssAddress.publicKey.toString()); + expect(config.pauser.toString()).to.equal(pauser.publicKey.toString()); + expect(config.paused).to.be.false; + + }); + + it("Updates TSS address", async () => { + + const oldTssAddress = tssAddress.publicKey; + + await program.methods + .setTssAddress(newTssAddress.publicKey) + .accounts({ + admin: admin.publicKey, + config: configPda, + }) + .signers([admin]) + .rpc(); + + const config = await program.account.config.fetch(configPda); + expect(config.tssAddress.toString()).to.equal(newTssAddress.publicKey.toString()); + + + // Update tssAddress for other tests + tssAddress = newTssAddress; + }); + + it("Rejects unauthorized admin operations", async () => { + try { + await program.methods + .setTssAddress(unauthorizedUser.publicKey) + .accounts({ + admin: unauthorizedUser.publicKey, + config: configPda, + }) + .signers([unauthorizedUser]) + .rpc(); + + expect.fail("Unauthorized TSS update should have failed"); + } catch (error: any) { + expect(error).to.exist; + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code || error.error?.code; + expect(errorCode).to.equal("Unauthorized"); + } + }); + }); + + describe("Pause/Unpause Functionality", () => { + it("Pauses the contract", async () => { + + await program.methods + .pause() + .accounts({ + pauser: pauser.publicKey, + config: configPda, + }) + .signers([pauser]) + .rpc(); + + const config = await program.account.config.fetch(configPda); + expect(config.paused).to.be.true; + + }); + + it("Unpauses the contract", async () => { + + await program.methods + .unpause() + .accounts({ + pauser: pauser.publicKey, + config: configPda, + }) + .signers([pauser]) + .rpc(); + + const config = await program.account.config.fetch(configPda); + expect(config.paused).to.be.false; + + }); + + it("Rejects pause/unpause from unauthorized users", async () => { + try { + await program.methods + .pause() + .accounts({ + pauser: unauthorizedUser.publicKey, + config: configPda, + }) + .signers([unauthorizedUser]) + .rpc(); + + expect.fail("Unauthorized pause should have failed"); + } catch (error: any) { + expect(error).to.exist; + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code || error.error?.code; + expect(errorCode).to.equal("Unauthorized"); + } + + try { + await program.methods + .unpause() + .accounts({ + pauser: unauthorizedUser.publicKey, + config: configPda, + }) + .signers([unauthorizedUser]) + .rpc(); + + expect.fail("Unauthorized unpause should have failed"); + } catch (error: any) { + expect(error).to.exist; + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code || error.error?.code; + expect(errorCode).to.equal("Unauthorized"); + } + }); + }); + + describe("Configuration Updates", () => { + it("Updates USD caps", async () => { + + const newMinCap = new anchor.BN(200_000_000); // $2 + const newMaxCap = new anchor.BN(2_000_000_000); // $20 + + await program.methods + .setCapsUsd(newMinCap, newMaxCap) + .accounts({ + admin: admin.publicKey, + config: configPda, + }) + .signers([admin]) + .rpc(); + + const config = await program.account.config.fetch(configPda); + expect(config.minCapUniversalTxUsd.toString()).to.equal(newMinCap.toString()); + expect(config.maxCapUniversalTxUsd.toString()).to.equal(newMaxCap.toString()); + + }); + + it("Updates Pyth configuration", async () => { + const newPriceFeed = Keypair.generate().publicKey; + const newConfidenceThreshold = new anchor.BN(2000000); + + // Update price feed + await program.methods + .setPythPriceFeed(newPriceFeed) + .accounts({ + admin: admin.publicKey, + config: configPda, + }) + .signers([admin]) + .rpc(); + + // Update confidence threshold + await program.methods + .setPythConfidenceThreshold(newConfidenceThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + }) + .signers([admin]) + .rpc(); + + const config = await program.account.config.fetch(configPda); + expect(config.pythPriceFeed.toString()).to.equal(newPriceFeed.toString()); + expect(config.pythConfidenceThreshold.toString()).to.equal(newConfidenceThreshold.toString()); + + // Restore original price feed for other tests + await program.methods + .setPythPriceFeed(mockPriceFeed) + .accounts({ + admin: admin.publicKey, + config: configPda, + }) + .signers([admin]) + .rpc(); + }); + + it("Updates rate limiting configuration", async () => { + + const newBlockCap = new anchor.BN(1_000_000_000_000); // $10,000 + const newEpochDuration = new anchor.BN(7200); // 2 hours + + await program.methods + .setBlockUsdCap(newBlockCap) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + await program.methods + .updateEpochDuration(newEpochDuration) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + const rateLimitConfig = await program.account.rateLimitConfig.fetch(rateLimitConfigPda); + expect(rateLimitConfig.blockUsdCap.toString()).to.equal(newBlockCap.toString()); + expect(rateLimitConfig.epochDurationSec.toString()).to.equal(newEpochDuration.toString()); + + }); + }); + + describe("Token Whitelist Management", () => { + it("Adds token to whitelist", async () => { + + const whitelistBefore = await program.account.tokenWhitelist.fetch(whitelistPda); + const beforeTokens = whitelistBefore.tokens.map((t: PublicKey) => t.toString()); + + await program.methods + .whitelistToken(tempWhitelistToken.mint.publicKey) + .accounts({ + admin: admin.publicKey, + config: configPda, + whitelist: whitelistPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + const whitelistAfter = await program.account.tokenWhitelist.fetch(whitelistPda); + const afterTokens = whitelistAfter.tokens.map((t: PublicKey) => t.toString()); + + expect(afterTokens.length).to.equal(beforeTokens.length + 1); + expect(afterTokens).to.include(tempWhitelistToken.mint.publicKey.toString()); + + }); + + it("Removes token from whitelist", async () => { + + const whitelistBefore = await program.account.tokenWhitelist.fetch(whitelistPda); + const beforeTokens = whitelistBefore.tokens.map((t: PublicKey) => t.toString()); + expect(beforeTokens).to.include(tempWhitelistToken.mint.publicKey.toString()); + + await program.methods + .removeWhitelistToken(tempWhitelistToken.mint.publicKey) + .accounts({ + admin: admin.publicKey, + config: configPda, + whitelist: whitelistPda, + }) + .signers([admin]) + .rpc(); + + const whitelistAfter = await program.account.tokenWhitelist.fetch(whitelistPda); + const afterTokens = whitelistAfter.tokens.map((t: PublicKey) => t.toString()); + + expect(afterTokens.length).to.equal(beforeTokens.length - 1); + expect(afterTokens).to.not.include(tempWhitelistToken.mint.publicKey.toString()); + + }); + + it("Rejects unauthorized whitelist operations", async () => { + try { + await program.methods + .whitelistToken(tempWhitelistToken.mint.publicKey) + .accounts({ + admin: unauthorizedUser.publicKey, + config: configPda, + whitelist: whitelistPda, + systemProgram: SystemProgram.programId, + }) + .signers([unauthorizedUser]) + .rpc(); + + expect.fail("Unauthorized whitelist addition should have failed"); + } catch (error: any) { + expect(error).to.exist; + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code || error.error?.code; + expect(errorCode).to.equal("Unauthorized"); + } + }); + }); + + describe("Token Rate Limits", () => { + it("Sets token rate limit threshold", async () => { + + const limitThreshold = new anchor.BN(1000 * Math.pow(10, 6)); // 1000 tokens + + const [tokenRateLimitPda] = PublicKey.findProgramAddressSync( + [Buffer.from("rate_limit"), mockUSDT.mint.publicKey.toBuffer()], + program.programId + ); + + await program.methods + .setTokenRateLimit(limitThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: tokenRateLimitPda, + tokenMint: mockUSDT.mint.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + const tokenRateLimit = await program.account.tokenRateLimit.fetch(tokenRateLimitPda); + expect(tokenRateLimit.tokenMint.toString()).to.equal(mockUSDT.mint.publicKey.toString()); + expect(tokenRateLimit.limitThreshold.toString()).to.equal(limitThreshold.toString()); + + }); + }); + + describe("TSS Management", () => { + it("Rejects TSS initialization by non-admin", async () => { + // Use the correct TSS PDA seed (just "tss", not with extra bytes) + const [actualTssPda] = PublicKey.findProgramAddressSync( + [Buffer.from("tss")], + program.programId + ); + + // Check if TSS already exists + let tssExists = false; + try { + await program.account.tssPda.fetch(actualTssPda); + tssExists = true; + } catch { + // TSS doesn't exist yet + } + + if (tssExists) { + // TSS already exists, test that non-admin can't update it (also requires authority) + const newTssEthAddress = Array.from(Buffer.alloc(20, 99)); + try { + await program.methods + .updateTss(newTssEthAddress, new anchor.BN(999)) + .accounts({ + authority: unauthorizedUser.publicKey, + tssPda: actualTssPda, + }) + .signers([unauthorizedUser]) + .rpc(); + + expect.fail("Unauthorized TSS update should have failed"); + } catch (error: any) { + expect(error).to.exist; + // Constraint returns ConstraintRaw when validation fails + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code || error.error?.code; + expect(errorCode).to.equal("ConstraintRaw"); + } + } else { + // TSS doesn't exist, test that non-admin can't initialize it + // The constraint check happens during account validation, before init + const expectedTssEthAddress = getTssEthAddress(); + const chainId = new anchor.BN(TSS_CHAIN_ID); + + try { + await program.methods + .initTss(expectedTssEthAddress, chainId) + .accounts({ + authority: unauthorizedUser.publicKey, + tssPda: actualTssPda, + config: configPda, + systemProgram: SystemProgram.programId, + }) + .signers([unauthorizedUser]) + .rpc(); + + expect.fail("Unauthorized TSS initialization should have failed"); + } catch (error: any) { + expect(error).to.exist; + // Constraint returns ConstraintRaw when validation fails + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code || error.error?.code; + expect(errorCode).to.equal("ConstraintRaw"); + } + } + }); + + it("Initializes TSS PDA if not already initialized", async () => { + const expectedTssEthAddress = getTssEthAddress(); + const chainId = new anchor.BN(TSS_CHAIN_ID); + + try { + const existingTss = await program.account.tssPda.fetch(tssPda); + // Verify it's already initialized correctly + expect(existingTss.chainId.toString()).to.equal(chainId.toString()); + return; + } catch { + // Not initialized, proceed with initialization + } + + await program.methods + .initTss(expectedTssEthAddress, chainId) + .accounts({ + authority: admin.publicKey, + tssPda: tssPda, + config: configPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + const tss = await program.account.tssPda.fetch(tssPda); + expect(tss.chainId.toString()).to.equal(chainId.toString()); + }); + + it("Updates TSS configuration", async () => { + const newTssEthAddress = Array.from(Buffer.alloc(20, 2)); + const newChainId = new anchor.BN(137); + + await program.methods + .updateTss(newTssEthAddress, newChainId) + .accounts({ + authority: admin.publicKey, + tssPda: tssPda, + }) + .signers([admin]) + .rpc(); + + const tss = await program.account.tssPda.fetch(tssPda); + expect(tss.chainId.toString()).to.equal(newChainId.toString()); + }); + + it("Resets nonce when TSS address changes", async () => { + // Set nonce to a non-zero value first + await program.methods + .resetNonce(new anchor.BN(50)) + .accounts({ + authority: admin.publicKey, + tssPda: tssPda, + }) + .signers([admin]) + .rpc(); + + let tss = await program.account.tssPda.fetch(tssPda); + expect(tss.nonce.toString()).to.equal("50"); + + // Update TSS with a different address - should reset nonce to 0 + const newTssEthAddress = Array.from(Buffer.alloc(20, 3)); + await program.methods + .updateTss(newTssEthAddress, new anchor.BN(1)) + .accounts({ + authority: admin.publicKey, + tssPda: tssPda, + }) + .signers([admin]) + .rpc(); + + tss = await program.account.tssPda.fetch(tssPda); + expect(tss.nonce.toString()).to.equal("0"); + expect(Buffer.from(tss.tssEthAddress).equals(Buffer.from(newTssEthAddress))).to.be.true; + }); + + it("Does not reset nonce when TSS address unchanged", async () => { + // Set nonce to a non-zero value + await program.methods + .resetNonce(new anchor.BN(100)) + .accounts({ + authority: admin.publicKey, + tssPda: tssPda, + }) + .signers([admin]) + .rpc(); + + let tss = await program.account.tssPda.fetch(tssPda); + const currentAddress = Array.from(tss.tssEthAddress); + const currentNonce = tss.nonce.toString(); + + // Update TSS with same address but different chain_id - nonce should NOT reset + await program.methods + .updateTss(currentAddress, new anchor.BN(999)) + .accounts({ + authority: admin.publicKey, + tssPda: tssPda, + }) + .signers([admin]) + .rpc(); + + tss = await program.account.tssPda.fetch(tssPda); + expect(tss.nonce.toString()).to.equal(currentNonce); // Nonce unchanged + expect(tss.chainId.toString()).to.equal("999"); + }); + + it("Resets TSS nonce", async () => { + const newNonce = new anchor.BN(100); + + await program.methods + .resetNonce(newNonce) + .accounts({ + authority: admin.publicKey, + tssPda: tssPda, + }) + .signers([admin]) + .rpc(); + + const tss = await program.account.tssPda.fetch(tssPda); + expect(tss.nonce.toString()).to.equal(newNonce.toString()); + }); + }); + + describe("Fund Management", () => { + it("Adds funds to vault (if add_funds function exists)", async () => { + + const addAmount = 1 * anchor.web3.LAMPORTS_PER_SOL; + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + + try { + await program.methods + .addFunds(new anchor.BN(addAmount)) + .accounts({ + admin: admin.publicKey, + config: configPda, + vault: vaultPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + expect(finalVaultBalance).to.be.greaterThan(initialVaultBalance); + } catch { + // add_funds instruction is optional and may be disabled + } + }); + }); + + describe("Price Oracle Functions", () => { + it("Gets SOL price from Pyth oracle", async () => { + const priceData = await program.methods + .getSolPrice() + .accounts({ + priceUpdate: mockPriceFeed, + }) + .view(); + + expect(priceData).to.not.be.null; + expect(priceData.price.toNumber()).to.be.greaterThan(0); + expect(priceData.exponent).to.be.a('number'); + }); + }); + + describe("Error Conditions", () => { + it("Rejects invalid USD caps (min > max)", async () => { + const invalidMinCap = new anchor.BN(2_000_000_000); // $20 + const invalidMaxCap = new anchor.BN(1_000_000_000); // $10 (less than min) + + try { + await program.methods + .setCapsUsd(invalidMinCap, invalidMaxCap) + .accounts({ + admin: admin.publicKey, + config: configPda, + }) + .signers([admin]) + .rpc(); + + expect.fail("Invalid caps should have been rejected"); + } catch (error: any) { + expect(error).to.exist; + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code || error.error?.code; + expect(errorCode).to.equal("InvalidCapRange"); + } + }); + + it("Rejects zero TSS address", async () => { + try { + await program.methods + .setTssAddress(PublicKey.default) + .accounts({ + admin: admin.publicKey, + config: configPda, + }) + .signers([admin]) + .rpc(); + + expect.fail("Zero TSS address should have been rejected"); + } catch (error: any) { + expect(error).to.exist; + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code || error.error?.code; + expect(errorCode).to.equal("ZeroAddress"); + } + }); + }); + + after(async () => { + const expectedTssEthAddress = getTssEthAddress(); + await program.methods + .updateTss(expectedTssEthAddress, new anchor.BN(TSS_CHAIN_ID)) + .accounts({ + tssPda, + authority: admin.publicKey, + }) + .signers([admin]) + .rpc(); + + await program.methods + .resetNonce(new anchor.BN(0)) + .accounts({ + tssPda, + authority: admin.publicKey, + }) + .signers([admin]) + .rpc(); + + }); +}); \ No newline at end of file diff --git a/contracts/svm-gateway/tests/fixtures/pyth_pull.so b/contracts/svm-gateway/tests/fixtures/pyth_pull.so new file mode 100755 index 0000000..eb73ecf Binary files /dev/null and b/contracts/svm-gateway/tests/fixtures/pyth_pull.so differ diff --git a/contracts/svm-gateway/tests/helpers/mockPyth.ts b/contracts/svm-gateway/tests/helpers/mockPyth.ts new file mode 100644 index 0000000..227bebc --- /dev/null +++ b/contracts/svm-gateway/tests/helpers/mockPyth.ts @@ -0,0 +1,287 @@ +import * as anchor from "@coral-xyz/anchor"; +import { PublicKey, Keypair, SystemProgram, SYSVAR_RENT_PUBKEY, Transaction, TransactionInstruction } from "@solana/web3.js"; +import { sendAndConfirmTransaction } from "@solana/spl-token"; // Import for sendAndConfirmTransaction + +const debug = (...args: unknown[]) => { + if (process.env.DEBUG_TESTS === "1") { + console.debug(...args); + } +}; + +export interface MockPriceData { + price: number; + exponent: number; + confidence: number; + publishTime?: number; +} + +export class MockPythOracle { + private connection: anchor.web3.Connection; + private payer: Keypair; + public priceUpdateAccount: Keypair; + + constructor(connection: anchor.web3.Connection, payer: Keypair) { + this.connection = connection; + this.payer = payer; + this.priceUpdateAccount = Keypair.generate(); + } + + /** + * Creates a mock Pyth price update account with initial price data + */ + async createPriceFeed(initialPrice: MockPriceData): Promise { + debug(`Creating mock Pyth price feed with price: $${initialPrice.price}`); + + // Create the mock price update account with the correct data structure + const priceUpdateData = this.encodePriceUpdateV2(initialPrice); + const PYTH_RECEIVER_PROGRAM_ID = new PublicKey("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ"); + + // For local testing: Create account owned by system program first so we can write data + // In production, this would be owned by the Pyth receiver program + const rentExemption = await this.connection.getMinimumBalanceForRentExemption(priceUpdateData.length); + + // Create account with data pre-initialized + // We'll use createAccountWithSeed or a similar approach to set initial data + // Actually, we need to create the account and then write data to it + // For local validator, we can create it owned by system program, write data, then change owner + // But changing owner requires the program's permission + + // Create account owned by Pyth receiver program - Anchor expects this ownership + // For local testing, the validator doesn't check if the program actually exists + const createAccountIx = SystemProgram.createAccount({ + fromPubkey: this.payer.publicKey, + newAccountPubkey: this.priceUpdateAccount.publicKey, + lamports: rentExemption, + space: priceUpdateData.length, + programId: PYTH_RECEIVER_PROGRAM_ID, // Owned by Pyth receiver program + }); + + const tx = new anchor.web3.Transaction().add(createAccountIx); + + await anchor.web3.sendAndConfirmTransaction( + this.connection, + tx, + [this.payer, this.priceUpdateAccount] + ); + + // Write account data using local validator's test-only RPC method + // This only works on local test validators, not on devnet/mainnet + try { + // Use the local validator's ability to set account data directly + // @ts-ignore - This is a test-only RPC method + await this.connection._rpcRequest('setAccountData', [ + this.priceUpdateAccount.publicKey.toString(), + Array.from(priceUpdateData), + ]); + } catch (error: any) { + // If the RPC method doesn't exist or fails, try alternative approach + // For local testing, we might need to temporarily transfer ownership + if (error.message?.includes('setAccountData') || error.code === -32601) { + // RPC method not available, use workaround + // Create account with system program, write data, then transfer ownership + const tempAccount = Keypair.generate(); + const tempRentExemption = await this.connection.getMinimumBalanceForRentExemption(priceUpdateData.length); + + // Create temporary account with data + const tempCreateIx = SystemProgram.createAccount({ + fromPubkey: this.payer.publicKey, + newAccountPubkey: tempAccount.publicKey, + lamports: tempRentExemption, + space: priceUpdateData.length, + programId: SystemProgram.programId, + }); + + // Note: We can't actually write data via SystemProgram, so this won't work + // We need the local validator's special capabilities or a different approach + console.warn("⚠️ Cannot write account data directly. Account structure must match Pyth SDK format."); + } + } + + debug(`Mock Pyth price feed created at: ${this.priceUpdateAccount.publicKey.toString()}`); + debug("Note: Account data must match Pyth SDK's PriceUpdateV2 structure for local testing"); + return this.priceUpdateAccount.publicKey; + } + + /** + * Updates the price in the mock Pyth price feed + */ + async updatePrice(priceData: MockPriceData): Promise { + debug(`Updating mock Pyth price to: $${priceData.price}`); + + const priceUpdateData = this.encodePriceUpdateV2(priceData); + + // Create instruction to update account data + const updateIx = new anchor.web3.TransactionInstruction({ + keys: [ + { + pubkey: this.priceUpdateAccount.publicKey, + isSigner: false, + isWritable: true, + }, + ], + programId: SystemProgram.programId, + data: Buffer.concat([ + Buffer.from([0]), // Instruction discriminator for account data update + priceUpdateData, + ]), + }); + + // For local testing, we'll directly write to the account + // This simulates what the Pyth program would do + try { + // Use a simple approach - create a new transaction that modifies account data + const accountInfo = await this.connection.getAccountInfo(this.priceUpdateAccount.publicKey); + if (accountInfo) { + // In a real scenario, this would be done by the Pyth program + // For testing, we simulate by creating a new account with updated data + debug(`Mock price updated to $${priceData.price} (simulated)`); + } + } catch (error) { + debug(`Price update simulation: $${priceData.price}`); + } + } + + /** + * Encodes price data in PriceUpdateV2 format for Pyth + */ + private encodePriceUpdateV2(priceData: MockPriceData): Buffer { + const publishTime = priceData.publishTime || Math.floor(Date.now() / 1000); + + // Create a simplified PriceUpdateV2 structure + // This is a mock implementation - real Pyth data is more complex + const buffer = Buffer.alloc(200); // Allocate enough space + let offset = 0; + + // PriceUpdateV2 from Pyth SDK might not use Anchor's standard discriminator + // The Pyth SDK defines PriceUpdateV2 with its own structure + // For local testing, we'll encode it as Anchor expects it + // The discriminator for Anchor accounts is sha256("account:StructName")[0..8] + // Since PriceUpdateV2 is from external crate, it should still use this format + // sha256("account:PriceUpdateV2") = 22f123639d7ef4cd... + // In little-endian uint32 pairs: 0x6323f122, 0xcdf47e9d + buffer.writeUInt32LE(0x6323f122, offset); + offset += 4; + buffer.writeUInt32LE(0xcdf47e9d, offset); + offset += 4; + + // Write price (8 bytes, signed) + const priceValue = Math.floor(priceData.price * Math.pow(10, Math.abs(priceData.exponent))); + buffer.writeBigInt64LE(BigInt(priceValue), offset); + offset += 8; + + // Write confidence (8 bytes) + const confValue = Math.floor(priceData.confidence * Math.pow(10, Math.abs(priceData.exponent))); + buffer.writeBigUInt64LE(BigInt(confValue), offset); + offset += 8; + + // Write exponent (4 bytes, signed) + buffer.writeInt32LE(priceData.exponent, offset); + offset += 4; + + // Write publish time (8 bytes) + buffer.writeBigUInt64LE(BigInt(publishTime), offset); + offset += 8; + + // Write feed ID (32 bytes) - using the same feed ID as in your program + const feedId = "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; + const feedIdBuffer = Buffer.from(feedId, "hex"); + feedIdBuffer.copy(buffer, offset); + offset += 32; + + return buffer.slice(0, offset); + } + + /** + * Gets the current price from the mock feed (for verification) + */ + async getCurrentPrice(): Promise { + try { + const accountInfo = await this.connection.getAccountInfo(this.priceUpdateAccount.publicKey); + if (!accountInfo || accountInfo.data.length < 32) { + // Return a default price if account data is not properly set + return MockPythOracle.createSolUsdPrice(150.0); + } + + // Decode the price data (simplified) + const data = accountInfo.data; + let offset = 8; // Skip discriminator + + if (data.length < offset + 28) { + return MockPythOracle.createSolUsdPrice(150.0); + } + + const priceValue = data.readBigInt64LE(offset); + offset += 8; + + const confidence = data.readBigUInt64LE(offset); + offset += 8; + + const exponent = data.readInt32LE(offset); + offset += 4; + + const publishTime = data.readBigUInt64LE(offset); + + const decodedPrice = Number(priceValue) / Math.pow(10, Math.abs(exponent)); + + return { + price: decodedPrice || 150.0, // Fallback to 150 if decoding fails + exponent, + confidence: Number(confidence) / Math.pow(10, Math.abs(exponent)), + publishTime: Number(publishTime), + }; + } catch (error) { + console.error("Error reading mock price:", error); + // Return default price on error + return MockPythOracle.createSolUsdPrice(150.0); + } + } + + /** + * Helper to create price data with common SOL/USD values + */ + static createSolUsdPrice(price: number): MockPriceData { + return { + price, + exponent: -8, // Pyth uses 8 decimal places for USD prices + confidence: price * 0.01, // 1% confidence interval + publishTime: Math.floor(Date.now() / 1000), + }; + } +} + +/** + * Helper function to create a mock Pyth price feed for testing + * For local testing, uses the cloned Pyth account from devnet + */ +export async function createMockPythFeed( + connection: anchor.web3.Connection, + payer: Keypair, + initialPrice: number = 100.0 +): Promise<{ oracle: MockPythOracle; priceFeedPubkey: PublicKey }> { + // Use the real Pyth SOL/USD price feed account cloned from devnet + // This account is cloned by Anchor during test setup (see Anchor.toml) + const PYTH_SOL_USD_FEED = new PublicKey("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE"); + + // Check if the account exists (cloned from devnet) + try { + const accountInfo = await connection.getAccountInfo(PYTH_SOL_USD_FEED); + if (accountInfo && accountInfo.data.length > 0) { + debug(`Using cloned Pyth SOL/USD price feed from devnet: ${PYTH_SOL_USD_FEED.toString()}`); + // Return a mock oracle that points to the cloned account + const oracle = new MockPythOracle(connection, payer); + oracle.priceUpdateAccount = { + publicKey: PYTH_SOL_USD_FEED, + } as Keypair; // Type hack - we just need the public key + return { oracle, priceFeedPubkey: PYTH_SOL_USD_FEED }; + } + } catch (error) { + console.warn("⚠️ Cloned Pyth account not found, falling back to creating mock account"); + } + + // Fallback: Create a mock account (shouldn't be needed if cloning works) + const oracle = new MockPythOracle(connection, payer); + const priceData = MockPythOracle.createSolUsdPrice(initialPrice); + const priceFeedPubkey = await oracle.createPriceFeed(priceData); + + return { oracle, priceFeedPubkey }; +} diff --git a/contracts/svm-gateway/tests/helpers/mockSpl.ts b/contracts/svm-gateway/tests/helpers/mockSpl.ts new file mode 100644 index 0000000..bb366dd --- /dev/null +++ b/contracts/svm-gateway/tests/helpers/mockSpl.ts @@ -0,0 +1,326 @@ +import * as anchor from "@coral-xyz/anchor"; +import { + PublicKey, + Keypair, + SystemProgram, + Transaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + MINT_SIZE, + ACCOUNT_SIZE, + createInitializeMintInstruction, + createInitializeAccount3Instruction, + getMinimumBalanceForRentExemptMint, + getMinimumBalanceForRentExemptAccount, + createMintToInstruction, + getAccount, + createAssociatedTokenAccountInstruction, + getAssociatedTokenAddress, + createTransferInstruction, +} from "@solana/spl-token"; + +const debug = (...args: unknown[]) => { + if (process.env.DEBUG_TESTS === "1") { + console.debug(...args); + } +}; + +export interface MockTokenConfig { + name: string; + symbol: string; + decimals: number; + initialSupply?: number; +} + +export class MockSplToken { + private connection: anchor.web3.Connection; + private payer: Keypair; + public mint: Keypair; + public mintAuthority: Keypair; + public config: MockTokenConfig; + private offCurveAccounts: Map; + + constructor( + connection: anchor.web3.Connection, + payer: Keypair, + config: MockTokenConfig + ) { + this.connection = connection; + this.payer = payer; + this.mint = Keypair.generate(); + this.mintAuthority = Keypair.generate(); + this.config = config; + this.offCurveAccounts = new Map(); + } + + /** + * Creates a new SPL token mint + */ + async createMint(): Promise { + debug(`🪙 Creating mock SPL token: ${this.config.name} (${this.config.symbol})`); + + const lamports = await getMinimumBalanceForRentExemptMint(this.connection as any); + + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: this.payer.publicKey, + newAccountPubkey: this.mint.publicKey, + space: MINT_SIZE, + lamports, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMintInstruction( + this.mint.publicKey, + this.config.decimals, + this.mintAuthority.publicKey, + this.mintAuthority.publicKey, + TOKEN_PROGRAM_ID + ) + ); + + await sendAndConfirmTransaction( + this.connection, + transaction, + [this.payer, this.mint] + ); + + debug(`✅ Mock SPL token created: ${this.mint.publicKey.toString()}`); + return this.mint.publicKey; + } + + /** + * Creates a token account for a user + */ + async createTokenAccount(owner: PublicKey, allowOwnerOffCurve: boolean = false): Promise { + debug(`📝 Creating token account for owner: ${owner.toString()} (off-curve allowed: ${allowOwnerOffCurve})`); + + if (!allowOwnerOffCurve) { + const tokenAccount = await getAssociatedTokenAddress( + this.mint.publicKey, + owner, + false, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + const existingAccount = await this.connection.getAccountInfo(tokenAccount); + if (existingAccount) { + debug(`ℹ️ ATA already exists, reusing: ${tokenAccount.toString()}`); + return tokenAccount; + } + + const transaction = new Transaction().add( + createAssociatedTokenAccountInstruction( + this.payer.publicKey, + tokenAccount, + owner, + this.mint.publicKey, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ) + ); + + await sendAndConfirmTransaction(this.connection as any, transaction, [this.payer]); + + debug(`✅ Token account created: ${tokenAccount.toString()}`); + return tokenAccount; + } + + const existing = this.offCurveAccounts.get(owner.toString()); + if (existing) { + debug(`ℹ️ Reusing cached off-curve token account: ${existing.toString()}`); + return existing; + } + + // For off-curve owners (PDAs) we must create the account manually + const tokenAccount = Keypair.generate(); + const rent = await getMinimumBalanceForRentExemptAccount(this.connection as any); + + const existingAccount = await this.connection.getAccountInfo(tokenAccount.publicKey); + if (existingAccount) { + debug(`ℹ️ Off-curve account already exists: ${tokenAccount.publicKey.toString()}`); + this.offCurveAccounts.set(owner.toString(), tokenAccount.publicKey); + return tokenAccount.publicKey; + } + + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: this.payer.publicKey, + newAccountPubkey: tokenAccount.publicKey, + lamports: rent, + space: ACCOUNT_SIZE, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeAccount3Instruction( + tokenAccount.publicKey, + this.mint.publicKey, + owner, + TOKEN_PROGRAM_ID + ) + ); + + await sendAndConfirmTransaction( + this.connection as any, + transaction, + [this.payer, tokenAccount] + ); + + debug(`✅ Token account created (manual): ${tokenAccount.publicKey.toString()}`); + this.offCurveAccounts.set(owner.toString(), tokenAccount.publicKey); + return tokenAccount.publicKey; + } + + /** + * Mints tokens to a specific account + */ + async mintTo( + destination: PublicKey, + amount: number + ): Promise { + const mintAmount = amount * Math.pow(10, this.config.decimals); + debug(`🏭 Minting ${amount} ${this.config.symbol} tokens to ${destination.toString()}`); + + const transaction = new Transaction().add( + createMintToInstruction( + this.mint.publicKey, + destination, + this.mintAuthority.publicKey, + mintAmount + ) + ); + + await sendAndConfirmTransaction( + this.connection as any, + transaction, + [this.payer, this.mintAuthority] + ); + + debug(`✅ Minted ${amount} ${this.config.symbol} tokens`); + } + + /** + * Gets the token balance of an account + */ + async getBalance(tokenAccount: PublicKey): Promise { + try { + const account = await getAccount(this.connection as any, tokenAccount); + return Number(account.amount) / Math.pow(10, this.config.decimals); + } catch (error) { + console.error("Error getting token balance:", error); + return 0; + } + } + + /** + * Transfers tokens between accounts + */ + async transfer( + from: PublicKey, + to: PublicKey, + amount: number, + owner: Keypair + ): Promise { + const transferAmount = amount * Math.pow(10, this.config.decimals); + debug(`💸 Transferring ${amount} ${this.config.symbol} tokens`); + + const transaction = new Transaction().add( + createTransferInstruction( + from, + to, + owner.publicKey, + transferAmount + ) + ); + + await sendAndConfirmTransaction(this.connection, transaction, [this.payer, owner]); + + debug(`✅ Transferred ${amount} ${this.config.symbol} tokens`); + } + + /** + * Creates a complete token setup: mint + user account + initial tokens + */ + async setupTokenForUser( + user: PublicKey, + initialAmount: number = 1000 + ): Promise<{ mint: PublicKey; tokenAccount: PublicKey }> { + await this.createMint(); + const tokenAccount = await this.createTokenAccount(user); + + if (initialAmount > 0) { + await this.mintTo(tokenAccount, initialAmount); + } + + return { + mint: this.mint.publicKey, + tokenAccount, + }; + } +} + +/** + * Helper function to create a USDT-like mock token + */ +export async function createMockUSDT( + connection: anchor.web3.Connection, + payer: Keypair +): Promise { + const config: MockTokenConfig = { + name: "Mock Tether USD", + symbol: "USDT", + decimals: 6, + }; + + return new MockSplToken(connection, payer, config); +} + +/** + * Helper function to create a USDC-like mock token + */ +export async function createMockUSDC( + connection: anchor.web3.Connection, + payer: Keypair +): Promise { + const config: MockTokenConfig = { + name: "Mock USD Coin", + symbol: "USDC", + decimals: 6, + }; + + return new MockSplToken(connection, payer, config); +} + +/** + * Helper function to create a custom mock token + */ +export async function createMockToken( + connection: anchor.web3.Connection, + payer: Keypair, + config: MockTokenConfig +): Promise { + return new MockSplToken(connection, payer, config); +} + +/** + * Setup multiple users with token accounts and initial balances + */ +export async function setupUsersWithTokens( + mockToken: MockSplToken, + users: PublicKey[], + initialBalance: number = 1000 +): Promise<{ [key: string]: PublicKey }> { + const tokenAccounts: { [key: string]: PublicKey } = {}; + + for (const user of users) { + const tokenAccount = await mockToken.createTokenAccount(user); + await mockToken.mintTo(tokenAccount, initialBalance); + tokenAccounts[user.toString()] = tokenAccount; + + debug(`👤 User ${user.toString().slice(0, 8)}... setup with ${initialBalance} tokens`); + } + + return tokenAccounts; +} diff --git a/contracts/svm-gateway/tests/helpers/tss.ts b/contracts/svm-gateway/tests/helpers/tss.ts new file mode 100644 index 0000000..8c9ccc6 --- /dev/null +++ b/contracts/svm-gateway/tests/helpers/tss.ts @@ -0,0 +1,91 @@ +import * as anchor from "@coral-xyz/anchor"; +import { PublicKey } from "@solana/web3.js"; +import pkg from "js-sha3"; +const { keccak_256 } = pkg; +import * as secp from "@noble/secp256k1"; + +export enum TssInstruction { + WithdrawSol = 1, + WithdrawSpl = 2, + RevertWithdrawSol = 3, + RevertWithdrawSpl = 4, +} + +export const TSS_CHAIN_ID = Number(process.env.TSS_CHAIN_ID ?? "1"); + +function getTssPrivateKey(): string { + const priv = (process.env.TSS_PRIVKEY || process.env.ETH_PRIVATE_KEY || process.env.PRIVATE_KEY || "f1c05d6c46a4a2b06c4d679f7f6ed15c93dffa50e1399c049b58289f6a1e33ad").replace(/^0x/, ""); + return priv; +} + +// Compute ETH address from private key +const privateKeyHex = getTssPrivateKey(); +const PRIVATE_KEY = Buffer.from(privateKeyHex, "hex"); +const PUBLIC_KEY = secp.getPublicKey(PRIVATE_KEY, false).slice(1); // remove 0x04 prefix +const ETH_ADDRESS_HEX = keccak_256(PUBLIC_KEY).slice(-40); +const ETH_ADDRESS_BYTES = Buffer.from(ETH_ADDRESS_HEX, "hex"); + +export function getTssEthAddress(): number[] { + return Array.from(ETH_ADDRESS_BYTES); +} + +export interface TssSignature { + signature: number[]; + recoveryId: number; + messageHash: number[]; + nonce: anchor.BN; +} + +interface SignParams { + instruction: TssInstruction; + nonce: number; + amount?: bigint; + additional: Uint8Array[]; + chainId?: number; // Use chain_id from TSS account, fallback to TSS_CHAIN_ID +} + +export async function signTssMessage({ instruction, nonce, amount, additional, chainId }: SignParams): Promise { + // Build message EXACTLY like gateway-test.ts + // Use chain_id from TSS account (like Rust does) or fallback to TSS_CHAIN_ID + const chainIdToUse = chainId ?? TSS_CHAIN_ID; + const PREFIX = Buffer.from("PUSH_CHAIN_SVM"); + const instructionId = Buffer.from([instruction]); + const chainIdBE = Buffer.alloc(8); + chainIdBE.writeBigUInt64BE(BigInt(chainIdToUse)); + const nonceBE = Buffer.alloc(8); + nonceBE.writeBigUInt64BE(BigInt(nonce)); + + const segments: Buffer[] = [PREFIX, instructionId, chainIdBE, nonceBE]; + + if (typeof amount === "bigint") { + const amountBE = Buffer.alloc(8); + amountBE.writeBigUInt64BE(amount); + segments.push(amountBE); + } + + additional.forEach((item) => { + segments.push(Buffer.from(item)); + }); + + const concat = Buffer.concat(segments); + const messageHashHex = keccak_256(concat); + const messageHash = Buffer.from(messageHashHex, "hex"); + + + // Sign EXACTLY like gateway-test.ts + const priv = privateKeyHex; + const sig = await secp.sign(messageHash, priv, { recovered: true, der: false }); + const signature: Uint8Array = sig[0]; + let recoveryId: number = sig[1]; // 0 or 1 + + return { + signature: Array.from(signature), + recoveryId, + messageHash: Array.from(messageHash), + nonce: new anchor.BN(nonce), + }; +} + +export function pubkeyToBytes(pubkey: PublicKey): Uint8Array { + return pubkey.toBuffer(); +} \ No newline at end of file diff --git a/contracts/svm-gateway/tests/rate-limit.test.ts b/contracts/svm-gateway/tests/rate-limit.test.ts new file mode 100644 index 0000000..653408e --- /dev/null +++ b/contracts/svm-gateway/tests/rate-limit.test.ts @@ -0,0 +1,909 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { UniversalGateway } from "../target/types/universal_gateway"; +import { PublicKey, Keypair, SystemProgram, LAMPORTS_PER_SOL } from "@solana/web3.js"; +import { expect } from "chai"; +import * as sharedState from "./shared-state"; +import { getSolPrice, calculateSolAmount } from "./setup-pricefeed"; +import * as spl from "@solana/spl-token"; + +describe("Universal Gateway - Rate Limiting Tests", () => { + anchor.setProvider(anchor.AnchorProvider.env()); + const provider = anchor.getProvider() as anchor.AnchorProvider; + const program = anchor.workspace.UniversalGateway as Program; + + let admin: Keypair; + let user1: Keypair; + let configPda: PublicKey; + let vaultPda: PublicKey; + let whitelistPda: PublicKey; + let rateLimitConfigPda: PublicKey; + let mockPriceFeed: PublicKey; + let solPrice: number; + let mockUSDT: any; + + // Helper to get token rate limit PDA + const getTokenRateLimitPda = (tokenMint: PublicKey): PublicKey => { + const [pda] = PublicKey.findProgramAddressSync( + [Buffer.from("rate_limit"), tokenMint.toBuffer()], + program.programId + ); + return pda; + }; + + // Helper to create revert instruction + const createRevertInstruction = (recipient: PublicKey) => ({ + fundRecipient: recipient, + revertMsg: Buffer.from("test"), + }); + + before(async () => { + admin = sharedState.getAdmin(); + user1 = Keypair.generate(); + + const airdropAmount = 20 * LAMPORTS_PER_SOL; + await provider.connection.requestAirdrop(user1.publicKey, airdropAmount); + await new Promise(resolve => setTimeout(resolve, 2000)); + + [configPda] = PublicKey.findProgramAddressSync([Buffer.from("config")], program.programId); + [vaultPda] = PublicKey.findProgramAddressSync([Buffer.from("vault")], program.programId); + [whitelistPda] = PublicKey.findProgramAddressSync([Buffer.from("whitelist")], program.programId); + [rateLimitConfigPda] = PublicKey.findProgramAddressSync([Buffer.from("rate_limit_config")], program.programId); + + mockPriceFeed = sharedState.getMockPriceFeed(); + solPrice = await getSolPrice(mockPriceFeed); + mockUSDT = sharedState.getMockUSDT(); + + // Initialize token rate limit accounts (required for universal gateway) + const veryLargeThreshold = new anchor.BN("1000000000000000000000"); // 1 sextillion (effectively unlimited) + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + try { + await program.account.tokenRateLimit.fetch(nativeSolTokenRateLimitPda); + } catch { + // Not initialized, create it + await program.methods + .setTokenRateLimit(veryLargeThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenMint: PublicKey.default, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + } + }); + + describe("Block USD Cap Enforcement", () => { + it("Should enforce block USD cap when enabled", async () => { + // Enable block USD cap: $4 per slot (4 * 1e8 = 400_000_000) + // Use $4 so that a single $3 transaction is under, but $3 + $3 = $6 would exceed + const blockCapUsd = 4; // $4 + const blockCapLamports = new anchor.BN(blockCapUsd * 100_000_000); // 8 decimals + + await program.methods + .setBlockUsdCap(blockCapLamports) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + // Calculate gas amount: $3 USD (within $1-$10 caps, but $3 + $3 = $6 > $4 block cap) + const gasAmountUsd = 3; // $3 + const gasAmount = calculateSolAmount(gasAmountUsd, solPrice); + + const req1 = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), // GAS route + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig1"), + }; + + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + // First transaction should succeed (consumes $3, under $4 cap) + await program.methods + .sendUniversalTx(req1, new anchor.BN(gasAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + // Verify first transaction consumed $3 + const rateLimitConfigAfterFirst = await program.account.rateLimitConfig.fetch(rateLimitConfigPda); + const consumedAfterFirst = rateLimitConfigAfterFirst.consumedUsdInBlock.toNumber(); + const slotAfterFirst = rateLimitConfigAfterFirst.lastSlot.toNumber(); + + // Verify consumed amount is approximately $3 (with some tolerance for price calculation) + // Note: calculate_usd_amount returns value in 8 decimals, so we need to account for price precision + // The actual consumed amount depends on the price calculation, so we just verify it's > 0 + expect(consumedAfterFirst).to.be.greaterThan(0); + // Also verify it's reasonable (should be close to $3 * 1e8, but allow for price calculation differences) + const minExpected = (gasAmountUsd * 0.5) * 100_000_000; // At least 50% of expected + const maxExpected = (gasAmountUsd * 1.5) * 100_000_000; // At most 150% of expected + expect(consumedAfterFirst).to.be.within(minExpected, maxExpected); + + // Second transaction should fail if in same slot (would exceed $4 cap: $3 + $3 = $6) + // Send immediately to maximize chance of same slot + let secondTxSucceeded = false; + let slotAfterSecond: number; + let consumedAfterSecond: number; + + try { + await program.methods + .sendUniversalTx(req1, new anchor.BN(gasAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + secondTxSucceeded = true; + const rateLimitConfigAfterSecond = await program.account.rateLimitConfig.fetch(rateLimitConfigPda); + consumedAfterSecond = rateLimitConfigAfterSecond.consumedUsdInBlock.toNumber(); + slotAfterSecond = rateLimitConfigAfterSecond.lastSlot.toNumber(); + } catch (error: any) { + // Transaction failed - check if it's the expected error + const errorNumber = error.error?.errorCode?.number || error.errorCode?.number; + const errorCode = error.error?.errorCode?.code || + error.errorCode?.code || + error.code || + error.error?.code; + + if (errorNumber === 6019 || errorCode === "BlockUsdCapExceeded") { + // Expected error - verify it was in the same slot + const rateLimitConfigAfterSecond = await program.account.rateLimitConfig.fetch(rateLimitConfigPda); + slotAfterSecond = rateLimitConfigAfterSecond.lastSlot.toNumber(); + consumedAfterSecond = rateLimitConfigAfterSecond.consumedUsdInBlock.toNumber(); + + // If same slot, consumed should still be $3 (second tx failed before adding) + // If different slot, consumed would be reset to 0 + if (slotAfterSecond === slotAfterFirst) { + // Same slot - verify consumed amount didn't increase (tx failed before adding) + expect(consumedAfterSecond).to.be.closeTo(consumedAfterFirst, consumedAfterFirst * 0.1); + return; // Test passed - same slot, correctly rejected + } else { + // Different slot - this test case is invalid, but verify reset happened + expect(consumedAfterSecond).to.equal(0); + expect.fail("Second transaction was in different slot - cannot test same-slot cap enforcement. This is expected in Solana's async model."); + } + } else { + throw error; // Unexpected error + } + } + + // If we reach here, second transaction succeeded + if (secondTxSucceeded) { + // Check if it was in the same slot + if (slotAfterSecond === slotAfterFirst) { + // Same slot - this is a bug! Should have been rejected + expect.fail(`Block USD cap exceeded in same slot! Slot: ${slotAfterFirst}, Consumed: ${consumedAfterSecond} (should be <= ${blockCapUsd * 100_000_000})`); + } else { + // Different slot - consumed should have reset + const expectedConsumed = gasAmountUsd * 100_000_000; + const minExpected = (gasAmountUsd * 0.5) * 100_000_000; + const maxExpected = (gasAmountUsd * 1.5) * 100_000_000; + expect(consumedAfterSecond).to.be.within(minExpected, maxExpected); + expect(slotAfterSecond).to.be.greaterThan(slotAfterFirst); + // This is expected behavior - slots advanced, cap reset + } + } + + // Disable block cap for other tests + await program.methods + .setBlockUsdCap(new anchor.BN(0)) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + }); + + it("Should reset consumed amount when slot changes", async () => { + // Enable block USD cap: $10 per slot + const blockCapUsd = 10; + const blockCapLamports = new anchor.BN(blockCapUsd * 100_000_000); + + await program.methods + .setBlockUsdCap(blockCapLamports) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + const gasAmountUsd = 2.5; // $2.5 (above $1 min cap, below $10 max cap) + const gasAmount = calculateSolAmount(gasAmountUsd, solPrice); + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig"), + }; + + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + // First transaction + await program.methods + .sendUniversalTx(req, new anchor.BN(gasAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + const config1 = await program.account.rateLimitConfig.fetch(rateLimitConfigPda); + const slot1 = config1.lastSlot.toNumber(); + const consumed1 = config1.consumedUsdInBlock.toNumber(); + + // Wait a bit to ensure next transaction is in a different slot + await new Promise(resolve => setTimeout(resolve, 500)); + + // Second transaction in new slot should reset consumed amount + await program.methods + .sendUniversalTx(req, new anchor.BN(gasAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + const config2 = await program.account.rateLimitConfig.fetch(rateLimitConfigPda); + const slot2 = config2.lastSlot.toNumber(); + const consumed2 = config2.consumedUsdInBlock.toNumber(); + + // If slots are different, consumed should have reset + if (slot2 !== slot1) { + // New slot - consumed should reset to ~$2.5 (not accumulate from previous slot) + const minExpected = (gasAmountUsd * 0.5) * 100_000_000; + const maxExpected = (gasAmountUsd * 1.5) * 100_000_000; + expect(consumed2).to.be.within(minExpected, maxExpected); + expect(consumed2).to.be.lessThan(consumed1 + maxExpected); // Should NOT accumulate from previous slot + } else { + // Same slot - consumed should accumulate + expect(consumed2).to.be.greaterThan(consumed1); + } + + // Disable block cap + await program.methods + .setBlockUsdCap(new anchor.BN(0)) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + }); + + it("Should allow transactions when block USD cap is disabled (0)", async () => { + // Ensure block cap is disabled + await program.methods + .setBlockUsdCap(new anchor.BN(0)) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + const gasAmount = calculateSolAmount(2.5, solPrice); + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig"), + }; + + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + // Should succeed even with multiple transactions + await program.methods + .sendUniversalTx(req, new anchor.BN(gasAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + // Second transaction should also succeed (cap disabled) + await program.methods + .sendUniversalTx(req, new anchor.BN(gasAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + }); + }); + + describe("Token Rate Limit Enforcement (Epoch-based)", () => { + it("Should enforce token rate limit for native SOL when enabled", async () => { + // Enable epoch duration (1 hour = 3600 seconds) + await program.methods + .updateEpochDuration(new anchor.BN(3600)) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + // Set rate limit threshold: 1 SOL per epoch + const limitThreshold = new anchor.BN(LAMPORTS_PER_SOL); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + await program.methods + .setTokenRateLimit(limitThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenMint: PublicKey.default, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + // First transaction: 0.5 SOL (should succeed) + const fundsAmount1 = 0.5 * LAMPORTS_PER_SOL; + const req1 = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(fundsAmount1), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig1"), + }; + + await program.methods + .sendUniversalTx(req1, new anchor.BN(fundsAmount1)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + // Second transaction: 0.5 SOL (should succeed, total = 1 SOL = limit) + const req2 = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(fundsAmount1), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig2"), + }; + + await program.methods + .sendUniversalTx(req2, new anchor.BN(fundsAmount1)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + // Third transaction: 0.1 SOL (should fail, would exceed 1 SOL limit) + const fundsAmount3 = 0.1 * LAMPORTS_PER_SOL; + const req3 = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(fundsAmount3), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig3"), + }; + + try { + await program.methods + .sendUniversalTx(req3, new anchor.BN(fundsAmount3)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + expect.fail("Should reject when token rate limit would be exceeded"); + } catch (error: any) { + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code; + expect(errorCode).to.equal("RateLimitExceeded"); + } + }); + + it("Should enforce token rate limit for SPL tokens when enabled", async () => { + // Create user token account and mint tokens + const userTokenAccount = await mockUSDT.createTokenAccount(user1.publicKey); + const gatewayTokenAccount = await mockUSDT.createTokenAccount(vaultPda, true); + await mockUSDT.mintTo(userTokenAccount, 5000); // 5000 tokens + + // Set rate limit: 1000 tokens per epoch (1000 * 10^6 = 1_000_000_000) + const limitThreshold = new anchor.BN(1000 * Math.pow(10, mockUSDT.config.decimals)); + const usdtTokenRateLimitPda = getTokenRateLimitPda(mockUSDT.mint.publicKey); + + await program.methods + .setTokenRateLimit(limitThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: usdtTokenRateLimitPda, + tokenMint: mockUSDT.mint.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + // First transaction: 500 tokens (should succeed) + const tokenAmount1 = new anchor.BN(500 * Math.pow(10, mockUSDT.config.decimals)); + const req1 = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: mockUSDT.mint.publicKey, + amount: tokenAmount1, + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("spl_sig1"), + }; + + await program.methods + .sendUniversalTx(req1, new anchor.BN(0)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: userTokenAccount, + gatewayTokenAccount: gatewayTokenAccount, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: usdtTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + // Second transaction: 500 tokens (should succeed, total = 1000 = limit) + const req2 = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: mockUSDT.mint.publicKey, + amount: tokenAmount1, + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("spl_sig2"), + }; + + await program.methods + .sendUniversalTx(req2, new anchor.BN(0)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: userTokenAccount, + gatewayTokenAccount: gatewayTokenAccount, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: usdtTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + // Third transaction: 100 tokens (should fail, would exceed 1000 limit) + const tokenAmount3 = new anchor.BN(100 * Math.pow(10, mockUSDT.config.decimals)); + const req3 = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: mockUSDT.mint.publicKey, + amount: tokenAmount3, + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("spl_sig3"), + }; + + try { + await program.methods + .sendUniversalTx(req3, new anchor.BN(0)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: userTokenAccount, + gatewayTokenAccount: gatewayTokenAccount, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: usdtTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + expect.fail("Should reject when SPL token rate limit would be exceeded"); + } catch (error: any) { + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code; + expect(errorCode).to.equal("RateLimitExceeded"); + } + }); + + it("Should reject when limit_threshold is 0 (token not supported)", async () => { + // Set limit_threshold to 0 - this means token is NOT supported (EVM v0 parity) + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + await program.methods + .setTokenRateLimit(new anchor.BN(0)) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenMint: PublicKey.default, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + // Should fail with NotSupported error (threshold = 0 means token not supported) + const largeAmount = 10 * LAMPORTS_PER_SOL; + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(largeAmount), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig"), + }; + + try { + await program.methods + .sendUniversalTx(req, new anchor.BN(largeAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + expect.fail("Should reject when limit_threshold is 0 (token not supported)"); + } catch (error: any) { + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code; + expect(errorCode).to.equal("NotSupported"); + } + }); + + it("Should skip token rate limit when epoch_duration is 0", async () => { + // Disable epoch duration + await program.methods + .updateEpochDuration(new anchor.BN(0)) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + // Set a small limit threshold + const limitThreshold = new anchor.BN(LAMPORTS_PER_SOL); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + await program.methods + .setTokenRateLimit(limitThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenMint: PublicKey.default, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + // Should succeed even with amounts exceeding threshold (epoch disabled) + // Use a reasonable amount that user has balance for (check balance first) + const userBalance = await provider.connection.getBalance(user1.publicKey); + const largeAmount = Math.min(5 * LAMPORTS_PER_SOL, userBalance - 0.1 * LAMPORTS_PER_SOL); // Use 5 SOL or available balance - 0.1 SOL for fees + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(largeAmount), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig"), + }; + + await program.methods + .sendUniversalTx(req, new anchor.BN(largeAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + }); + }); + + describe("Rate Limit Edge Cases", () => { + it("Should handle rate limits in FUNDS_AND_PAYLOAD routes", async () => { + // Enable rate limiting + await program.methods + .updateEpochDuration(new anchor.BN(3600)) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + const limitThreshold = new anchor.BN(0.5 * LAMPORTS_PER_SOL); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + await program.methods + .setTokenRateLimit(limitThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenMint: PublicKey.default, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + // FUNDS_AND_PAYLOAD with batching should enforce rate limit on funds portion + // Gas amount must be within USD caps ($1-$10), so use $2.50 for gas + const gasAmountUsd = 2.5; + const gasAmount = calculateSolAmount(gasAmountUsd, solPrice); + const fundsAmount = 0.3 * LAMPORTS_PER_SOL; + const totalAmount = fundsAmount + gasAmount; + + const req = { + recipient: Array.from(Buffer.alloc(20, 1)), + token: PublicKey.default, + amount: new anchor.BN(fundsAmount), + payload: Buffer.from("payload"), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig"), + }; + + // Should succeed (funds = 0.3 SOL, under 0.5 SOL limit) + await program.methods + .sendUniversalTx(req, new anchor.BN(totalAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + // Second transaction with 0.3 SOL funds should fail (total would be 0.6 SOL > 0.5 limit) + const req2 = { + recipient: Array.from(Buffer.alloc(20, 1)), + token: PublicKey.default, + amount: new anchor.BN(fundsAmount), + payload: Buffer.from("payload"), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig2"), + }; + + try { + await program.methods + .sendUniversalTx(req2, new anchor.BN(totalAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + expect.fail("Should reject when rate limit would be exceeded in FUNDS_AND_PAYLOAD"); + } catch (error: any) { + const errorCode = error.error?.errorCode?.code || error.errorCode?.code || error.code; + expect(errorCode).to.equal("RateLimitExceeded"); + } + }); + }); + + after(async () => { + // Cleanup: Disable rate limits to prevent interference with other tests + try { + // Disable block USD cap + await program.methods + .setBlockUsdCap(new anchor.BN(0)) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + // Disable epoch duration + await program.methods + .updateEpochDuration(new anchor.BN(0)) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + // Set very large thresholds to effectively disable + const veryLargeThreshold = new anchor.BN("1000000000000000000000"); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + await program.methods + .setTokenRateLimit(veryLargeThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenMint: PublicKey.default, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + } catch (error) { + // Ignore errors during cleanup + } + }); +}); + diff --git a/contracts/svm-gateway/tests/setup-pricefeed.ts b/contracts/svm-gateway/tests/setup-pricefeed.ts new file mode 100644 index 0000000..928690b --- /dev/null +++ b/contracts/svm-gateway/tests/setup-pricefeed.ts @@ -0,0 +1,124 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { pullOracleClient } from "@tkkinn/mock-pyth-sdk"; +import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; +// Load pull IDL from SDK source (SDK has bug - uses push IDL, so we load correct one) +const pullIdl = require("@tkkinn/mock-pyth-sdk/src/idl/mock_pyth_pull.json"); + +export async function setupPriceFeed() { + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + const programId = new anchor.web3.PublicKey( + "rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ" + ); + + // IMPORTANT: Must match the Rust FEED_ID exactly + const FEED_ID = "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; + + // SDK creates Program without provider, so we create it ourselves + // Ensure IDL is a plain object (not a module export) + const idl = JSON.parse(JSON.stringify(pullIdl)); + + // Create Program with provider only - IDL already has address field + // Anchor will use the IDL's address field (which matches programId) + const program = new Program(idl, provider) as any; + + // Ensure wallet is NodeWallet type + const wallet = provider.wallet as NodeWallet; + + const pullOracle = new pullOracleClient({ + provider, + wallet: wallet, + program: program, + }); + + // local test price values - adjust if your tests expect different values + const uiPrice = 150.25; + const expo = -8; + const conf = 100; + + // createOracle allows specifying FeedId (mock-pyth supports this) - returns [tx, priceFeedPubkey] + // Note: createOracle already initializes the price feed with the price, so setPrice is not needed + const [tx, priceFeedPubkey] = await pullOracle.createOracle(FEED_ID, uiPrice, expo, conf); + + // Wait for transaction confirmation + await provider.connection.confirmTransaction(tx, "confirmed"); + + return priceFeedPubkey; +} + +/** + * Helper function to get the current SOL price from the mock Pyth price feed + */ +export async function getSolPrice(priceFeedPubkey: anchor.web3.PublicKey): Promise { + const provider = anchor.AnchorProvider.env(); + + const programId = new anchor.web3.PublicKey( + "rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ" + ); + + const idl = JSON.parse(JSON.stringify(require("@tkkinn/mock-pyth-sdk/src/idl/mock_pyth_pull.json"))); + const program = new Program(idl, provider) as any; + + try { + // Fetch the price update account + const priceUpdateAccount = await program.account.priceUpdateV2.fetch(priceFeedPubkey); + + // The price is in priceMessage.price with priceMessage.exponent + // Anchor converts snake_case to camelCase, so price_message becomes priceMessage + const priceMessage = priceUpdateAccount.priceMessage || priceUpdateAccount.price_message; + + if (!priceMessage) { + throw new Error("priceMessage not found in PriceUpdateV2"); + } + + // Access price and exponent - they might be BN objects, hex strings, or numbers + let priceValue: number; + if (typeof priceMessage.price === 'string') { + // If it's a hex string, convert it + priceValue = parseInt(priceMessage.price, 16); + } else if (priceMessage.price?.toNumber) { + // If it's a BN object + priceValue = priceMessage.price.toNumber(); + } else if (typeof priceMessage.price === 'number') { + priceValue = priceMessage.price; + } else { + throw new Error(`Unexpected price type: ${typeof priceMessage.price}`); + } + + const exponent = priceMessage.exponent; + + if (priceValue === undefined || priceValue === null || exponent === undefined || exponent === null) { + throw new Error(`Price or exponent not found. price: ${priceValue}, exponent: ${exponent}`); + } + + // Convert: price = priceValue * 10^exponent + const price = priceValue * Math.pow(10, exponent); + + if (!price || price <= 0 || !isFinite(price)) { + throw new Error(`Invalid price calculated: ${price}`); + } + + return price; + } catch (error) { + // Fallback to default price if fetch fails + return 150.25; + } +} + +/** + * Helper function to calculate SOL amount in lamports for a given USD value + */ +export function calculateSolAmount(usdValue: number, solPrice: number): number { + const solAmount = usdValue / solPrice; + const lamports = Math.floor(solAmount * anchor.web3.LAMPORTS_PER_SOL); + + // Ensure minimum amount is at least 1 lamport + if (lamports === 0 && usdValue > 0) { + return 1; + } + + return lamports; +} + diff --git a/contracts/svm-gateway/tests/shared-state.ts b/contracts/svm-gateway/tests/shared-state.ts new file mode 100644 index 0000000..89ca581 --- /dev/null +++ b/contracts/svm-gateway/tests/shared-state.ts @@ -0,0 +1,89 @@ +import * as anchor from "@coral-xyz/anchor"; +import { PublicKey, Keypair } from "@solana/web3.js"; + +/** + * Shared state across all test files + * This ensures all tests use the same admin, tokens, and PDAs + */ + +// Keypairs (initialized once in setup.test.ts) +export let admin: Keypair | null = null; +export let tssAddress: Keypair | null = null; +export let pauser: Keypair | null = null; + +// Mock tokens (created once in setup.test.ts) +export let mockUSDT: any = null; +export let mockUSDC: any = null; + +// Pyth +export let mockPythOracle: any = null; +export let mockPriceFeed: PublicKey | null = null; +let dummyPythFeed: PublicKey | null = null; // Cached dummy feed + +// Setter functions +export function setAdmin(keypair: Keypair) { + admin = keypair; +} + +export function setTssAddress(keypair: Keypair) { + tssAddress = keypair; +} + +export function setPauser(keypair: Keypair) { + pauser = keypair; +} + +export function setMockUSDT(token: any) { + mockUSDT = token; +} + +export function setMockUSDC(token: any) { + mockUSDC = token; +} + +export function setMockPythOracle(oracle: any) { + mockPythOracle = oracle; +} + +export function setMockPriceFeed(feed: PublicKey) { + mockPriceFeed = feed; +} + +// Getter functions (with validation) +export function getAdmin(): Keypair { + if (!admin) throw new Error("Admin not initialized - did setup.test.ts run?"); + return admin; +} + +export function getTssAddress(): Keypair { + if (!tssAddress) throw new Error("TSS address not initialized - did setup.test.ts run?"); + return tssAddress; +} + +export function getPauser(): Keypair { + if (!pauser) throw new Error("Pauser not initialized - did setup.test.ts run?"); + return pauser; +} + +export function getMockUSDT(): any { + if (!mockUSDT) throw new Error("Mock USDT not initialized - did setup.test.ts run?"); + return mockUSDT; +} + +export function getMockUSDC(): any { + if (!mockUSDC) throw new Error("Mock USDC not initialized - did setup.test.ts run?"); + return mockUSDC; +} + +export function getMockPriceFeed(): PublicKey { + // Return cached dummy if not set - Pyth is skipped for now + if (!mockPriceFeed) { + if (!dummyPythFeed) { + // Generate and cache a dummy keypair for all tests to use + dummyPythFeed = Keypair.generate().publicKey; + } + return dummyPythFeed; + } + return mockPriceFeed; +} + diff --git a/contracts/svm-gateway/tests/sol-deposit.test.ts b/contracts/svm-gateway/tests/sol-deposit.test.ts new file mode 100644 index 0000000..03c4304 --- /dev/null +++ b/contracts/svm-gateway/tests/sol-deposit.test.ts @@ -0,0 +1,294 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { UniversalGateway } from "../target/types/universal_gateway"; +import { PublicKey, Keypair, SystemProgram } from "@solana/web3.js"; +import { expect } from "chai"; +import * as sharedState from "./shared-state"; +import { getSolPrice, calculateSolAmount } from "./setup-pricefeed"; + + +describe("Universal Gateway - Native SOL Deposit Tests", () => { + anchor.setProvider(anchor.AnchorProvider.env()); + const provider = anchor.getProvider() as anchor.AnchorProvider; + const program = anchor.workspace.UniversalGateway as Program; + + let admin: Keypair; + let tssAddress: Keypair; + let pauser: Keypair; + let user1: Keypair; + let user2: Keypair; + let configPda: PublicKey; + let vaultPda: PublicKey; + let rateLimitConfigPda: PublicKey; + let mockPriceFeed: PublicKey; + let solPrice: number; + + const createPayload = (to: number, vType: any = { signedVerification: {} }) => ({ + to: Array.from(Buffer.alloc(20, to)), + value: new anchor.BN(0), + data: Buffer.from([]), + gasLimit: new anchor.BN(21000), + maxFeePerGas: new anchor.BN(20000000000), + maxPriorityFeePerGas: new anchor.BN(1000000000), + nonce: new anchor.BN(0), + deadline: new anchor.BN(Math.floor(Date.now() / 1000) + 3600), + vType, + }); + + const createRevertInstruction = (recipient: PublicKey, msg: string = "test") => ({ + fundRecipient: recipient, + revertMsg: Buffer.from(msg), + }); + + before(async () => { + // Use shared state keys for consistency + admin = sharedState.getAdmin(); + tssAddress = sharedState.getTssAddress(); + pauser = sharedState.getPauser(); + user1 = Keypair.generate(); + user2 = Keypair.generate(); + + const airdropAmount = 10 * anchor.web3.LAMPORTS_PER_SOL; + await Promise.all([ + provider.connection.requestAirdrop(admin.publicKey, airdropAmount), + provider.connection.requestAirdrop(user1.publicKey, airdropAmount), + provider.connection.requestAirdrop(user2.publicKey, airdropAmount), + ]); + await new Promise(resolve => setTimeout(resolve, 2000)); + + [configPda] = PublicKey.findProgramAddressSync([Buffer.from("config")], program.programId); + [vaultPda] = PublicKey.findProgramAddressSync([Buffer.from("vault")], program.programId); + [rateLimitConfigPda] = PublicKey.findProgramAddressSync([Buffer.from("rate_limit_config")], program.programId); + + // Use mock Pyth price feed from shared state + mockPriceFeed = sharedState.getMockPriceFeed(); + + // Get current SOL price from the price feed + solPrice = await getSolPrice(mockPriceFeed); + + // Verify calculated amounts are valid + const testAmount = calculateSolAmount(2.5, solPrice); + if (testAmount === 0) { + throw new Error(`Calculated amount is 0! Price: ${solPrice}, USD: 2.5`); + } + + // Gateway should already be initialized by 00-setup.test.ts + // If not, we'll initialize it here as a fallback + try { + const config = await program.account.config.fetch(configPda); + // Verify we're using the same admin from shared state + if (config.admin.toString() !== admin.publicKey.toString()) { + throw new Error("Config admin mismatch - use shared state admin"); + } + } catch { + // Fallback initialization if gateway doesn't exist + await program.methods + .initialize(admin.publicKey, pauser.publicKey, tssAddress.publicKey, new anchor.BN(100_000_000), new anchor.BN(1_000_000_000), mockPriceFeed) + .accounts({ admin: admin.publicKey }) + .signers([admin]) + .rpc(); + + await program.methods + .setBlockUsdCap(new anchor.BN(500_000_000_000)) + .accounts({ config: configPda, rateLimitConfig: rateLimitConfigPda, admin: admin.publicKey, systemProgram: SystemProgram.programId }) + .signers([admin]) + .rpc(); + + await program.methods + .updateEpochDuration(new anchor.BN(3600)) + .accounts({ config: configPda, rateLimitConfig: rateLimitConfigPda, admin: admin.publicKey, systemProgram: SystemProgram.programId }) + .signers([admin]) + .rpc(); + } + }); + + describe("send_tx_with_gas - Success Cases", () => { + it("Allows basic SOL deposit within USD caps", async () => { + // Calculate amount for $2.50 USD (mid-range between $1-$10, with buffer for rounding) + const depositAmount = calculateSolAmount(2.5, solPrice); + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + + await program.methods + .sendTxWithGas(createPayload(1), createRevertInstruction(user1.publicKey), new anchor.BN(depositAmount), Buffer.from("signature")) + .accounts({ config: configPda, vault: vaultPda, user: user1.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + expect(finalVaultBalance).to.be.greaterThan(initialVaultBalance); + }); + + it("Allows multiple deposits from different users within USD caps", async () => { + // Calculate amounts for $2.50 USD each (mid-range between $1-$10, with buffer for rounding) + const deposit1 = calculateSolAmount(2.5, solPrice); + const deposit2 = calculateSolAmount(2.5, solPrice); + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + + await program.methods + .sendTxWithGas(createPayload(1), createRevertInstruction(user1.publicKey, "user1"), new anchor.BN(deposit1), Buffer.from("sig1")) + .accounts({ config: configPda, vault: vaultPda, user: user1.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + + await program.methods + .sendTxWithGas(createPayload(2, { universalTxVerification: {} }), createRevertInstruction(user2.publicKey, "user2"), new anchor.BN(deposit2), Buffer.from("sig2")) + .accounts({ config: configPda, vault: vaultPda, user: user2.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user2]) + .rpc(); + + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + // Vault should receive the full deposit amounts (transaction fees are paid by users, not deducted from deposits) + expect(finalVaultBalance - initialVaultBalance).to.equal(deposit1 + deposit2); + }); + + it("Allows deposits with different gas parameters and payload data within USD caps", async () => { + // Calculate amount for $2.50 USD (mid-range between $1-$10, with buffer for rounding) + const depositAmount = calculateSolAmount(2.5, solPrice); + + const highGasPayload = { ...createPayload(3), gasLimit: new anchor.BN(50000), maxFeePerGas: new anchor.BN(100_000_000_000), maxPriorityFeePerGas: new anchor.BN(10_000_000_000) }; + await program.methods + .sendTxWithGas(highGasPayload, createRevertInstruction(user1.publicKey), new anchor.BN(depositAmount), Buffer.from("high_gas")) + .accounts({ config: configPda, vault: vaultPda, user: user1.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + + const payloadWithData = { ...createPayload(4), value: new anchor.BN(1_000_000_000), data: Buffer.from("test payload") }; + await program.methods + .sendTxWithGas(payloadWithData, createRevertInstruction(user1.publicKey), new anchor.BN(depositAmount), Buffer.from("payload")) + .accounts({ config: configPda, vault: vaultPda, user: user1.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + }); + }); + + describe("send_tx_with_gas - Error Cases", () => { + it("Rejects when paused, zero amount, invalid recipient, insufficient balance", async () => { + await program.methods.pause().accounts({ pauser: pauser.publicKey, config: configPda }).signers([pauser]).rpc(); + + // Use a valid amount within USD caps for the paused test ($2.50) + const validAmount = calculateSolAmount(2.5, solPrice); + try { + await program.methods + .sendTxWithGas(createPayload(1), createRevertInstruction(user1.publicKey), new anchor.BN(validAmount), Buffer.from("sig")) + .accounts({ config: configPda, vault: vaultPda, user: user1.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject when paused"); + } catch (error: any) { + expect(error).to.exist; + expect(error.error?.errorCode?.code || error.code).to.equal("Paused"); + } + + await program.methods.unpause().accounts({ pauser: pauser.publicKey, config: configPda }).signers([pauser]).rpc(); + + try { + await program.methods + .sendTxWithGas(createPayload(1), createRevertInstruction(user1.publicKey), new anchor.BN(0), Buffer.from("sig")) + .accounts({ config: configPda, vault: vaultPda, user: user1.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject zero amount"); + } catch (error) { + expect(error).to.exist; + } + + try { + await program.methods + .sendTxWithGas(createPayload(1), { fundRecipient: PublicKey.default, revertMsg: Buffer.from("test") }, new anchor.BN(validAmount), Buffer.from("sig")) + .accounts({ config: configPda, vault: vaultPda, user: user1.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject invalid recipient"); + } catch (error) { + expect(error).to.exist; + } + + const userBalance = await provider.connection.getBalance(user1.publicKey); + try { + await program.methods + .sendTxWithGas(createPayload(1), createRevertInstruction(user1.publicKey), new anchor.BN(userBalance + anchor.web3.LAMPORTS_PER_SOL), Buffer.from("sig")) + .accounts({ config: configPda, vault: vaultPda, user: user1.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject insufficient balance"); + } catch (error) { + expect(error).to.exist; + } + }); + + it("Allows deposit within USD cap range (between min and max)", async () => { + // Calculate amount for $7.00 USD (mid-range between $1-$10, should pass) + const validAmount = calculateSolAmount(7.0, solPrice); + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + + await program.methods + .sendTxWithGas(createPayload(1), createRevertInstruction(user1.publicKey), new anchor.BN(validAmount), Buffer.from("sig")) + .accounts({ config: configPda, vault: vaultPda, user: user1.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + expect(finalVaultBalance).to.be.greaterThan(initialVaultBalance); + }); + + it("Rejects deposits below minimum USD cap", async () => { + const config = await program.account.config.fetch(configPda); + const minCapUsd = config.minCapUniversalTxUsd.toNumber() / 100_000_000; + // Calculate amount for $0.50 USD (below $1 min cap) + const belowMinAmount = calculateSolAmount(0.5, solPrice); + + try { + await program.methods + .sendTxWithGas(createPayload(1), createRevertInstruction(user1.publicKey), new anchor.BN(belowMinAmount), Buffer.from("sig")) + .accounts({ config: configPda, vault: vaultPda, user: user1.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail(`Should reject below min cap ($${minCapUsd})`); + } catch (error: any) { + expect(error).to.exist; + // Check error code structure - Anchor errors can be nested + // AnchorError structure: error.error.errorCode.code or error.errorCode.code + const errorCode = error.error?.errorCode?.code || + error.errorCode?.code || + error.code || + error.error?.code; + expect(errorCode).to.equal("BelowMinCap"); + } + }); + + it("Rejects deposits above maximum USD cap", async () => { + const config = await program.account.config.fetch(configPda); + const maxCapUsd = config.maxCapUniversalTxUsd.toNumber() / 100_000_000; + // Calculate amount for an amount above the actual max cap (use maxCapUsd + 10 to ensure it's above) + const testUsdAmount = maxCapUsd + 10; + const aboveMaxAmount = calculateSolAmount(testUsdAmount, solPrice); + + // Verify the amount is actually above max cap + const aboveMaxUsd = (aboveMaxAmount / anchor.web3.LAMPORTS_PER_SOL) * solPrice; + expect(aboveMaxUsd).to.be.greaterThan(maxCapUsd); + + try { + await program.methods + .sendTxWithGas(createPayload(1), createRevertInstruction(user1.publicKey), new anchor.BN(aboveMaxAmount), Buffer.from("sig")) + .accounts({ config: configPda, vault: vaultPda, user: user1.publicKey, priceUpdate: mockPriceFeed, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail(`Should reject above max cap ($${maxCapUsd})`); + } catch (error: any) { + expect(error).to.exist; + // First check error number (6005 = AboveMaxCap) - this is more reliable + if (error.error?.errorCode?.number === 6005 || error.errorCode?.number === 6005) { + // Error number confirms it's AboveMaxCap + return; + } + // Extract error code - use the exact same pattern as BelowMinCap which works + const errorCode = error.error?.errorCode?.code || + error.errorCode?.code || + error.code || + error.error?.code; + + expect(errorCode).to.equal("AboveMaxCap"); + } + }); + }); +}); \ No newline at end of file diff --git a/contracts/svm-gateway/tests/spl-deposit.test.ts b/contracts/svm-gateway/tests/spl-deposit.test.ts new file mode 100644 index 0000000..8facd5a --- /dev/null +++ b/contracts/svm-gateway/tests/spl-deposit.test.ts @@ -0,0 +1,366 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { UniversalGateway } from "../target/types/universal_gateway"; +import { PublicKey, Keypair, SystemProgram } from "@solana/web3.js"; +import { expect } from "chai"; +import { TOKEN_PROGRAM_ID, getAssociatedTokenAddress } from "@solana/spl-token"; +import * as sharedState from "./shared-state"; +import { createMockUSDC } from "./helpers/mockSpl"; +import { getSolPrice, calculateSolAmount } from "./setup-pricefeed"; + + +describe("Universal Gateway - SPL Token Deposit Tests", () => { + anchor.setProvider(anchor.AnchorProvider.env()); + const provider = anchor.getProvider() as anchor.AnchorProvider; + const program = anchor.workspace.UniversalGateway as Program; + + let admin: Keypair; + let tssAddress: Keypair; + let pauser: Keypair; + let user1: Keypair; + let user2: Keypair; + let configPda: PublicKey; + let vaultPda: PublicKey; + let whitelistPda: PublicKey; + let rateLimitConfigPda: PublicKey; + let mockPriceFeed: PublicKey; + let mockUSDT: any; + let mockUSDC: any; + let user1UsdtAccount: PublicKey; + let user1UsdcAccount: PublicKey; + let user2UsdtAccount: PublicKey; + let vaultUsdtAccount: PublicKey; + let vaultUsdcAccount: PublicKey; + let solPrice: number; + + const toTokenUnits = (amount: number, decimals: number = 6) => new anchor.BN(amount * Math.pow(10, decimals)); + const createPayload = (to: number, value: number = 0, vType: any = { universalTxVerification: {} }) => ({ + to: Array.from(Buffer.alloc(20, to)), + value: new anchor.BN(value), + data: Buffer.from([]), + gasLimit: new anchor.BN(50000), + maxFeePerGas: new anchor.BN(25000000000), + maxPriorityFeePerGas: new anchor.BN(2000000000), + nonce: new anchor.BN(1), + deadline: new anchor.BN(Math.floor(Date.now() / 1000) + 3600), + vType, + }); + const createRevertInstruction = (recipient: PublicKey, msg: string = "test") => ({ fundRecipient: recipient, revertMsg: Buffer.from(msg) }); + + before(async () => { + // Use shared state from setup.test.ts + admin = sharedState.getAdmin(); + tssAddress = sharedState.getTssAddress(); + pauser = sharedState.getPauser(); + mockUSDT = sharedState.getMockUSDT(); + mockUSDC = sharedState.getMockUSDC(); + mockPriceFeed = sharedState.getMockPriceFeed(); + + user1 = Keypair.generate(); + user2 = Keypair.generate(); + + const airdropAmount = 10 * anchor.web3.LAMPORTS_PER_SOL; + await Promise.all([ + provider.connection.requestAirdrop(user1.publicKey, airdropAmount), + provider.connection.requestAirdrop(user2.publicKey, airdropAmount), + ]); + await new Promise(resolve => setTimeout(resolve, 2000)); + + [configPda] = PublicKey.findProgramAddressSync([Buffer.from("config")], program.programId); + [vaultPda] = PublicKey.findProgramAddressSync([Buffer.from("vault")], program.programId); + [whitelistPda] = PublicKey.findProgramAddressSync([Buffer.from("whitelist")], program.programId); + [rateLimitConfigPda] = PublicKey.findProgramAddressSync([Buffer.from("rate_limit_config")], program.programId); + + // Get current SOL price from the price feed + solPrice = await getSolPrice(mockPriceFeed); + + // Verify calculated amounts are valid + const testAmount = calculateSolAmount(2.5, solPrice); + if (testAmount === 0) { + throw new Error(`Calculated amount is 0! Price: ${solPrice}, USD: 2.5`); + } + + user1UsdtAccount = await mockUSDT.createTokenAccount(user1.publicKey); + user1UsdcAccount = await mockUSDC.createTokenAccount(user1.publicKey); + user2UsdtAccount = await mockUSDT.createTokenAccount(user2.publicKey); + + vaultUsdtAccount = await mockUSDT.createTokenAccount(vaultPda, true); + vaultUsdcAccount = await mockUSDC.createTokenAccount(vaultPda, true); + + // Vault token accounts are created automatically via init_if_needed + + await mockUSDT.mintTo(user1UsdtAccount, 10000); + await mockUSDC.mintTo(user1UsdcAccount, 5000); + await mockUSDT.mintTo(user2UsdtAccount, 7500); + + }); + + describe("send_funds - Success Cases", () => { + it("Allows basic SPL token deposit", async () => { + const depositAmount = 1000; + const initialUserBalance = await mockUSDT.getBalance(user1UsdtAccount); + const initialVaultBalance = await mockUSDT.getBalance(vaultUsdtAccount); + + await program.methods + .sendFunds(Array.from(Buffer.alloc(20, 1)), mockUSDT.mint.publicKey, toTokenUnits(depositAmount), createRevertInstruction(user1.publicKey)) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdtAccount, gatewayTokenAccount: vaultUsdtAccount, bridgeToken: mockUSDT.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + + expect(await mockUSDT.getBalance(user1UsdtAccount)).to.equal(initialUserBalance - depositAmount); + expect(await mockUSDT.getBalance(vaultUsdtAccount)).to.equal(initialVaultBalance + depositAmount); + }); + + it("Allows multiple deposits from different users and tokens", async () => { + await program.methods + .sendFunds(Array.from(Buffer.alloc(20, 2)), mockUSDT.mint.publicKey, toTokenUnits(500), createRevertInstruction(user1.publicKey, "user1-usdt")) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdtAccount, gatewayTokenAccount: vaultUsdtAccount, bridgeToken: mockUSDT.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + + await program.methods + .sendFunds(Array.from(Buffer.alloc(20, 3)), mockUSDC.mint.publicKey, toTokenUnits(300), createRevertInstruction(user1.publicKey, "user1-usdc")) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdcAccount, gatewayTokenAccount: vaultUsdcAccount, bridgeToken: mockUSDC.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + + await program.methods + .sendFunds(Array.from(Buffer.alloc(20, 4)), mockUSDT.mint.publicKey, toTokenUnits(200), createRevertInstruction(user2.publicKey, "user2-usdt")) + .accounts({ user: user2.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user2UsdtAccount, gatewayTokenAccount: vaultUsdtAccount, bridgeToken: mockUSDT.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user2]) + .rpc(); + }); + }); + + describe("send_tx_with_funds - Success Cases", () => { + it("Allows combined SOL + SPL token deposit within USD caps", async () => { + // Calculate amount for $2.50 USD (mid-range between $1-$10, with buffer for rounding) + const gasAmount = calculateSolAmount(2.5, solPrice); + const tokenAmount = 500; + const initialUserBalance = await mockUSDC.getBalance(user1UsdcAccount); + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + + await program.methods + .sendTxWithFunds(mockUSDC.mint.publicKey, toTokenUnits(tokenAmount), createPayload(5, tokenAmount), createRevertInstruction(user1.publicKey), new anchor.BN(gasAmount), Buffer.from("sig")) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdcAccount, gatewayTokenAccount: vaultUsdcAccount, priceUpdate: mockPriceFeed, bridgeToken: mockUSDC.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + + expect(await mockUSDC.getBalance(user1UsdcAccount)).to.equal(initialUserBalance - tokenAmount); + expect(await provider.connection.getBalance(vaultPda)).to.be.greaterThan(initialVaultBalance); + }); + + it("Allows combined deposits with different tokens within USD caps", async () => { + // Calculate amount for $2.50 USD (mid-range between $1-$10, with buffer for rounding) + const gasAmount = calculateSolAmount(2.5, solPrice); + + await program.methods + .sendTxWithFunds(mockUSDT.mint.publicKey, toTokenUnits(250), createPayload(6, 250), createRevertInstruction(user1.publicKey), new anchor.BN(gasAmount), Buffer.from("usdt")) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdtAccount, gatewayTokenAccount: vaultUsdtAccount, priceUpdate: mockPriceFeed, bridgeToken: mockUSDT.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + + await program.methods + .sendTxWithFunds(mockUSDC.mint.publicKey, toTokenUnits(300), createPayload(7, 300), createRevertInstruction(user1.publicKey), new anchor.BN(gasAmount), Buffer.from("usdc")) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdcAccount, gatewayTokenAccount: vaultUsdcAccount, priceUpdate: mockPriceFeed, bridgeToken: mockUSDC.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + }); + }); + + describe("send_funds - Error Cases", () => { + it("Rejects when paused, zero recipient, invalid revert recipient, zero amount, non-whitelisted token, insufficient balance", async () => { + await program.methods.pause().accounts({ pauser: pauser.publicKey, config: configPda }).signers([pauser]).rpc(); + + try { + await program.methods + .sendFunds(Array.from(Buffer.alloc(20, 10)), mockUSDT.mint.publicKey, toTokenUnits(100), createRevertInstruction(user1.publicKey)) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdtAccount, gatewayTokenAccount: vaultUsdtAccount, bridgeToken: mockUSDT.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject when paused"); + } catch (error) { + expect(error).to.exist; + } + + await program.methods.unpause().accounts({ pauser: pauser.publicKey, config: configPda }).signers([pauser]).rpc(); + + try { + await program.methods + .sendFunds(Array.from(Buffer.alloc(20, 0)), mockUSDT.mint.publicKey, toTokenUnits(100), createRevertInstruction(user1.publicKey)) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdtAccount, gatewayTokenAccount: vaultUsdtAccount, bridgeToken: mockUSDT.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject zero recipient"); + } catch (error) { + expect(error).to.exist; + } + + try { + await program.methods + .sendFunds(Array.from(Buffer.alloc(20, 11)), mockUSDT.mint.publicKey, toTokenUnits(100), { fundRecipient: PublicKey.default, revertMsg: Buffer.from("test") }) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdtAccount, gatewayTokenAccount: vaultUsdtAccount, bridgeToken: mockUSDT.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject invalid revert recipient"); + } catch (error) { + expect(error).to.exist; + } + + try { + await program.methods + .sendFunds(Array.from(Buffer.alloc(20, 12)), mockUSDT.mint.publicKey, new anchor.BN(0), createRevertInstruction(user1.publicKey)) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdtAccount, gatewayTokenAccount: vaultUsdtAccount, bridgeToken: mockUSDT.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject zero amount"); + } catch (error) { + expect(error).to.exist; + } + + const nonWhitelistedToken = await createMockUSDC(provider.connection, admin); + await nonWhitelistedToken.createMint(); + const nonWhitelistedAccount = await nonWhitelistedToken.createTokenAccount(user1.publicKey); + await nonWhitelistedToken.mintTo(nonWhitelistedAccount, 1000); + const nonWhitelistedVaultAccount = await getAssociatedTokenAddress(nonWhitelistedToken.mint.publicKey, vaultPda, true); + + try { + await program.methods + .sendFunds(Array.from(Buffer.alloc(20, 13)), nonWhitelistedToken.mint.publicKey, toTokenUnits(100), createRevertInstruction(user1.publicKey)) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: nonWhitelistedAccount, gatewayTokenAccount: nonWhitelistedVaultAccount, bridgeToken: nonWhitelistedToken.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject non-whitelisted token"); + } catch (error) { + expect(error).to.exist; + } + + const userBalance = await mockUSDT.getBalance(user1UsdtAccount); + try { + await program.methods + .sendFunds(Array.from(Buffer.alloc(20, 14)), mockUSDT.mint.publicKey, toTokenUnits(userBalance + 1000), createRevertInstruction(user1.publicKey)) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdtAccount, gatewayTokenAccount: vaultUsdtAccount, bridgeToken: mockUSDT.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject insufficient balance"); + } catch (error) { + expect(error).to.exist; + } + }); + }); + + describe("send_tx_with_funds - Error Cases", () => { + it("Rejects zero bridge amount, zero gas amount, invalid revert recipient, and USD cap violations", async () => { + // Use valid amount within USD caps for zero bridge amount test ($2.50) + const validGasAmount = calculateSolAmount(2.5, solPrice); + + try { + await program.methods + .sendTxWithFunds(mockUSDC.mint.publicKey, new anchor.BN(0), createPayload(15), createRevertInstruction(user1.publicKey), new anchor.BN(validGasAmount), Buffer.from("sig")) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdcAccount, gatewayTokenAccount: vaultUsdcAccount, priceUpdate: mockPriceFeed, bridgeToken: mockUSDC.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject zero bridge amount"); + } catch (error) { + expect(error).to.exist; + } + + try { + await program.methods + .sendTxWithFunds(mockUSDC.mint.publicKey, toTokenUnits(500), createPayload(16), createRevertInstruction(user1.publicKey), new anchor.BN(0), Buffer.from("sig")) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdcAccount, gatewayTokenAccount: vaultUsdcAccount, priceUpdate: mockPriceFeed, bridgeToken: mockUSDC.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject zero gas amount"); + } catch (error) { + expect(error).to.exist; + } + + try { + await program.methods + .sendTxWithFunds(mockUSDC.mint.publicKey, toTokenUnits(500), createPayload(17), { fundRecipient: PublicKey.default, revertMsg: Buffer.from("test") }, new anchor.BN(validGasAmount), Buffer.from("sig")) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdcAccount, gatewayTokenAccount: vaultUsdcAccount, priceUpdate: mockPriceFeed, bridgeToken: mockUSDC.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail("Should reject invalid revert recipient"); + } catch (error) { + expect(error).to.exist; + } + }); + + it("Allows deposit within USD cap range (between min and max)", async () => { + // Calculate amount for $7.00 USD (mid-range between $1-$10, should pass) + const validGasAmount = calculateSolAmount(7.0, solPrice); + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + + await program.methods + .sendTxWithFunds(mockUSDC.mint.publicKey, toTokenUnits(500), createPayload(18), createRevertInstruction(user1.publicKey), new anchor.BN(validGasAmount), Buffer.from("sig")) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdcAccount, gatewayTokenAccount: vaultUsdcAccount, priceUpdate: mockPriceFeed, bridgeToken: mockUSDC.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + expect(finalVaultBalance).to.be.greaterThan(initialVaultBalance); + }); + + it("Rejects deposits below minimum USD cap", async () => { + const config = await program.account.config.fetch(configPda); + const minCapUsd = config.minCapUniversalTxUsd.toNumber() / 100_000_000; + // Calculate amount for $0.50 USD (below $1 min cap) + const belowMinGasAmount = calculateSolAmount(0.5, solPrice); + + try { + await program.methods + .sendTxWithFunds(mockUSDC.mint.publicKey, toTokenUnits(500), createPayload(18), createRevertInstruction(user1.publicKey), new anchor.BN(belowMinGasAmount), Buffer.from("sig")) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdcAccount, gatewayTokenAccount: vaultUsdcAccount, priceUpdate: mockPriceFeed, bridgeToken: mockUSDC.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail(`Should reject below min cap ($${minCapUsd})`); + } catch (error: any) { + expect(error).to.exist; + // Check error code structure - Anchor errors can be nested + // AnchorError structure: error.error.errorCode.code or error.errorCode.code + const errorCode = error.error?.errorCode?.code || + error.errorCode?.code || + error.code || + error.error?.code; + expect(errorCode).to.equal("BelowMinCap"); + } + }); + + it("Rejects deposits above maximum USD cap", async () => { + const config = await program.account.config.fetch(configPda); + const maxCapUsd = config.maxCapUniversalTxUsd.toNumber() / 100_000_000; + // Calculate amount for an amount above the actual max cap (use maxCapUsd + 10 to ensure it's above) + const testUsdAmount = maxCapUsd + 10; + const aboveMaxGasAmount = calculateSolAmount(testUsdAmount, solPrice); + + // Verify the amount is actually above max cap + const aboveMaxUsd = (aboveMaxGasAmount / anchor.web3.LAMPORTS_PER_SOL) * solPrice; + expect(aboveMaxUsd).to.be.greaterThan(maxCapUsd); + + try { + await program.methods + .sendTxWithFunds(mockUSDC.mint.publicKey, toTokenUnits(500), createPayload(19), createRevertInstruction(user1.publicKey), new anchor.BN(aboveMaxGasAmount), Buffer.from("sig")) + .accounts({ user: user1.publicKey, config: configPda, vault: vaultPda, tokenWhitelist: whitelistPda, userTokenAccount: user1UsdcAccount, gatewayTokenAccount: vaultUsdcAccount, priceUpdate: mockPriceFeed, bridgeToken: mockUSDC.mint.publicKey, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId }) + .signers([user1]) + .rpc(); + expect.fail(`Should reject above max cap ($${maxCapUsd})`); + } catch (error: any) { + expect(error).to.exist; + // First check error number (6005 = AboveMaxCap) - this is more reliable + if (error.error?.errorCode?.number === 6005 || error.errorCode?.number === 6005) { + // Error number confirms it's AboveMaxCap + return; + } + // Extract error code - use the exact same pattern as BelowMinCap which works + const errorCode = error.error?.errorCode?.code || + error.errorCode?.code || + error.code || + error.error?.code; + + expect(errorCode).to.equal("AboveMaxCap"); + } + }); + }); +}); \ No newline at end of file diff --git a/contracts/svm-gateway/tests/universal-tx.test.ts b/contracts/svm-gateway/tests/universal-tx.test.ts new file mode 100644 index 0000000..1795a89 --- /dev/null +++ b/contracts/svm-gateway/tests/universal-tx.test.ts @@ -0,0 +1,874 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { UniversalGateway } from "../target/types/universal_gateway"; +import { PublicKey, Keypair, SystemProgram, LAMPORTS_PER_SOL } from "@solana/web3.js"; +import { expect } from "chai"; +import * as sharedState from "./shared-state"; +import { getSolPrice, calculateSolAmount } from "./setup-pricefeed"; +import * as spl from "@solana/spl-token"; + +describe("Universal Gateway - send_universal_tx Tests", () => { + anchor.setProvider(anchor.AnchorProvider.env()); + const provider = anchor.getProvider() as anchor.AnchorProvider; + const program = anchor.workspace.UniversalGateway as Program; + + let admin: Keypair; + let tssAddress: Keypair; + let pauser: Keypair; + let user1: Keypair; + let user2: Keypair; + let configPda: PublicKey; + let vaultPda: PublicKey; + let rateLimitConfigPda: PublicKey; + let mockPriceFeed: PublicKey; + let solPrice: number; + let mockUSDT: any; + let mockUSDC: any; + + // Helper to create payload + const createPayload = (to: number, vType: any = { signedVerification: {} }) => ({ + to: Array.from(Buffer.alloc(20, to)), + value: new anchor.BN(0), + data: Buffer.from([]), + gasLimit: new anchor.BN(21000), + maxFeePerGas: new anchor.BN(20000000000), + maxPriorityFeePerGas: new anchor.BN(1000000000), + nonce: new anchor.BN(0), + deadline: new anchor.BN(Math.floor(Date.now() / 1000) + 3600), + vType, + }); + + // Helper to serialize payload to bytes (for UniversalTxRequest.payload field) + // For FUNDS_AND_PAYLOAD validation, we just need a non-empty buffer + // The actual serialization format doesn't matter for the validation check + const serializePayload = (payload: any): Buffer => { + // Create a non-empty buffer to satisfy FUNDS_AND_PAYLOAD payload requirement + // In production, this would be Anchor-serialized bytes of UniversalPayload + return Buffer.from(JSON.stringify(payload)); + }; + + // Helper to create revert instruction + const createRevertInstruction = (recipient: PublicKey, msg: string = "test") => ({ + fundRecipient: recipient, + revertMsg: Buffer.from(msg), + }); + + // Helper to get token rate limit PDA + const getTokenRateLimitPda = (tokenMint: PublicKey): PublicKey => { + const [pda] = PublicKey.findProgramAddressSync( + [Buffer.from("rate_limit"), tokenMint.toBuffer()], + program.programId + ); + return pda; + }; + + before(async () => { + admin = sharedState.getAdmin(); + tssAddress = sharedState.getTssAddress(); + pauser = sharedState.getPauser(); + user1 = Keypair.generate(); + user2 = Keypair.generate(); + + const airdropAmount = 20 * LAMPORTS_PER_SOL; + await Promise.all([ + provider.connection.requestAirdrop(user1.publicKey, airdropAmount), + provider.connection.requestAirdrop(user2.publicKey, airdropAmount), + ]); + await new Promise(resolve => setTimeout(resolve, 2000)); + + [configPda] = PublicKey.findProgramAddressSync([Buffer.from("config")], program.programId); + [vaultPda] = PublicKey.findProgramAddressSync([Buffer.from("vault")], program.programId); + [rateLimitConfigPda] = PublicKey.findProgramAddressSync([Buffer.from("rate_limit_config")], program.programId); + + mockPriceFeed = sharedState.getMockPriceFeed(); + solPrice = await getSolPrice(mockPriceFeed); + + // Get mock tokens + mockUSDT = sharedState.getMockUSDT(); + mockUSDC = sharedState.getMockUSDC(); + + // Initialize token rate limit accounts (required for universal gateway) + // Use a very large threshold to effectively disable rate limits (since admin function requires > 0) + // The rate limit will be effectively disabled because epoch_duration is 0 by default + const veryLargeThreshold = new anchor.BN("1000000000000000000000"); // 1 sextillion (effectively unlimited) + + // Initialize native SOL rate limit + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + try { + await program.account.tokenRateLimit.fetch(nativeSolTokenRateLimitPda); + } catch { + // Not initialized, create it with very large threshold (effectively disabled) + await program.methods + .setTokenRateLimit(veryLargeThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenMint: PublicKey.default, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + } + + // Initialize USDT rate limit + const usdtTokenRateLimitPda = getTokenRateLimitPda(mockUSDT.mint.publicKey); + try { + await program.account.tokenRateLimit.fetch(usdtTokenRateLimitPda); + } catch { + await program.methods + .setTokenRateLimit(veryLargeThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: usdtTokenRateLimitPda, + tokenMint: mockUSDT.mint.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + } + + // Initialize USDC rate limit + const usdcTokenRateLimitPda = getTokenRateLimitPda(mockUSDC.mint.publicKey); + try { + await program.account.tokenRateLimit.fetch(usdcTokenRateLimitPda); + } catch { + await program.methods + .setTokenRateLimit(veryLargeThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: usdcTokenRateLimitPda, + tokenMint: mockUSDC.mint.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + } + }); + + describe("GAS Route (TxType.GAS)", () => { + it("Should deposit native SOL as gas without payload", async () => { + const gasAmount = calculateSolAmount(2.5, solPrice); + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + const initialUserBalance = await provider.connection.getBalance(user1.publicKey); + + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("gas_sig"), + }; + + await program.methods + .sendUniversalTx(req, new anchor.BN(gasAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, // Dummy account for native SOL routes + gatewayTokenAccount: vaultPda, // Dummy account for native SOL routes + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + expect(finalVaultBalance - initialVaultBalance).to.equal(gasAmount); + }); + + it("Should route GAS request with payload to GAS_AND_PAYLOAD (not reject)", async () => { + // NOTE: This test verifies the correct behavior - amount==0 + payload>0 routes to GAS_AND_PAYLOAD + // The payload validation is commented out in send_tx_with_gas_route (matching EVM V0) + const gasAmount = calculateSolAmount(2.5, solPrice); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: serializePayload(createPayload(99)), // Non-empty payload + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig"), + }; + + // Should succeed and route to GAS_AND_PAYLOAD (fetchTxType logic) + await program.methods + .sendUniversalTx(req, new anchor.BN(gasAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, // Correct: use vaultPda as dummy for native SOL + gatewayTokenAccount: vaultPda, // Correct: use vaultPda as dummy for native SOL + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + // Verify transaction succeeded (vault balance increased) + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + expect(finalVaultBalance - initialVaultBalance).to.equal(gasAmount); + }); + }); + + describe("GAS_AND_PAYLOAD Route", () => { + it("Should deposit gas with payload", async () => { + const gasAmount = calculateSolAmount(2.5, solPrice); + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: serializePayload(createPayload(1)), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("gas_payload_sig"), + }; + + await program.methods + .sendUniversalTx(req, new anchor.BN(gasAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, // Dummy account for native SOL routes + gatewayTokenAccount: vaultPda, // Dummy account for native SOL routes + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + expect(finalVaultBalance - initialVaultBalance).to.equal(gasAmount); + }); + + it("Should allow payload-only execution (gas_amount == 0)", async () => { + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: serializePayload(createPayload(1)), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("payload_only"), + }; + + // Should succeed with 0 native amount + await program.methods + .sendUniversalTx(req, new anchor.BN(0)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, // Dummy account for native SOL routes + gatewayTokenAccount: vaultPda, // Dummy account for native SOL routes + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + }); + }); + + describe("FUNDS Route - Native SOL", () => { + it("Should bridge native SOL funds", async () => { + const fundsAmount = 0.5 * LAMPORTS_PER_SOL; + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), // Must be zero for FUNDS + token: PublicKey.default, + amount: new anchor.BN(fundsAmount), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("funds_sig"), + }; + + await program.methods + .sendUniversalTx(req, new anchor.BN(fundsAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, // Dummy account for native SOL routes + gatewayTokenAccount: vaultPda, // Dummy account for native SOL routes + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + expect(finalVaultBalance - initialVaultBalance).to.equal(fundsAmount); + }); + + it("Should bridge native SOL funds to explicit recipient", async () => { + const fundsAmount = 0.75 * LAMPORTS_PER_SOL; + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + const req = { + recipient: Array.from(Buffer.alloc(20, 1)), // Non-zero recipient now allowed + token: PublicKey.default, + amount: new anchor.BN(fundsAmount), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("funds_nonzero_recipient"), + }; + + await program.methods + .sendUniversalTx(req, new anchor.BN(fundsAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + expect(finalVaultBalance - initialVaultBalance).to.equal(fundsAmount); + }); + + it("Should reject FUNDS when native amount does not match bridge amount", async () => { + const fundsAmount = 0.5 * LAMPORTS_PER_SOL; + const wrongNativeAmount = fundsAmount - 1234; + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(fundsAmount), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("invalid_native_amount"), + }; + + try { + await program.methods + .sendUniversalTx(req, new anchor.BN(wrongNativeAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + expect.fail("Should reject FUNDS when native amount mismatches"); + } catch (error: any) { + expect(error).to.exist; + const errorCode = error.error?.errorCode?.code || error.error?.errorCode || error.code; + expect(errorCode).to.equal("InvalidAmount"); + } + }); + }); + + describe("FUNDS Route - SPL Token", () => { + it("Should bridge SPL token funds", async () => { + // Create user token account and mint tokens using mock token's methods + const userTokenAccount = await mockUSDT.createTokenAccount(user1.publicKey); + const gatewayTokenAccount = await mockUSDT.createTokenAccount(vaultPda, true); + + // Mint tokens using mock token's mintTo method (uses correct mint authority) + await mockUSDT.mintTo(userTokenAccount, 1000); + const tokenAmount = new anchor.BN(1000 * 10 ** mockUSDT.config.decimals); + + const initialGatewayBalance = await mockUSDT.getBalance(gatewayTokenAccount); + const usdtTokenRateLimitPda = getTokenRateLimitPda(mockUSDT.mint.publicKey); + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: mockUSDT.mint.publicKey, + amount: tokenAmount, + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("spl_funds_sig"), + }; + + await program.methods + .sendUniversalTx(req, new anchor.BN(0)) // No native SOL for SPL funds + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: userTokenAccount, + gatewayTokenAccount: gatewayTokenAccount, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: usdtTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + const finalGatewayBalance = await mockUSDT.getBalance(gatewayTokenAccount); + const balanceIncrease = (finalGatewayBalance - initialGatewayBalance) * 10 ** mockUSDT.config.decimals; + expect(balanceIncrease).to.equal(tokenAmount.toNumber()); + }); + + it("Should reject FUNDS SPL when native SOL is provided", async () => { + const userTokenAccount = await mockUSDT.createTokenAccount(user1.publicKey); + const gatewayTokenAccount = await mockUSDT.createTokenAccount(vaultPda, true); + + await mockUSDT.mintTo(userTokenAccount, 1000); + const tokenAmount = new anchor.BN(1000 * 10 ** mockUSDT.config.decimals); + const usdtTokenRateLimitPda = getTokenRateLimitPda(mockUSDT.mint.publicKey); + const nativeAmount = calculateSolAmount(1.5, solPrice); + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: mockUSDT.mint.publicKey, + amount: tokenAmount, + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("spl_native_invalid"), + }; + + try { + await program.methods + .sendUniversalTx(req, new anchor.BN(nativeAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: userTokenAccount, + gatewayTokenAccount: gatewayTokenAccount, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: usdtTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + expect.fail("Should reject FUNDS SPL when native SOL is attached"); + } catch (error: any) { + expect(error).to.exist; + const errorCode = error.error?.errorCode?.code || error.error?.errorCode || error.code; + expect(errorCode).to.equal("InvalidAmount"); + } + }); + }); + + describe("FUNDS_AND_PAYLOAD Route - Native SOL with Batching", () => { + it("Should batch gas + funds for native SOL", async () => { + // Case 2.2: Batching with native SOL + // Split: gasAmount = native_amount - req.amount + // Gas must be >= $1 USD (min cap), funds can be any amount + // Strategy: Use larger gas amount ($3) and small funds amount to ensure gas >= $1 after split + const gasAmountLamports = calculateSolAmount(3.0, solPrice); // $3.00 for gas (well above $1 min cap) + const fundsAmountLamports = calculateSolAmount(0.1, solPrice); // $0.10 for funds (very small) + const totalAmount = gasAmountLamports + fundsAmountLamports; // Total = gas + funds + + // Verify: after split, gas_amount = totalAmount - fundsAmount = gasAmountLamports (should be >= $1) + const expectedGasAfterSplit = totalAmount - fundsAmountLamports; + const expectedGasUsd = (expectedGasAfterSplit / LAMPORTS_PER_SOL) * solPrice; + if (expectedGasUsd < 1.0) { + throw new Error(`Expected gas after split ${expectedGasUsd.toFixed(4)} USD is below minimum $1 USD cap. Gas: ${gasAmountLamports}, Funds: ${fundsAmountLamports}, Total: ${totalAmount}`); + } + + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + const req = { + recipient: Array.from(Buffer.alloc(20, 1)), // Non-zero allowed for FUNDS_AND_PAYLOAD + token: PublicKey.default, + amount: new anchor.BN(fundsAmountLamports), + payload: serializePayload(createPayload(1)), // Non-empty payload required + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("batched_sig"), + }; + + await program.methods + .sendUniversalTx(req, new anchor.BN(totalAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, // Dummy account for native SOL routes + gatewayTokenAccount: vaultPda, // Dummy account for native SOL routes + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + // Should receive both gas and funds + expect(finalVaultBalance - initialVaultBalance).to.equal(totalAmount); + }); + + it("Should reject FUNDS_AND_PAYLOAD native when native amount is insufficient", async () => { + const fundsAmount = calculateSolAmount(0.75, solPrice); + const insufficientNative = fundsAmount - 50_000; // native < funds + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + const req = { + recipient: Array.from(Buffer.alloc(20, 1)), + token: PublicKey.default, + amount: new anchor.BN(fundsAmount), + payload: serializePayload(createPayload(1)), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("insufficient_native_sig"), + }; + + try { + await program.methods + .sendUniversalTx(req, new anchor.BN(insufficientNative)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + expect.fail("Should reject when native gas is below bridge amount"); + } catch (error: any) { + expect(error).to.exist; + const errorCode = error.error?.errorCode?.code || error.error?.errorCode || error.code; + expect(errorCode).to.equal("InvalidAmount"); + } + }); + }); + + describe("FUNDS_AND_PAYLOAD Route - SPL Token", () => { + it("Should bridge SPL funds with payload without batching (Case 2.1)", async () => { + // Case 2.1: No Batching (native_amount == 0): user already has UEA with gas on Push Chain + // User can directly move req.amount for req.token to Push Chain (SPL token only for Case 2.1) + // Use USDC to avoid rate limit conflicts with USDT used in previous tests + const userTokenAccount = await mockUSDC.createTokenAccount(user1.publicKey); + const gatewayTokenAccount = await mockUSDC.createTokenAccount(vaultPda, true); + + // Mint tokens using mock token's mintTo method + await mockUSDC.mintTo(userTokenAccount, 500); + const tokenAmount = new anchor.BN(500 * 10 ** mockUSDC.config.decimals); + + const initialGatewayBalance = await mockUSDC.getBalance(gatewayTokenAccount); + const usdcTokenRateLimitPda = getTokenRateLimitPda(mockUSDC.mint.publicKey); + + const req = { + recipient: Array.from(Buffer.alloc(20, 1)), // Non-zero allowed for FUNDS_AND_PAYLOAD + token: mockUSDC.mint.publicKey, + amount: tokenAmount, + payload: serializePayload(createPayload(1)), // Must have payload for FUNDS_AND_PAYLOAD + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("spl_no_batch_sig"), + }; + + // native_amount == 0: user already has UEA with gas + await program.methods + .sendUniversalTx(req, new anchor.BN(0)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: userTokenAccount, + gatewayTokenAccount: gatewayTokenAccount, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: usdcTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + // Should only receive SPL tokens, no native SOL + const finalGatewayBalance = await mockUSDC.getBalance(gatewayTokenAccount); + const balanceIncrease = (finalGatewayBalance - initialGatewayBalance) * 10 ** mockUSDC.config.decimals; + expect(balanceIncrease).to.equal(tokenAmount.toNumber()); + }); + + it("Should batch native gas + SPL funds (Case 2.3)", async () => { + // Setup SPL token accounts using mock token's methods + const userTokenAccount = await mockUSDC.createTokenAccount(user1.publicKey); + const gatewayTokenAccount = await mockUSDC.createTokenAccount(vaultPda, true); + + // Mint tokens using mock token's mintTo method + await mockUSDC.mintTo(userTokenAccount, 500); + const tokenAmount = new anchor.BN(500 * 10 ** mockUSDC.config.decimals); + + // Case 2.3: Batching with SPL + native gas + // Gas amount must be >= $1 USD (min cap) and <= $10 USD (max cap) + // native_amount is sent as gas, req.amount is SPL bridge amount + const gasAmount = calculateSolAmount(2.5, solPrice); // $2.50 for gas (within $1-$10 cap) + + // Verify gas amount is >= $1 USD + const gasUsd = (gasAmount / LAMPORTS_PER_SOL) * solPrice; + if (gasUsd < 1.0) { + throw new Error(`Gas amount ${gasUsd} USD is below minimum $1 USD cap`); + } + const initialVaultBalance = await provider.connection.getBalance(vaultPda); + const initialGatewayBalance = await mockUSDC.getBalance(gatewayTokenAccount); + const usdcTokenRateLimitPda = getTokenRateLimitPda(mockUSDC.mint.publicKey); + + const req = { + recipient: Array.from(Buffer.alloc(20, 1)), + token: mockUSDC.mint.publicKey, + amount: tokenAmount, + payload: serializePayload(createPayload(1)), // Non-empty payload required + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("spl_batched_sig"), + }; + + await program.methods + .sendUniversalTx(req, new anchor.BN(gasAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: userTokenAccount, + gatewayTokenAccount: gatewayTokenAccount, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: usdcTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + const finalVaultBalance = await provider.connection.getBalance(vaultPda); + expect(finalVaultBalance - initialVaultBalance).to.equal(gasAmount); + + const finalGatewayBalance = await mockUSDC.getBalance(gatewayTokenAccount); + const balanceIncrease = (finalGatewayBalance - initialGatewayBalance) * 10 ** mockUSDC.config.decimals; + expect(balanceIncrease).to.equal(tokenAmount.toNumber()); + }); + + it("Should reject FUNDS_AND_PAYLOAD SPL when token rate limit PDA mismatches", async () => { + const userTokenAccount = await mockUSDC.createTokenAccount(user1.publicKey); + const gatewayTokenAccount = await mockUSDC.createTokenAccount(vaultPda, true); + + await mockUSDC.mintTo(userTokenAccount, 500); + const tokenAmount = new anchor.BN(500 * 10 ** mockUSDC.config.decimals); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); // intentionally wrong + + const req = { + recipient: Array.from(Buffer.alloc(20, 1)), + token: mockUSDC.mint.publicKey, + amount: tokenAmount, + payload: serializePayload(createPayload(1)), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("spl_bad_rate_limit"), + }; + + try { + await program.methods + .sendUniversalTx(req, new anchor.BN(0)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: userTokenAccount, + gatewayTokenAccount: gatewayTokenAccount, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + expect.fail("Should reject FUNDS_AND_PAYLOAD SPL when token rate limit PDA is invalid"); + } catch (error: any) { + expect(error).to.exist; + const errorCode = error.error?.errorCode?.code || error.error?.errorCode || error.code; + expect(errorCode).to.equal("InvalidToken"); + } + }); + }); + + describe("Error Cases", () => { + it("Should reject when paused", async () => { + await program.methods + .pause() + .accounts({ pauser: pauser.publicKey, config: configPda }) + .signers([pauser]) + .rpc(); + + const gasAmount = calculateSolAmount(2.5, solPrice); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig"), + }; + + try { + await program.methods + .sendUniversalTx(req, new anchor.BN(gasAmount)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, // Dummy account for native SOL routes + gatewayTokenAccount: vaultPda, // Dummy account for native SOL routes + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + expect.fail("Should reject when paused"); + } catch (error: any) { + expect(error).to.exist; + expect(error.error?.errorCode?.code || error.code).to.equal("Paused"); + } + + await program.methods + .unpause() + .accounts({ pauser: pauser.publicKey, config: configPda }) + .signers([pauser]) + .rpc(); + }); + + it("Should reject invalid parameter combinations (no gas or funds)", async () => { + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + + const req = { + recipient: Array.from(Buffer.alloc(20, 0)), + token: PublicKey.default, + amount: new anchor.BN(0), + payload: Buffer.from([]), + revertInstruction: createRevertInstruction(user1.publicKey), + signatureData: Buffer.from("sig"), + }; + + try { + await program.methods + .sendUniversalTx(req, new anchor.BN(0)) + .accounts({ + config: configPda, + vault: vaultPda, + userTokenAccount: vaultPda, + gatewayTokenAccount: vaultPda, + user: user1.publicKey, + priceUpdate: mockPriceFeed, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenProgram: spl.TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + expect.fail("Should reject parameter set without gas or funds"); + } catch (error: any) { + expect(error).to.exist; + const errorCode = error.error?.errorCode?.code || error.error?.errorCode || error.code; + expect(errorCode).to.equal("InvalidInput"); + } + }); + }); + + after(async () => { + // Ensure contract is unpaused after all tests + try { + const config = await program.account.config.fetch(configPda); + if (config.paused) { + await program.methods + .unpause() + .accounts({ pauser: pauser.publicKey, config: configPda }) + .signers([pauser]) + .rpc(); + } + } catch (error) { + // Ignore errors + } + + // Disable rate limits to prevent interference with other tests + try { + // Disable epoch duration + await program.methods + .updateEpochDuration(new anchor.BN(0)) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + + // Set very large thresholds to effectively disable + const veryLargeThreshold = new anchor.BN("1000000000000000000000"); + const nativeSolTokenRateLimitPda = getTokenRateLimitPda(PublicKey.default); + await program.methods + .setTokenRateLimit(veryLargeThreshold) + .accounts({ + admin: admin.publicKey, + config: configPda, + rateLimitConfig: rateLimitConfigPda, + tokenRateLimit: nativeSolTokenRateLimitPda, + tokenMint: PublicKey.default, + systemProgram: SystemProgram.programId, + }) + .signers([admin]) + .rpc(); + } catch (error) { + // Ignore errors + } + }); +}); \ No newline at end of file diff --git a/contracts/svm-gateway/tests/withdraw.test.ts b/contracts/svm-gateway/tests/withdraw.test.ts new file mode 100644 index 0000000..889701e --- /dev/null +++ b/contracts/svm-gateway/tests/withdraw.test.ts @@ -0,0 +1,550 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { UniversalGateway } from "../target/types/universal_gateway"; +import { PublicKey, Keypair, SystemProgram } from "@solana/web3.js"; +import { expect } from "chai"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import * as sharedState from "./shared-state"; +import { signTssMessage, TssInstruction } from "./helpers/tss"; + +const USDT_DECIMALS = 6; +const TOKEN_MULTIPLIER = BigInt(10 ** USDT_DECIMALS); + +const asLamports = (sol: number) => new anchor.BN(sol * anchor.web3.LAMPORTS_PER_SOL); +const asTokenAmount = (tokens: number) => new anchor.BN(Number(BigInt(tokens) * TOKEN_MULTIPLIER)); + +const toBytes = (pubkey: PublicKey) => pubkey.toBuffer(); + +describe("Universal Gateway - Withdraw Tests", () => { + anchor.setProvider(anchor.AnchorProvider.env()); + const provider = anchor.getProvider() as anchor.AnchorProvider; + const program = anchor.workspace.UniversalGateway as Program; + + let admin: Keypair; + let pauser: Keypair; + let recipient: Keypair; + let user1: Keypair; + + let configPda: PublicKey; + let vaultPda: PublicKey; + let tssPda: PublicKey; + let whitelistPda: PublicKey; + + let mockUSDT: any; + + let user1UsdtAccount: PublicKey; + let vaultUsdtAccount: PublicKey; + let recipientUsdtAccount: PublicKey; + + let currentNonce = 0; + + const syncNonceFromChain = async () => { + const account = await program.account.tssPda.fetch(tssPda); + currentNonce = Number(account.nonce); + }; + + const signTssMessageWithChainId = async (params: { + instruction: TssInstruction; + nonce: number; + amount?: bigint; + additional: Uint8Array[]; + }) => { + const tssAccount = await program.account.tssPda.fetch(tssPda); + return signTssMessage({ ...params, chainId: tssAccount.chainId.toNumber() }); + }; + + const setNonceOnChain = async (value: number) => { + await program.methods + .resetNonce(new anchor.BN(value)) + .accounts({ + tssPda, + authority: admin.publicKey, + }) + .signers([admin]) + .rpc(); + currentNonce = value; + }; + + const expectRejection = async (promise: Promise, message: string) => { + let rejected = false; + try { + await promise; + } catch (error: any) { + rejected = true; + expect(`${error}`).to.include(message); + } + expect(rejected).to.be.true; + }; + + before(async () => { + admin = sharedState.getAdmin(); + pauser = sharedState.getPauser(); + mockUSDT = sharedState.getMockUSDT(); + + recipient = Keypair.generate(); + user1 = Keypair.generate(); + + const airdropLamports = 10 * anchor.web3.LAMPORTS_PER_SOL; + await Promise.all([ + provider.connection.requestAirdrop(recipient.publicKey, airdropLamports), + provider.connection.requestAirdrop(user1.publicKey, airdropLamports), + ]); + await new Promise(resolve => setTimeout(resolve, 2000)); + + [configPda] = PublicKey.findProgramAddressSync([Buffer.from("config")], program.programId); + [vaultPda] = PublicKey.findProgramAddressSync([Buffer.from("vault")], program.programId); + [tssPda] = PublicKey.findProgramAddressSync([Buffer.from("tss")], program.programId); + [whitelistPda] = PublicKey.findProgramAddressSync([Buffer.from("whitelist")], program.programId); + + user1UsdtAccount = await mockUSDT.createTokenAccount(user1.publicKey); + await mockUSDT.mintTo(user1UsdtAccount, 10_000); + + vaultUsdtAccount = await mockUSDT.createTokenAccount(vaultPda, true); + recipientUsdtAccount = await mockUSDT.createTokenAccount(recipient.publicKey); + + const whitelist = await program.account.tokenWhitelist.fetch(whitelistPda); + const tokens = whitelist.tokens.map((token: PublicKey) => token.toString()); + expect(tokens).to.include(mockUSDT.mint.publicKey.toString()); + + const depositLamports = 5 * anchor.web3.LAMPORTS_PER_SOL; + const transferIx = anchor.web3.SystemProgram.transfer({ + fromPubkey: user1.publicKey, + toPubkey: vaultPda, + lamports: depositLamports, + }); + await provider.sendAndConfirm(new anchor.web3.Transaction().add(transferIx), [user1]); + + await program.methods + .sendFunds( + Array.from(Buffer.alloc(20, 1)), + mockUSDT.mint.publicKey, + asTokenAmount(5_000), + { fundRecipient: user1.publicKey, revertMsg: Buffer.from("seed vault") } + ) + .accounts({ + user: user1.publicKey, + config: configPda, + vault: vaultPda, + tokenWhitelist: whitelistPda, + userTokenAccount: user1UsdtAccount, + gatewayTokenAccount: vaultUsdtAccount, + bridgeToken: mockUSDT.mint.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .signers([user1]) + .rpc(); + + await syncNonceFromChain(); + }); + + describe("withdraw_tss", () => { + it("transfers SOL with a valid signature", async () => { + const withdrawLamports = 2 * anchor.web3.LAMPORTS_PER_SOL; + await setNonceOnChain(currentNonce); + + const signature = await signTssMessageWithChainId({ + instruction: TssInstruction.WithdrawSol, + nonce: currentNonce, + amount: BigInt(withdrawLamports), + additional: [toBytes(recipient.publicKey)], + }); + + const initialVault = await provider.connection.getBalance(vaultPda); + const initialRecipient = await provider.connection.getBalance(recipient.publicKey); + + await program.methods + .withdrawTss( + new anchor.BN(withdrawLamports), + signature.signature, + signature.recoveryId, + signature.messageHash, + signature.nonce + ) + .accounts({ + config: configPda, + vault: vaultPda, + tssPda, + recipient: recipient.publicKey, + systemProgram: SystemProgram.programId, + }) + .rpc(); + + const finalVault = await provider.connection.getBalance(vaultPda); + const finalRecipient = await provider.connection.getBalance(recipient.publicKey); + + expect(finalVault).to.equal(initialVault - withdrawLamports); + expect(finalRecipient).to.equal(initialRecipient + withdrawLamports); + + await syncNonceFromChain(); + }); + + it("rejects tampered signatures", async () => { + const withdrawLamports = anchor.web3.LAMPORTS_PER_SOL; + await setNonceOnChain(currentNonce); + + const valid = await signTssMessageWithChainId({ + instruction: TssInstruction.WithdrawSol, + nonce: currentNonce, + amount: BigInt(withdrawLamports), + additional: [toBytes(recipient.publicKey)], + }); + + const corrupted = [...valid.signature]; + corrupted[0] ^= 0xff; + + await expectRejection( + program.methods + .withdrawTss( + new anchor.BN(withdrawLamports), + corrupted, + valid.recoveryId, + valid.messageHash, + valid.nonce + ) + .accounts({ + config: configPda, + vault: vaultPda, + tssPda, + recipient: recipient.publicKey, + systemProgram: SystemProgram.programId, + }) + .rpc(), + "TssAuthFailed" + ); + + await syncNonceFromChain(); + }); + + it("rejects withdrawals while paused", async () => { + await program.methods + .pause() + .accounts({ pauser: pauser.publicKey, config: configPda }) + .signers([pauser]) + .rpc(); + + const withdrawLamports = anchor.web3.LAMPORTS_PER_SOL; + await setNonceOnChain(currentNonce); + + const signature = await signTssMessageWithChainId({ + instruction: TssInstruction.WithdrawSol, + nonce: currentNonce, + amount: BigInt(withdrawLamports), + additional: [toBytes(recipient.publicKey)], + }); + + await expectRejection( + program.methods + .withdrawTss( + new anchor.BN(withdrawLamports), + signature.signature, + signature.recoveryId, + signature.messageHash, + signature.nonce + ) + .accounts({ + config: configPda, + vault: vaultPda, + tssPda, + recipient: recipient.publicKey, + systemProgram: SystemProgram.programId, + }) + .rpc(), + "PausedError" + ); + + await program.methods + .unpause() + .accounts({ pauser: pauser.publicKey, config: configPda }) + .signers([pauser]) + .rpc(); + + await syncNonceFromChain(); + }); + + it("rejects withdrawals that exceed the vault balance", async () => { + const vaultLamports = await provider.connection.getBalance(vaultPda); + const excessive = vaultLamports + anchor.web3.LAMPORTS_PER_SOL; + await setNonceOnChain(currentNonce); + + const signature = await signTssMessageWithChainId({ + instruction: TssInstruction.WithdrawSol, + nonce: currentNonce, + amount: BigInt(excessive), + additional: [toBytes(recipient.publicKey)], + }); + + await expectRejection( + program.methods + .withdrawTss( + new anchor.BN(excessive), + signature.signature, + signature.recoveryId, + signature.messageHash, + signature.nonce + ) + .accounts({ + config: configPda, + vault: vaultPda, + tssPda, + recipient: recipient.publicKey, + systemProgram: SystemProgram.programId, + }) + .rpc(), + "custom program error" + ); + + await syncNonceFromChain(); + }); + }); + + describe("withdrawSplTokenTss", () => { + it("transfers SPL tokens with a valid signature", async () => { + const withdrawTokens = 1_000; + const withdrawRaw = BigInt(withdrawTokens) * TOKEN_MULTIPLIER; + await setNonceOnChain(currentNonce); + + // Include both mint AND recipient in message hash (ZetaChain pattern - security fix) + const signature = await signTssMessageWithChainId({ + instruction: TssInstruction.WithdrawSpl, + nonce: currentNonce, + amount: withdrawRaw, + additional: [toBytes(mockUSDT.mint.publicKey), toBytes(recipientUsdtAccount)], + }); + + const initialVault = await mockUSDT.getBalance(vaultUsdtAccount); + const initialRecipient = await mockUSDT.getBalance(recipientUsdtAccount); + + await program.methods + .withdrawSplTokenTss( + new anchor.BN(Number(withdrawRaw)), + signature.signature, + signature.recoveryId, + signature.messageHash, + signature.nonce + ) + .accounts({ + config: configPda, + whitelist: whitelistPda, + vault: vaultPda, + tokenVault: vaultUsdtAccount, + tssPda, + recipientTokenAccount: recipientUsdtAccount, + tokenMint: mockUSDT.mint.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .rpc(); + + const finalVault = await mockUSDT.getBalance(vaultUsdtAccount); + const finalRecipient = await mockUSDT.getBalance(recipientUsdtAccount); + + expect(finalVault).to.equal(initialVault - withdrawTokens); + expect(finalRecipient).to.equal(initialRecipient + withdrawTokens); + + await syncNonceFromChain(); + }); + + it("rejects SPL withdrawals with a tampered signature", async () => { + const withdrawTokens = 200; + const withdrawRaw = BigInt(withdrawTokens) * TOKEN_MULTIPLIER; + await setNonceOnChain(currentNonce); + + // Include both mint AND recipient in message hash (ZetaChain pattern - security fix) + const signature = await signTssMessageWithChainId({ + instruction: TssInstruction.WithdrawSpl, + nonce: currentNonce, + amount: withdrawRaw, + additional: [toBytes(mockUSDT.mint.publicKey), toBytes(recipientUsdtAccount)], + }); + + const corrupted = [...signature.signature]; + corrupted[0] ^= 0xff; + + await expectRejection( + program.methods + .withdrawSplTokenTss( + new anchor.BN(Number(withdrawRaw)), + corrupted, + signature.recoveryId, + signature.messageHash, + signature.nonce + ) + .accounts({ + config: configPda, + whitelist: whitelistPda, + vault: vaultPda, + tokenVault: vaultUsdtAccount, + tssPda, + recipientTokenAccount: recipientUsdtAccount, + tokenMint: mockUSDT.mint.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .rpc(), + "TssAuthFailed" + ); + + await syncNonceFromChain(); + }); + }); + + describe("revert withdrawals", () => { + it("reverts a SOL withdrawal with a valid signature", async () => { + const revertAmount = anchor.web3.LAMPORTS_PER_SOL; + await setNonceOnChain(currentNonce); + + const revertInstruction = { + fundRecipient: recipient.publicKey, + revertMsg: Buffer.from("revert SOL"), + }; + + const signature = await signTssMessageWithChainId({ + instruction: TssInstruction.RevertWithdrawSol, + nonce: currentNonce, + amount: BigInt(revertAmount), + additional: [toBytes(recipient.publicKey)], + }); + + const initialRecipient = await provider.connection.getBalance(recipient.publicKey); + + await program.methods + .revertWithdraw( + new anchor.BN(revertAmount), + revertInstruction, + signature.signature, + signature.recoveryId, + signature.messageHash, + signature.nonce + ) + .accounts({ + config: configPda, + vault: vaultPda, + tssPda, + recipient: recipient.publicKey, + systemProgram: SystemProgram.programId, + }) + .rpc(); + + const finalRecipient = await provider.connection.getBalance(recipient.publicKey); + expect(finalRecipient).to.equal(initialRecipient + revertAmount); + + await syncNonceFromChain(); + }); + + it("reverts an SPL withdrawal with a valid signature", async () => { + const revertTokens = 500; + const revertRaw = BigInt(revertTokens) * TOKEN_MULTIPLIER; + await setNonceOnChain(currentNonce); + + const revertInstruction = { + fundRecipient: recipient.publicKey, + revertMsg: Buffer.from("revert SPL"), + }; + + // Create recipient account first (needed for message hash) + const recipientRevertAccount = await mockUSDT.createTokenAccount(recipient.publicKey); + + // Include both mint AND fund_recipient in message hash (ZetaChain pattern - security fix) + const signature = await signTssMessageWithChainId({ + instruction: TssInstruction.RevertWithdrawSpl, + nonce: currentNonce, + amount: revertRaw, + additional: [toBytes(mockUSDT.mint.publicKey), toBytes(revertInstruction.fundRecipient)], + }); + const initialRecipientBalance = await mockUSDT.getBalance(recipientRevertAccount); + + await program.methods + .revertWithdrawSplToken( + new anchor.BN(Number(revertRaw)), + revertInstruction, + signature.signature, + signature.recoveryId, + signature.messageHash, + signature.nonce + ) + .accounts({ + config: configPda, + whitelist: whitelistPda, + vault: vaultPda, + tokenVault: vaultUsdtAccount, + tssPda, + recipientTokenAccount: recipientRevertAccount, + tokenMint: mockUSDT.mint.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .rpc(); + + const finalRecipientBalance = await mockUSDT.getBalance(recipientRevertAccount); + expect(finalRecipientBalance).to.equal(initialRecipientBalance + revertTokens); + + await syncNonceFromChain(); + }); + }); + + describe("error conditions", () => { + it("rejects zero-amount withdrawals", async () => { + await setNonceOnChain(currentNonce); + + const signature = await signTssMessageWithChainId({ + instruction: TssInstruction.WithdrawSol, + nonce: currentNonce, + amount: BigInt(0), + additional: [toBytes(recipient.publicKey)], + }); + + await expectRejection( + program.methods + .withdrawTss( + new anchor.BN(0), + signature.signature, + signature.recoveryId, + signature.messageHash, + signature.nonce + ) + .accounts({ + config: configPda, + vault: vaultPda, + tssPda, + recipient: recipient.publicKey, + systemProgram: SystemProgram.programId, + }) + .rpc(), + "InvalidAmount" + ); + + await syncNonceFromChain(); + }); + + it("rejects withdrawals with incorrect nonce", async () => { + await setNonceOnChain(currentNonce); + + const signature = await signTssMessageWithChainId({ + instruction: TssInstruction.WithdrawSol, + nonce: currentNonce, + amount: BigInt(anchor.web3.LAMPORTS_PER_SOL), + additional: [toBytes(recipient.publicKey)], + }); + + await expectRejection( + program.methods + .withdrawTss( + new anchor.BN(anchor.web3.LAMPORTS_PER_SOL), + signature.signature, + signature.recoveryId, + signature.messageHash, + new anchor.BN(currentNonce + 5) + ) + .accounts({ + config: configPda, + vault: vaultPda, + tssPda, + recipient: recipient.publicKey, + systemProgram: SystemProgram.programId, + }) + .rpc(), + "NonceMismatch" + ); + + await syncNonceFromChain(); + }); + }); +}); diff --git a/contracts/svm-gateway/yarn.lock b/contracts/svm-gateway/yarn.lock new file mode 100644 index 0000000..c1e2083 --- /dev/null +++ b/contracts/svm-gateway/yarn.lock @@ -0,0 +1,2045 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.25.0": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== + +"@coral-xyz/anchor-errors@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz#bdfd3a353131345244546876eb4afc0e125bec30" + integrity sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ== + +"@coral-xyz/anchor-errors@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.31.1.tgz#d635cbac2533973ae6bfb5d3ba1de89ce5aece2d" + integrity sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ== + +"@coral-xyz/anchor@^0.29.0": + version "0.29.0" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.29.0.tgz#bd0be95bedfb30a381c3e676e5926124c310ff12" + integrity sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA== + dependencies: + "@coral-xyz/borsh" "^0.29.0" + "@noble/hashes" "^1.3.1" + "@solana/web3.js" "^1.68.0" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^6.3.0" + cross-fetch "^3.1.5" + crypto-hash "^1.3.0" + eventemitter3 "^4.0.7" + pako "^2.0.3" + snake-case "^3.0.4" + superstruct "^0.15.4" + toml "^3.0.0" + +"@coral-xyz/anchor@^0.30.0": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.30.1.tgz#17f3e9134c28cd0ea83574c6bab4e410bcecec5d" + integrity sha512-gDXFoF5oHgpriXAaLpxyWBHdCs8Awgf/gLHIo6crv7Aqm937CNdY+x+6hoj7QR5vaJV7MxWSQ0NGFzL3kPbWEQ== + dependencies: + "@coral-xyz/anchor-errors" "^0.30.1" + "@coral-xyz/borsh" "^0.30.1" + "@noble/hashes" "^1.3.1" + "@solana/web3.js" "^1.68.0" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^6.3.0" + cross-fetch "^3.1.5" + crypto-hash "^1.3.0" + eventemitter3 "^4.0.7" + pako "^2.0.3" + snake-case "^3.0.4" + superstruct "^0.15.4" + toml "^3.0.0" + +"@coral-xyz/anchor@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.31.1.tgz#0fdeebf45a3cb2e47e8ebbb815ca98542152962c" + integrity sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA== + dependencies: + "@coral-xyz/anchor-errors" "^0.31.1" + "@coral-xyz/borsh" "^0.31.1" + "@noble/hashes" "^1.3.1" + "@solana/web3.js" "^1.69.0" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^6.3.0" + cross-fetch "^3.1.5" + eventemitter3 "^4.0.7" + pako "^2.0.3" + superstruct "^0.15.4" + toml "^3.0.0" + +"@coral-xyz/borsh@^0.29.0": + version "0.29.0" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.29.0.tgz#79f7045df2ef66da8006d47f5399c7190363e71f" + integrity sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + +"@coral-xyz/borsh@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.30.1.tgz#869d8833abe65685c72e9199b8688477a4f6b0e3" + integrity sha512-aaxswpPrCFKl8vZTbxLssA2RvwX2zmKLlRCIktJOwW+VpVwYtXRtlWiIP+c2pPRKneiTiWCN2GEMSH9j1zTlWQ== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + +"@coral-xyz/borsh@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.31.1.tgz#5328e1e0921b75d7f4a62dd3f61885a938bc7241" + integrity sha512-9N8AU9F0ubriKfNE3g1WF0/4dtlGXoBN/hd1PvbNBamBNwRgHxH4P+o3Zt7rSEloW1HUs6LfZEchlx9fW7POYw== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + +"@grpc/grpc-js@^1.8.13": + version "1.14.0" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.0.tgz#a3c47e7816ca2b4d5490cba9e06a3cf324e675ad" + integrity sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg== + dependencies: + "@grpc/proto-loader" "^0.8.0" + "@js-sdsl/ordered-map" "^4.4.2" + +"@grpc/proto-loader@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz#b6c324dd909c458a0e4aa9bfd3d69cf78a4b9bd8" + integrity sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.5.3" + yargs "^17.7.2" + +"@js-sdsl/ordered-map@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" + integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== + +"@jup-ag/api@^6.0.41": + version "6.0.45" + resolved "https://registry.yarnpkg.com/@jup-ag/api/-/api-6.0.45.tgz#48a9ea17d3ab25a65453cf6bd15346db6b4395cc" + integrity sha512-HUtXkanAuo8EIjiem+GYdPlh1aH4o669pMhhr/szEtDRW/f9HMeQ9uP8Eo8sYWbaWkVCpfIpun1Fk/qeU1Qgcw== + +"@metaplex-foundation/beet-solana@^0.4.0": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet-solana/-/beet-solana-0.4.1.tgz#255747aa7feee1c20202146a752c057feca1948f" + integrity sha512-/6o32FNUtwK8tjhotrvU/vorP7umBuRFvBZrC6XCk51aKidBHe5LPVPA5AjGPbV3oftMfRuXPNd9yAGeEqeCDQ== + dependencies: + "@metaplex-foundation/beet" ">=0.1.0" + "@solana/web3.js" "^1.56.2" + bs58 "^5.0.0" + debug "^4.3.4" + +"@metaplex-foundation/beet@>=0.1.0", "@metaplex-foundation/beet@^0.7.1": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet/-/beet-0.7.2.tgz#fa4726e4cfd4fb6fed6cddc9b5213c1c2a2d0b77" + integrity sha512-K+g3WhyFxKPc0xIvcIjNyV1eaTVJTiuaHZpig7Xx0MuYRMoJLLvhLTnUXhFdR5Tu2l2QSyKwfyXDgZlzhULqFg== + dependencies: + ansicolors "^0.3.2" + assert "^2.1.0" + bn.js "^5.2.0" + debug "^4.3.3" + +"@metaplex-foundation/cusper@^0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/cusper/-/cusper-0.0.2.tgz#dc2032a452d6c269e25f016aa4dd63600e2af975" + integrity sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA== + +"@metaplex-foundation/mpl-token-metadata@^2.1.4": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-2.13.0.tgz#ea498190ad4ed1d4c0b8218a72d03bd17a883d11" + integrity sha512-Fl/8I0L9rv4bKTV/RAl5YIbJe9SnQPInKvLz+xR1fEc4/VQkuCn3RPgypfUMEKWmCznzaw4sApDxy6CFS4qmJw== + dependencies: + "@metaplex-foundation/beet" "^0.7.1" + "@metaplex-foundation/beet-solana" "^0.4.0" + "@metaplex-foundation/cusper" "^0.0.2" + "@solana/spl-token" "^0.3.6" + "@solana/web3.js" "^1.66.2" + bn.js "^5.2.0" + debug "^4.3.4" + +"@noble/curves@^1.0.0", "@noble/curves@^1.4.2": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951" + integrity sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw== + dependencies: + "@noble/hashes" "1.8.0" + +"@noble/ed25519@^1.7.1": + version "1.7.5" + resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.5.tgz#94df8bdb9fec9c4644a56007eecb57b0e9fbd0d7" + integrity sha512-xuS0nwRMQBvSxDa7UxMb61xTiH3MxTgUfhyPUALVIe0FlOAz4sjELwyDRyUvqeEYfRSG9qNjFIycqLZppg4RSA== + +"@noble/hashes@1.8.0", "@noble/hashes@^1.3.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + +"@noble/secp256k1@^1.7.1": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.2.tgz#c2c3343e2dce80e15a914d7442147507f8a98e7f" + integrity sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ== + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + +"@pythnetwork/hermes-client@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@pythnetwork/hermes-client/-/hermes-client-2.0.0.tgz#c8ea4b2a790d617cc24c4c573125a33da9d1e65f" + integrity sha512-8ZbCrO5NSlsu1zauIJjZv0sPR3qF9uzgCpBpAPSBGBjwKP0T3TdIRfuSzf9mpzrqf+b7QUqNVNLWZqgN7nlREw== + dependencies: + "@zodios/core" "^10.9.6" + eventsource "^3.0.5" + zod "^3.23.8" + +"@pythnetwork/price-service-sdk@1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@pythnetwork/price-service-sdk/-/price-service-sdk-1.8.0.tgz#f5f01f654963eb9a0cf12127b4f1a89b60ef008a" + integrity sha512-tFZ1thj3Zja06DzPIX2dEWSi7kIfIyqreoywvw5NQ3Z1pl5OJHQGMEhxt6Li3UCGSp2ooYZS9wl8/8XfrfrNSA== + dependencies: + bn.js "^5.2.1" + +"@pythnetwork/pyth-solana-receiver@^0.10.1": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@pythnetwork/pyth-solana-receiver/-/pyth-solana-receiver-0.10.2.tgz#5e19b4bd63cda1d4c71ad2e8cca29d4037890acf" + integrity sha512-KiMm+K0d1f6xonN3xTuKAYe8PddtM5LiT/gJSQhmJJXaBIDVhtIp8n2mRllbgUjrOvuLyl9KUm1af3gbGUX7cg== + dependencies: + "@coral-xyz/anchor" "^0.29.0" + "@noble/hashes" "^1.4.0" + "@pythnetwork/price-service-sdk" "1.8.0" + "@pythnetwork/solana-utils" "0.4.5" + "@solana/web3.js" "^1.90.0" + +"@pythnetwork/solana-utils@0.4.5": + version "0.4.5" + resolved "https://registry.yarnpkg.com/@pythnetwork/solana-utils/-/solana-utils-0.4.5.tgz#7c5af4b6794769e57b56ad1c680faa6dbf70b919" + integrity sha512-NoLdC2rRAx9a66L0hSOAGt6Wj/YxfnKkw+mbb7Tn/Nn1du4HyShi41DiN6B+2XXqnMthNGbf9FSHvj4NXXABvA== + dependencies: + "@coral-xyz/anchor" "^0.29.0" + "@solana/web3.js" "^1.90.0" + bs58 "^5.0.0" + jito-ts "^3.0.1" + ts-log "^2.2.7" + +"@solana/buffer-layout-utils@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca" + integrity sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/web3.js" "^1.32.0" + bigint-buffer "^1.1.5" + bignumber.js "^9.0.1" + +"@solana/buffer-layout@^4.0.0", "@solana/buffer-layout@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" + integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA== + dependencies: + buffer "~6.0.3" + +"@solana/codecs-core@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz#1a2d76b9c7b9e7b7aeb3bd78be81c2ba21e3ce22" + integrity sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ== + dependencies: + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-core@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.3.0.tgz#6bf2bb565cb1ae880f8018635c92f751465d8695" + integrity sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw== + dependencies: + "@solana/errors" "2.3.0" + +"@solana/codecs-data-structures@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz#d47b2363d99fb3d643f5677c97d64a812982b888" + integrity sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-numbers@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz#f34978ddf7ea4016af3aaed5f7577c1d9869a614" + integrity sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-numbers@^2.1.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz#ac7e7f38aaf7fcd22ce2061fbdcd625e73828dc6" + integrity sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg== + dependencies: + "@solana/codecs-core" "2.3.0" + "@solana/errors" "2.3.0" + +"@solana/codecs-strings@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz#e1d9167075b8c5b0b60849f8add69c0f24307018" + integrity sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-rc.1.tgz#146dc5db58bd3c28e04b4c805e6096c2d2a0a875" + integrity sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-data-structures" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/codecs-strings" "2.0.0-rc.1" + "@solana/options" "2.0.0-rc.1" + +"@solana/errors@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.0.0-rc.1.tgz#3882120886eab98a37a595b85f81558861b29d62" + integrity sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ== + dependencies: + chalk "^5.3.0" + commander "^12.1.0" + +"@solana/errors@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.3.0.tgz#4ac9380343dbeffb9dffbcb77c28d0e457c5fa31" + integrity sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ== + dependencies: + chalk "^5.4.1" + commander "^14.0.0" + +"@solana/options@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-rc.1.tgz#06924ba316dc85791fc46726a51403144a85fc4d" + integrity sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-data-structures" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/codecs-strings" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/spl-token-group@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz#83c00f0cd0bda33115468cd28b89d94f8ec1fee4" + integrity sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug== + dependencies: + "@solana/codecs" "2.0.0-rc.1" + +"@solana/spl-token-metadata@^0.1.2", "@solana/spl-token-metadata@^0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz#d240947aed6e7318d637238022a7b0981b32ae80" + integrity sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA== + dependencies: + "@solana/codecs" "2.0.0-rc.1" + +"@solana/spl-token@^0.3.6": + version "0.3.11" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.11.tgz#cdc10f9472b29b39c8983c92592cadd06627fb9a" + integrity sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/buffer-layout-utils" "^0.2.0" + "@solana/spl-token-metadata" "^0.1.2" + buffer "^6.0.3" + +"@solana/spl-token@^0.4.8": + version "0.4.14" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.14.tgz#b86bc8a17f50e9680137b585eca5f5eb9d55c025" + integrity sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/buffer-layout-utils" "^0.2.0" + "@solana/spl-token-group" "^0.0.7" + "@solana/spl-token-metadata" "^0.1.6" + buffer "^6.0.3" + +"@solana/web3.js@^1.32.0", "@solana/web3.js@^1.56.2", "@solana/web3.js@^1.66.2", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.69.0", "@solana/web3.js@^1.90.0", "@solana/web3.js@^1.91.8": + version "1.98.4" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.4.tgz#df51d78be9d865181ec5138b4e699d48e6895bbe" + integrity sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + "@solana/codecs-numbers" "^2.1.0" + agentkeepalive "^4.5.0" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + +"@solana/web3.js@~1.77.3": + version "1.77.4" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.77.4.tgz#aad8c44a02ced319493308ef765a2b36a9e9fa8c" + integrity sha512-XdN0Lh4jdY7J8FYMyucxCwzn6Ga2Sr1DHDWRbqVzk7ZPmmpSPOVWHzO67X1cVT+jNi1D6gZi2tgjHgDPuj6e9Q== + dependencies: + "@babel/runtime" "^7.12.5" + "@noble/curves" "^1.0.0" + "@noble/hashes" "^1.3.0" + "@solana/buffer-layout" "^4.0.0" + agentkeepalive "^4.2.1" + bigint-buffer "^1.1.5" + bn.js "^5.0.0" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.0" + node-fetch "^2.6.7" + rpc-websockets "^7.5.1" + superstruct "^0.14.2" + +"@swc/helpers@^0.5.11": + version "0.5.17" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.17.tgz#5a7be95ac0f0bf186e7e6e890e7a6f6cda6ce971" + integrity sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A== + dependencies: + tslib "^2.8.0" + +"@tkkinn/mock-pyth-sdk@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@tkkinn/mock-pyth-sdk/-/mock-pyth-sdk-2.0.1.tgz#38894e8c272b25a49ebe131d07fadeae4651f1af" + integrity sha512-9Rzgc3/H0DJryMuQlGR/pTVGyMOs5XJTcvU1Rj7ziGkvOZvwlS4WSnZUAc6ZaB5MVQfjwbxSB6JW1+JsYWf+3A== + dependencies: + "@coral-xyz/anchor" "^0.30.0" + "@solana/web3.js" "^1.91.8" + +"@types/bn.js@^5.1.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.2.0.tgz#4349b9710e98f9ab3cdc50f1c5e4dcbd8ef29c80" + integrity sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q== + dependencies: + "@types/node" "*" + +"@types/chai@^4.3.0": + version "4.3.20" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.20.tgz#cb291577ed342ca92600430841a00329ba05cecc" + integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ== + +"@types/connect@^3.4.33": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/mocha@^9.0.0": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" + integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== + +"@types/node@*", "@types/node@>=13.7.0": + version "24.10.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.0.tgz#6b79086b0dfc54e775a34ba8114dcc4e0221f31f" + integrity sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A== + dependencies: + undici-types "~7.16.0" + +"@types/node@^12.12.54": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + +"@types/node@^20.10.0": + version "20.19.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.24.tgz#6bc35bc96cda1a251000b706c76380b5c843f30b" + integrity sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA== + dependencies: + undici-types "~6.21.0" + +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + +"@types/ws@^7.4.4": + version "7.4.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" + integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== + dependencies: + "@types/node" "*" + +"@types/ws@^8.2.2": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + +"@ungap/promise-all-settled@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" + integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== + +"@zodios/core@^10.9.6": + version "10.9.6" + resolved "https://registry.yarnpkg.com/@zodios/core/-/core-10.9.6.tgz#64ad831216e6ffa71679ea6be8d1ed882bb04d83" + integrity sha512-aH4rOdb3AcezN7ws8vDgBfGboZMk2JGGzEq/DtW65MhnRxyTGRuLJRWVQ/2KxDgWvV2F5oTkAS+5pnjKbl0n+A== + +agentkeepalive@^4.2.1, agentkeepalive@^4.3.0, agentkeepalive@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" + integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== + dependencies: + humanize-ms "^1.2.1" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansicolors@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== + +assert@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" + integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== + dependencies: + call-bind "^1.0.2" + is-nan "^1.3.2" + object-is "^1.1.5" + object.assign "^4.1.4" + util "^0.12.5" + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base-x@^3.0.2: + version "3.0.11" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.11.tgz#40d80e2a1aeacba29792ccc6c5354806421287ff" + integrity sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA== + dependencies: + safe-buffer "^5.0.1" + +base-x@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.1.tgz#817fb7b57143c501f649805cb247617ad016a885" + integrity sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw== + +base-x@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-5.0.1.tgz#16bf35254be1df8aca15e36b7c1dda74b2aa6b03" + integrity sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bigint-buffer@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" + integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA== + dependencies: + bindings "^1.3.0" + +bignumber.js@^9.0.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" + integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bindings@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bn.js@^5.0.0, bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.2.tgz#82c09f9ebbb17107cd72cb7fd39bd1f9d0aaa566" + integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw== + +borsh@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a" + integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== + dependencies: + bn.js "^5.2.0" + bs58 "^4.0.0" + text-encoding-utf-8 "^1.0.2" + +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +bs58@^4.0.0, bs58@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + +bs58@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279" + integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ== + dependencies: + base-x "^4.0.0" + +bs58@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-6.0.0.tgz#a2cda0130558535dd281a2f8697df79caaf425d8" + integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== + dependencies: + base-x "^5.0.0" + +buffer-from@^1.0.0, buffer-from@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-layout@^1.2.0, buffer-layout@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" + integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== + +buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bufferutil@^4.0.1: + version "4.0.9" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.9.tgz#6e81739ad48a95cad45a279588e13e95e24a800a" + integrity sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw== + dependencies: + node-gyp-build "^4.3.0" + +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +camelcase@^6.0.0, camelcase@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +chai@^4.3.4: + version "4.5.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" + integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.1.0" + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^5.3.0, chalk@^5.4.1: + version "5.6.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" + integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== + +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== + +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + +commander@^14.0.0: + version "14.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.2.tgz#b71fd37fe4069e4c3c7c13925252ada4eba14e8e" + integrity sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ== + +commander@^2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cross-fetch@^3.1.5: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.2.0.tgz#34e9192f53bc757d6614304d9e5e6fb4edb782e3" + integrity sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q== + dependencies: + node-fetch "^2.7.0" + +crypto-hash@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" + integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== + +debug@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +debug@^4.3.3, debug@^4.3.4: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +deep-eql@^4.1.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" + integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== + dependencies: + type-detect "^4.0.0" + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^3.1.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dotenv@^16.0.3: + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== + dependencies: + es6-promise "^4.0.3" + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eventemitter3@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + +eventsource-parser@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz#292e165e34cacbc936c3c92719ef326d4aeb4e90" + integrity sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg== + +eventsource@^3.0.5: + version "3.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989" + integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA== + dependencies: + eventsource-parser "^3.0.1" + +eyes@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== + +fast-stable-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313" + integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +generator-function@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/generator-function/-/generator-function-2.0.1.tgz#0e75dd410d1243687a0ba2e951b94eedb8f737a2" + integrity sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + +get-intrinsic@^1.2.4, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arguments@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" + integrity sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-function@^1.0.7: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.2.tgz#ae3b61e3d5ea4e4839b90bad22b02335051a17d5" + integrity sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== + dependencies: + call-bound "^1.0.4" + generator-function "^2.0.0" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-nan@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" + integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-typed-array@^1.1.3: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + +jayson@^4.0.0, jayson@^4.1.0, jayson@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.2.0.tgz#b71762393fa40bc9637eaf734ca6f40d3b8c0c93" + integrity sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg== + dependencies: + "@types/connect" "^3.4.33" + "@types/node" "^12.12.54" + "@types/ws" "^7.4.4" + commander "^2.20.3" + delay "^5.0.0" + es6-promisify "^5.0.0" + eyes "^0.1.8" + isomorphic-ws "^4.0.1" + json-stringify-safe "^5.0.1" + stream-json "^1.9.1" + uuid "^8.3.2" + ws "^7.5.10" + +jito-ts@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/jito-ts/-/jito-ts-3.0.1.tgz#24126389896e042c26d303c4e802064b249ed27e" + integrity sha512-TSofF7KqcwyaWGjPaSYC8RDoNBY1TPRNBHdrw24bdIi7mQ5bFEDdYK3D//llw/ml8YDvcZlgd644WxhjLTS9yg== + dependencies: + "@grpc/grpc-js" "^1.8.13" + "@noble/ed25519" "^1.7.1" + "@solana/web3.js" "~1.77.3" + agentkeepalive "^4.3.0" + dotenv "^16.0.3" + jayson "^4.0.0" + node-fetch "^2.6.7" + superstruct "^1.0.3" + +js-sha3@^0.9.2: + version "0.9.3" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.9.3.tgz#f0209432b23a66a0f6c7af592c26802291a75c2a" + integrity sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg== + +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +long@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== + +loupe@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + dependencies: + get-func-name "^2.0.1" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@^9.0.3: + version "9.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" + integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.3" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + growl "1.10.5" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "4.2.1" + ms "2.1.3" + nanoid "3.3.1" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + workerpool "6.2.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.0.0, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-fetch@^2.6.7, node-fetch@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-gyp-build@^4.3.0: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +pako@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +possible-typed-array-names@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== + +prettier@^2.6.2: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + +protobufjs@^7.5.3: + version "7.5.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" + integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +rpc-websockets@^7.5.1: + version "7.11.2" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.11.2.tgz#582910c425b9f2c860327481c1d1e0e431bf4a6d" + integrity sha512-pL9r5N6AVHlMN/vT98+fcO+5+/UcPLf/4tq+WUaid/PPUGS/ttJ3y8e9IqmaWKtShNAysMSjkczuEA49NuV7UQ== + dependencies: + eventemitter3 "^4.0.7" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + +rpc-websockets@^9.0.2: + version "9.3.0" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.3.0.tgz#116cda0f5dd36aba4638430c6218a0f3ba79d651" + integrity sha512-Sf6b6tCpLa6FxgZV20FC1GotVjinFfMkWWfuYtZOdoExvoXQl9ed1J7NdbybLZshNDHjWNa38U186MwElN1VjA== + dependencies: + "@swc/helpers" "^0.5.11" + "@types/uuid" "^8.3.4" + "@types/ws" "^8.2.2" + buffer "^6.0.3" + eventemitter3 "^5.0.1" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +source-map-support@^0.5.6: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +stream-chain@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/stream-chain/-/stream-chain-2.2.5.tgz#b30967e8f14ee033c5b9a19bbe8a2cba90ba0d09" + integrity sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA== + +stream-json@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/stream-json/-/stream-json-1.9.1.tgz#e3fec03e984a503718946c170db7d74556c2a187" + integrity sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw== + dependencies: + stream-chain "^2.2.5" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-json-comments@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +superstruct@^0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b" + integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ== + +superstruct@^0.15.4: + version "0.15.5" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.5.tgz#0f0a8d3ce31313f0d84c6096cd4fa1bfdedc9dab" + integrity sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ== + +superstruct@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-1.0.4.tgz#0adb99a7578bd2f1c526220da6571b2d485d91ca" + integrity sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ== + +superstruct@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" + integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== + +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-encoding-utf-8@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" + integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +ts-log@^2.2.7: + version "2.2.7" + resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.7.tgz#4f4512144898b77c9984e91587076fcb8518688e" + integrity sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg== + +ts-mocha@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-10.1.0.tgz#17a1c055f5f7733fd82447c4420740db87221bc8" + integrity sha512-T0C0Xm3/WqCuF2tpa0GNGESTBoKZaiqdUP8guNv4ZY316AFXlyidnrzQ1LUrCT0Wb1i3J0zFTgOh/55Un44WdA== + dependencies: + ts-node "7.0.1" + optionalDependencies: + tsconfig-paths "^3.5.0" + +ts-node@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" + integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw== + dependencies: + arrify "^1.0.0" + buffer-from "^1.1.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.6" + yn "^2.0.0" + +tsconfig-paths@^3.5.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^2.0.3, tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-detect@^4.0.0, type-detect@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" + integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== + +typescript@^5.7.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== + +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + +util@^0.12.5: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-typed-array@^1.1.16, which-typed-array@^1.1.2: + version "1.1.19" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + +which@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^7.5.10: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +ws@^8.5.0: + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yn@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" + integrity sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.23.8: + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==