diff --git a/contracts/evm-gateway/.env.sample b/contracts/evm-gateway/.env.sample deleted file mode 100644 index d75abf8..0000000 --- a/contracts/evm-gateway/.env.sample +++ /dev/null @@ -1,10 +0,0 @@ -# API for RPC Calls -SEPOLIA_RPC_URL=YOUR_RPC_URL -SEPOLIA_RPC_URL=YOUR_RPC_URL -POLYGON_RPC_URL=YOUR_RPC_URL -ARBITRUM_RPC_URL=YOUR_RPC_URL - -# API for Verification -ETHERSCAN_API=YOUR_API_KEY -POLYGONSCAN_API=YOUR_API_KEY -ARBISCAN_API=YOUR_API_KEY \ No newline at end of file diff --git a/contracts/evm-gateway/README.md b/contracts/evm-gateway/README.md index b71541e..d8d3cd9 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 revertMsg; }` +- `RevertInstructions { address fundRecipient; bytes revertContext; }` - `UniversalPayload { to, value, data, gasLimit, maxFeePerGas, maxPriorityFeePerGas, nonce, deadline, vType }` - `EpochUsage { uint64 epoch; uint192 used; }` diff --git a/contracts/evm-gateway/lcov.info b/contracts/evm-gateway/lcov.info index 482a7a3..736ec94 100644 --- a/contracts/evm-gateway/lcov.info +++ b/contracts/evm-gateway/lcov.info @@ -261,837 +261,1364 @@ BRH:0 end_of_record TN: SF:src/UniversalGateway.sol -DA:116,229 -FN:116,UniversalGateway.initialize -FNDA:229,UniversalGateway.initialize -DA:126,229 -BRDA:126,0,0,4 -DA:127,4 -DA:130,0 -DA:131,0 -DA:132,0 -DA:133,0 -DA:135,225 -DA:136,225 -DA:137,225 -DA:139,225 -DA:140,225 -DA:141,225 -DA:143,225 -DA:144,225 -BRDA:144,1,0,2 -DA:145,2 -DA:146,2 -DA:149,225 -DA:151,225 -DA:153,225 -DA:157,12 -FN:157,UniversalGateway.onlyTSS -FNDA:12,UniversalGateway.onlyTSS -DA:158,12 -BRDA:158,2,0,1 -DA:165,25 -FN:165,UniversalGateway.pause -FNDA:25,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 +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 FNDA:7,UniversalGateway.setTSS -DA:175,5 -BRDA:175,3,0,1 -DA:176,4 -DA:179,4 -BRDA:179,4,0,4 -DA:180,4 -DA:182,4 -DA:188,9 -FN:188,UniversalGateway.setCapsUSD +DA:181,6 +BRDA:181,3,0,1 +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:189,6 -BRDA:189,5,0,1 -DA:191,5 -DA:192,5 -DA:193,5 -DA:197,18 -FN:197,UniversalGateway.setBlockUsdCap +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:198,17 -DA:203,5 -FN:203,UniversalGateway.setDefaultSwapDeadline +DA:218,17 +DA:223,5 +FN:223,UniversalGateway.setDefaultSwapDeadline FNDA:5,UniversalGateway.setDefaultSwapDeadline -DA:204,2 -BRDA:204,6,0,1 -DA:205,1 -DA:211,73 -FN:211,UniversalGateway.setRouters -FNDA:73,UniversalGateway.setRouters -DA:212,70 -BRDA:212,7,0,2 -DA:213,68 -DA:214,68 -DA:220,280 -FN:220,UniversalGateway.setTokenLimitThresholds -FNDA:280,UniversalGateway.setTokenLimitThresholds -DA:224,277 -BRDA:224,8,0,2 -DA:225,275 -DA:226,403 -DA:227,403 -DA:234,10 -FN:234,UniversalGateway.updateTokenLimitThreshold +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:238,8 -BRDA:238,9,0,1 -DA:239,7 -DA:240,7 -DA:241,7 -DA:247,11 -FN:247,UniversalGateway.updateEpochDuration +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:248,9 -DA:249,9 -DA:250,9 -DA:257,55 -FN:257,UniversalGateway.setV3FeeOrder -FNDA:55,UniversalGateway.setV3FeeOrder -DA:258,52 -DA:263,264 -FN:263,UniversalGateway.setEthUsdFeed -FNDA:264,UniversalGateway.setEthUsdFeed -DA:264,262 -BRDA:264,10,0,1 -DA:265,261 -DA:267,261 -DA:268,261 -DA:269,261 -DA:274,25 -FN:274,UniversalGateway.setChainlinkStalePeriod +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:275,22 -DA:281,25 -FN:281,UniversalGateway.setL2SequencerFeed +DA:295,22 +DA:301,25 +FN:301,UniversalGateway.setL2SequencerFeed FNDA:25,UniversalGateway.setL2SequencerFeed -DA:282,22 -DA:287,4 -FN:287,UniversalGateway.setL2SequencerGracePeriod +DA:302,22 +DA:307,4 +FN:307,UniversalGateway.setL2SequencerGracePeriod FNDA:4,UniversalGateway.setL2SequencerGracePeriod -DA:288,1 -DA:296,67 -FN:296,UniversalGateway.sendTxWithGas +DA:308,1 +DA:316,67 +FN:316,UniversalGateway.sendTxWithGas FNDA:67,UniversalGateway.sendTxWithGas -DA:302,67 -DA:308,11 -FN:308,UniversalGateway.sendTxWithGas -FNDA:11,UniversalGateway.sendTxWithGas -DA:317,11 -BRDA:317,11,0,1 -DA:318,10 -BRDA:318,12,0,1 -DA:319,9 -BRDA:319,13,0,1 -DA:320,8 -BRDA:320,14,0,1 -DA:323,7 -DA:325,7 -DA:332,82 -FN:332,UniversalGateway._sendTxWithGas -FNDA:82,UniversalGateway._sendTxWithGas -DA:340,82 -BRDA:340,15,0,1 -DA:343,81 -DA:344,65 -DA:345,55 -DA:347,81 -DA:364,84 -FN:364,UniversalGateway.sendFunds +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:370,84 -BRDA:370,16,0,2 -DA:372,82 -BRDA:372,17,0,13 -BRDA:372,17,1,2 -DA:373,13 -BRDA:373,18,0,2 -DA:374,11 -DA:375,9 -DA:377,69 -BRDA:377,19,0,2 -DA:378,67 -DA:379,53 -DA:382,11 -DA:395,5 -FN:395,UniversalGateway.sendTxWithFunds -FNDA:5,UniversalGateway.sendTxWithFunds -DA:402,5 -BRDA:402,20,0,- -DA:403,5 -DA:404,5 -BRDA:404,21,0,- -DA:406,5 -DA:409,3 -DA:410,3 -DA:412,5 -DA:425,18 -FN:425,UniversalGateway.sendTxWithFunds +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:436,18 -BRDA:436,22,0,1 -DA:437,17 -BRDA:437,23,0,2 -DA:438,15 -BRDA:438,24,0,2 -DA:441,13 -DA:443,8 -DA:446,5 -DA:447,4 -DA:448,13 -DA:462,69 -FN:462,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:472,69 -BRDA:472,25,0,- -DA:474,69 -BRDA:474,26,0,7 -DA:475,7 -BRDA:475,27,0,- -DA:476,0 -DA:480,69 -DA:493,11 -FN:493,UniversalGateway.withdrawFunds -FNDA:11,UniversalGateway.withdrawFunds -DA:499,11 -BRDA:499,28,0,1 -DA:500,10 -BRDA:500,29,0,1 -DA:502,9 -BRDA:502,30,0,5 -BRDA:502,30,1,4 -DA:503,5 -DA:505,4 -DA:508,9 -DA:512,7 -FN:512,UniversalGateway.revertWithdrawFunds -FNDA:7,UniversalGateway.revertWithdrawFunds -DA:518,7 -BRDA:518,31,0,1 -DA:519,6 -BRDA:519,32,0,1 -DA:521,5 -BRDA:521,33,0,3 -BRDA:521,33,1,2 -DA:522,3 -DA:524,2 -DA:527,5 -DA:538,14 -FN:538,UniversalGateway.getMinMaxValueForNative -FNDA:14,UniversalGateway.getMinMaxValueForNative -DA:539,14 -DA:541,14 -DA:542,14 -DA:551,148 -FN:551,UniversalGateway.getEthUsdPrice -FNDA:148,UniversalGateway.getEthUsdPrice -DA:552,148 -BRDA:552,34,0,- -DA:555,148 +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 +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:556,0 -DA:562,0 -DA:565,0 -BRDA:565,36,0,- -DA:568,0 -BRDA:568,37,0,- -DA:569,0 -DA:573,148 -DA:576,148 -BRDA:576,38,0,4 -DA:577,144 -BRDA:577,39,0,1 -DA:578,143 -BRDA:578,40,0,2 -DA:579,2 -DA:582,141 -DA:583,141 -BRDA:583,41,0,1 -DA:584,1 -BRDA:584,42,0,1 -DA:585,0 -DA:586,1 -BRDA:586,42,1,1 -DA:588,1 -DA:592,141 -DA:595,141 -BRDA:595,43,0,1 -DA:596,140 -DA:598,140 -DA:605,125 -FN:605,UniversalGateway.quoteEthAmountInUsd1e18 -FNDA:125,UniversalGateway.quoteEthAmountInUsd1e18 -DA:606,125 -DA:607,123 -DA:609,123 -DA:619,85 -FN:619,UniversalGateway._checkUSDCaps -FNDA:85,UniversalGateway._checkUSDCaps -DA:620,85 -DA:621,81 -BRDA:621,45,0,6 -DA:622,75 -BRDA:622,46,0,8 -DA:629,65 -FN:629,UniversalGateway._checkBlockUSDCap +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: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:630,65 -DA:631,65 -DA:633,32 -BRDA:633,48,0,15 -DA:634,15 -DA:635,15 -DA:638,32 -DA:640,32 -BRDA:640,49,0,1 -DA:643,31 -DA:644,31 -BRDA:644,50,0,9 -DA:645,22 -DA:652,64 -FN:652,UniversalGateway._handleNativeDeposit +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:653,64 -DA:654,64 -BRDA:654,51,0,1 -DA:655,0 -DA:662,60 -FN:662,UniversalGateway._handleTokenDeposit +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:663,60 -BRDA:663,52,0,- -DA:664,60 -DA:670,8 -FN:670,UniversalGateway._handleNativeWithdraw -FNDA:8,UniversalGateway._handleNativeWithdraw -DA:671,8 -DA:672,8 -BRDA:672,53,0,2 -DA:680,6 -FN:680,UniversalGateway._handleTokenWithdraw -FNDA:6,UniversalGateway._handleTokenWithdraw -DA:682,6 -BRDA:682,54,0,1 -DA:683,5 -DA:691,86 -FN:691,UniversalGateway._consumeRateLimit +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:692,86 -DA:693,86 -BRDA:693,55,0,6 -DA:695,80 -DA:696,80 -BRDA:696,56,0,2 -DA:698,78 -DA:699,78 -DA:701,78 -BRDA:701,57,0,22 -DA:702,22 -DA:703,22 -DA:707,78 -DA:708,78 -BRDA:708,58,0,9 -DA:709,69 -DA:717,60 -FN:717,UniversalGateway.currentTokenUsage +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:718,60 -DA:719,60 -DA:721,56 -DA:722,56 -DA:724,55 -DA:725,55 -DA:726,55 -DA:728,55 -DA:729,55 -DA:742,20 -FN:742,UniversalGateway.swapToNative -FNDA:20,UniversalGateway.swapToNative -DA:746,20 -BRDA:746,61,0,- -DA:748,20 -BRDA:748,62,0,1 -BRDA:748,62,1,1 -DA:749,1 -DA:750,19 -BRDA:750,63,0,1 -DA:751,1 -DA:753,19 -BRDA:753,64,0,1 -DA:755,18 -BRDA:755,65,0,3 -DA:757,3 -DA:759,3 -DA:760,3 -DA:761,3 -DA:764,3 -BRDA:764,66,0,- -DA:765,3 -DA:767,15 -DA:769,10 -DA:770,8 -DA:772,15 -DA:783,15 -DA:785,7 -DA:787,7 -DA:788,7 -DA:789,7 -DA:792,7 -BRDA:792,67,0,- -DA:795,15 -FN:795,UniversalGateway._findV3PoolWithNative -FNDA:15,UniversalGateway._findV3PoolWithNative -DA:796,15 -BRDA:796,68,0,- -DA:797,15 -BRDA:797,69,0,- -DA:799,0 -DA:803,15 -DA:804,25 -DA:805,25 -DA:806,25 -BRDA:806,70,0,10 -DA:807,10 -DA:810,5 -DA:815,12 -FN:815,UniversalGateway.receive -FNDA:12,UniversalGateway.receive -DA:817,12 -BRDA:817,71,0,1 -FNF:40 -FNH:40 -LF:254 -LH:239 -BRF:73 -BRH:59 +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 +end_of_record +TN: +SF:src/UniversalGatewayPC.sol +DA:49,56 +FN:49,UniversalGatewayPC.initialize +FNDA:56,UniversalGatewayPC.initialize +DA:50,56 +BRDA:50,0,0,4 +DA:52,0 +DA:53,0 +DA:54,0 +DA:56,52 +DA:57,52 +DA:59,52 +DA:60,52 +DA:65,5 +FN:65,UniversalGatewayPC.setVaultPC +FNDA:5,UniversalGatewayPC.setVaultPC +DA:66,3 +BRDA:66,1,0,2 +DA:67,1 +DA:68,1 +DA:69,1 +DA:72,10 +FN:72,UniversalGatewayPC.pause +FNDA:10,UniversalGatewayPC.pause +DA:73,0 +DA:76,4 +FN:76,UniversalGatewayPC.unpause +FNDA:4,UniversalGatewayPC.unpause +DA:77,0 +DA:81,20 +FN:81,UniversalGatewayPC.withdraw +FNDA:20,UniversalGatewayPC.withdraw +DA:88,20 +DA:91,20 +DA:92,20 +DA:94,14 +DA:95,12 +DA:97,10 +DA:98,10 +DA:104,15 +FN:104,UniversalGatewayPC.withdrawAndExecute +FNDA:15,UniversalGatewayPC.withdrawAndExecute +DA:112,15 +DA:115,15 +DA:116,15 +DA:117,11 +DA:119,9 +DA:121,8 +DA:122,8 +DA:135,35 +FN:135,UniversalGatewayPC._validateCommon +FNDA:35,UniversalGatewayPC._validateCommon +DA:141,35 +BRDA:141,2,0,2 +DA:142,33 +BRDA:142,3,0,2 +DA:143,31 +BRDA:143,4,0,2 +DA:144,29 +BRDA:144,5,0,2 +DA:155,27 +FN:155,UniversalGatewayPC._calculateGasFeesWithLimit +FNDA:27,UniversalGatewayPC._calculateGasFeesWithLimit +DA:160,27 +BRDA:160,6,0,2 +BRDA:160,6,1,- +DA:161,2 +DA:163,0 +DA:166,27 +DA:167,25 +BRDA:167,7,0,- +DA:169,25 +DA:176,25 +FN:176,UniversalGatewayPC._moveFees +FNDA:25,UniversalGatewayPC._moveFees +DA:177,25 +DA:178,25 +BRDA:178,8,0,- +DA:180,25 +DA:181,21 +BRDA:181,9,0,- +DA:184,21 +FN:184,UniversalGatewayPC._burnPRC20 +FNDA:21,UniversalGatewayPC._burnPRC20 +DA:186,21 +DA:189,18 +DA:190,18 +BRDA:190,10,0,- +FNF:10 +FNH:10 +LF:55 +LH:49 +BRF:12 +BRH:7 end_of_record TN: SF:src/UniversalGatewayV0.sol -DA:117,0 -FN:117,UniversalGatewayV0.initialize -FNDA:0,UniversalGatewayV0.initialize -DA:130,0 -BRDA:130,0,0,- -DA:131,0 +DA:132,0 +FN:132,UniversalGatewayV0.updateVault +FNDA:0,UniversalGatewayV0.updateVault +DA:133,0 +BRDA:133,0,0,- DA:134,0 -DA:135,0 -DA:136,0 DA:137,0 -DA:139,0 +BRDA:137,1,0,- +DA:138,0 DA:140,0 DA:141,0 -DA:143,0 DA:144,0 -DA:145,0 -DA:147,0 -DA:148,0 -BRDA:148,1,0,- -DA:149,0 -DA:150,0 -DA:153,0 -DA:156,0 +FN:144,UniversalGatewayV0.initialize +FNDA:0,UniversalGatewayV0.initialize DA:157,0 DA:158,0 DA:159,0 -DA:161,0 +DA:160,0 +BRDA:160,2,0,- +DA:162,0 +DA:163,0 +DA:164,0 DA:165,0 -FN:165,UniversalGatewayV0.onlyTSS -FNDA:0,UniversalGatewayV0.onlyTSS -DA:166,0 -BRDA:166,2,0,- -DA:170,0 -FN:170,UniversalGatewayV0.version -FNDA:0,UniversalGatewayV0.version +DA:167,0 +DA:168,0 +DA:169,0 DA:171,0 +DA:172,0 +DA:173,0 +DA:175,0 +DA:176,0 +BRDA:176,3,0,- DA:177,0 -FN:177,UniversalGatewayV0.pause -FNDA:0,UniversalGatewayV0.pause DA:178,0 -DA:181,0 -FN:181,UniversalGatewayV0.unpause -FNDA:0,UniversalGatewayV0.unpause DA:182,0 +DA:185,0 +DA:186,0 +DA:187,0 DA:188,0 -FN:188,UniversalGatewayV0.setTSSAddress -FNDA:0,UniversalGatewayV0.setTSSAddress -DA:189,0 -BRDA:189,3,0,- -DA:190,0 DA:193,0 -BRDA:193,4,0,- +FN:193,UniversalGatewayV0.onlyTSS +FNDA:0,UniversalGatewayV0.onlyTSS DA:194,0 -DA:196,0 -DA:202,0 -FN:202,UniversalGatewayV0.setCapsUSD -FNDA:0,UniversalGatewayV0.setCapsUSD -DA:203,0 -BRDA:203,5,0,- +BRDA:194,4,0,- +DA:198,0 +FN:198,UniversalGatewayV0.version +FNDA:0,UniversalGatewayV0.version +DA:199,0 DA:205,0 +FN:205,UniversalGatewayV0.pause +FNDA:0,UniversalGatewayV0.pause DA:206,0 -DA:207,0 -DA:211,0 -FN:211,UniversalGatewayV0.setBlockUsdCap -FNDA:0,UniversalGatewayV0.setBlockUsdCap -DA:212,0 +DA:209,0 +FN:209,UniversalGatewayV0.unpause +FNDA:0,UniversalGatewayV0.unpause +DA:210,0 +DA:216,0 +FN:216,UniversalGatewayV0.setTSSAddress +FNDA:0,UniversalGatewayV0.setTSSAddress DA:217,0 -FN:217,UniversalGatewayV0.setDefaultSwapDeadline -FNDA:0,UniversalGatewayV0.setDefaultSwapDeadline +BRDA:217,5,0,- DA:218,0 -BRDA:218,6,0,- -DA:219,0 -DA:220,0 -DA:226,0 -FN:226,UniversalGatewayV0.setRouters -FNDA:0,UniversalGatewayV0.setRouters -DA:227,0 -BRDA:227,7,0,- -DA:228,0 -DA:229,0 -DA:236,0 -FN:236,UniversalGatewayV0.modifySupportForToken -FNDA:0,UniversalGatewayV0.modifySupportForToken +DA:221,0 +BRDA:221,6,0,- +DA:222,0 +DA:224,0 +DA:230,0 +FN:230,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 +FNDA:0,UniversalGatewayV0.setDefaultSwapDeadline DA:241,0 BRDA:241,8,0,- DA:242,0 -DA:243,0 -DA:244,0 -DA:252,0 -FN:252,UniversalGatewayV0.setV3FeeOrder -FNDA:0,UniversalGatewayV0.setV3FeeOrder -DA:253,0 -DA:254,0 +DA:248,0 +FN:248,UniversalGatewayV0.setRouters +FNDA:0,UniversalGatewayV0.setRouters +DA:249,0 +BRDA:249,9,0,- +DA:250,0 +DA:251,0 DA:258,0 -FN:258,UniversalGatewayV0.setEthUsdFeed -FNDA:0,UniversalGatewayV0.setEthUsdFeed +FN:258,UniversalGatewayV0.modifySupportForToken +FNDA:0,UniversalGatewayV0.modifySupportForToken DA:259,0 -BRDA:259,9,0,- +BRDA:259,10,0,- DA:260,0 -DA:262,0 -DA:263,0 -DA:264,0 -DA:265,0 -DA:270,0 -FN:270,UniversalGatewayV0.setChainlinkStalePeriod -FNDA:0,UniversalGatewayV0.setChainlinkStalePeriod +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 +FNDA:0,UniversalGatewayV0.setEthUsdFeed DA:277,0 -FN:277,UniversalGatewayV0.setL2SequencerFeed -FNDA:0,UniversalGatewayV0.setL2SequencerFeed +BRDA:277,11,0,- DA:278,0 -DA:279,0 -DA:284,0 -FN:284,UniversalGatewayV0.setL2SequencerGracePeriod +DA:280,0 +DA:281,0 +DA:282,0 +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 +DA:299,0 +FN:299,UniversalGatewayV0.setL2SequencerGracePeriod FNDA:0,UniversalGatewayV0.setL2SequencerGracePeriod -DA:285,0 -DA:286,0 -DA:303,0 -FN:303,UniversalGatewayV0.addFunds -FNDA:0,UniversalGatewayV0.addFunds +DA:300,0 DA:304,0 -DA:307,0 -FN:307,UniversalGatewayV0._addFunds -FNDA:0,UniversalGatewayV0._addFunds -DA:309,0 -DA:310,0 +FN:304,UniversalGatewayV0.setBlockUsdCap +FNDA:0,UniversalGatewayV0.setBlockUsdCap +DA:305,0 DA:311,0 -DA:314,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:319,0 -DA:320,0 -DA:322,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 DA:332,0 -DA:335,0 -DA:336,0 -DA:337,0 +DA:338,0 +FN:338,UniversalGatewayV0.updateEpochDuration +FNDA:0,UniversalGatewayV0.updateEpochDuration DA:339,0 +DA:340,0 DA:341,0 -DA:345,0 -FN:345,UniversalGatewayV0.sendTxWithGas -FNDA:0,UniversalGatewayV0.sendTxWithGas -DA:350,0 -DA:351,0 -DA:352,0 -DA:353,0 -DA:359,0 -FN:359,UniversalGatewayV0.sendTxWithGas -FNDA:0,UniversalGatewayV0.sendTxWithGas -DA:368,0 -BRDA:368,10,0,- +DA:362,0 +FN:362,UniversalGatewayV0.addFunds +FNDA:0,UniversalGatewayV0.addFunds +DA:363,0 +DA:366,0 +FN:366,UniversalGatewayV0._addFunds +FNDA:0,UniversalGatewayV0._addFunds DA:369,0 -BRDA:369,11,0,- DA:370,0 -BRDA:370,12,0,- -DA:372,0 -BRDA:372,13,0,- -DA:375,0 -DA:381,0 +DA:371,0 +DA:374,0 +DA:377,0 +DA:378,0 +DA:379,0 +DA:380,0 DA:382,0 -DA:394,0 -FN:394,UniversalGatewayV0._sendTxWithGas -FNDA:0,UniversalGatewayV0._sendTxWithGas -DA:402,0 -BRDA:402,14,0,- -DA:404,0 +DA:392,0 +DA:395,0 +DA:396,0 +DA:397,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.sendFunds -FNDA:0,UniversalGatewayV0.sendFunds -DA:427,0 -BRDA:427,15,0,- -DA:429,0 -BRDA:429,16,0,- -BRDA:429,16,1,- +FN:421,UniversalGatewayV0.sendTxWithGas +FNDA:0,UniversalGatewayV0.sendTxWithGas +DA:430,0 +BRDA:430,14,0,- DA:431,0 -BRDA:431,17,0,- +BRDA:431,15,0,- DA:432,0 +BRDA:432,16,0,- DA:434,0 -BRDA:434,18,0,- -DA:435,0 -DA:438,0 -DA:451,0 -FN:451,UniversalGatewayV0.sendTxWithFunds -FNDA:0,UniversalGatewayV0.sendTxWithFunds -DA:458,0 -BRDA:458,19,0,- -DA:459,0 -DA:460,0 -BRDA:460,20,0,- -DA:465,0 -DA:468,0 +BRDA:434,17,0,- +DA:437,0 +DA:439,0 +DA:456,0 +FN:456,UniversalGatewayV0._sendTxWithGas +FNDA:0,UniversalGatewayV0._sendTxWithGas +DA:464,0 +BRDA:464,18,0,- DA:469,0 -DA:482,0 -FN:482,UniversalGatewayV0.sendTxWithFunds -FNDA:0,UniversalGatewayV0.sendTxWithFunds -DA:493,0 -BRDA:493,21,0,- -DA:494,0 -BRDA:494,22,0,- +DA:470,0 +DA:472,0 +DA:489,0 +FN:489,UniversalGatewayV0.sendFunds +FNDA:0,UniversalGatewayV0.sendFunds DA:495,0 -BRDA:495,23,0,- +BRDA:495,19,0,- +DA:497,0 +BRDA:497,20,0,- +BRDA:497,20,1,- DA:498,0 -DA:503,0 -DA:505,0 -DA:506,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 -FN:527,UniversalGatewayV0._sendTxWithFunds -FNDA:0,UniversalGatewayV0._sendTxWithFunds +BRDA:527,23,0,- +DA:528,0 +DA:529,0 +BRDA:529,24,0,- +DA:533,0 +DA:536,0 DA:537,0 -BRDA:537,24,0,- -DA:539,0 -BRDA:539,25,0,- -DA:540,0 -BRDA:540,26,0,- -DA:541,0 -DA:545,0 -DA:562,0 -FN:562,UniversalGatewayV0.withdrawFunds -FNDA:0,UniversalGatewayV0.withdrawFunds -DA:568,0 -BRDA:568,27,0,- -DA:569,0 -BRDA:569,28,0,- -DA:571,0 -BRDA:571,29,0,- -BRDA:571,29,1,- -DA:572,0 -DA:574,0 -DA:577,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.revertWithdrawFunds -FNDA:0,UniversalGatewayV0.revertWithdrawFunds -DA:587,0 -BRDA:587,30,0,- -DA:588,0 -BRDA:588,31,0,- -DA:590,0 -BRDA:590,32,0,- -BRDA:590,32,1,- -DA:591,0 +FN:581,UniversalGatewayV0.sendTxWithFunds +FNDA:0,UniversalGatewayV0.sendTxWithFunds +DA:592,0 +BRDA:592,27,0,- DA:593,0 -DA:596,0 -DA:607,0 -FN:607,UniversalGatewayV0.getMinMaxValueForNative -FNDA:0,UniversalGatewayV0.getMinMaxValueForNative -DA:608,0 -DA:612,0 -DA:613,0 -DA:624,0 -FN:624,UniversalGatewayV0.getEthUsdPrice -FNDA:0,UniversalGatewayV0.getEthUsdPrice -DA:625,0 -BRDA:625,33,0,- +BRDA:593,28,0,- +DA:594,0 +BRDA:594,29,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,34,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:638,0 -BRDA:638,35,0,- -DA:641,0 -BRDA:641,36,0,- -DA:642,0 -DA:646,0 -DA:649,0 -BRDA:649,37,0,- -DA:650,0 -BRDA:650,38,0,- -DA:651,0 -BRDA:651,39,0,- -DA:652,0 -DA:655,0 -DA:658,0 -BRDA:658,40,0,- -DA:659,0 -BRDA:659,41,0,- -DA:660,0 +DA:639,0 +DA:640,0 DA:661,0 -BRDA:661,41,1,- -DA:663,0 -DA:668,0 +FN:661,UniversalGatewayV0._sendTxWithFunds +FNDA:0,UniversalGatewayV0._sendTxWithFunds DA:671,0 -BRDA:671,42,0,- -DA:672,0 -DA:674,0 +BRDA:671,33,0,- +DA:673,0 +BRDA:673,34,0,- +DA:675,0 +DA:676,0 DA:677,0 -FN:677,UniversalGatewayV0.getEthUsdPrice_old -FNDA:0,UniversalGatewayV0.getEthUsdPrice_old +BRDA:677,35,0,- DA:678,0 -DA:679,0 -DA:681,0 -BRDA:681,43,0,- -BRDA:681,43,1,- DA:682,0 -DA:690,0 -FN:690,UniversalGatewayV0.quoteEthAmountInUsd1e18 -FNDA:0,UniversalGatewayV0.quoteEthAmountInUsd1e18 -DA:691,0 -DA:692,0 -DA:695,0 +DA:699,0 +FN:699,UniversalGatewayV0.withdrawFunds +FNDA:0,UniversalGatewayV0.withdrawFunds +DA:704,0 +BRDA:704,36,0,- DA:705,0 -FN:705,UniversalGatewayV0._checkUSDCaps -FNDA:0,UniversalGatewayV0._checkUSDCaps -DA:706,0 +BRDA:705,37,0,- DA:707,0 -BRDA:707,45,0,- +BRDA:707,38,0,- +BRDA:707,38,1,- DA:708,0 -BRDA:708,46,0,- -DA:715,0 -FN:715,UniversalGatewayV0._checkBlockUSDCap -FNDA:0,UniversalGatewayV0._checkBlockUSDCap -DA:716,0 +DA:710,0 +DA:713,0 DA:717,0 -DA:720,0 -BRDA:720,48,0,- -DA:721,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:728,0 -BRDA:728,49,0,- DA:731,0 -DA:732,0 -BRDA:732,50,0,- -DA:733,0 DA:738,0 -FN:738,UniversalGatewayV0._handleNativeDeposit -FNDA:0,UniversalGatewayV0._handleNativeDeposit -DA:739,0 -DA:740,0 -BRDA:740,51,0,- -DA:741,0 -DA:748,0 -FN:748,UniversalGatewayV0._handleTokenDeposit -FNDA:0,UniversalGatewayV0._handleTokenDeposit +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 -BRDA:749,52,0,- -DA:750,0 -DA:754,0 -FN:754,UniversalGatewayV0._handleNativeWithdraw -FNDA:0,UniversalGatewayV0._handleNativeWithdraw DA:755,0 -DA:756,0 -BRDA:756,53,0,- -DA:764,0 -FN:764,UniversalGatewayV0._handleTokenWithdraw -FNDA:0,UniversalGatewayV0._handleTokenWithdraw +FN:755,UniversalGatewayV0.revertNative +FNDA:0,UniversalGatewayV0.revertNative +DA:762,0 +BRDA:762,44,0,- +DA:763,0 +BRDA:763,45,0,- +DA:765,0 +DA:766,0 +BRDA:766,46,0,- DA:768,0 -BRDA:768,54,0,- -DA:769,0 -DA:782,0 -FN:782,UniversalGatewayV0.swapToNative -FNDA:0,UniversalGatewayV0.swapToNative -DA:786,0 -BRDA:786,55,0,- +DA:778,0 +FN:778,UniversalGatewayV0.isTokenSupported +FNDA:0,UniversalGatewayV0.isTokenSupported +DA:780,0 +DA:787,0 +FN:787,UniversalGatewayV0.getMinMaxValueForNative +FNDA:0,UniversalGatewayV0.getMinMaxValueForNative DA:788,0 -BRDA:788,56,0,- -BRDA:788,56,1,- -DA:789,0 -DA:790,0 -BRDA:790,57,0,- -DA:791,0 +DA:792,0 DA:793,0 -BRDA:793,58,0,- -DA:795,0 -BRDA:795,59,0,- -DA:797,0 -DA:799,0 -DA:800,0 -DA:801,0 DA:804,0 -BRDA:804,60,0,- +FN:804,UniversalGatewayV0.getEthUsdPrice +FNDA:0,UniversalGatewayV0.getEthUsdPrice DA:805,0 +BRDA:805,47,0,- +DA:808,0 +BRDA:808,48,0,- DA:809,0 -DA:813,0 -DA:814,0 -DA:817,0 -DA:828,0 -DA:831,0 -DA:834,0 +DA:815,0 +DA:818,0 +BRDA:818,49,0,- +DA:821,0 +BRDA:821,50,0,- +DA:822,0 +DA:826,0 +DA:832,0 DA:835,0 +BRDA:835,51,0,- DA:836,0 -DA:839,0 -BRDA:839,61,0,- +BRDA:836,52,0,- +DA:837,0 +BRDA:837,53,0,- +DA:838,0 +DA:841,0 DA:844,0 -FN:844,UniversalGatewayV0._findV3PoolWithNative -FNDA:0,UniversalGatewayV0._findV3PoolWithNative +BRDA:844,54,0,- DA:845,0 -BRDA:845,62,0,- +BRDA:845,55,0,- DA:846,0 -BRDA:846,63,0,- -DA:848,0 -DA:852,0 -DA:853,0 +DA:847,0 +BRDA:847,55,1,- +DA:849,0 DA:854,0 -DA:855,0 -BRDA:855,64,0,- -DA:856,0 -DA:862,0 -DA:870,0 -FN:870,UniversalGatewayV0.receive +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,- +DA:869,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: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: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,- +DA:926,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:946,0 +DA:947,0 +BRDA:947,68,0,- +DA:955,0 +FN:955,UniversalGatewayV0._handleTokenWithdraw +FNDA:0,UniversalGatewayV0._handleTokenWithdraw +DA:959,0 +BRDA:959,69,0,- +DA:960,0 +DA:968,0 +FN:968,UniversalGatewayV0._consumeRateLimit +FNDA:0,UniversalGatewayV0._consumeRateLimit +DA:969,0 +DA:970,0 +BRDA:970,70,0,- +DA:972,0 +DA:973,0 +BRDA:973,71,0,- +DA:975,0 +DA:976,0 +DA:978,0 +BRDA:978,72,0,- +DA:979,0 +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:1003,0 +DA:1005,0 +DA:1006,0 +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 +DA:1098,0 +DA:1099,0 +BRDA:1099,85,0,- +DA:1100,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:1175,0 +BRDA:1175,92,0,- +DA:1177,0 +BRDA:1177,93,0,- +DA:1178,0 +BRDA:1178,94,0,- +DA:1179,0 +BRDA:1179,95,0,- +DA:1181,0 +DA:1183,0 +DA:1185,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: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: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:1223,0 +DA:1224,0 +BRDA:1224,102,0,- +DA:1225,0 +DA:1229,0 +FN:1229,UniversalGatewayV0.receive FNDA:0,UniversalGatewayV0.receive -DA:872,0 -BRDA:872,65,0,- -FNF:40 +DA:1231,0 +BRDA:1231,103,0,- +FNF:56 FNH:0 -LF:253 +LF:368 LH:0 -BRF:70 +BRF:107 BRH:0 end_of_record TN: +SF:src/Vault.sol +DA:55,83 +FN:55,Vault.initialize +FNDA:83,Vault.initialize +DA:56,83 +BRDA:56,0,0,4 +DA:57,4 +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 +FN:80,Vault.pause +FNDA:12,Vault.pause +DA:81,0 +DA:86,3 +FN:86,Vault.unpause +FNDA:3,Vault.unpause +DA:87,0 +DA:93,8 +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 +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 +FN:120,Vault.sweep +FNDA:6,Vault.sweep +DA:121,5 +BRDA:121,4,0,2 +DA:122,3 +DA:129,23 +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 +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 +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 +FN:187,Vault._enforceSupported +FNDA:31,Vault._enforceSupported +DA:189,31 +BRDA:189,14,0,6 +FNF:10 +FNH:10 +LF:59 +LH:53 +BRF:15 +BRH:15 +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: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 +end_of_record +TN: SF:test/BaseTest.t.sol -DA:91,191 +DA:91,192 FN:91,BaseTest.setUp -FNDA:191,BaseTest.setUp +FNDA:192,BaseTest.setUp DA:92,0 DA:93,0 DA:94,0 @@ -1102,269 +1629,269 @@ DA:98,0 DA:99,0 DA:100,0 DA:101,0 -DA:107,191 +DA:107,192 FN:107,BaseTest._createActors -FNDA:191,BaseTest._createActors -DA:108,191 -DA:109,191 -DA:110,191 -DA:111,191 -DA:112,191 -DA:113,191 -DA:114,191 -DA:115,191 -DA:116,191 -DA:117,191 -DA:119,191 -DA:120,191 -DA:121,191 -DA:122,191 -DA:123,191 -DA:124,191 -DA:125,191 -DA:126,191 -DA:127,191 -DA:128,191 -DA:131,191 +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 FN:131,BaseTest._fundActors -FNDA:191,BaseTest._fundActors -DA:132,191 -DA:133,191 -DA:134,191 -DA:135,191 -DA:136,191 -DA:137,191 -DA:138,191 -DA:145,191 +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 FN:145,BaseTest._deployMocks -FNDA:191,BaseTest._deployMocks -DA:146,191 -DA:147,191 -DA:148,191 -DA:150,191 -DA:151,191 -DA:152,191 -DA:158,191 +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 FN:158,BaseTest._deployUniswapPlaceholders -FNDA:191,BaseTest._deployUniswapPlaceholders -DA:161,191 -DA:162,191 -DA:168,191 +FNDA:192,BaseTest._deployUniswapPlaceholders +DA:161,192 +DA:162,192 +DA:168,192 FN:168,BaseTest._deployGateway -FNDA:191,BaseTest._deployGateway -DA:170,191 -DA:173,191 -DA:176,191 -DA:188,191 -DA:191,191 -DA:193,191 -DA:194,191 -DA:195,191 -DA:198,191 -FN:198,BaseTest._initializeGateway -FNDA:191,BaseTest._initializeGateway -DA:201,191 -DA:202,191 -DA:203,191 -DA:204,191 -DA:210,191 -FN:210,BaseTest._deployOracles -FNDA:191,BaseTest._deployOracles -DA:211,191 -DA:213,191 -DA:216,191 -DA:218,191 -DA:221,191 -FN:221,BaseTest._wireOraclesToGateway -FNDA:191,BaseTest._wireOraclesToGateway -DA:223,191 -DA:224,191 -DA:228,191 -FN:228,BaseTest._setupNativeTokenSupport -FNDA:191,BaseTest._setupNativeTokenSupport -DA:230,191 -DA:231,191 -DA:232,191 -DA:233,191 -DA:235,191 -DA:236,191 -DA:240,0 -FN:240,BaseTest.setEthUsdPrice1e8 -FNDA:0,BaseTest.setEthUsdPrice1e8 +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 -DA:244,0 -FN:244,BaseTest.setChainlinkStalePeriod -FNDA:0,BaseTest.setChainlinkStalePeriod +FN:241,BaseTest.setEthUsdPrice1e8 +FNDA:0,BaseTest.setEthUsdPrice1e8 +DA:242,0 DA:245,0 +FN:245,BaseTest.setChainlinkStalePeriod +FNDA:0,BaseTest.setChainlinkStalePeriod DA:246,0 -DA:249,0 -FN:249,BaseTest.enableSequencerFeed -FNDA:0,BaseTest.enableSequencerFeed +DA:247,0 DA:250,0 +FN:250,BaseTest.enableSequencerFeed +FNDA:0,BaseTest.enableSequencerFeed DA:251,0 -DA:254,0 -FN:254,BaseTest.setSequencerStatusDown -FNDA:0,BaseTest.setSequencerStatusDown +DA:252,0 DA:255,0 -DA:258,0 -FN:258,BaseTest.setSequencerGracePeriod -FNDA:0,BaseTest.setSequencerGracePeriod +FN:255,BaseTest.setSequencerStatusDown +FNDA:0,BaseTest.setSequencerStatusDown +DA:256,0 DA:259,0 +FN:259,BaseTest.setSequencerGracePeriod +FNDA:0,BaseTest.setSequencerGracePeriod DA:260,0 -DA:266,191 -FN:266,BaseTest._mintAndApproveTokens -FNDA:191,BaseTest._mintAndApproveTokens -DA:267,191 -DA:268,191 -DA:269,191 -DA:270,191 -DA:271,191 -DA:272,191 -DA:275,191 -DA:276,955 -DA:280,191 -DA:281,955 -DA:285,191 -DA:286,955 -DA:287,955 -DA:288,955 -DA:295,0 -FN:295,BaseTest.buildMinimalPayload +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 FNDA:0,BaseTest.buildMinimalPayload -DA:300,0 -DA:311,0 -DA:314,14 -FN:314,BaseTest.buildValuePayload +DA:301,0 +DA:312,0 +DA:315,14 +FN:315,BaseTest.buildValuePayload FNDA:14,BaseTest.buildValuePayload -DA:319,14 -DA:330,14 -DA:333,29 -FN:333,BaseTest.revertCfg -FNDA:29,BaseTest.revertCfg +DA:320,14 +DA:331,14 DA:334,29 -DA:339,41 -FN:339,BaseTest.buildDefaultPayload -FNDA:41,BaseTest.buildDefaultPayload +FN:334,BaseTest.revertCfg +FNDA:29,BaseTest.revertCfg +DA:335,29 DA:340,41 -DA:355,41 -FN:355,BaseTest.buildDefaultRevertInstructions -FNDA:41,BaseTest.buildDefaultRevertInstructions +FN:340,BaseTest.buildDefaultPayload +FNDA:41,BaseTest.buildDefaultPayload +DA:341,41 DA:356,41 -DA:362,1910 -FN:362,BaseTest.mintAndApprove -FNDA:1910,BaseTest.mintAndApprove -DA:364,1910 -BRDA:364,0,0,955 -BRDA:364,0,1,- -DA:365,955 -DA:366,955 -BRDA:366,1,0,955 -BRDA:366,1,1,- -DA:367,955 -DA:369,0 -DA:373,1910 -DA:374,1910 -DA:377,955 -FN:377,BaseTest.mintWETH -FNDA:955,BaseTest.mintWETH -DA:378,955 -DA:379,955 -DA:380,955 -DA:387,0 -FN:387,BaseTest.setV3FeeOrder -FNDA:0,BaseTest.setV3FeeOrder +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 +FNDA:0,BaseTest.setV3FeeOrder DA:389,0 -DA:392,0 -FN:392,BaseTest.setRouters -FNDA:0,BaseTest.setRouters +DA:390,0 DA:393,0 +FN:393,BaseTest.setRouters +FNDA:0,BaseTest.setRouters DA:394,0 -DA:397,0 -FN:397,BaseTest.setCaps -FNDA:0,BaseTest.setCaps +DA:395,0 DA:398,0 +FN:398,BaseTest.setCaps +FNDA:0,BaseTest.setCaps DA:399,0 -DA:402,0 -FN:402,BaseTest.toggleSupport -FNDA:0,BaseTest.toggleSupport +DA:400,0 DA:403,0 +FN:403,BaseTest.toggleSupport +FNDA:0,BaseTest.toggleSupport DA:404,0 DA:405,0 DA:406,0 -DA:408,0 -DA:410,0 +DA:407,0 +DA:409,0 DA:411,0 DA:412,0 -DA:418,0 -FN:418,BaseTest.recordAndGetLogs -FNDA:0,BaseTest.recordAndGetLogs +DA:413,0 DA:419,0 +FN:419,BaseTest.recordAndGetLogs +FNDA:0,BaseTest.recordAndGetLogs DA:420,0 -DA:423,0 -FN:423,BaseTest.assertDualEmitOrder -FNDA:0,BaseTest.assertDualEmitOrder +DA:421,0 DA:424,0 +FN:424,BaseTest.assertDualEmitOrder +FNDA:0,BaseTest.assertDualEmitOrder DA:425,0 -DA:427,0 +DA:426,0 DA:428,0 -BRDA:428,2,0,- DA:429,0 -DA:431,0 -BRDA:431,3,0,- +BRDA:429,2,0,- +DA:430,0 DA:432,0 +BRDA:432,3,0,- DA:433,0 -BRDA:433,4,0,- -BRDA:433,4,1,- -DA:437,0 -BRDA:437,5,0,- -BRDA:437,5,1,- -DA:455,4 -FN:455,BaseTest.buildERC20Payload +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:461,4 -DA:473,4 -DA:475,4 -DA:482,0 -FN:482,BaseTest.fundUserWithMainnetTokens +DA:462,4 +DA:474,4 +DA:476,4 +DA:483,0 +FN:483,BaseTest.fundUserWithMainnetTokens FNDA:0,BaseTest.fundUserWithMainnetTokens -DA:484,0 DA:485,0 -BRDA:485,6,0,- -BRDA:485,6,1,- -DA:487,0 +DA:486,0 +BRDA:486,6,0,- +BRDA:486,6,1,- DA:488,0 -BRDA:488,7,0,- -BRDA:488,7,1,- -DA:490,0 +DA:489,0 +BRDA:489,7,0,- +BRDA:489,7,1,- DA:491,0 -BRDA:491,8,0,- -BRDA:491,8,1,- -DA:493,0 +DA:492,0 +BRDA:492,8,0,- +BRDA:492,8,1,- DA:494,0 -BRDA:494,9,0,- -BRDA:494,9,1,- -DA:496,0 +DA:495,0 +BRDA:495,9,0,- +BRDA:495,9,1,- DA:497,0 -BRDA:497,10,0,- -BRDA:497,10,1,- -DA:499,0 +DA:498,0 +BRDA:498,10,0,- +BRDA:498,10,1,- DA:500,0 -BRDA:500,11,0,- -BRDA:500,11,1,- -DA:502,0 -DA:504,0 -DA:508,0 +DA:501,0 +BRDA:501,11,0,- +BRDA:501,11,1,- +DA:503,0 +DA:505,0 DA:509,0 -BRDA:509,12,0,- -BRDA:509,12,1,- -DA:512,0 +DA:510,0 +BRDA:510,12,0,- +BRDA:510,12,1,- DA:513,0 -BRDA:513,13,0,- -BRDA:513,13,1,- -DA:515,0 -DA:517,0 -DA:519,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 @@ -1387,24 +1914,24 @@ BRH:0 end_of_record TN: SF:test/mocks/MockAggregatorV3.sol -DA:9,212 +DA:9,213 FN:9,MockAggregatorV3.constructor -FNDA:212,MockAggregatorV3.constructor -DA:10,212 -DA:11,212 -DA:12,212 -DA:13,212 -DA:16,214 +FNDA:213,MockAggregatorV3.constructor +DA:10,213 +DA:11,213 +DA:12,213 +DA:13,213 +DA:16,215 FN:16,MockAggregatorV3.setAnswer -FNDA:214,MockAggregatorV3.setAnswer -DA:17,214 -DA:18,214 -DA:19,214 -DA:20,214 -DA:23,210 +FNDA:215,MockAggregatorV3.setAnswer +DA:17,215 +DA:18,215 +DA:19,215 +DA:20,215 +DA:23,211 FN:23,MockAggregatorV3.decimals -FNDA:210,MockAggregatorV3.decimals -DA:24,210 +FNDA:211,MockAggregatorV3.decimals +DA:24,211 DA:27,100 FN:27,MockAggregatorV3.latestRoundData FNDA:100,MockAggregatorV3.latestRoundData @@ -1418,27 +1945,27 @@ BRH:0 end_of_record TN: SF:test/mocks/MockERC20.sol -DA:30,447 +DA:30,810 FN:30,MockERC20.constructor -FNDA:447,MockERC20.constructor -DA:31,447 -DA:32,447 -BRDA:32,0,0,62 -DA:33,62 +FNDA:810,MockERC20.constructor +DA:31,810 +DA:32,810 +BRDA:32,0,0,423 +DA:33,423 DA:40,0 FN:40,MockERC20.decimals FNDA:0,MockERC20.decimals DA:41,0 -DA:49,2073 +DA:49,2444 FN:49,MockERC20.mint -FNDA:2073,MockERC20.mint -DA:50,2073 +FNDA:2444,MockERC20.mint +DA:50,2444 BRDA:50,1,0,- BRDA:50,1,1,- -DA:51,2073 +DA:51,2444 BRDA:51,2,0,- BRDA:51,2,1,- -DA:52,2073 +DA:52,2444 DA:60,0 FN:60,MockERC20.burn FNDA:0,MockERC20.burn @@ -1459,32 +1986,32 @@ DA:72,0 BRDA:72,6,0,- BRDA:72,6,1,- DA:73,0 -DA:79,5 +DA:79,94 FN:79,MockERC20.transfer -FNDA:5,MockERC20.transfer -DA:80,5 +FNDA:94,MockERC20.transfer +DA:80,94 BRDA:80,7,0,- BRDA:80,7,1,- -DA:81,5 +DA:81,94 BRDA:81,8,0,- BRDA:81,8,1,- -DA:82,5 +DA:82,94 BRDA:82,9,0,- BRDA:82,9,1,- -DA:83,5 -DA:89,40 +DA:83,94 +DA:89,48 FN:89,MockERC20.transferFrom -FNDA:40,MockERC20.transferFrom -DA:90,40 +FNDA:48,MockERC20.transferFrom +DA:90,48 BRDA:90,10,0,- BRDA:90,10,1,- -DA:91,40 +DA:91,48 BRDA:91,11,0,- BRDA:91,11,1,- -DA:92,40 +DA:92,48 BRDA:92,12,0,- BRDA:92,12,1,- -DA:93,40 +DA:93,48 DA:99,0 FN:99,MockERC20.pauseTransfers FNDA:0,MockERC20.pauseTransfers @@ -1581,13 +2108,304 @@ BRF:29 BRH:1 end_of_record TN: +SF:test/mocks/MockPRC20.sol +DA:57,0 +FN:57,MockPRC20.onlyUniversalExecutor +FNDA:0,MockPRC20.onlyUniversalExecutor +DA:58,0 +BRDA:58,0,0,- +BRDA:58,0,1,- +DA:65,217 +FN:65,MockPRC20.constructor +FNDA:217,MockPRC20.constructor +DA:75,217 +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:89,0 +FN:89,MockPRC20.name +FNDA:0,MockPRC20.name +DA:90,0 +DA:93,0 +FN:93,MockPRC20.symbol +FNDA:0,MockPRC20.symbol +DA:94,0 +DA:97,0 +FN:97,MockPRC20.decimals +FNDA:0,MockPRC20.decimals +DA:98,0 +DA:101,0 +FN:101,MockPRC20.totalSupply +FNDA:0,MockPRC20.totalSupply +DA:102,0 +DA:105,99 +FN:105,MockPRC20.balanceOf +FNDA:99,MockPRC20.balanceOf +DA:106,99 +DA:109,0 +FN:109,MockPRC20.allowance +FNDA:0,MockPRC20.allowance +DA:110,0 +DA:114,31 +FN:114,MockPRC20.transfer +FNDA:31,MockPRC20.transfer +DA:115,31 +DA:116,31 +DA:119,46 +FN:119,MockPRC20.transferFrom +FNDA:46,MockPRC20.transferFrom +DA:120,46 +DA:122,41 +DA:123,41 +BRDA:123,2,0,- +BRDA:123,2,1,- +DA:126,41 +DA:128,46 +DA:130,39 +DA:133,217 +FN:133,MockPRC20.approve +FNDA:217,MockPRC20.approve +DA:134,217 +BRDA:134,3,0,- +BRDA:134,3,1,- +DA:135,217 +DA:136,217 +DA:137,217 +DA:140,18 +FN:140,MockPRC20.burn +FNDA:18,MockPRC20.burn +DA:141,18 +DA:142,18 +DA:151,0 +FN:151,MockPRC20.deposit +FNDA:0,MockPRC20.deposit +DA:152,0 +BRDA:152,4,0,- +BRDA:152,4,1,- +DA:157,0 +DA:159,0 +DA:160,0 +DA:166,0 +FN:166,MockPRC20.GAS_LIMIT +FNDA:0,MockPRC20.GAS_LIMIT +DA:167,0 +DA:171,0 +FN:171,MockPRC20.withdrawGasFeeWithGasLimit +FNDA:0,MockPRC20.withdrawGasFeeWithGasLimit +DA:172,0 +DA:179,0 +FN:179,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 +FNDA:0,MockPRC20.updateProtocolFlatFee +DA:187,0 +DA:188,0 +DA:192,0 +FN:192,MockPRC20.setName +FNDA:0,MockPRC20.setName +DA:193,0 +DA:197,0 +FN:197,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 +FNDA:18,MockPRC20._burn +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 +DA:253,18 +BRDA:253,12,0,- +BRDA:253,12,1,- +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 +FNDA:1,MockPRC20.setBalance +DA:268,1 +DA:271,0 +FN:271,MockPRC20.setAllowance +FNDA:0,MockPRC20.setAllowance +DA:272,0 +FNF:25 +FNH:11 +LF:89 +LH:55 +BRF:26 +BRH:2 +end_of_record +TN: +SF:test/mocks/MockReentrantContract.sol +DA:25,135 +FN:25,MockReentrantContract.constructor +FNDA:135,MockReentrantContract.constructor +DA:26,135 +DA:27,135 +DA:28,135 +DA:35,1 +FN:35,MockReentrantContract.attemptReentrancy +FNDA:1,MockReentrantContract.attemptReentrancy +DA:41,1 +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 +FNDA:0,MockReentrantContract.enableVaultReentry +DA:74,0 +DA:75,0 +DA:76,0 +DA:79,0 +FN:79,MockReentrantContract.pullTokens +FNDA:0,MockReentrantContract.pullTokens +DA:80,0 +DA:82,0 +BRDA:82,0,0,- +DA:83,0 +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,- +DA:92,0 +BRDA:92,3,0,- +BRDA:92,3,1,- +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 +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 +FNDA:0,MockReentrantContract.transferFrom +DA:123,0 +DA:127,0 +DA:130,0 +FN:130,MockReentrantContract.onERC721Received +FNDA:0,MockReentrantContract.onERC721Received +DA:131,0 +FNF:10 +FNH:5 +LF:37 +LH:12 +BRF:14 +BRH:0 +end_of_record +TN: +SF:test/mocks/MockRevertingTarget.sol +DA:15,3 +FN:15,MockRevertingTarget.receiveFunds +FNDA:3,MockRevertingTarget.receiveFunds +DA:16,3 +DA:23,2 +FN:23,MockRevertingTarget.receiveFundsGasHeavy +FNDA:2,MockRevertingTarget.receiveFundsGasHeavy +DA:25,2 +DA:26,336014 +DA:33,0 +FN:33,MockRevertingTarget.receiveFundsWithCustomRevert +FNDA:0,MockRevertingTarget.receiveFundsWithCustomRevert +DA:34,0 +DA:41,0 +FN:41,MockRevertingTarget.receiveFundsNonPayable +FNDA:0,MockRevertingTarget.receiveFundsNonPayable +DA:48,0 +FN:48,MockRevertingTarget.pullTokensRevertWithReason +FNDA:0,MockRevertingTarget.pullTokensRevertWithReason +DA:49,0 +DA:55,0 +FN:55,MockRevertingTarget.pullTokensRevertNoReason +FNDA:0,MockRevertingTarget.pullTokensRevertNoReason +DA:56,0 +DA:62,0 +FN:62,MockRevertingTarget.pullTokens +FNDA:0,MockRevertingTarget.pullTokens +DA:63,0 +FNF:7 +FNH:2 +LF:14 +LH:5 +BRF:0 +BRH:0 +end_of_record +TN: SF:test/mocks/MockSequencerUptimeFeed.sol -DA:7,192 +DA:7,193 FN:7,MockSequencerUptimeFeed.setStatus -FNDA:192,MockSequencerUptimeFeed.setStatus -DA:8,192 -DA:9,192 -DA:10,192 +FNDA:193,MockSequencerUptimeFeed.setStatus +DA:8,193 +DA:9,193 +DA:10,193 DA:13,0 FN:13,MockSequencerUptimeFeed.decimals FNDA:0,MockSequencerUptimeFeed.decimals @@ -1604,25 +2422,331 @@ BRF:0 BRH:0 end_of_record TN: +SF:test/mocks/MockTarget.sol +DA:12,15 +FN:12,MockTarget.receiveFunds +FNDA:15,MockTarget.receiveFunds +DA:13,15 +DA:14,15 +DA:18,8 +FN:18,MockTarget.receiveToken +FNDA:8,MockTarget.receiveToken +DA:19,8 +DA:20,8 +DA:21,8 +DA:24,8 +DA:28,0 +FN:28,MockTarget.fallback +FNDA:0,MockTarget.fallback +DA:29,0 +DA:30,0 +DA:34,3 +FN:34,MockTarget.receive +FNDA:3,MockTarget.receive +DA:35,3 +DA:36,3 +FNF:4 +FNH:3 +LF:14 +LH:11 +BRF:0 +BRH:0 +end_of_record +TN: +SF:test/mocks/MockTokenApprovalVariants.sol +DA:27,119 +FN:27,MockTokenApprovalVariants.constructor +FNDA:119,MockTokenApprovalVariants.constructor +DA:28,119 +DA:35,9 +FN:35,MockTokenApprovalVariants.setApprovalBehavior +FNDA:9,MockTokenApprovalVariants.setApprovalBehavior +DA:36,9 +DA:45,21 +FN:45,MockTokenApprovalVariants.approve +FNDA:21,MockTokenApprovalVariants.approve +DA:47,21 +BRDA:47,0,0,2 +DA:48,2 +DA:51,19 +BRDA:51,1,0,2 +DA:52,2 +DA:56,17 +DA:59,17 +BRDA:59,2,0,7 +DA:60,7 +DA:65,0 +FNF:3 +FNH:3 +LF:13 +LH:12 +BRF:3 +BRH:3 +end_of_record +TN: +SF:test/mocks/MockUSDTToken.sol +DA:13,11 +FN:13,MockUSDTToken.approve +FNDA:11,MockUSDTToken.approve +DA:15,11 +BRDA:15,0,0,1 +DA:16,1 +DA:20,10 +DA:21,10 +FNF:1 +FNH:1 +LF:5 +LH:5 +BRF:1 +BRH:1 +end_of_record +TN: +SF:test/mocks/MockUniversalCoreReal.sol +DA:77,114 +FN:77,MockUniversalCoreReal.constructor +FNDA:114,MockUniversalCoreReal.constructor +DA:78,114 +DA:79,114 +DA:80,114 +DA:84,0 +FN:84,MockUniversalCoreReal.onlyUEModule +FNDA:0,MockUniversalCoreReal.onlyUEModule +DA:85,0 +BRDA:85,0,0,- +BRDA:85,0,1,- +DA:89,50 +FN:89,MockUniversalCoreReal.onlyRole +FNDA:50,MockUniversalCoreReal.onlyRole +DA:90,50 +BRDA:90,1,0,- +BRDA:90,1,1,- +DA:94,0 +FN:94,MockUniversalCoreReal.whenNotPaused +FNDA:0,MockUniversalCoreReal.whenNotPaused +DA:95,0 +BRDA:95,2,0,- +BRDA:95,2,1,- +DA:100,216 +FN:100,MockUniversalCoreReal.hasRole +FNDA:216,MockUniversalCoreReal.hasRole +DA:101,216 +DA:104,0 +FN:104,MockUniversalCoreReal.grantRole +FNDA:0,MockUniversalCoreReal.grantRole +DA:105,0 +DA:108,0 +FN:108,MockUniversalCoreReal.revokeRole +FNDA:0,MockUniversalCoreReal.revokeRole +DA:109,0 +DA:113,31 +FN:113,MockUniversalCoreReal.isSupportedToken +FNDA:31,MockUniversalCoreReal.isSupportedToken +DA:114,31 +DA:117,115 +FN:117,MockUniversalCoreReal.setSupportedToken +FNDA:115,MockUniversalCoreReal.setSupportedToken +DA:118,115 +DA:122,0 +FN:122,MockUniversalCoreReal.depositPRC20Token +FNDA:0,MockUniversalCoreReal.depositPRC20Token +DA:123,0 +BRDA:123,3,0,- +BRDA:123,3,1,- +DA:124,0 +BRDA:124,4,0,- +BRDA:124,4,1,- +DA:125,0 +BRDA:125,5,0,- +BRDA:125,5,1,- +DA:129,0 +DA:130,0 +BRDA:130,6,0,- +BRDA:130,6,1,- +DA:133,0 +FN:133,MockUniversalCoreReal.depositPRC20WithAutoSwap +FNDA:0,MockUniversalCoreReal.depositPRC20WithAutoSwap +DA:141,0 +BRDA:141,7,0,- +BRDA:141,7,1,- +DA:142,0 +BRDA:142,8,0,- +BRDA:142,8,1,- +DA:143,0 +BRDA:143,9,0,- +BRDA:143,9,1,- +DA:144,0 +BRDA:144,10,0,- +BRDA:144,10,1,- +DA:147,0 +BRDA:147,11,0,- +DA:148,0 +DA:149,0 +BRDA:149,12,0,- +BRDA:149,12,1,- +DA:152,0 +BRDA:152,13,0,- +DA:153,0 +DA:156,0 +BRDA:156,14,0,- +BRDA:156,14,1,- +DA:159,0 +DA:162,0 +DA:166,0 +FN:166,MockUniversalCoreReal.setGasPCPool +FNDA:0,MockUniversalCoreReal.setGasPCPool +DA:167,0 +BRDA:167,15,0,- +BRDA:167,15,1,- +DA:171,0 +DA:173,0 +DA:174,0 +DA:177,50 +FN:177,MockUniversalCoreReal.setGasPrice +FNDA:50,MockUniversalCoreReal.setGasPrice +DA:178,50 +DA:179,50 +DA:182,51 +FN:182,MockUniversalCoreReal.setGasTokenPRC20 +FNDA:51,MockUniversalCoreReal.setGasTokenPRC20 +DA:183,51 +BRDA:183,16,0,- +BRDA:183,16,1,- +DA:184,51 +DA:185,51 +DA:189,0 +FN:189,MockUniversalCoreReal.onlyOwner +FNDA:0,MockUniversalCoreReal.onlyOwner +DA:190,0 +BRDA:190,17,0,- +BRDA:190,17,1,- +DA:194,0 +FN:194,MockUniversalCoreReal.setAutoSwapSupported +FNDA:0,MockUniversalCoreReal.setAutoSwapSupported +DA:195,0 +DA:196,0 +DA:199,0 +FN:199,MockUniversalCoreReal.setWPCContractAddress +FNDA:0,MockUniversalCoreReal.setWPCContractAddress +DA:200,0 +BRDA:200,18,0,- +BRDA:200,18,1,- +DA:201,0 +DA:202,0 +DA:205,0 +FN:205,MockUniversalCoreReal.setUniswapV3Addresses +FNDA:0,MockUniversalCoreReal.setUniswapV3Addresses +DA:206,0 +BRDA:206,19,0,- +BRDA:206,19,1,- +DA:209,0 +DA:210,0 +DA:211,0 +DA:212,0 +DA:215,0 +FN:215,MockUniversalCoreReal.setDefaultFeeTier +FNDA:0,MockUniversalCoreReal.setDefaultFeeTier +DA:216,0 +BRDA:216,20,0,- +BRDA:216,20,1,- +DA:217,0 +BRDA:217,21,0,- +BRDA:217,21,1,- +DA:218,0 +DA:219,0 +DA:222,0 +FN:222,MockUniversalCoreReal.setSlippageTolerance +FNDA:0,MockUniversalCoreReal.setSlippageTolerance +DA:223,0 +BRDA:223,22,0,- +BRDA:223,22,1,- +DA:224,0 +BRDA:224,23,0,- +BRDA:224,23,1,- +DA:225,0 +DA:226,0 +DA:229,0 +FN:229,MockUniversalCoreReal.setDefaultDeadlineMins +FNDA:0,MockUniversalCoreReal.setDefaultDeadlineMins +DA:230,0 +DA:231,0 +DA:234,0 +FN:234,MockUniversalCoreReal.pause +FNDA:0,MockUniversalCoreReal.pause +DA:235,0 +DA:238,0 +FN:238,MockUniversalCoreReal.unpause +FNDA:0,MockUniversalCoreReal.unpause +DA:239,0 +DA:243,0 +FN:243,MockUniversalCoreReal.paused +FNDA:0,MockUniversalCoreReal.paused +DA:244,0 +DA:247,0 +FN:247,MockUniversalCoreReal.getSwapQuote +FNDA:0,MockUniversalCoreReal.getSwapQuote +DA:249,0 +DA:252,0 +FN:252,MockUniversalCoreReal.withdrawGasFee +FNDA:0,MockUniversalCoreReal.withdrawGasFee +DA:253,0 +DA:255,0 +DA:256,0 +BRDA:256,24,0,- +BRDA:256,24,1,- +DA:258,0 +DA:259,0 +BRDA:259,25,0,- +BRDA:259,25,1,- +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 +FNDA:0,MockUniversalCoreReal.updateBaseGasLimit +DA:279,0 +DA:280,0 +DA:281,0 +DA:285,0 +FN:285,MockUniversalCoreReal.setUniversalExecutorModule +FNDA:0,MockUniversalCoreReal.setUniversalExecutorModule +FNF:29 +FNH:8 +LF:106 +LH:26 +BRF:54 +BRH:0 +end_of_record +TN: SF:test/mocks/MockWETH.sol DA:41,0 FN:41,MockWETH.decimals FNDA:0,MockWETH.decimals DA:42,0 -DA:48,955 +DA:48,960 FN:48,MockWETH.deposit -FNDA:955,MockWETH.deposit -DA:49,955 +FNDA:960,MockWETH.deposit +DA:49,960 BRDA:49,0,0,- BRDA:49,0,1,- -DA:50,955 +DA:50,960 BRDA:50,1,0,- BRDA:50,1,1,- -DA:51,955 +DA:51,960 BRDA:51,2,0,- BRDA:51,2,1,- -DA:53,955 -DA:54,955 +DA:53,960 +DA:54,960 DA:61,0 FN:61,MockWETH.withdraw FNDA:0,MockWETH.withdraw @@ -1640,19 +2764,19 @@ BRDA:65,6,0,- BRDA:65,6,1,- DA:67,0 DA:71,0 -DA:80,955 +DA:80,960 FN:80,MockWETH.transfer -FNDA:955,MockWETH.transfer -DA:81,955 +FNDA:960,MockWETH.transfer +DA:81,960 BRDA:81,7,0,- BRDA:81,7,1,- -DA:82,955 +DA:82,960 BRDA:82,8,0,- BRDA:82,8,1,- -DA:83,955 +DA:83,960 BRDA:83,9,0,- BRDA:83,9,1,- -DA:84,955 +DA:84,960 DA:90,0 FN:90,MockWETH.transferFrom FNDA:0,MockWETH.transferFrom diff --git a/contracts/evm-gateway/src/UniversalGateway.sol b/contracts/evm-gateway/src/UniversalGateway.sol index 9206644..e9b220e 100644 --- a/contracts/evm-gateway/src/UniversalGateway.sol +++ b/contracts/evm-gateway/src/UniversalGateway.sol @@ -66,10 +66,13 @@ contract UniversalGateway is using SafeERC20 for IERC20; bytes32 public constant TSS_ROLE = keccak256("TSS_ROLE"); - bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant VAULT_ROLE = keccak256("VAULT_ROLE"); /// @notice The current TSS address for UniversalGateway address public TSS_ADDRESS; + + /// @notice The Vault contract address + address public VAULT; /// @notice Rate-Limiting CAPS and States uint256 public BLOCK_USD_CAP; @@ -98,16 +101,14 @@ contract UniversalGateway is uint256 public l2SequencerGracePeriodSec; // L2 Sequencer grace period. (e.g., 300 seconds) AggregatorV3Interface public l2SequencerFeed; // L2 Sequencer uptime feed & grace period for rollups (if set, enforce sequencer up + grace) - - - /// @notice Gap for future upgrades. - uint256[43] private __gap; + /// @notice Map to track if a payload has been executed + mapping(bytes32 => bool) public isExecuted; /** * @notice Initialize the UniversalGateway contract * @param admin DEFAULT_ADMIN_ROLE holder - * @param pauser PAUSER_ROLE * @param tss initial TSS address + * @param vaultAddress Vault contract address * @param minCapUsd min USD cap (1e18 decimals) * @param maxCapUsd max USD cap (1e18 decimals) * @param factory UniswapV2 factory @@ -115,15 +116,15 @@ contract UniversalGateway is */ function initialize( address admin, - address pauser, address tss, + address vaultAddress, uint256 minCapUsd, uint256 maxCapUsd, address factory, address router, address _wethAddress ) external initializer { - if (admin == address(0) || pauser == address(0) || tss == address(0) || _wethAddress == address(0)) { + if (admin == address(0) || tss == address(0) || vaultAddress == address(0) || _wethAddress == address(0)) { revert Errors.ZeroAddress(); } @@ -133,10 +134,11 @@ contract UniversalGateway is __AccessControl_init(); _grantRole(DEFAULT_ADMIN_ROLE, admin); - _grantRole(PAUSER_ROLE, pauser); _grantRole(TSS_ROLE, tss); + _grantRole(VAULT_ROLE, vaultAddress); TSS_ADDRESS = tss; + VAULT = vaultAddress; MIN_CAP_UNIVERSAL_TX_USD = minCapUsd; MAX_CAP_UNIVERSAL_TX_USD = maxCapUsd; @@ -162,16 +164,16 @@ contract UniversalGateway is // ========================= // ADMIN ACTIONS // ========================= - function pause() external whenNotPaused onlyRole(PAUSER_ROLE) { + function pause() external whenNotPaused onlyRole(DEFAULT_ADMIN_ROLE) { _pause(); } - function unpause() external whenPaused onlyRole(PAUSER_ROLE) { + function unpause() external whenPaused onlyRole(DEFAULT_ADMIN_ROLE) { _unpause(); } /// @notice Allows the admin to set the TSS address /// @param newTSS new TSS address - function setTSS(address newTSS) external onlyRole(DEFAULT_ADMIN_ROLE) whenNotPaused { + function setTSS(address newTSS) external onlyRole(DEFAULT_ADMIN_ROLE) { if (newTSS == address(0)) revert Errors.ZeroAddress(); address old = TSS_ADDRESS; @@ -181,6 +183,20 @@ contract UniversalGateway is TSS_ADDRESS = newTSS; } + + /// @notice Allows the admin to update the Vault address + /// @param newVault new Vault address + function updateVault(address newVault) external onlyRole(DEFAULT_ADMIN_ROLE) whenPaused { + if (newVault == address(0)) revert Errors.ZeroAddress(); + address old = VAULT; + + // transfer role + if (hasRole(VAULT_ROLE, old)) _revokeRole(VAULT_ROLE, old); + _grantRole(VAULT_ROLE, newVault); + + VAULT = newVault; + emit VaultUpdated(old, newVault); + } /// @notice Allows the admin to set the USD cap ranges /// @param minCapUsd minimum USD cap @@ -490,54 +506,143 @@ contract UniversalGateway is } /// @inheritdoc IUniversalGateway - function withdrawFunds(address recipient, address token, uint256 amount) + function revertUniversalTxToken(address token, uint256 amount, RevertInstructions calldata revertInstruction) external nonReentrant whenNotPaused - onlyTSS + onlyRole(VAULT_ROLE) { - if (recipient == address(0)) revert Errors.InvalidRecipient(); + if (revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); if (amount == 0) revert Errors.InvalidAmount(); - - if (token == address(0)) { - _handleNativeWithdraw(recipient, amount); - } else { - _handleTokenWithdraw(token, recipient, amount); - } - - emit WithdrawFunds(recipient, amount, token); + + IERC20(token).safeTransfer(revertInstruction.fundRecipient, amount); + + emit RevertUniversalTx(revertInstruction.fundRecipient, token, amount, revertInstruction); } - + /// @inheritdoc IUniversalGateway - function revertWithdrawFunds(address token, uint256 amount, RevertInstructions calldata revertInstruction) + function revertUniversalTx(uint256 amount, RevertInstructions calldata revertInstruction) external + payable nonReentrant whenNotPaused onlyTSS { if (revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + if (amount == 0 || msg.value != amount) revert Errors.InvalidAmount(); + + (bool ok,) = payable(revertInstruction.fundRecipient).call{ value: amount }(""); + if (!ok) revert Errors.WithdrawFailed(); + + emit RevertUniversalTx(revertInstruction.fundRecipient, address(0), amount, revertInstruction); + } + + // ========================= + // GATEWAY Withdraw and Payload Execution Paths + // ========================= + + function withdrawToken( + bytes32 txID, + address originCaller, + address token, + address to, + uint256 amount + ) external nonReentrant whenNotPaused onlyRole(VAULT_ROLE) { + if (isExecuted[txID]) 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); + isExecuted[txID] = true; + IERC20(token).safeTransfer(to, amount); + emit WithdrawToken(txID, originCaller, token, to, amount); + } + + /// @notice Executes a Universal Transaction on this chain triggered by TSS after validation on Push Chain. + /// @dev Allows outbound payload execution from Push Chain to external chains. + /// - The tokens used for payload execution, are to be burnt on Push Chain. + /// - approval and reset of approval is handled by the gateway. + /// - tokens are transferred from Vault to Gateway before calling this function + /// @param txID unique transaction identifier + /// @param originCaller original caller/user on source chain + /// @param token token address (ERC20 token) + /// @param target target contract address to execute call + /// @param amount amount of token to send along + /// @param payload calldata to be executed on target + function executeUniversalTx( + bytes32 txID, + address originCaller, + address token, + address target, + uint256 amount, + bytes calldata payload + ) external nonReentrant whenNotPaused onlyRole(VAULT_ROLE) { + if (isExecuted[txID]) 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 + + if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); + + isExecuted[txID] = true; + + _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 + + // Return any remaining tokens to the Vault + uint256 remainingBalance = IERC20(token).balanceOf(address(this)); + if (remainingBalance > 0) { + IERC20(token).safeTransfer(VAULT, remainingBalance); } + + emit UniversalTxExecuted(txID, originCaller, target, token, amount, payload); + } + + /// @notice Executes a Universal Transaction with native tokens on this chain triggered by TSS after validation on Push Chain. + /// @dev Allows outbound payload execution from Push Chain to external chains with native tokens. + /// @param txID unique transaction identifier + /// @param originCaller original caller/user on source chain + /// @param target target contract address to execute call + /// @param amount amount of native token to send along + /// @param payload calldata to be executed on target + function executeUniversalTx( + bytes32 txID, + address originCaller, + address target, + uint256 amount, + bytes calldata payload + ) external payable nonReentrant whenNotPaused onlyRole(TSS_ROLE) { + if (isExecuted[txID]) 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(); - emit WithdrawFunds(revertInstruction.fundRecipient, amount, token); + isExecuted[txID] = true; + + _executeCall(target, payload, amount); + + emit UniversalTxExecuted(txID, originCaller, target, address(0), amount, payload); } // ========================= // PUBLIC HELPERS // ========================= - /// @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() public view returns (uint256 minValue, uint256 maxValue) { - (uint256 ethUsdPrice,) = getEthUsdPrice(); // ETH price in USD (1e18 scaled) + /// @inheritdoc IUniversalGateway + function isSupportedToken(address token) public view returns (bool) { + return tokenToLimitThreshold[token] != 0; + } + /// @inheritdoc IUniversalGateway + function getMinMaxValueForNative() external view returns (uint256 minValue, uint256 maxValue) { + (uint256 ethUsdPrice,) = getEthUsdPrice(); // ETH price in USD (1e18 scaled) minValue = (MIN_CAP_UNIVERSAL_TX_USD * 1e18) / ethUsdPrice; maxValue = (MAX_CAP_UNIVERSAL_TX_USD * 1e18) / ethUsdPrice; } @@ -655,32 +760,55 @@ contract UniversalGateway is return amount; } - /// @dev Lock ERC20 in this contract for bridging. + /// @dev Lock ERC20 in the Vault contract for bridging. /// Token must be supported, i.e., tokenToLimitThreshold[token] != 0. /// @param token token address to deposit /// @param amount amount of token to deposit function _handleTokenDeposit(address token, uint256 amount) internal { if (tokenToLimitThreshold[token] == 0) revert Errors.NotSupported(); - IERC20(token).safeTransferFrom(_msgSender(), address(this), amount); + IERC20(token).safeTransferFrom(_msgSender(), VAULT, amount); } - /// @dev Native withdraw by TSS - /// @param recipient recipient address - /// @param amount amount of native ETH to withdraw - function _handleNativeWithdraw(address recipient, uint256 amount) internal { - (bool ok,) = payable(recipient).call{ value: amount }(""); - if (!ok) revert Errors.WithdrawFailed(); + // _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) = + token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, 0)); + if (!success) { + // Some non-standard tokens revert on zero-approval; treat as reset-ok to avoid breaking the flow. + return; + } + // If token returns a boolean, ensure it is true; if no return data, assume success (USDT-style). + if (returnData.length > 0) { + bool approved = abi.decode(returnData, (bool)); + if (!approved) revert Errors.InvalidData(); + } } - /// @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 { + /// @dev Safely approve ERC20 token spending to a target contract. + /// Low-level call must succeed AND (if returns data) decode to true; otherwise revert. + function _safeApprove(address token, address spender, uint256 amount) internal { + (bool success, bytes memory returnData) = + token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); + if (!success) { + revert Errors.InvalidData(); // approval failed + } + if (returnData.length > 0) { + bool approved = abi.decode(returnData, (bool)); + if (!approved) { + revert Errors.InvalidData(); // approval failed + } + } + } - if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); - IERC20(token).safeTransfer(recipient, amount); + /// @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) { + (bool success, bytes memory ret) = target.call{value: value}(payload); + if (!success) revert Errors.ExecutionFailed(); + return ret; } /// @dev Enforce and consume the per-token epoch rate limit. @@ -756,9 +884,9 @@ contract UniversalGateway is // Fast-path: pull WETH from user and unwrap to native IERC20(WETH).safeTransferFrom(_msgSender(), address(this), amountIn); - uint256 balBefore = address(this).balance; + uint256 balanceBeforeUnwrap = address(this).balance; IWETH(WETH).withdraw(amountIn); - ethOut = address(this).balance - balBefore; + ethOut = address(this).balance - balanceBeforeUnwrap; // Slippage bound still applies for a consistent interface (caller can set to amountIn) if (ethOut < amountOutMinETH) revert Errors.SlippageExceededOrExpired(); @@ -784,9 +912,9 @@ contract UniversalGateway is IERC20(tokenIn).forceApprove(address(uniV3Router), 0); - uint256 balBefore = address(this).balance; + uint256 balanceBeforeSwapUnwrap = address(this).balance; IWETH(WETH).withdraw(wethOut); - ethOut = address(this).balance - balBefore; + ethOut = address(this).balance - balanceBeforeSwapUnwrap; // Defensive: enforce the bound again after unwrap if (ethOut < amountOutMinETH) revert Errors.SlippageExceededOrExpired(); diff --git a/contracts/evm-gateway/src/UniversalGatewayPC.sol b/contracts/evm-gateway/src/UniversalGatewayPC.sol new file mode 100644 index 0000000..dbe935b --- /dev/null +++ b/contracts/evm-gateway/src/UniversalGatewayPC.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/** + * @title UniversalGatewayPC + * @notice Universal Gateway implementation for Push Chain + * + * @dev + * - Strictly to be deployed on Push Chain. + * - Allows users to withdraw PRC20 (wrapped) tokens back to the origin chain. + * - Allows users to withdraw PRC20 and attach a payload for arbitrary call execution on the origin chain. + * - This contract does NOT handle deposits or inbound transfers. + * - This contract does NOT custody user assets; PRC20 are burned at request time. + * - The Gateway includes a withdrawal fees for withdrwal from Push Chain to origin chain. + */ +import { Errors } from "./libraries/Errors.sol"; +import { IPRC20 } from "./interfaces/IPRC20.sol"; +import { IVaultPC } from "./interfaces/IVaultPC.sol"; +import { 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 { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +contract UniversalGatewayPC is + Initializable, + AccessControlUpgradeable, + ReentrancyGuardUpgradeable, + PausableUpgradeable, + IUniversalGatewayPC +{ + /// @notice Pauser role for pausing the contract. + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + /// @notice UniversalCore on Push Chain (provides gas coin/prices + UEM address). + address public UNIVERSAL_CORE; + + /// @notice VaultPC on Push Chain (custody vault for fees collected from outbound flows). + IVaultPC public VAULT_PC; + + /// @notice Initializes the contract. + /// @param admin address of the admin. + /// @param pauser address of the pauser. + /// @param universalCore address of the UniversalCore. + /// @param vaultPC address of the VaultPC. + function initialize(address admin, address pauser, address universalCore, address vaultPC) external initializer { + if (admin == address(0) || pauser == address(0) || universalCore == address(0) || vaultPC == address(0)) revert Errors.ZeroAddress(); + + __AccessControl_init(); + __ReentrancyGuard_init(); + __Pausable_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(PAUSER_ROLE, pauser); + + UNIVERSAL_CORE = universalCore; + VAULT_PC = IVaultPC(vaultPC); + } + + /// @notice Sets the VaultPC address. + /// @param vaultPC address of the VaultPC. + function setVaultPC(address vaultPC) external onlyRole(DEFAULT_ADMIN_ROLE) whenNotPaused { + if (vaultPC == address(0)) revert Errors.ZeroAddress(); + address oldVaultPC = address(VAULT_PC); + VAULT_PC = IVaultPC(vaultPC); + emit VaultPCUpdated(oldVaultPC, vaultPC); + } + + function pause() external onlyRole(PAUSER_ROLE) whenNotPaused { + _pause(); + } + + function unpause() external onlyRole(PAUSER_ROLE) whenPaused { + _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 + ); + } + + /// @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 + ); + } + + // ========= Helpers ========= + + /// @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(); + } + + /** + * @dev Use UniversalCore's withdrawGasFeeWithGasLimit to compute fee (gas coin + amount). + * If gasLimit = 0, pull the default BASE_GAS_LIMIT from UniversalCore. + * @return gasToken PRC20 address to be used for fee payment. + * @return gasFee amount of gasToken to collect from the user (includes protocol fee). + * @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) + internal + view + returns (address gasToken, uint256 gasFee, uint256 gasLimitUsed, uint256 protocolFee) + { + if (gasLimit == 0) { + gasLimitUsed = IUniversalCore(UNIVERSAL_CORE).BASE_GAS_LIMIT(); + } else { + gasLimitUsed = gasLimit; + } + + (gasToken, gasFee) = IUniversalCore(UNIVERSAL_CORE).withdrawGasFeeWithGasLimit(token, gasLimitUsed); + if (gasToken == address(0) || gasFee == 0) revert Errors.InvalidData(); + + protocolFee = IPRC20(token).PC_PROTOCOL_FEE(); + } + + /** + * @dev Pull fee from user into the VaultPC. + * Caller must have approved `gasToken` for at least `gasFee`. + */ + function _moveFees(address from, address gasToken, uint256 gasFee) internal { + address _vaultPC = address(VAULT_PC); + if (_vaultPC == address(0)) revert Errors.ZeroAddress(); + + bool ok = IPRC20(gasToken).transferFrom(from, _vaultPC, gasFee); + if (!ok) revert Errors.GasFeeTransferFailed(gasToken, from, gasFee); + } + + function _burnPRC20(address from, address token, uint256 amount) internal { + // Pull PRC20 into this gateway first + IPRC20(token).transferFrom(from, address(this), amount); + + // Then burn from this contract's balance + bool ok = IPRC20(token).burn(amount); + if (!ok) revert Errors.TokenBurnFailed(token, amount); + } +} diff --git a/contracts/evm-gateway/src/UniversalGatewayV0.sol b/contracts/evm-gateway/src/UniversalGatewayV0.sol index f60b261..eb4296f 100644 --- a/contracts/evm-gateway/src/UniversalGatewayV0.sol +++ b/contracts/evm-gateway/src/UniversalGatewayV0.sol @@ -109,7 +109,13 @@ contract UniversalGatewayV0 is mapping(address => EpochUsage) private _usage; // Current-epoch usage per token (address(0) represents native). - uint256[40] private __gap; + /// @notice The Vault contract address + address public VAULT; + bytes32 public constant VAULT_ROLE = keccak256("VAULT_ROLE"); + /// @notice Map to track if a payload has been executed + mapping(bytes32 => bool) public isExecuted; + + uint256[38] private __gap; // Reduced gap to account for new state variables /** * @notice Initialize the UniversalGateway contract @@ -121,6 +127,20 @@ contract UniversalGatewayV0 is * @param factory UniswapV2 factory * @param router UniswapV2 router */ + /// @notice Update the Vault address + /// @param newVault new Vault address + function updateVault(address newVault) external onlyRole(DEFAULT_ADMIN_ROLE) whenPaused { + if (newVault == address(0)) revert Errors.ZeroAddress(); + address old = VAULT; + + // transfer role + if (hasRole(VAULT_ROLE, old)) _revokeRole(VAULT_ROLE, old); + _grantRole(VAULT_ROLE, newVault); + + VAULT = newVault; + emit VaultUpdated(old, newVault); + } + function initialize( address admin, address pauser, @@ -193,7 +213,7 @@ contract UniversalGatewayV0 is /// @notice Allows the admin to set the TSS address /// @param newTSS The new TSS address /// Todo: TSS Implementation could be changed based on ESDCA vs BLS sign schemes. - function setTSSAddress(address newTSS) external onlyRole(DEFAULT_ADMIN_ROLE) whenNotPaused { + function setTSSAddress(address newTSS) external onlyRole(DEFAULT_ADMIN_ROLE) { if (newTSS == address(0)) revert Errors.ZeroAddress(); address old = TSS_ADDRESS; @@ -710,11 +730,56 @@ contract UniversalGatewayV0 is emit WithdrawFunds(revertInstruction.fundRecipient, amount, token); } + + /// @notice Revert tokens to the recipient specified in revertInstruction + /// @param token token address to revert + /// @param amount amount of token to revert + /// @param revertInstruction revert settings + function revertTokens(address token, uint256 amount, RevertInstructions calldata revertInstruction) + external + nonReentrant + whenNotPaused + onlyRole(VAULT_ROLE) + { + if (revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + if (amount == 0) revert Errors.InvalidAmount(); + + IERC20(token).safeTransfer(revertInstruction.fundRecipient, amount); + + emit RevertWithdraw(revertInstruction.fundRecipient, token, amount, revertInstruction); + } + + /// @notice Revert native tokens to the recipient specified in revertInstruction + /// @param amount amount of native token to revert + /// @param revertInstruction revert settings + function revertNative(uint256 amount, RevertInstructions calldata revertInstruction) + external + payable + nonReentrant + whenNotPaused + onlyTSS + { + if (revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); + if (amount == 0 || msg.value != amount) revert Errors.InvalidAmount(); + + (bool ok,) = payable(revertInstruction.fundRecipient).call{ value: amount }(""); + if (!ok) revert Errors.WithdrawFailed(); + + emit RevertWithdraw(revertInstruction.fundRecipient, address(0), amount, revertInstruction); + } // ========================= // 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 isTokenSupported(address token) public view returns (bool) { + // Check if token has a non-zero limit threshold + 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 @@ -861,13 +926,19 @@ contract UniversalGatewayV0 is return amount; } - /// @dev Lock ERC20 in this contract for bridging (must be isSupported). - /// Tokens are stored in gateway contract. + /// @dev Lock ERC20 in the Vault contract for bridging. + /// Token must be supported, i.e., tokenToLimitThreshold[token] != 0. /// @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); + if (tokenToLimitThreshold[token] == 0) revert Errors.NotSupported(); + if (VAULT != address(0)) { + // If Vault is set, transfer tokens to Vault + IERC20(token).safeTransferFrom(_msgSender(), VAULT, amount); + } else { + // Legacy behavior - transfer tokens to this contract + IERC20(token).safeTransferFrom(_msgSender(), address(this), amount); + } } /// @dev Native withdraw by TSS @@ -1000,9 +1071,9 @@ contract UniversalGatewayV0 is IERC20(tokenIn).forceApprove(address(uniV3Router), 0); // Unwrap WETH -> native and compute exact ETH out - uint256 balBefore = address(this).balance; + uint256 _balBefore = address(this).balance; IWETH(WETH).withdraw(wethOut); - ethOut = address(this).balance - balBefore; + ethOut = address(this).balance - _balBefore; // Defensive: enforce the bound again after unwrap if (ethOut < amountOutMinETH) revert Errors.SlippageExceededOrExpired(); @@ -1040,6 +1111,120 @@ contract UniversalGatewayV0 is // RECEIVE/FALLBACK // ========================= + // ========================= + // GATEWAY Payload Execution Paths + // ========================= + + /// @notice Executes a Universal Transaction on this chain triggered by TSS after validation on Push Chain. + /// @dev Allows outbound payload execution from Push Chain to external chains. + /// - The tokens used for payload execution, are to be burnt on Push Chain. + /// - approval and reset of approval is handled by the gateway. + /// - tokens are transferred from Vault to Gateway before calling this function + /// @param txID unique transaction identifier + /// @param originCaller original caller/user on source chain + /// @param token token address (ERC20 token) + /// @param target target contract address to execute call + /// @param amount amount of token to send along + /// @param payload calldata to be executed on target + function executeUniversalTx( + bytes32 txID, + address originCaller, + address token, + address target, + uint256 amount, + bytes calldata payload + ) external nonReentrant whenNotPaused onlyRole(VAULT_ROLE) { + if (isExecuted[txID]) 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 + + if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); + + isExecuted[txID] = true; + + _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 + + // Return any remaining tokens to the Vault + uint256 remainingBalance = IERC20(token).balanceOf(address(this)); + if (remainingBalance > 0 && VAULT != address(0)) { + IERC20(token).safeTransfer(VAULT, remainingBalance); + } + + emit UniversalTxExecuted(txID, originCaller, target, token, amount, payload); + } + + /// @notice Executes a Universal Transaction with native tokens on this chain triggered by TSS after validation on Push Chain. + /// @dev Allows outbound payload execution from Push Chain to external chains with native tokens. + /// @param txID unique transaction identifier + /// @param originCaller original caller/user on source chain + /// @param target target contract address to execute call + /// @param amount amount of native token to send along + /// @param payload calldata to be executed on target + function executeUniversalTx( + bytes32 txID, + address originCaller, + address target, + uint256 amount, + bytes calldata payload + ) external payable nonReentrant whenNotPaused onlyRole(TSS_ROLE) { + if (isExecuted[txID]) 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; + + _executeCall(target, payload, amount); + + emit UniversalTxExecuted(txID, originCaller, target, address(0), amount, payload); + } + + /// @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) = + token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, 0)); + if (!success) { + // Some non-standard tokens revert on zero-approval; treat as reset-ok to avoid breaking the flow. + return; + } + // If token returns a boolean, ensure it is true; if no return data, assume success (USDT-style). + if (returnData.length > 0) { + bool approved = abi.decode(returnData, (bool)); + if (!approved) revert Errors.InvalidData(); + } + } + + /// @dev Safely approve ERC20 token spending to a target contract. + /// Low-level call must succeed AND (if returns data) decode to true; otherwise revert. + function _safeApprove(address token, address spender, uint256 amount) internal { + (bool success, bytes memory returnData) = + token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); + if (!success) { + revert Errors.InvalidData(); // approval failed + } + if (returnData.length > 0) { + bool approved = abi.decode(returnData, (bool)); + if (!approved) { + revert Errors.InvalidData(); // approval failed + } + } + } + + /// @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) { + (bool success, bytes memory ret) = target.call{value: value}(payload); + if (!success) revert Errors.ExecutionFailed(); + return ret; + } + /// @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 new file mode 100644 index 0000000..c2d7088 --- /dev/null +++ b/contracts/evm-gateway/src/Vault.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/** + * @title Vault + * @notice ERC20 custody vault for outbound flows (withdraw / withdraw+call) managed by TSS. + * @dev - TransparentUpgradeable (OZ Initializable pattern) + * - ERC20-only (no native); native is handled by the gateway directly. + * - Token support is gated by UniversalGateway.isSupportedToken(token) to keep a single source of truth. + * - Uses safe-approve -> call -> reset-approval pattern (USDT-safe). + */ + +import {Errors} from "./libraries/Errors.sol"; +import {IVault} from "./interfaces/IVault.sol"; +import {RevertInstructions} from "./libraries/Types.sol"; +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 {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"; + + +contract Vault is + Initializable, + ContextUpgradeable, + PausableUpgradeable, + ReentrancyGuardUpgradeable, + AccessControlUpgradeable, + IVault +{ + using SafeERC20 for IERC20; + + // ========================= + // ROLES + // ========================= + bytes32 public constant TSS_ROLE = keccak256("TSS_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + // ========================= + // STATE + // ========================= + /// @notice UniversalGateway on the same chain; source of truth for token support. + IUniversalGateway public gateway; + + /// @notice The current TSS address for Vault + address public TSS_ADDRESS; + + // ========================= + // INITIALIZER + // ========================= + function initialize(address admin, address pauser, address tss, address gw) external initializer { + if (admin == address(0) || pauser == address(0) || tss == address(0) || gw == address(0)) { + revert Errors.ZeroAddress(); + } + + __Context_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + __AccessControl_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(PAUSER_ROLE, pauser); + _grantRole(TSS_ROLE, tss); + + gateway = IUniversalGateway(gw); + TSS_ADDRESS = tss; + emit GatewayUpdated(address(0), gw); + emit TSSUpdated(address(0), tss); + } + + // ========================= + // ADMIN OPS + // ========================= + /// @notice Allows the admin to pause the contract + /// @dev Only callable by PAUSER_ROLE + function pause() external whenNotPaused onlyRole(PAUSER_ROLE) { + _pause(); + } + + /// @notice Allows the admin to unpause the contract + /// @dev Only callable by PAUSER_ROLE + function unpause() external whenPaused onlyRole(PAUSER_ROLE) { + _unpause(); + } + + /// @notice Allows the admin to update the UniversalGateway address + /// @dev Only callable by DEFAULT_ADMIN_ROLE + /// @param gw New UniversalGateway address + function setGateway(address gw) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (gw == address(0)) revert Errors.ZeroAddress(); + address old = address(gateway); + gateway = IUniversalGateway(gw); + emit GatewayUpdated(old, gw); + } + + /// @notice Allows the admin to update the TSS address + /// @dev Only callable by DEFAULT_ADMIN_ROLE + /// @param newTss New TSS address + function setTSS(address newTss) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (newTss == address(0)) revert Errors.ZeroAddress(); + address old = TSS_ADDRESS; + + // transfer role + if (hasRole(TSS_ROLE, old)) _revokeRole(TSS_ROLE, old); + _grantRole(TSS_ROLE, newTss); + + TSS_ADDRESS = newTss; + emit TSSUpdated(old, newTss); + } + + /// @notice Allows the admin to sweep tokens from the contract + /// @dev Only callable by DEFAULT_ADMIN_ROLE + /// @param token Token address + /// @param to Recipient address + /// @param amount Amount of token to sweep + function sweep(address token, address to, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (token == address(0) || to == address(0)) revert Errors.ZeroAddress(); + IERC20(token).safeTransfer(to, amount); + } + + // ========================= + // WITHDRAW + // ========================= + /// @inheritdoc IVault + function withdraw(bytes32 txID, address originCaller, address token, address to, uint256 amount) + external + nonReentrant + whenNotPaused + onlyRole(TSS_ROLE) + { + if (token == address(0) || to == address(0)) revert Errors.ZeroAddress(); + if (amount == 0) revert Errors.InvalidAmount(); + _enforceSupported(token); + if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); + + IERC20(token).safeTransfer(address(gateway), amount); + gateway.withdrawToken(txID, originCaller, token, to, amount); + emit VaultWithdraw(txID, originCaller, token, to, amount); + } + + /// @inheritdoc IVault + function withdrawAndExecute(bytes32 txID, address originCaller, address token, address target, uint256 amount, bytes calldata data) + external + nonReentrant + whenNotPaused + onlyRole(TSS_ROLE) + { + if (token == address(0) || target == address(0)) revert Errors.ZeroAddress(); + if (amount == 0) revert Errors.InvalidAmount(); + _enforceSupported(token); + if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); + + // Transfer tokens to gateway + IERC20(token).safeTransfer(address(gateway), amount); + + // Forward execution call to gateway + gateway.executeUniversalTx(txID, originCaller, token, target, amount, data); + + emit VaultWithdrawAndExecute(token, target, amount, data); + } + + /// @inheritdoc IVault + function revertWithdraw(address token, address to, uint256 amount, RevertInstructions calldata revertInstruction) + external + nonReentrant + whenNotPaused + onlyRole(TSS_ROLE) + { + if (token == address(0) || to == address(0)) revert Errors.ZeroAddress(); + if (amount == 0) revert Errors.InvalidAmount(); + _enforceSupported(token); + if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); + + IERC20(token).safeTransfer(address(gateway), amount); + gateway.revertUniversalTxToken(token, amount, revertInstruction); + + emit VaultRevert(token, to, amount, revertInstruction); + } + + // ========================= + // INTERNALS + // ========================= + function _enforceSupported(address token) internal view { + // Single source of truth lives in UniversalGateway + if (!gateway.isSupportedToken(token)) revert Errors.NotSupported(); + } +} diff --git a/contracts/evm-gateway/src/VaultPC.sol b/contracts/evm-gateway/src/VaultPC.sol new file mode 100644 index 0000000..6357104 --- /dev/null +++ b/contracts/evm-gateway/src/VaultPC.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/** + * @title VaultPC + * @notice Custody vault to store fees collected from outbound flows on Push Chain. + * @dev - TransparentUpgradeable (OZ Initializable pattern) + * - Only supports PRC20 tokens. + * - Funds stored are managed by the FUND_MANAGER_ROLE. + * - UniversalCore is the single source of truth for supported PRC20 tokens for Push Chain. + * - All fees earned via outbound flows are stored and handled in this contract by FUND_MANAGER_ROLE. + */ + +import {Errors} from "./libraries/Errors.sol"; +import {IVaultPC} from "./interfaces/IVaultPC.sol"; +import {IUniversalCore} from "./interfaces/IUniversalCore.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 {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"; + + +contract VaultPC is + Initializable, + ContextUpgradeable, + PausableUpgradeable, + ReentrancyGuardUpgradeable, + AccessControlUpgradeable, + IVaultPC +{ + using SafeERC20 for IERC20; + + /// @notice UniversalCore on Push Chain (provides gas coin/prices + UEM address). + address public UNIVERSAL_CORE; + + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant FUND_MANAGER_ROLE = keccak256("FUND_MANAGER_ROLE"); + + /** + * @param admin DEFAULT_ADMIN_ROLE holder + * @param pauser PAUSER_ROLE + * @param fundManager FUND_MANAGER_ROLE + */ + function initialize(address admin, address pauser, address fundManager, address universalCore) external initializer { + if (admin == address(0) || pauser == address(0) || fundManager == address(0) || universalCore == address(0)) revert Errors.ZeroAddress(); + + __Context_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + __AccessControl_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(PAUSER_ROLE, pauser); + _grantRole(FUND_MANAGER_ROLE, fundManager); + + UNIVERSAL_CORE = universalCore; + } + + // ========================= + // ADMIN OPS + // ========================= + function pause() external whenNotPaused onlyRole(PAUSER_ROLE) { + _pause(); + } + function unpause() external whenPaused onlyRole(PAUSER_ROLE) { + _unpause(); + } + + function updateUniversalCore(address universalCore) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (universalCore == address(0)) revert Errors.ZeroAddress(); + UNIVERSAL_CORE = universalCore; + } + + /// @notice Optional admin sweep for mistakenly sent tokens (never native). + function sweep(address token, address to, uint256 amount) external onlyRole(FUND_MANAGER_ROLE) { + if (token == address(0) || to == address(0)) revert Errors.ZeroAddress(); + IERC20(token).safeTransfer(to, amount); + } + + // ========================= + // WITHDRAW + // ========================= + /// @inheritdoc IVaultPC + function withdraw(address token, address to, uint256 amount) + external + nonReentrant + whenNotPaused + onlyRole(FUND_MANAGER_ROLE) + { + _enforceSupportedToken(token); + if (token == address(0) || to == address(0)) revert Errors.ZeroAddress(); + if (amount == 0) revert Errors.InvalidAmount(); + if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); + + IERC20(token).safeTransfer(to, amount); + emit FeesWithdrawn(msg.sender, token, amount); + } + + // ========================= + // INTERNALS + // ========================= + /** + * @notice Enforce that a token is supported + * @param token Token address + */ + function _enforceSupportedToken(address token) internal view { + // Single source of truth lives in UniversalCore + if (!IUniversalCore(UNIVERSAL_CORE).isSupportedToken(token)) revert Errors.NotSupported(); + } +} diff --git a/contracts/evm-gateway/src/interfaces/IPRC20.sol b/contracts/evm-gateway/src/interfaces/IPRC20.sol new file mode 100644 index 0000000..4c64b57 --- /dev/null +++ b/contracts/evm-gateway/src/interfaces/IPRC20.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/** + * @dev Interface for PRC20 tokens + */ +interface IPRC20 { + /** + * @notice ERC-20 metadata + */ + function decimals() external view returns (uint8); + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function SOURCE_CHAIN_ID() external view returns (string memory); + function PC_PROTOCOL_FEE() external view returns (uint256); + function GAS_LIMIT() external view returns (uint256); + function withdrawGasFeeWithGasLimit(uint256 gasLimit) external view returns (address gasToken, uint256 gasFee); + + /** + * @notice ERC-20 standard functions + */ + function burn(uint256 amount) external returns (bool); + 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); + +} diff --git a/contracts/evm-gateway/src/interfaces/IUniversalCore.sol b/contracts/evm-gateway/src/interfaces/IUniversalCore.sol new file mode 100644 index 0000000..68b8b97 --- /dev/null +++ b/contracts/evm-gateway/src/interfaces/IUniversalCore.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +interface IUniversalCore { + + /** + * @notice Check if a token is supported + * @param token Token address + * @return bool True if the token is supported, false otherwise + */ + function isSupportedToken(address token) external view returns (bool); + + /** + * @notice Get gas token PRC20 address for a chain + * @param chainId Chain ID + * @return gasToken Gas token address + */ + function gasTokenPRC20ByChainId(string memory chainId) external view returns (address gasToken); + + /** + * @notice Get gas price for a chain + * @param chainId Chain ID + * @return price Gas price + */ + function gasPriceByChainId(string memory chainId) external view returns (uint256 price); + + /** + * @notice Get base gas limit for a chain + * @return baseGasLimit Base gas limit + */ + function BASE_GAS_LIMIT() external view returns (uint256 baseGasLimit); + + /** + * @notice Get the Universal Executor Module address + * @return executorModule Universal Executor Module address + */ + function UNIVERSAL_EXECUTOR_MODULE() external view returns (address executorModule); + + /** + * @notice Get gas fee for a PRC20 token. + * @dev Uses BASE_GAS_LIMIT for the gas limit used in the fee computation. + * @param _prc20 PRC20 address + * @return gasToken Gas token address + * @return gasFee Gas fee + */ + function withdrawGasFee(address _prc20) external view returns (address gasToken, uint256 gasFee); + + /** + * @notice Get gas fee for a PRC20 token with a custom gas limit + * @dev Uses the provided gas limit for the fee computation. + * @param _prc20 PRC20 address + * @param gasLimit Gas limit + * @return gasToken Gas token address + * @return gasFee Gas fee + */ + function withdrawGasFeeWithGasLimit(address _prc20, uint256 gasLimit) external view returns (address gasToken, uint256 gasFee); +} \ 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 2699aff..6595ffd 100644 --- a/contracts/evm-gateway/src/interfaces/IUniversalGateway.sol +++ b/contracts/evm-gateway/src/interfaces/IUniversalGateway.sol @@ -8,7 +8,41 @@ interface IUniversalGateway { // EVENTS // ========================= - /// @notice Universal tx deposit (gas funding). Emits for both gas refil and funds+payload movement. + /// @notice Caps updated event + /// @param minCapUsd Minimum cap in USD + /// @param maxCapUsd Maximum cap in USD + event CapsUpdated(uint256 minCapUsd, uint256 maxCapUsd); + + /// @notice Rate-limit / config events + /// @param oldDuration Previous epoch duration: Duration of the epoch before the update. + /// @param newDuration New epoch duration: Duration of the epoch after the update. + event EpochDurationUpdated(uint256 oldDuration, uint256 newDuration); + + /// @notice Token limit threshold updated event + /// @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 + /// @param recipient Recipient address on Push Chain: for address(0), recipient = sender's UEA on Push Chain. + /// @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 txType Transaction type: TX_TYPE enum + /// @param signatureData Signature data: for signedVerification, signatureData is the signature of the sender. event UniversalTx( address indexed sender, address indexed recipient, @@ -19,14 +53,44 @@ interface IUniversalGateway { 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 Universal tx execution event that is executed on External Chains. + /// @param txID Unique transaction identifier + /// @param originCaller Original caller/user on source chain ( Push Chain) + /// @param target Target contract address to execute call + /// @param token Token address being sent + /// @param amount Amount of token being sent + /// @param data Calldata to be executed on target contract on external chain + event UniversalTxExecuted( + bytes32 indexed txID, + address indexed originCaller, + address indexed target, + address token, + uint256 amount, + bytes data + ); + + /// @notice Vault updated event + /// @param oldVault Previous Vault address + /// @param newVault New Vault address + event VaultUpdated(address indexed oldVault, address indexed newVault); + + /// @notice W Withdraw token event + /// @param txID Unique transaction identifier + /// @param originCaller Original caller/user on source chain ( Push Chain) + /// @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); + + /// @notice Revert withdraw event: For withdrwals/actions during a revert + /// @param to Recipient address on external chain + /// @param token Token address being reverted + /// @param amount Amount of token being reverted + /// @param revertInstruction Revert settings configuration + event RevertUniversalTx(address indexed to, address indexed token, uint256 amount, RevertInstructions revertInstruction); + + // ========================= // sendTxWithGas - Fee Abstraction Route // ========================= @@ -178,16 +242,57 @@ interface IUniversalGateway { /// @notice Withdraw functions (TSS-only) - /// @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; - - /// @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; + /// @notice Revert universal transaction with tokens to the recipient specified in revertInstruction + /// @param token token address to revert + /// @param amount amount of token to revert + /// @param revertCFG revert settings + function revertUniversalTxToken(address token, uint256 amount, RevertInstructions calldata revertCFG) external; + + /// @notice Revert native tokens to the recipient specified in revertInstruction + /// @param amount amount of native token to revert + /// @param revertCFG revert settings + function revertUniversalTx(uint256 amount, RevertInstructions calldata revertCFG) external payable; + + + // ========================= + // Withdraw and Payload Execution Paths + // ========================= + + /// @notice Withdraw token from the gateway + /// @param txID unique transaction identifier + /// @param originCaller original caller/user on source chain + /// @param token token address (ERC20 token) + /// @param to recipient address + /// @param amount amount of token to withdraw + function withdrawToken(bytes32 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 + /// @param originCaller original caller/user on source chain + /// @param token token address (ERC20 token) + /// @param target target contract address to execute call + /// @param amount amount of token to send along + /// @param payload calldata to be executed on target + function executeUniversalTx( + bytes32 txID, + address originCaller, + address token, + address target, + uint256 amount, + bytes calldata payload + ) external; + + /// @notice Executes a Universal Transaction with native tokens on this chain triggered by TSS after validation on Push Chain. + /// @param txID unique transaction identifier + /// @param originCaller original caller/user on source chain + /// @param target target contract address to execute call + /// @param amount amount of native token to send along + /// @param payload calldata to be executed on target + function executeUniversalTx( + bytes32 txID, + address originCaller, + address target, + uint256 amount, + bytes calldata payload + ) external payable; } diff --git a/contracts/evm-gateway/src/interfaces/IUniversalGatewayPC.sol b/contracts/evm-gateway/src/interfaces/IUniversalGatewayPC.sol new file mode 100644 index 0000000..0021bf6 --- /dev/null +++ b/contracts/evm-gateway/src/interfaces/IUniversalGatewayPC.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {RevertInstructions} from "../libraries/Types.sol"; + +/** + * @title IUniversalGatewayPC + * @notice Interface for UniversalGatewayPC contract + * @dev Defines all public functions and events for the Push Chain outbound gateway + */ +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( + address indexed sender, + string indexed chainId, + address indexed token, + bytes target, + uint256 amount, + address gasToken, + uint256 gasFee, + uint256 gasLimit, + bytes payload, + uint256 protocolFee, + RevertInstructions revertInstruction + ); + + /// @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(). + * @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( + bytes calldata target, + address token, + uint256 amount, + bytes calldata payload, + uint256 gasLimit, + RevertInstructions calldata revertInstruction + ) external; + + // ========= 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..f3597ce 100644 --- a/contracts/evm-gateway/src/interfaces/IUniversalGatewayV0.sol +++ b/contracts/evm-gateway/src/interfaces/IUniversalGatewayV0.sol @@ -4,6 +4,13 @@ pragma solidity 0.8.26; import { RevertInstructions, UniversalPayload, TX_TYPE } from "../libraries/Types.sol"; interface IUniversalGatewayV0 { + // ========================= + // 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 isTokenSupported(address token) external view returns (bool); // ========================= // EVENTS // ========================= @@ -20,7 +27,20 @@ interface IUniversalGatewayV0 { bytes signatureData ); /// @notice Withdraw funds event - event WithdrawFunds(address indexed recipient, uint256 amount, address tokenAddress); + event WithdrawFunds(address indexed recipient, uint256 amount, address tokenAddress); + /// @notice Universal tx execution event. Emits for outbound transactions from Push Chain to external chains + event UniversalTxExecuted( + bytes32 indexed txID, + address indexed originCaller, + address indexed target, + address token, + uint256 amount, + bytes data + ); + /// @notice Vault updated event + event VaultUpdated(address indexed oldVault, address indexed newVault); + /// @notice Revert withdraw event + event RevertWithdraw(address indexed to, address indexed token, uint256 amount, RevertInstructions revertInstruction); /// @notice Caps updated event event CapsUpdated(uint256 minCapUsd, uint256 maxCapUsd); /// @notice Rate-limit / config events @@ -190,4 +210,45 @@ interface IUniversalGatewayV0 { /// @param amount amount to refund /// @param revertCFG (fundRecipient, revertMsg) function revertWithdrawFunds(address token, uint256 amount, RevertInstructions calldata revertCFG) external; + + /// @notice Revert tokens to the recipient specified in revertInstruction + /// @param token token address to revert + /// @param amount amount of token to revert + /// @param revertCFG revert settings + function revertTokens(address token, uint256 amount, RevertInstructions calldata revertCFG) external; + + /// @notice Revert native tokens to the recipient specified in revertInstruction + /// @param amount amount of native token to revert + /// @param revertCFG revert settings + function revertNative(uint256 amount, RevertInstructions calldata revertCFG) external payable; + + /// @notice Executes a Universal Transaction on this chain triggered by Vault after validation on Push Chain. + /// @param txID unique transaction identifier + /// @param originCaller original caller/user on source chain + /// @param token token address (ERC20 token) + /// @param target target contract address to execute call + /// @param amount amount of token to send along + /// @param payload calldata to be executed on target + function executeUniversalTx( + bytes32 txID, + address originCaller, + address token, + address target, + uint256 amount, + bytes calldata payload + ) external; + + /// @notice Executes a Universal Transaction with native tokens on this chain triggered by TSS after validation on Push Chain. + /// @param txID unique transaction identifier + /// @param originCaller original caller/user on source chain + /// @param target target contract address to execute call + /// @param amount amount of native token to send along + /// @param payload calldata to be executed on target + function executeUniversalTx( + bytes32 txID, + address originCaller, + address target, + uint256 amount, + bytes calldata payload + ) external payable; } diff --git a/contracts/evm-gateway/src/interfaces/IVault.sol b/contracts/evm-gateway/src/interfaces/IVault.sol new file mode 100644 index 0000000..2ff9ef5 --- /dev/null +++ b/contracts/evm-gateway/src/interfaces/IVault.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { RevertInstructions } from "../libraries/Types.sol"; +/** + * @title IVault + * @notice Interface for ERC20 custody vault for outbound flows (withdraw / withdraw+call) managed by TSS. + */ +interface IVault { + // ========================= + // EVENTS + // ========================= + + /// @notice Gateway updated event + /// @param oldGateway Previous Gateway address + /// @param newGateway New Gateway address + event GatewayUpdated(address indexed oldGateway, address indexed newGateway); + + /// @notice TSS updated event + /// @param oldTss Previous TSS address + /// @param newTss New TSS address + event TSSUpdated(address indexed oldTss, address indexed newTss); + + /// @notice Vault withdraw and execute event + /// @param token Token address + /// @param target Target contract address + /// @param amount Amount of token + /// @param data Calldata for the target execution + event VaultWithdrawAndExecute(address indexed token, address indexed target, uint256 amount, bytes data); + + /// @notice Vault withdraw event + /// @param txID Unique transaction identifier + /// @param originCaller Original caller/user on source chain + /// @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); + + /// @notice Vault revert event + /// @param token Token address + /// @param to Recipient address + /// @param amount Amount of token + /// @param revertInstruction Revert instructions configuration + event VaultRevert(address indexed token, address indexed to, uint256 amount, RevertInstructions revertInstruction); + + // ========================= + // WITHDRAW + // ========================= + /** + * @notice TSS-only withdraw to an external recipient on external chains + * @dev Moves token to gateway contract and then transfers to recipient or executes the payload. + * @param txID unique transaction identifier on external chain + * @param originCaller original caller/user on source chain ( Push Chain) + * @param token ERC20 token to transfer (must be supported by gateway) on external chain + * @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; + + /** + * @notice TSS-only withdraw and execute transaction via gateway on external chains + * @dev Moves token to gateway contract and then transfers to recipient or executes the payload. + * @param txID unique transaction identifier on external chain + * @param originCaller original caller/user on source chain ( Push Chain) + * @param token ERC20 token to transfer (must be supported by gateway) on external chain + * @param target contract to call via gateway on external chain + * @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; + + /** + * @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 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 + */ + function revertWithdraw(address token, address to, uint256 amount, RevertInstructions calldata revertInstruction) external; +} + diff --git a/contracts/evm-gateway/src/interfaces/IVaultPC.sol b/contracts/evm-gateway/src/interfaces/IVaultPC.sol new file mode 100644 index 0000000..15d33b9 --- /dev/null +++ b/contracts/evm-gateway/src/interfaces/IVaultPC.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/** + * @title IVaultPC + * @notice Interface for VaultPC - Custody vault to store fees collected from outbound flows on Push Chain. + */ +interface IVaultPC { + // ========================= + // EVENTS + // ========================= + + /** + * @notice Emitted when the UniversalCore address is updated + * @param oldVaultPC The previous VaultPC address + * @param newVaultPC The new VaultPC address + */ + event VaultPCUpdated(address indexed oldVaultPC, address indexed newVaultPC); + + /** + * @notice Emitted when fees are withdrawn from the vault + * @param caller The address that initiated the withdrawal (FUND_MANAGER_ROLE) + * @param token The PRC20 token address + * @param amount The amount withdrawn + */ + event FeesWithdrawn(address indexed caller, address indexed token, uint256 amount); + + // ========================= + // WITHDRAW + // ========================= + + /** + * @notice Allows owner/manager to withdraw tokens collected via Outbound Flows. + * @dev Only callable by FUND_MANAGER_ROLE + * @dev Token must be supported by UniversalCore + * @param token PRC20 token address to transfer (must be supported) + * @param to Recipient address on external chain + * @param amount Amount of token to transfer on external chain + */ + function withdraw(address token, address to, uint256 amount) external; +} + diff --git a/contracts/evm-gateway/src/libraries/Errors.sol b/contracts/evm-gateway/src/libraries/Errors.sol index c7d5da4..69b4aa4 100644 --- a/contracts/evm-gateway/src/libraries/Errors.sol +++ b/contracts/evm-gateway/src/libraries/Errors.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity 0.8.26; library Errors { @@ -10,6 +11,7 @@ library Errors { error InvalidAmount(); error InvalidTxType(); error InvalidCapRange(); + error PayloadExecuted(); // ========================= // UniversalGateway ERRORS @@ -17,8 +19,11 @@ library Errors { error NotSupported(); error DepositFailed(); error WithdrawFailed(); + error ExecutionFailed(); error InvalidRecipient(); error RateLimitExceeded(); error BlockCapLimitExceeded(); error SlippageExceededOrExpired(); + error TokenBurnFailed(address token, uint256 amount); + error GasFeeTransferFailed(address token, address from, uint256 amount); } diff --git a/contracts/evm-gateway/src/libraries/Types.sol b/contracts/evm-gateway/src/libraries/Types.sol index 798b111..e69de02 100644 --- a/contracts/evm-gateway/src/libraries/Types.sol +++ b/contracts/evm-gateway/src/libraries/Types.sol @@ -21,7 +21,7 @@ struct RevertInstructions { /// where funds go in revert / refund cases address fundRecipient; /// arbitrary message for relayers/UEA - bytes revertMsg; + bytes revertContext; } /// @notice Packed per-token usage for the current epoch only (no on-chain history kept). diff --git a/contracts/evm-gateway/test/BaseTest.t.sol b/contracts/evm-gateway/test/BaseTest.t.sol index bc306d1..d480228 100644 --- a/contracts/evm-gateway/test/BaseTest.t.sol +++ b/contracts/evm-gateway/test/BaseTest.t.sol @@ -176,8 +176,8 @@ abstract contract BaseTest is Test { bytes memory initData = abi.encodeWithSelector( UniversalGateway.initialize.selector, admin, - pauser, tss, + address(this), // vault address MIN_CAP_USD, MAX_CAP_USD, uniV3Factory, @@ -331,7 +331,7 @@ abstract contract BaseTest is Test { } function revertCfg(address fundRecipient_) internal pure returns (RevertInstructions memory) { - return RevertInstructions({ fundRecipient: fundRecipient_, revertMsg: "" }); + return RevertInstructions({ fundRecipient: fundRecipient_, revertContext: bytes("") }); } /// @notice Build a default payload for testing (commonly used across test files) @@ -353,7 +353,7 @@ 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), revertMsg: bytes("") }); + return RevertInstructions({ fundRecipient: address(0x456), revertContext: bytes("") }); } // ========================= @@ -470,7 +470,7 @@ abstract contract BaseTest is Test { vType: VerificationType(0) }); - RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: to, revertMsg: "" }); + RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: to, revertContext: bytes("") }); return (payload, revertCfg_); } diff --git a/contracts/evm-gateway/test/gateway/1_GatewayAdminSetters.t.sol b/contracts/evm-gateway/test/gateway/1_GatewayAdminSetters.t.sol index a50aa77..eca79b5 100644 --- a/contracts/evm-gateway/test/gateway/1_GatewayAdminSetters.t.sol +++ b/contracts/evm-gateway/test/gateway/1_GatewayAdminSetters.t.sol @@ -25,31 +25,31 @@ contract GatewayAdminSettersTest is BaseTest { super.setUp(); } - function testPauseOnlyPauser() public { - // Non-pauser should not be able to pause + function testPauseOnlyAdmin() public { + // Non-admin should not be able to pause vm.prank(user1); vm.expectRevert(); gateway.pause(); - // Pauser should be able to pause - vm.prank(pauser); + // Admin should be able to pause + vm.prank(admin); gateway.pause(); assertTrue(gateway.paused()); } - function testUnpauseOnlyPauser() public { + function testUnpauseOnlyAdmin() public { // First pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); assertTrue(gateway.paused()); - // Non-pauser should not be able to unpause + // Non-admin should not be able to unpause vm.prank(user1); vm.expectRevert(); gateway.unpause(); - // Pauser should be able to unpause - vm.prank(pauser); + // Admin should be able to unpause + vm.prank(admin); gateway.unpause(); assertFalse(gateway.paused()); } @@ -58,12 +58,12 @@ contract GatewayAdminSettersTest is BaseTest { assertFalse(gateway.paused()); // Pause - vm.prank(pauser); + vm.prank(admin); gateway.pause(); assertTrue(gateway.paused()); // Unpause - vm.prank(pauser); + vm.prank(admin); gateway.unpause(); assertFalse(gateway.paused()); } @@ -105,13 +105,13 @@ contract GatewayAdminSettersTest is BaseTest { function testSetTSSAddressWhenPaused() public { // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); - // Should not be able to set TSS when paused + // Admin functions like setTSS should work even when paused vm.prank(admin); - vm.expectRevert(); gateway.setTSS(address(0x123)); + assertEq(gateway.TSS_ADDRESS(), address(0x123)); } function testSetCapsUSD() public { @@ -151,7 +151,7 @@ contract GatewayAdminSettersTest is BaseTest { function testSetCapsUSDWhenPaused() public { // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); // Should not be able to set caps when paused @@ -204,7 +204,7 @@ contract GatewayAdminSettersTest is BaseTest { function testSetRoutersWhenPaused() public { // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); // Should not be able to set routers when paused @@ -299,7 +299,7 @@ contract GatewayAdminSettersTest is BaseTest { } function testSetDefaultSwapDeadlineWhenPaused() public { - vm.prank(pauser); + vm.prank(admin); gateway.pause(); vm.prank(admin); vm.expectRevert(); @@ -324,7 +324,7 @@ contract GatewayAdminSettersTest is BaseTest { } function testSetV3FeeOrderWhenPaused() public { - vm.prank(pauser); + vm.prank(admin); gateway.pause(); vm.prank(admin); vm.expectRevert(); @@ -360,7 +360,7 @@ contract GatewayAdminSettersTest is BaseTest { } function testSetEthUsdFeedWhenPaused() public { - vm.prank(pauser); + vm.prank(admin); gateway.pause(); MockAggregatorV3 newFeed = new MockAggregatorV3(8); vm.prank(admin); @@ -381,7 +381,7 @@ contract GatewayAdminSettersTest is BaseTest { } function testSetChainlinkStalePeriodWhenPaused() public { - vm.prank(pauser); + vm.prank(admin); gateway.pause(); vm.prank(admin); vm.expectRevert(); @@ -408,7 +408,7 @@ contract GatewayAdminSettersTest is BaseTest { } function testSetL2SequencerFeedWhenPaused() public { - vm.prank(pauser); + vm.prank(admin); gateway.pause(); MockSequencerUptimeFeed seq = new MockSequencerUptimeFeed(); vm.prank(admin); @@ -429,7 +429,7 @@ contract GatewayAdminSettersTest is BaseTest { } function testSetL2SequencerGracePeriodWhenPaused() public { - vm.prank(pauser); + vm.prank(admin); gateway.pause(); vm.prank(admin); vm.expectRevert(); @@ -459,7 +459,7 @@ contract GatewayAdminSettersTest is BaseTest { // ========================= function testPauseBlocksAllStateChangingFunctions() public { // Pause first - vm.prank(pauser); + vm.prank(admin); gateway.pause(); // Admin setters should be blocked when paused @@ -494,6 +494,6 @@ contract GatewayAdminSettersTest is BaseTest { // TSS operations should be blocked vm.prank(tss); vm.expectRevert(); - gateway.withdrawFunds(user2, address(0), 1); + gateway.revertUniversalTx(1, RevertInstructions(user2, "")); } } diff --git a/contracts/evm-gateway/test/gateway/2_GatewayDepositNative.t.sol b/contracts/evm-gateway/test/gateway/2_GatewayDepositNative.t.sol index 6102cd3..96d9992 100644 --- a/contracts/evm-gateway/test/gateway/2_GatewayDepositNative.t.sol +++ b/contracts/evm-gateway/test/gateway/2_GatewayDepositNative.t.sol @@ -315,7 +315,7 @@ contract GatewayDepositNativeTest is BaseTest { /// @notice Test sendTxWithGas (native) when contract is paused function testSendTxWithGas_NativeETH_WhenPaused_Reverts() public { // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); // Setup: Create a valid payload and revert config @@ -333,7 +333,7 @@ contract GatewayDepositNativeTest is BaseTest { /// @notice Test sendFunds when contract is paused function testSendFunds_WhenPaused_Reverts() public { // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); // Setup: Create revert config diff --git a/contracts/evm-gateway/test/gateway/3_GatewayDepositNonNative.t.sol b/contracts/evm-gateway/test/gateway/3_GatewayDepositNonNative.t.sol index 3ae2287..73bdd03 100644 --- a/contracts/evm-gateway/test/gateway/3_GatewayDepositNonNative.t.sol +++ b/contracts/evm-gateway/test/gateway/3_GatewayDepositNonNative.t.sol @@ -155,6 +155,7 @@ contract GatewayDepositNonNativeTest is BaseTest { admin, // admin pauser, // pauser tss, // tss + address(this), // vault address MIN_CAP_USD, MAX_CAP_USD, uniV3Factory, @@ -240,7 +241,7 @@ contract GatewayDepositNonNativeTest is BaseTest { /// @notice Test sendFunds (ERC20) with valid parameters function testSendFunds_ERC20_HappyPath() public { // Setup: Create revert config - RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertMsg: bytes("") }); + RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertContext: bytes("") }); // Use USDC for bridging uint256 bridgeAmount = 1000e6; // 1000 USDC (6 decimals) @@ -271,11 +272,11 @@ contract GatewayDepositNonNativeTest is BaseTest { // Verify user's token balance decreased assertEq(mainnetUSDC.balanceOf(user1), initialUserTokenBalance - bridgeAmount, "User should pay ERC20 tokens"); - // Verify gateway's token balance increased + // Verify VAULT's token balance increased (tokens are now transferred to VAULT) assertEq( - mainnetUSDC.balanceOf(address(gateway)), - initialGatewayTokenBalance + bridgeAmount, - "Gateway should receive ERC20 tokens" + mainnetUSDC.balanceOf(gateway.VAULT()), + bridgeAmount, + "VAULT should receive ERC20 tokens" ); } @@ -352,16 +353,16 @@ contract GatewayDepositNonNativeTest is BaseTest { // Verify gateway's token balance increased assertEq( - mainnetUSDC.balanceOf(address(gateway)), - initialGatewayTokenBalance + bridgeAmount, - "Gateway should receive USDC for bridging" + 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, revertMsg: bytes("") }); + RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertContext: bytes("") }); uint256 minAmount = 1; // Minimum amount @@ -382,16 +383,16 @@ contract GatewayDepositNonNativeTest is BaseTest { // Test passes if no revert occurs assertEq( - IERC20(MAINNET_USDC).balanceOf(address(gateway)), - initialGatewayBalance + minAmount, - "Gateway should receive USDC" + 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, revertMsg: bytes("") }); + RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertContext: bytes("") }); // Test sendFunds with maximum amount uint256 maxTokenAmount = 1000000e6; // Large token amount @@ -572,12 +573,12 @@ contract GatewayDepositNonNativeTest is BaseTest { 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, revertMsg: bytes("") }); + 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 @@ -753,7 +754,7 @@ contract GatewayDepositNonNativeTest is BaseTest { /// @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, revertMsg: bytes("") }); + RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertContext: bytes("") }); uint256 tokenAmount = 1000e6; // 1000 USDC (6 decimals) @@ -778,9 +779,9 @@ contract GatewayDepositNonNativeTest is BaseTest { ); assertEq( - mainnetUSDC.balanceOf(address(gateway)), - initialGatewayBalance + tokenAmount, - "Gateway should receive the tokens" + mainnetUSDC.balanceOf(gateway.VAULT()), + tokenAmount, + "VAULT should receive the tokens" ); } @@ -829,9 +830,9 @@ contract GatewayDepositNonNativeTest is BaseTest { ); assertEq( - mainnetUSDC.balanceOf(address(gateway)), - initialGatewayBalance + bridgeAmount, - "Gateway should receive both bridge and gas tokens" + mainnetUSDC.balanceOf(gateway.VAULT()), + bridgeAmount, + "VAULT should receive the bridge tokens" ); assertApproxEqAbs( @@ -846,7 +847,7 @@ contract GatewayDepositNonNativeTest is BaseTest { /// @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, revertMsg: bytes("") }); + RevertInstructions memory revertCfg_ = RevertInstructions({ fundRecipient: recipient, revertContext: bytes("") }); uint256 amount = 1000e6; // 1000 USDC (6 decimals) @@ -1009,7 +1010,7 @@ contract GatewayDepositNonNativeTest is BaseTest { vType: VerificationType.signedVerification }); - RevertInstructions memory revertCfg = RevertInstructions({ fundRecipient: to, revertMsg: bytes("") }); + RevertInstructions memory revertCfg = RevertInstructions({ fundRecipient: to, revertContext: bytes("") }); return (payload, revertCfg); } @@ -1397,6 +1398,7 @@ contract GatewayDepositNonNativeTest is BaseTest { address(0), // Zero admin pauser, tss, + address(this), // vault address 100e18, // minCapUsd 10000e18, // maxCapUsd address(0x123), // factory @@ -1410,6 +1412,7 @@ contract GatewayDepositNonNativeTest is BaseTest { admin, address(0), // Zero pauser tss, + address(this), // vault address 100e18, 10000e18, address(0x123), @@ -1423,6 +1426,7 @@ contract GatewayDepositNonNativeTest is BaseTest { admin, pauser, address(0), // Zero tss + address(this), // vault address 100e18, 10000e18, address(0x123), @@ -1436,6 +1440,7 @@ contract GatewayDepositNonNativeTest is BaseTest { admin, pauser, tss, + address(this), // vault address 100e18, 10000e18, address(0x123), @@ -1448,6 +1453,7 @@ contract GatewayDepositNonNativeTest is BaseTest { admin, pauser, tss, + address(this), // vault address 100e18, 10000e18, address(0x123), // Non-zero factory @@ -1471,6 +1477,7 @@ contract GatewayDepositNonNativeTest is BaseTest { admin, pauser, tss, + address(this), // vault address 50e18, 5000e18, address(0), // Zero factory diff --git a/contracts/evm-gateway/test/gateway/4_GatewayTSSFunctions.t.sol b/contracts/evm-gateway/test/gateway/4_GatewayTSSFunctions.t.sol index 6ea9aed..d0c41d6 100644 --- a/contracts/evm-gateway/test/gateway/4_GatewayTSSFunctions.t.sol +++ b/contracts/evm-gateway/test/gateway/4_GatewayTSSFunctions.t.sol @@ -10,7 +10,7 @@ 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 withdrawFunds, revertWithdrawFunds, onlyTSS modifier, and sendTxWithFunds overload +/// @dev Tests revertNative, revertTokens, onlyTSS modifier, and sendTxWithFunds overload contract GatewayTSSFunctionsTest is BaseTest { // ========================= // SETUP @@ -21,6 +21,18 @@ contract GatewayTSSFunctionsTest is BaseTest { // Fund the gateway with some ETH and tokens for withdrawal tests vm.deal(address(gateway), 10 ether); + // Configure token support + 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[1] = 1000000e18; // 1M tokenA + + vm.prank(admin); + gateway.setTokenLimitThresholds(tokens, thresholds); + // Mint and transfer some test tokens to the gateway usdc.mint(address(gateway), 1000e6); tokenA.mint(address(gateway), 1000e18); @@ -34,15 +46,16 @@ contract GatewayTSSFunctionsTest is BaseTest { // Non-TSS user should not be able to call TSS functions vm.prank(user1); vm.expectRevert(abi.encodeWithSelector(Errors.WithdrawFailed.selector)); - gateway.withdrawFunds(user1, address(0), 1 ether); + gateway.revertUniversalTx(1 ether, RevertInstructions(user1, "")); } function testOnlyTSS_TSSShouldSucceed() public { // TSS should be able to call TSS functions uint256 initialBalance = user1.balance; + vm.deal(tss, 1 ether); vm.prank(tss); - gateway.withdrawFunds(user1, address(0), 1 ether); + gateway.revertUniversalTx{value: 1 ether}(1 ether, RevertInstructions(user1, "")); assertEq(user1.balance, initialBalance + 1 ether); } @@ -53,18 +66,17 @@ contract GatewayTSSFunctionsTest is BaseTest { function testWithdrawFunds_NativeETH_Success() public { uint256 withdrawAmount = 2 ether; - uint256 initialGatewayBalance = address(gateway).balance; uint256 initialRecipientBalance = user1.balance; - // Expect WithdrawFunds event + // Expect RevertUniversalTx event vm.expectEmit(true, true, true, true); - emit IUniversalGateway.WithdrawFunds(user1, withdrawAmount, address(0)); + emit IUniversalGateway.RevertUniversalTx(user1, address(0), withdrawAmount, RevertInstructions(user1, "")); + vm.deal(tss, withdrawAmount); vm.prank(tss); - gateway.withdrawFunds(user1, address(0), withdrawAmount); + gateway.revertUniversalTx{value: withdrawAmount}(withdrawAmount, RevertInstructions(user1, "")); // Check balances - assertEq(address(gateway).balance, initialGatewayBalance - withdrawAmount); assertEq(user1.balance, initialRecipientBalance + withdrawAmount); } @@ -73,12 +85,12 @@ contract GatewayTSSFunctionsTest is BaseTest { uint256 initialGatewayBalance = usdc.balanceOf(address(gateway)); uint256 initialRecipientBalance = usdc.balanceOf(user1); - // Expect WithdrawFunds event + // Expect RevertUniversalTx event vm.expectEmit(true, true, true, true); - emit IUniversalGateway.WithdrawFunds(user1, withdrawAmount, address(usdc)); + emit IUniversalGateway.RevertUniversalTx(user1, address(usdc), withdrawAmount, RevertInstructions(user1, "")); - vm.prank(tss); - gateway.withdrawFunds(user1, address(usdc), withdrawAmount); + // revertUniversalTxToken requires VAULT_ROLE (test contract has this role) + gateway.revertUniversalTxToken(address(usdc), withdrawAmount, RevertInstructions(user1, "")); // Check balances assertEq(usdc.balanceOf(address(gateway)), initialGatewayBalance - withdrawAmount); @@ -88,21 +100,23 @@ contract GatewayTSSFunctionsTest is BaseTest { function testWithdrawFunds_InvalidRecipient_Revert() public { vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidRecipient.selector)); - gateway.withdrawFunds(address(0), address(0), 1 ether); + gateway.revertUniversalTx(1 ether, RevertInstructions(address(0), "")); } function testWithdrawFunds_InvalidAmount_Revert() public { vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); - gateway.withdrawFunds(user1, address(0), 0); + gateway.revertUniversalTx(0, RevertInstructions(user1, "")); } function testWithdrawFunds_InsufficientBalance_Revert() public { - uint256 excessiveAmount = address(gateway).balance + 1 ether; + uint256 amount = 1 ether; + uint256 wrongValue = 0.5 ether; + vm.deal(tss, wrongValue); vm.prank(tss); - vm.expectRevert(); - gateway.withdrawFunds(user1, address(0), excessiveAmount); + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); + gateway.revertUniversalTx{value: wrongValue}(amount, RevertInstructions(user1, "")); } function testWithdrawFunds_ERC20InsufficientBalance_Revert() public { @@ -110,7 +124,7 @@ contract GatewayTSSFunctionsTest is BaseTest { vm.prank(tss); vm.expectRevert(); - gateway.withdrawFunds(user1, address(usdc), excessiveAmount); + gateway.revertUniversalTxToken(address(usdc), excessiveAmount, RevertInstructions(user1, "")); } // ========================= @@ -119,20 +133,19 @@ contract GatewayTSSFunctionsTest is BaseTest { function testRevertWithdrawFunds_NativeETH_Success() public { uint256 withdrawAmount = 1.5 ether; - uint256 initialGatewayBalance = address(gateway).balance; uint256 initialRecipientBalance = user1.balance; RevertInstructions memory revertCfg = revertCfg(user1); - // Expect WithdrawFunds event + // Expect RevertUniversalTx event vm.expectEmit(true, true, true, true); - emit IUniversalGateway.WithdrawFunds(user1, withdrawAmount, address(0)); + emit IUniversalGateway.RevertUniversalTx(user1, address(0), withdrawAmount, revertCfg); + vm.deal(tss, withdrawAmount); vm.prank(tss); - gateway.revertWithdrawFunds(address(0), withdrawAmount, revertCfg); + gateway.revertUniversalTx{value: withdrawAmount}(withdrawAmount, revertCfg); // Check balances - assertEq(address(gateway).balance, initialGatewayBalance - withdrawAmount); assertEq(user1.balance, initialRecipientBalance + withdrawAmount); } @@ -143,12 +156,12 @@ contract GatewayTSSFunctionsTest is BaseTest { RevertInstructions memory revertCfg = revertCfg(user1); - // Expect WithdrawFunds event + // Expect RevertUniversalTx event vm.expectEmit(true, true, true, true); - emit IUniversalGateway.WithdrawFunds(user1, withdrawAmount, address(usdc)); + emit IUniversalGateway.RevertUniversalTx(user1, address(usdc), withdrawAmount, revertCfg); - vm.prank(tss); - gateway.revertWithdrawFunds(address(usdc), withdrawAmount, revertCfg); + // revertUniversalTxToken requires VAULT_ROLE (test contract has this role) + gateway.revertUniversalTxToken(address(usdc), withdrawAmount, revertCfg); // Check balances assertEq(usdc.balanceOf(address(gateway)), initialGatewayBalance - withdrawAmount); @@ -160,7 +173,7 @@ contract GatewayTSSFunctionsTest is BaseTest { vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidRecipient.selector)); - gateway.revertWithdrawFunds(address(0), 1 ether, revertCfg); + gateway.revertUniversalTx(1 ether, revertCfg); } function testRevertWithdrawFunds_InvalidAmount_Revert() public { @@ -168,16 +181,18 @@ contract GatewayTSSFunctionsTest is BaseTest { vm.prank(tss); vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); - gateway.revertWithdrawFunds(address(0), 0, revertCfg); + gateway.revertUniversalTx(0, revertCfg); } function testRevertWithdrawFunds_InsufficientBalance_Revert() public { - uint256 excessiveAmount = address(gateway).balance + 1 ether; + uint256 amount = 1 ether; + uint256 wrongValue = 0.8 ether; RevertInstructions memory revertCfg = revertCfg(user1); + vm.deal(tss, wrongValue); vm.prank(tss); - vm.expectRevert(); - gateway.revertWithdrawFunds(address(0), excessiveAmount, revertCfg); + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); + gateway.revertUniversalTx{value: wrongValue}(amount, revertCfg); } // ========================= @@ -186,32 +201,33 @@ contract GatewayTSSFunctionsTest is BaseTest { function testWithdrawFunds_WhenPaused_Revert() public { // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); vm.prank(tss); vm.expectRevert(); - gateway.withdrawFunds(user1, address(0), 1 ether); + gateway.revertUniversalTx(1 ether, RevertInstructions(user1, "")); } function testRevertWithdrawFunds_WhenPaused_Revert() public { // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); RevertInstructions memory revertCfg = revertCfg(user1); vm.prank(tss); vm.expectRevert(); - gateway.revertWithdrawFunds(address(0), 1 ether, revertCfg); + gateway.revertUniversalTx(1 ether, revertCfg); } function testWithdrawFunds_ReentrancyProtection() public { // 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.withdrawFunds(user1, address(0), 1 ether); + gateway.revertUniversalTx{value: 1 ether}(1 ether, RevertInstructions(user1, "")); // If we get here without reverting, the reentrancy protection is working assertTrue(true); @@ -223,22 +239,21 @@ contract GatewayTSSFunctionsTest is BaseTest { uint256 tokenAAmount = 100e18; uint256 ethAmount = 0.5 ether; - // Withdraw USDC + // Withdraw USDC (requires VAULT_ROLE) uint256 initialUsdcBalance = usdc.balanceOf(user1); - vm.prank(tss); - gateway.withdrawFunds(user1, address(usdc), usdcAmount); + gateway.revertUniversalTxToken(address(usdc), usdcAmount, RevertInstructions(user1, "")); assertEq(usdc.balanceOf(user1), initialUsdcBalance + usdcAmount); - // Withdraw TokenA + // Withdraw TokenA (requires VAULT_ROLE) uint256 initialTokenABalance = tokenA.balanceOf(user1); - vm.prank(tss); - gateway.withdrawFunds(user1, address(tokenA), tokenAAmount); + gateway.revertUniversalTxToken(address(tokenA), tokenAAmount, RevertInstructions(user1, "")); assertEq(tokenA.balanceOf(user1), initialTokenABalance + tokenAAmount); - // Withdraw ETH + // Withdraw ETH (requires TSS_ROLE) uint256 initialEthBalance = user1.balance; + vm.deal(tss, ethAmount); vm.prank(tss); - gateway.withdrawFunds(user1, address(0), ethAmount); + gateway.revertUniversalTx{value: ethAmount}(ethAmount, RevertInstructions(user1, "")); assertEq(user1.balance, initialEthBalance + ethAmount); } @@ -249,16 +264,16 @@ contract GatewayTSSFunctionsTest is BaseTest { uint256 usdcAmount = 25e6; uint256 ethAmount = 0.25 ether; - // Revert USDC + // Revert USDC (requires VAULT_ROLE) uint256 initialUsdcBalance = usdc.balanceOf(user1); - vm.prank(tss); - gateway.revertWithdrawFunds(address(usdc), usdcAmount, revertCfg); + gateway.revertUniversalTxToken(address(usdc), usdcAmount, revertCfg); assertEq(usdc.balanceOf(user1), initialUsdcBalance + usdcAmount); - // Revert ETH + // Revert ETH (requires TSS_ROLE) uint256 initialEthBalance = user1.balance; + vm.deal(tss, ethAmount); vm.prank(tss); - gateway.revertWithdrawFunds(address(0), ethAmount, revertCfg); + gateway.revertUniversalTx{value: ethAmount}(ethAmount, revertCfg); assertEq(user1.balance, initialEthBalance + ethAmount); } } diff --git a/contracts/evm-gateway/test/gateway/5_GatewayBlockRateLimit.t.sol b/contracts/evm-gateway/test/gateway/5_GatewayBlockRateLimit.t.sol index 3f5a0df..ad0fc89 100644 --- a/contracts/evm-gateway/test/gateway/5_GatewayBlockRateLimit.t.sol +++ b/contracts/evm-gateway/test/gateway/5_GatewayBlockRateLimit.t.sol @@ -489,7 +489,7 @@ contract GatewayBlockRateLimitTest is BaseTest { ); // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); // Try to send tx while paused - should revert due to pause @@ -500,7 +500,7 @@ contract GatewayBlockRateLimitTest is BaseTest { ); // Unpause - vm.prank(pauser); + vm.prank(admin); gateway.unpause(); // Should be able to use remaining $5 of cap diff --git a/contracts/evm-gateway/test/gateway/6_GatewayGlobalRateLimit.t.sol b/contracts/evm-gateway/test/gateway/6_GatewayGlobalRateLimit.t.sol index c49f92d..4db30b8 100644 --- a/contracts/evm-gateway/test/gateway/6_GatewayGlobalRateLimit.t.sol +++ b/contracts/evm-gateway/test/gateway/6_GatewayGlobalRateLimit.t.sol @@ -56,7 +56,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { // Use buildDefaultPayload() and buildDefaultRevertInstructions() from BaseTest function _buildDefaultRevertInstructions() internal view returns (RevertInstructions memory) { - return RevertInstructions({ fundRecipient: user1, revertMsg: bytes("") }); + return RevertInstructions({ fundRecipient: user1, revertContext: bytes("") }); } function _getCurrentEpoch() internal view returns (uint256) { @@ -1040,7 +1040,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { function testSetTokenLimitThresholdsWhenPaused() public { // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); // Setup token thresholds @@ -1063,7 +1063,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { gateway.setTokenLimitThresholds(tokens, thresholds); // Unpause the contract - vm.prank(pauser); + vm.prank(admin); gateway.unpause(); } @@ -1079,7 +1079,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { gateway.setTokenLimitThresholds(tokens, thresholds); // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); // Update threshold @@ -1100,7 +1100,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { gateway.updateTokenLimitThreshold(tokens, thresholds); // Unpause the contract - vm.prank(pauser); + vm.prank(admin); gateway.unpause(); } @@ -1110,7 +1110,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { uint256 newDuration = oldDuration * 2; // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); // Admin should be able to update epoch duration while paused @@ -1126,7 +1126,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { gateway.updateEpochDuration(oldDuration); // Unpause the contract - vm.prank(pauser); + vm.prank(admin); gateway.unpause(); } @@ -1142,7 +1142,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { gateway.setTokenLimitThresholds(tokens, thresholds); // Pause the contract - vm.prank(pauser); + vm.prank(admin); gateway.pause(); // Try to send funds while paused @@ -1158,7 +1158,7 @@ contract GatewayGlobalRateLimitTest is BaseTest { vm.stopPrank(); // Unpause the contract - vm.prank(pauser); + vm.prank(admin); gateway.unpause(); // Now sending funds should work diff --git a/contracts/evm-gateway/test/gateway/7_GatewayExecuteUniversalTx.t.sol b/contracts/evm-gateway/test/gateway/7_GatewayExecuteUniversalTx.t.sol new file mode 100644 index 0000000..243c041 --- /dev/null +++ b/contracts/evm-gateway/test/gateway/7_GatewayExecuteUniversalTx.t.sol @@ -0,0 +1,646 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { Test } from "forge-std/Test.sol"; +import { UniversalGateway } from "../../src/UniversalGateway.sol"; +import { Errors } from "../../src/libraries/Errors.sol"; +import { MockERC20 } from "../mocks/MockERC20.sol"; +import { MockTarget } from "../mocks/MockTarget.sol"; +import { MockRevertingTarget } from "../mocks/MockRevertingTarget.sol"; +import { MockUSDTToken } from "../mocks/MockUSDTToken.sol"; +import { MockTokenApprovalVariants } from "../mocks/MockTokenApprovalVariants.sol"; + +contract GatewayExecuteUniversalTxTest is Test { + UniversalGateway public gateway; + MockERC20 public token; + MockTarget public target; + MockRevertingTarget public revertingTarget; + MockUSDTToken public usdtToken; + MockTokenApprovalVariants public approvalToken; + + address public admin = address(0x1); + address public pauser = address(0x2); + address public tss = address(0x3); + 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"); + address public constant ORIGIN_CALLER = address(0x6); + address public constant ZERO_ADDRESS = address(0); + uint256 public constant AMOUNT = 1000e18; + bytes public constant PAYLOAD = abi.encodeWithSignature("receiveFunds()"); + bytes public constant EMPTY_PAYLOAD = ""; + + event UniversalTxExecuted( + bytes32 indexed txID, + address indexed originCaller, + address indexed target, + address token, + uint256 amount, + bytes payload + ); + + function setUp() public { + // Deploy contracts + gateway = new UniversalGateway(); + token = new MockERC20("Test Token", "TT", 18, 1000000e18); + target = new MockTarget(); + revertingTarget = new MockRevertingTarget(); + usdtToken = new MockUSDTToken(); + approvalToken = new MockTokenApprovalVariants(); + + // Initialize gateway + gateway.initialize( + admin, + tss, + address(this), // vault address + 1e18, // minCapUsd + 10e18, // maxCapUsd + address(0), // factory + address(0), // router + address(0x123) // weth (dummy address for testing) + ); + + // Set up token support + address[] memory tokens = new address[](3); + uint256[] memory thresholds = new uint256[](3); + tokens[0] = address(token); + tokens[1] = address(usdtToken); + tokens[2] = address(approvalToken); + thresholds[0] = 10000e18; + thresholds[1] = 10000e18; + thresholds[2] = 10000e18; + vm.prank(admin); + gateway.setTokenLimitThresholds(tokens, thresholds); + + // Fund test contract (acting as Vault) with tokens + token.mint(address(this), 100000e18); + usdtToken.mint(address(this), 100000e18); + approvalToken.mint(address(this), 100000e18); + } + + // ========================= + // executeUniversalTx Tests + // ========================= + + function testExecuteUniversalTx_NotTSS_Reverts() public { + vm.expectRevert(); + vm.prank(user); + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + } + + function testExecuteUniversalTx_WhenPaused_Reverts() public { + vm.prank(admin); + gateway.pause(); + + vm.expectRevert(); + vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + } + + function testExecuteUniversalTx_ValidTSS_Succeeds() public { + // 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)); + } + + // Input Validation Tests + function testExecuteUniversalTx_DuplicateTxID_Reverts() public { + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT * 2); + + // First execution succeeds + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + + // Second execution with same txID reverts + vm.expectRevert(abi.encodeWithSelector(Errors.PayloadExecuted.selector)); + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + } + + function testExecuteUniversalTx_ZeroOriginCaller_Reverts() public { + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); + gateway.executeUniversalTx(TX_ID, ZERO_ADDRESS, address(token), address(target), AMOUNT, PAYLOAD); + } + + function testExecuteUniversalTx_ZeroTarget_Reverts() public { + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidInput.selector)); + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), ZERO_ADDRESS, AMOUNT, PAYLOAD); + } + + function testExecuteUniversalTx_ZeroAmount_Reverts() public { + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), 0, PAYLOAD); + } + + function testExecuteUniversalTx_EmptyPayload_Succeeds() public { + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, EMPTY_PAYLOAD); + + assertTrue(gateway.isExecuted(TX_ID)); + // Verify the target received the call + assertEq(target.lastCaller(), address(gateway)); + } + + // Native (ETH) Path Tests + function testExecuteUniversalTx_NativePath_Success() public { + uint256 initialBalance = address(target).balance; + + vm.deal(tss, 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 + ); + + assertTrue(gateway.isExecuted(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.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); + 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.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); + gateway.executeUniversalTx{ value: AMOUNT + 1 }( + TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD + ); + } + + function testExecuteUniversalTx_NativePath_NonPayableTarget_Reverts() public { + vm.deal(tss, AMOUNT); + + bytes memory nonPayablePayload = abi.encodeWithSignature("receiveFundsNonPayable()"); + + vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); + gateway.executeUniversalTx{ value: AMOUNT }( + TX_ID, ORIGIN_CALLER, address(revertingTarget), AMOUNT, nonPayablePayload + ); + } + + function testExecuteUniversalTx_NativePath_TargetReverts_Reverts() public { + vm.deal(tss, AMOUNT); + + vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); + gateway.executeUniversalTx{ value: AMOUNT }( + TX_ID, ORIGIN_CALLER, address(revertingTarget), AMOUNT, PAYLOAD + ); + + assertFalse(gateway.isExecuted(TX_ID)); + } + + function testExecuteUniversalTx_NativePath_GasExhaustion_Reverts() public { + vm.deal(tss, AMOUNT); + + bytes memory gasHeavyPayload = abi.encodeWithSignature("receiveFundsGasHeavy()"); + + vm.prank(tss); // Native token executeUniversalTx requires TSS_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)); + } + + // ERC-20 Path Tests + function testExecuteUniversalTx_ERC20Path_WithValue_Reverts() public { + vm.deal(tss, 1); + + 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); + } + + function testExecuteUniversalTx_ERC20Path_InsufficientBalance_Reverts() public { + uint256 largeAmount = 1000000e18; + + // Transfer a small amount of tokens to Gateway + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidAmount.selector)); + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), largeAmount, PAYLOAD); + } + + function testExecuteUniversalTx_ERC20Path_Success() public { + // Initial setup + uint256 initialBalance = token.balanceOf(address(target)); + bytes memory tokenPayload = abi.encodeWithSignature("receiveToken(address,uint256)", address(token), AMOUNT); + + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + vm.expectEmit(true, true, true, true); + emit UniversalTxExecuted(TX_ID, ORIGIN_CALLER, address(target), address(token), AMOUNT, tokenPayload); + + // 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)); + assertEq(token.balanceOf(address(target)), initialBalance + AMOUNT); + } + + function testExecuteUniversalTx_ERC20Path_TargetReverts_Reverts() public { + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(revertingTarget), AMOUNT, PAYLOAD); + + assertFalse(gateway.isExecuted(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)); + } + + function testExecuteUniversalTx_ERC20Path_ResetApprovalFails_Reverts() public { + approvalToken.setApprovalBehavior(MockTokenApprovalVariants.ApprovalBehavior.RETURN_FALSE); + + // Transfer tokens from Vault (test contract) to Gateway + approvalToken.transfer(address(gateway), AMOUNT); + + vm.prank(address(gateway)); + approvalToken.approve(address(target), AMOUNT); + + bytes memory tokenPayload = + abi.encodeWithSignature("receiveToken(address,uint256)", address(approvalToken), AMOUNT); + + // This should revert because the token returns false on approve(0) + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidData.selector)); + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, tokenPayload); + + // Transaction should not be executed + assertFalse(gateway.isExecuted(TX_ID)); + } + + // Reentrancy Tests + // Event & State Tests + function testExecuteUniversalTx_EventEmittedCorrectly() public { + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + vm.expectEmit(true, true, true, true); + emit UniversalTxExecuted(TX_ID, ORIGIN_CALLER, address(target), address(token), AMOUNT, PAYLOAD); + + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + } + + function testExecuteUniversalTx_StateUpdatedCorrectly() public { + assertFalse(gateway.isExecuted(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)); + } + + // ========================= + // _resetApproval Tests + // ========================= + + function testResetApproval_StandardERC20_Success() public { + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + vm.prank(address(gateway)); + token.approve(address(target), 1000); + assertEq(token.allowance(address(gateway), address(target)), 1000); + + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + + assertEq(token.allowance(address(gateway), address(target)), 0); + } + + function testResetApproval_USDTStyle_Success() public { + // Transfer tokens from Vault (test contract) to Gateway + usdtToken.transfer(address(gateway), AMOUNT); + + vm.prank(address(gateway)); + usdtToken.approve(address(target), 1000); + assertEq(usdtToken.allowance(address(gateway), address(target)), 1000); + + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(usdtToken), address(target), AMOUNT, PAYLOAD); + + assertEq(usdtToken.allowance(address(gateway), address(target)), 0); + } + + function testResetApproval_NoReturnData_Success() public { + // Configure approvalToken for no return data behavior + approvalToken.setApprovalBehavior(MockTokenApprovalVariants.ApprovalBehavior.NO_RETURN_DATA); + + // Transfer tokens from Vault (test contract) to Gateway + approvalToken.transfer(address(gateway), AMOUNT); + + vm.prank(address(gateway)); + approvalToken.approve(address(target), AMOUNT); + assertEq(approvalToken.allowance(address(gateway), address(target)), AMOUNT); + + bytes memory tokenPayload = + abi.encodeWithSignature("receiveToken(address,uint256)", address(approvalToken), AMOUNT); + + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, tokenPayload); + + assertTrue(gateway.isExecuted(TX_ID)); + + assertEq(approvalToken.balanceOf(address(target)), AMOUNT); + } + + function testResetApproval_RevertsOnZeroApproval_Success() public { + approvalToken.setApprovalBehavior(MockTokenApprovalVariants.ApprovalBehavior.REVERT_ON_ZERO); + + // Transfer tokens from Vault (test contract) to Gateway + approvalToken.transfer(address(gateway), AMOUNT); + + vm.prank(address(gateway)); + approvalToken.approve(address(target), AMOUNT); + assertEq(approvalToken.allowance(address(gateway), address(target)), AMOUNT); + + bytes memory tokenPayload = + abi.encodeWithSignature("receiveToken(address,uint256)", address(approvalToken), AMOUNT); + + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, tokenPayload); + + assertTrue(gateway.isExecuted(TX_ID)); + + assertEq(approvalToken.balanceOf(address(target)), AMOUNT); + } + + function testResetApproval_ReturnsFalse_Reverts() public { + approvalToken.setApprovalBehavior(MockTokenApprovalVariants.ApprovalBehavior.RETURN_FALSE); + + // Transfer tokens from Vault (test contract) to Gateway + approvalToken.transfer(address(gateway), AMOUNT); + + vm.prank(address(gateway)); + approvalToken.approve(address(target), AMOUNT); + assertEq(approvalToken.allowance(address(gateway), address(target)), AMOUNT); + + bytes memory tokenPayload = + abi.encodeWithSignature("receiveToken(address,uint256)", address(approvalToken), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidData.selector)); + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, tokenPayload); + + assertFalse(gateway.isExecuted(TX_ID)); + } + + // ========================= + // _safeApprove Tests + // ========================= + + function testSafeApprove_StandardERC20_Success() public { + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + vm.prank(address(gateway)); + token.approve(address(target), 0); + assertEq(token.allowance(address(gateway), address(target)), 0); + + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + + assertEq(token.allowance(address(gateway), address(target)), 0); + } + + function testSafeApprove_USDTStyle_FromZero_Success() public { + // Transfer tokens from Vault (test contract) to Gateway + usdtToken.transfer(address(gateway), AMOUNT); + + vm.prank(address(gateway)); + usdtToken.approve(address(target), 0); + assertEq(usdtToken.allowance(address(gateway), address(target)), 0); + + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(usdtToken), address(target), AMOUNT, PAYLOAD); + + assertEq(usdtToken.allowance(address(gateway), address(target)), 0); + } + + function testSafeApprove_USDTStyle_FromNonZero_Reverts() public { + // Create a fresh USDT-style token + MockUSDTToken mockUsdtToken = new MockUSDTToken(); + + mockUsdtToken.mint(address(gateway), AMOUNT); + + vm.startPrank(address(gateway)); + mockUsdtToken.approve(address(target), 0); + mockUsdtToken.approve(address(target), 1000); + vm.stopPrank(); + assertEq(mockUsdtToken.allowance(address(gateway), address(target)), 1000); + + bytes memory tokenPayload = + abi.encodeWithSignature("receiveToken(address,uint256)", address(mockUsdtToken), AMOUNT); + + vm.startPrank(address(gateway)); + vm.expectRevert("USDT: Cannot approve from non-zero to non-zero"); + mockUsdtToken.approve(address(target), AMOUNT); + vm.stopPrank(); + + // The full executeUniversalTx would actually succeed because it calls _resetApproval first + // So we're just testing that the direct approve would fail without the reset + + // Transaction should not be executed + assertFalse(gateway.isExecuted(TX_ID)); + } + + function testSafeApprove_NoReturnData_Success() public { + approvalToken.setApprovalBehavior(MockTokenApprovalVariants.ApprovalBehavior.NO_RETURN_DATA); + + // Transfer tokens from Vault (test contract) to Gateway + approvalToken.transfer(address(gateway), AMOUNT); + + vm.prank(address(gateway)); + approvalToken.approve(address(target), 0); + assertEq(approvalToken.allowance(address(gateway), address(target)), 0); + + bytes memory tokenPayload = + abi.encodeWithSignature("receiveToken(address,uint256)", address(approvalToken), AMOUNT); + + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, tokenPayload); + + assertTrue(gateway.isExecuted(TX_ID)); + + assertEq(approvalToken.balanceOf(address(target)), AMOUNT); + } + + function testSafeApprove_ReturnsFalse_Reverts() public { + approvalToken.setApprovalBehavior(MockTokenApprovalVariants.ApprovalBehavior.RETURN_FALSE); + + // Transfer tokens from Vault (test contract) to Gateway + approvalToken.transfer(address(gateway), AMOUNT); + + vm.prank(address(gateway)); + approvalToken.approve(address(target), 0); + assertEq(approvalToken.allowance(address(gateway), address(target)), 0); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidData.selector)); + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(approvalToken), address(target), AMOUNT, PAYLOAD); + } + + function testSafeApprove_RevertsOnApprove_Reverts() public { + // Configure approvalToken to always revert on approve + approvalToken.setApprovalBehavior(MockTokenApprovalVariants.ApprovalBehavior.ALWAYS_REVERT); + + // 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); + } + + function testSafeApprove_Idempotency_Success() public { + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + // Approve X, reset to 0, approve X again + vm.prank(address(gateway)); + token.approve(address(target), AMOUNT); + assertEq(token.allowance(address(gateway), address(target)), AMOUNT); + + // Reset to 0 + vm.prank(address(gateway)); + token.approve(address(target), 0); + assertEq(token.allowance(address(gateway), address(target)), 0); + + // Approve again + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + + assertEq(token.allowance(address(gateway), address(target)), 0); + } + + // ========================= + // _executeCall Tests + // ========================= + + function testExecuteCall_SuccessNoValue() public { + // 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); + + assertEq(target.lastCaller(), address(gateway)); + assertEq(target.lastAmount(), 0); + assertTrue(gateway.isExecuted(TX_ID)); + } + + function testExecuteCall_SuccessWithValue() public { + uint256 initialBalance = address(target).balance; + + vm.deal(tss, AMOUNT); + + vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + gateway.executeUniversalTx{ value: AMOUNT }( + TX_ID, ORIGIN_CALLER, address(target), AMOUNT, PAYLOAD + ); + + assertEq(address(target).balance, initialBalance + AMOUNT); + assertTrue(gateway.isExecuted(TX_ID)); + } + + function testExecuteCall_NonPayableTargetWithValue_Reverts() public { + vm.deal(tss, AMOUNT); + + bytes memory nonPayablePayload = abi.encodeWithSignature("receiveFundsNonPayable()"); + + vm.prank(tss); // Native token executeUniversalTx requires TSS_ROLE + vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); + gateway.executeUniversalTx{ value: AMOUNT }( + TX_ID, ORIGIN_CALLER, address(revertingTarget), AMOUNT, nonPayablePayload + ); + } + + function testExecuteCall_TargetReverts_Reverts() public { + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(revertingTarget), AMOUNT, PAYLOAD); + } + + function testExecuteCall_GasExhaustion_Reverts() public { + bytes memory gasHeavyPayload = abi.encodeWithSignature("receiveFundsGasHeavy()"); + + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(Errors.ExecutionFailed.selector)); + gateway.executeUniversalTx( + TX_ID, ORIGIN_CALLER, address(token), address(revertingTarget), AMOUNT, gasHeavyPayload + ); + } + + function testExecuteUniversalTx_ERC20WithArbitraryCall() public { + bytes memory approvePayload = abi.encodeWithSignature("approve(address,uint256)", address(0x7), 500e18); + + // Transfer tokens from Vault (test contract) to Gateway + token.transfer(address(gateway), AMOUNT); + + gateway.executeUniversalTx(TX_ID, ORIGIN_CALLER, address(token), address(token), AMOUNT, approvePayload); + + assertTrue(gateway.isExecuted(TX_ID)); + assertEq(token.allowance(address(gateway), address(0x7)), 500e18); + } + + function testExecuteUniversalTx_MultipleExecutionsDifferentTxIDs() public { + bytes32 txId1 = keccak256("tx1"); + bytes32 txId2 = keccak256("tx2"); + + // Transfer tokens from Vault (test contract) to Gateway for first execution + token.transfer(address(gateway), AMOUNT); + + gateway.executeUniversalTx(txId1, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + + // Transfer more tokens for second execution + token.transfer(address(gateway), AMOUNT); + + gateway.executeUniversalTx(txId2, ORIGIN_CALLER, address(token), address(target), AMOUNT, PAYLOAD); + + assertTrue(gateway.isExecuted(txId1)); + assertTrue(gateway.isExecuted(txId2)); + } + + function testIsExecutedMapping() public { + assertFalse(gateway.isExecuted(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)); + } +} diff --git a/contracts/evm-gateway/test/gateway/8_GatewayPC.t.sol b/contracts/evm-gateway/test/gateway/8_GatewayPC.t.sol new file mode 100644 index 0000000..a3e4026 --- /dev/null +++ b/contracts/evm-gateway/test/gateway/8_GatewayPC.t.sol @@ -0,0 +1,1133 @@ +// 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/mocks/MockPRC20.sol b/contracts/evm-gateway/test/mocks/MockPRC20.sol new file mode 100644 index 0000000..527e5fa --- /dev/null +++ b/contracts/evm-gateway/test/mocks/MockPRC20.sol @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { IUniversalCore } from "../../src/interfaces/IUniversalCore.sol"; +import { IPRC20 } from "../../src/interfaces/IPRC20.sol"; + +/** + * @title MockPRC20 + * @notice Accurate mock implementation of PRC20 for testing + * @dev This mock closely follows the real PRC20 implementation from pc-core-2nd + */ +contract MockPRC20 is IPRC20 { + // ========= Constants ========= + /// @notice The protocol's privileged executor module (auth & fee sink) + address public immutable UNIVERSAL_EXECUTOR_MODULE = 0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7; + + // ========= State ========= + /// @notice Source chain this PRC20 mirrors (used for oracle lookups) + string public SOURCE_CHAIN_ID; + /// @notice Source Chain ERC20 address of the PRC20 + string public SOURCE_TOKEN_ADDRESS; + + /// @notice Classification of this synthetic + enum TokenType { + PC, + NATIVE, + ERC20 + } + + TokenType public TOKEN_TYPE; + + /// @notice UniversalCore contract providing gas oracles (gas coin token & gas price) + address public UNIVERSAL_CORE; + + /// @notice Flat fee (absolute units in gas coin PRC20), NOT basis points + uint256 public PC_PROTOCOL_FEE; + + string private _name; + string private _symbol; + uint8 private _decimals; + + uint256 private _totalSupply; + mapping(address => uint256) private _balances; + mapping(address => mapping(address => uint256)) private _allowances; + + // ========= Events ========= + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + event Deposit(bytes indexed from, address indexed to, uint256 amount); + event Withdrawal(address indexed from, bytes indexed to, uint256 amount, uint256 gasFee, uint256 protocolFee); + event UpdatedUniversalCore(address addr); + event UpdatedProtocolFlatFee(uint256 protocolFlatFee); + + //*** MODIFIERS ***// + + /// @notice Restricts to the Universal Executor Module (protocol owner) + modifier onlyUniversalExecutor() { + require(msg.sender == UNIVERSAL_EXECUTOR_MODULE, "MockPRC20: caller is not Universal Executor"); + _; + } + + //*** CONSTRUCTOR ***// + + /// @dev For testing convenience, we use a constructor instead of initialize pattern + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + string memory sourceChainId_, + TokenType tokenType_, + uint256 protocolFlatFee_, + address universalCore_, + string memory sourceTokenAddress_ + ) { + require(universalCore_ != address(0), "MockPRC20: zero address"); + + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + + SOURCE_CHAIN_ID = sourceChainId_; + TOKEN_TYPE = tokenType_; + PC_PROTOCOL_FEE = protocolFlatFee_; + UNIVERSAL_CORE = universalCore_; + SOURCE_TOKEN_ADDRESS = sourceTokenAddress_; + } + + // ========= View Functions ========= + function name() external view returns (string memory) { + return _name; + } + + function symbol() external view returns (string memory) { + return _symbol; + } + + function decimals() external view returns (uint8) { + return _decimals; + } + + function totalSupply() external view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) external view returns (uint256) { + return _balances[account]; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return _allowances[owner][spender]; + } + + // ========= IPRC20 Implementation ========= + function transfer(address recipient, uint256 amount) external returns (bool) { + _transfer(msg.sender, recipient, amount); + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { + _transfer(sender, recipient, amount); + + uint256 currentAllowance = _allowances[sender][msg.sender]; + require(currentAllowance >= amount, "MockPRC20: insufficient allowance"); + + unchecked { + _allowances[sender][msg.sender] = currentAllowance - amount; + } + emit Approval(sender, msg.sender, _allowances[sender][msg.sender]); + + return true; + } + + function approve(address spender, uint256 amount) external returns (bool) { + require(spender != address(0), "MockPRC20: approve to zero address"); + _allowances[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function burn(uint256 amount) external returns (bool) { + _burn(msg.sender, amount); + return true; + } + + //*** BRIDGE ENTRYPOINTS ***// + + /// @notice Mint PRC20 on inbound bridge (lock on source) + /// @dev Only callable by UNIVERSAL_CORE or UNIVERSAL_EXECUTOR_MODULE + /// @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" + ); + + _mint(to, amount); + + emit Deposit(abi.encodePacked(UNIVERSAL_EXECUTOR_MODULE), to, amount); + return true; + } + + //*** GAS FEE DELEGATION TO UNIVERSAL CORE ***// + + /// @notice Get the gas limit (delegates to UniversalCore) + function GAS_LIMIT() external view returns (uint256) { + return IUniversalCore(UNIVERSAL_CORE).BASE_GAS_LIMIT(); + } + + /// @notice Get gas fee with custom gas limit (delegates to UniversalCore) + function withdrawGasFeeWithGasLimit(uint256 gasLimit) external view returns (address gasToken, uint256 gasFee) { + return IUniversalCore(UNIVERSAL_CORE).withdrawGasFeeWithGasLimit(address(this), gasLimit); + } + + //*** ADMIN FUNCTIONS ***// + + /// @notice Update UniversalCore contract (gas coin & price oracle source) + /// @dev only Universal Executor may update + function updateUniversalCore(address addr) external onlyUniversalExecutor { + require(addr != address(0), "MockPRC20: zero address"); + UNIVERSAL_CORE = addr; + emit UpdatedUniversalCore(addr); + } + + /// @notice Update flat protocol fee (absolute units in gas coin PRC20) + function updateProtocolFlatFee(uint256 protocolFlatFee_) external onlyUniversalExecutor { + PC_PROTOCOL_FEE = protocolFlatFee_; + emit UpdatedProtocolFlatFee(protocolFlatFee_); + } + + /// @notice Update token name + function setName(string memory newName) external onlyUniversalExecutor { + _name = newName; + } + + /// @notice Update token symbol + function setSymbol(string memory newSymbol) external onlyUniversalExecutor { + _symbol = newSymbol; + } + + //*** INTERNAL ERC-20 HELPERS ***// + + /** + * @notice Internal function to transfer PRC20 tokens between addresses + * @dev Handles the core transfer logic with balance and zero address checks + * @param sender Address to transfer tokens from + * @param recipient Address to transfer tokens to + * @param amount Amount of PRC20 tokens to transfer + */ + function _transfer(address sender, address recipient, uint256 amount) internal { + require(sender != address(0) && recipient != address(0), "MockPRC20: zero address"); + + uint256 senderBalance = _balances[sender]; + require(senderBalance >= amount, "MockPRC20: insufficient balance"); + + unchecked { + _balances[sender] = senderBalance - amount; + _balances[recipient] += amount; + } + + emit Transfer(sender, recipient, amount); + } + + /** + * @notice Internal function to mint new PRC20 tokens + * @dev Creates new tokens and assigns them to the specified account + * @dev Increases total supply and account balance + * @param account Address to mint tokens to + * @param amount Amount of PRC20 tokens to mint + */ + function _mint(address account, uint256 amount) internal { + require(account != address(0), "MockPRC20: zero address"); + require(amount > 0, "MockPRC20: zero amount"); + + unchecked { + _totalSupply += amount; + _balances[account] += amount; + } + emit Transfer(address(0), account, amount); + } + + /** + * @notice Internal function to burn PRC20 tokens + * @dev Burns tokens from the specified account and reduces total supply + * @param account Address to burn tokens from + * @param amount Amount of PRC20 tokens to burn + */ + function _burn(address account, uint256 amount) internal { + require(account != address(0), "MockPRC20: zero address"); + require(amount > 0, "MockPRC20: zero amount"); + + uint256 bal = _balances[account]; + require(bal >= amount, "MockPRC20: insufficient balance"); + + unchecked { + _balances[account] = bal - amount; + _totalSupply -= amount; + } + emit Transfer(account, address(0), amount); + } + + // ========= Test Helper Functions ========= + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function setBalance(address account, uint256 amount) external { + _balances[account] = amount; + } + + function setAllowance(address owner, address spender, uint256 amount) external { + _allowances[owner][spender] = amount; + } +} diff --git a/contracts/evm-gateway/test/mocks/MockReentrantContract.sol b/contracts/evm-gateway/test/mocks/MockReentrantContract.sol new file mode 100644 index 0000000..4035e98 --- /dev/null +++ b/contracts/evm-gateway/test/mocks/MockReentrantContract.sol @@ -0,0 +1,133 @@ +// 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"; + +/** + * @title MockReentrantContract + * @notice Mock contract that attempts to reenter gateway contracts + * @dev Used for testing reentrancy protection on UniversalGatewayPC and Vault + */ +contract MockReentrantContract { + address public gateway; + address public prc20Token; + address public gasToken; + + // Vault-specific reentrancy state + address public vault; + address public vaultPC; + address public token; + bool public shouldReenter; + uint256 public reenterType; // 0=withdraw, 1=withdrawAndCall, 2=revertWithdraw + + constructor(address _gateway, address _prc20Token, address _gasToken) { + gateway = _gateway; + prc20Token = _prc20Token; + gasToken = _gasToken; + } + + // ============================================================================ + // 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 attemptReentrancyWithExecute( + bytes calldata target, + uint256 amount, + bytes calldata payload, + uint256 gasLimit, + RevertInstructions calldata revertCfg + ) external { + IUniversalGatewayPC(gateway).withdrawAndExecute( + target, + prc20Token, + amount, + payload, + gasLimit, + revertCfg + ); + } + + // ============================================================================ + // Vault Reentrancy Functions + // ============================================================================ + + function setVault(address _vault) external { + vault = _vault; + } + + function setVaultPC(address _vaultPC) external { + vaultPC = _vaultPC; + } + + function enableVaultReentry(address _token, uint256 _type) external { + token = _token; + shouldReenter = true; + reenterType = _type; + } + + 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) + ); + 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, "") + ); + require(success, "Reentry failed"); + } else if (reenterType == 2) { + // Attempt to reenter revertWithdraw + (bool success,) = vault.call( + abi.encodeWithSignature("revertWithdraw(address,address,uint256)", _token, address(this), 1) + ); + require(success, "Reentry failed"); + } + } + } + + // ============================================================================ + // VaultPC Reentrancy Functions + // ============================================================================ + + 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) + ); + 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) + ); + // The reentrancy should fail, so we just return true for the original transfer + return true; + } + + 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 new file mode 100644 index 0000000..6aad63f --- /dev/null +++ b/contracts/evm-gateway/test/mocks/MockRevertingTarget.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title MockRevertingTarget + * @notice Consolidated mock contract for testing various call failure scenarios + * @dev Includes payable reverts, non-payable failures, and gas exhaustion + */ +contract MockRevertingTarget { + /** + * @notice Standard revert for basic revert testing + */ + function receiveFunds() external payable { + revert("Mock revert"); + } + + /** + * @notice Gas-heavy function that causes gas exhaustion + * @dev Consumes excessive gas to simulate out-of-gas scenarios + */ + function receiveFundsGasHeavy() external payable { + // Consume a lot of gas + for (uint256 i = 0; i < 1000000; i++) { + keccak256(abi.encode(i)); + } + } + + /** + * @notice Function that reverts with a custom message + */ + function receiveFundsWithCustomRevert() external payable { + revert("Custom revert message"); + } + + /** + * @notice Non-payable function that fails when ETH is sent + * @dev Replaces MockNonPayableTarget - tests ETH transfer to non-payable function + */ + function receiveFundsNonPayable() external { + // Non-payable function - will revert if ETH is sent + } + + /** + * @notice Pull tokens and revert with reason + */ + function pullTokensRevertWithReason(address, address, uint256) external pure { + revert("Pull failed with reason"); + } + + /** + * @notice Pull tokens and revert without reason + */ + function pullTokensRevertNoReason(address, address, uint256) external pure { + revert(); + } + + /** + * @notice Successfully pull tokens from sender + */ + function pullTokens(address token, address from, uint256 amount) external { + IERC20(token).transferFrom(from, address(this), amount); + } +} diff --git a/contracts/evm-gateway/test/mocks/MockTarget.sol b/contracts/evm-gateway/test/mocks/MockTarget.sol new file mode 100644 index 0000000..ca3529f --- /dev/null +++ b/contracts/evm-gateway/test/mocks/MockTarget.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MockTarget { + address public lastCaller; + uint256 public lastAmount; + address public lastToken; + uint256 public lastTokenAmount; + + function receiveFunds() external payable { + lastCaller = msg.sender; + lastAmount = msg.value; + } + + // Function to receive tokens via transferFrom + function receiveToken(address token, uint256 amount) external { + lastCaller = msg.sender; + lastToken = token; + lastTokenAmount = amount; + + // Transfer the tokens from sender to this contract + IERC20(token).transferFrom(msg.sender, address(this), amount); + } + + // Fallback function to handle any calls + fallback() external payable { + lastCaller = msg.sender; + lastAmount = msg.value; + } + + // Receive function for empty calls + receive() external payable { + lastCaller = msg.sender; + lastAmount = msg.value; + } +} diff --git a/contracts/evm-gateway/test/mocks/MockTokenApprovalVariants.sol b/contracts/evm-gateway/test/mocks/MockTokenApprovalVariants.sol new file mode 100644 index 0000000..1deeea8 --- /dev/null +++ b/contracts/evm-gateway/test/mocks/MockTokenApprovalVariants.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { MockERC20 } from "./MockERC20.sol"; + +/** + * @title MockTokenApprovalVariants + * @notice Consolidated mock token for testing various approve behavior scenarios + * @dev Replaces MockTokenReturnsFalse, MockTokenNoReturnData, and MockTokenRevertsOnZeroApproval + * Use setApprovalBehavior() to configure the desired behavior + */ +contract MockTokenApprovalVariants is MockERC20 { + /** + * @notice Enum defining different approval behaviors for testing + */ + enum ApprovalBehavior { + NORMAL, // Standard ERC20 behavior (returns true) + RETURN_FALSE, // Returns false instead of true on approve + NO_RETURN_DATA, // Simulates tokens with no return value (but still returns true for interface) + REVERT_ON_ZERO, // Reverts when approving amount = 0 + ALWAYS_REVERT // Always reverts on approve + + } + + ApprovalBehavior public behavior; + + constructor() MockERC20("Approval Variants Token", "AVT", 18, 1000000e18) { + behavior = ApprovalBehavior.NORMAL; + } + + /** + * @notice Configure the approval behavior for testing + * @param _behavior The desired approval behavior + */ + function setApprovalBehavior(ApprovalBehavior _behavior) external { + behavior = _behavior; + } + + /** + * @notice Overridden approve function with configurable behavior + * @param spender The address to approve + * @param amount The amount to approve + * @return success Boolean indicating success based on configured behavior + */ + function approve(address spender, uint256 amount) public override returns (bool) { + // Handle revert cases first + if (behavior == ApprovalBehavior.ALWAYS_REVERT) { + revert("Approve always fails"); + } + + if (behavior == ApprovalBehavior.REVERT_ON_ZERO && amount == 0) { + revert("Cannot approve zero amount"); + } + + // Store the approval for all non-reverting cases + _approve(msg.sender, spender, amount); + + // Return based on behavior + if (behavior == ApprovalBehavior.RETURN_FALSE) { + return false; + } + + // NORMAL and NO_RETURN_DATA both return true + // (NO_RETURN_DATA simulates tokens that don't return, but we still return true for interface compatibility) + return true; + } +} diff --git a/contracts/evm-gateway/test/mocks/MockUSDTToken.sol b/contracts/evm-gateway/test/mocks/MockUSDTToken.sol new file mode 100644 index 0000000..26925e7 --- /dev/null +++ b/contracts/evm-gateway/test/mocks/MockUSDTToken.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { MockERC20 } from "./MockERC20.sol"; + +/** + * @title MockUSDTToken + * @notice A mock token that simulates USDT's zero-first approval requirement + */ +contract MockUSDTToken is MockERC20 { + constructor() MockERC20("USDT", "USDT", 6, 1000000e6) { } + + function approve(address spender, uint256 amount) public override returns (bool) { + // USDT-style: requires zero-first approval + if (amount > 0 && allowance(msg.sender, spender) > 0) { + revert("USDT: Cannot approve from non-zero to non-zero"); + } + + // Otherwise, approve normally + _approve(msg.sender, spender, amount); + return true; + } +} diff --git a/contracts/evm-gateway/test/mocks/MockUniversalCoreReal.sol b/contracts/evm-gateway/test/mocks/MockUniversalCoreReal.sol new file mode 100644 index 0000000..083c13b --- /dev/null +++ b/contracts/evm-gateway/test/mocks/MockUniversalCoreReal.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { IUniversalCore } from "../../src/interfaces/IUniversalCore.sol"; +import { IPRC20 } from "../../src/interfaces/IPRC20.sol"; + +/** + * @title MockUniversalCoreReal + * @notice Accurate mock implementation of UniversalCore for testing + * @dev This mock closely follows the real UniversalCore implementation from pc-core-2nd + */ +contract MockUniversalCoreReal is IUniversalCore { + // ========= State ========= + /// @notice Fungible address is always the same, it's on protocol level. + address public immutable UNIVERSAL_EXECUTOR_MODULE; + + /// @notice Map to know the gas price of each chain given a chain id. + mapping(string => uint256) public gasPriceByChainId; + + /// @notice Map to know the PRC20 address of a token given a chain id, ex pETH, pBNB etc. + mapping(string => address) public gasTokenPRC20ByChainId; + + /// @notice Map to know Uniswap V3 pool of PC/PRC20 given a chain id. + mapping(string => address) public gasPCPoolByChainId; + + /// @notice Supproted token list for auto swap to PC using Uniswap V3. + mapping(address => bool) public isAutoSwapSupported; + + /// @notice Default fee tier for each token (0 = not set) + mapping(address => uint24) public defaultFeeTier; + + /// @notice Slippage tolerance for each token in basis points (e.g., 300 = 3%) + mapping(address => uint256) public slippageTolerance; + + /// @notice Default deadline in minutes for swaps + uint256 public defaultDeadlineMins = 20; + + /// @notice Uniswap V3 addresses. + address public uniswapV3FactoryAddress; + address public uniswapV3SwapRouterAddress; + address public uniswapV3QuoterAddress; + + /// @notice Address of the wrapped PC to interact with Uniswap V3. + address public wPCContractAddress; + + /// @notice Base gas limit for the cross-chain outbound transactions. + uint256 public BASE_GAS_LIMIT = 500_000; + + /// @notice Role for managing gas-related configurations + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + // Role assignments + mapping(bytes32 => mapping(address => bool)) private _roles; + + // Pause state + bool private _paused; + + // Supported tokens mapping + mapping(address => bool) private _supportedTokens; + + // ========= Events ========= + event SetGasPrice(string indexed chainID, uint256 price); + event SetGasToken(string indexed chainID, address token); + event SetGasPCPool(string indexed chainID, address pool, uint24 fee); + event SetAutoSwapSupported(address indexed token, bool supported); + event SetWPC(address addr); + event SetUniswapV3Addresses(address factory, address swapRouter, address quoter); + event SetDefaultFeeTier(address indexed token, uint24 feeTier); + event SetSlippageTolerance(address indexed token, uint256 tolerance); + event SetDefaultDeadlineMins(uint256 mins); + event BaseGasLimitUpdated(uint256 oldLimit, uint256 newLimit); + event DepositPRC20WithAutoSwap( + address indexed prc20, uint256 amountIn, address indexed wpc, uint256 pcOut, uint24 fee, address indexed target + ); + + constructor(address uem) { + UNIVERSAL_EXECUTOR_MODULE = uem; + _roles[DEFAULT_ADMIN_ROLE][msg.sender] = true; + _roles[MANAGER_ROLE][uem] = true; + } + + // ========= Modifiers ========= + modifier onlyUEModule() { + require(msg.sender == UNIVERSAL_EXECUTOR_MODULE, "MockUniversalCore: caller is not UEM"); + _; + } + + modifier onlyRole(bytes32 role) { + require(hasRole(role, msg.sender), "MockUniversalCore: caller doesn't have role"); + _; + } + + modifier whenNotPaused() { + require(!_paused, "MockUniversalCore: paused"); + _; + } + + // ========= Role Functions ========= + function hasRole(bytes32 role, address account) public view returns (bool) { + return _roles[role][account]; + } + + function grantRole(bytes32 role, address account) external onlyRole(DEFAULT_ADMIN_ROLE) { + _roles[role][account] = true; + } + + function revokeRole(bytes32 role, address account) external onlyRole(DEFAULT_ADMIN_ROLE) { + _roles[role][account] = false; + } + + // ========= Token Support Functions ========= + function isSupportedToken(address token) external view returns (bool) { + return _supportedTokens[token]; + } + + function setSupportedToken(address token, bool supported) external onlyRole(DEFAULT_ADMIN_ROLE) { + _supportedTokens[token] = supported; + } + + // ========= Core Functions ========= + function depositPRC20Token(address prc20, uint256 amount, address target) external onlyUEModule whenNotPaused { + require(target != UNIVERSAL_EXECUTOR_MODULE && target != address(this), "MockUniversalCore: invalid target"); + require(prc20 != address(0), "MockUniversalCore: zero address"); + require(amount > 0, "MockUniversalCore: zero amount"); + + // Call deposit directly on the contract without interface casting + // since MockPRC20 has deposit() but IPRC20 interface doesn't + (bool success,) = prc20.call(abi.encodeWithSignature("deposit(address,uint256)", target, amount)); + require(success, "MockUniversalCore: deposit failed"); + } + + function depositPRC20WithAutoSwap( + address prc20, + uint256 amount, + address target, + uint24 fee, + uint256 minPCOut, + uint256 deadline + ) external onlyUEModule whenNotPaused { + require(target != UNIVERSAL_EXECUTOR_MODULE && target != address(this), "MockUniversalCore: invalid target"); + require(prc20 != address(0), "MockUniversalCore: zero address"); + require(amount > 0, "MockUniversalCore: zero amount"); + require(isAutoSwapSupported[prc20], "MockUniversalCore: auto swap not supported"); + + // Use default fee tier if not provided + if (fee == 0) { + fee = defaultFeeTier[prc20]; + require(fee != 0, "MockUniversalCore: invalid fee tier"); + } + + if (deadline == 0) { + deadline = block.timestamp + (defaultDeadlineMins * 1 minutes); + } + + require(block.timestamp <= deadline, "MockUniversalCore: deadline expired"); + + // In the mock, we'll simulate the swap without actually doing it + uint256 pcOut = amount; // Simplified for testing + + // Emit the event to track the call + emit DepositPRC20WithAutoSwap(prc20, amount, wPCContractAddress, pcOut, fee, target); + } + + // ========= Manager Functions ========= + function setGasPCPool(string memory chainID, address gasToken, uint24 fee) external onlyRole(MANAGER_ROLE) { + require(gasToken != address(0), "MockUniversalCore: zero address"); + + // In the real implementation, we would get the pool from Uniswap V3 Factory + // For the mock, we'll just use the gasToken as the pool address + address pool = gasToken; // Simplified for testing + + gasPCPoolByChainId[chainID] = pool; + emit SetGasPCPool(chainID, pool, fee); + } + + function setGasPrice(string memory chainID, uint256 price) external onlyRole(MANAGER_ROLE) { + gasPriceByChainId[chainID] = price; + emit SetGasPrice(chainID, price); + } + + function setGasTokenPRC20(string memory chainID, address prc20) external onlyRole(MANAGER_ROLE) { + require(prc20 != address(0), "MockUniversalCore: zero address"); + gasTokenPRC20ByChainId[chainID] = prc20; + emit SetGasToken(chainID, prc20); + } + + // ========= Admin Functions ========= + modifier onlyOwner() { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "MockUniversalCore: caller is not owner"); + _; + } + + function setAutoSwapSupported(address token, bool supported) external onlyOwner { + isAutoSwapSupported[token] = supported; + emit SetAutoSwapSupported(token, supported); + } + + function setWPCContractAddress(address addr) external onlyOwner { + require(addr != address(0), "MockUniversalCore: zero address"); + wPCContractAddress = addr; + emit SetWPC(addr); + } + + function setUniswapV3Addresses(address factory, address swapRouter, address quoter) external onlyOwner { + require( + factory != address(0) && swapRouter != address(0) && quoter != address(0), "MockUniversalCore: zero address" + ); + uniswapV3FactoryAddress = factory; + uniswapV3SwapRouterAddress = swapRouter; + uniswapV3QuoterAddress = quoter; + emit SetUniswapV3Addresses(factory, swapRouter, quoter); + } + + function setDefaultFeeTier(address token, uint24 feeTier) external onlyOwner { + require(token != address(0), "MockUniversalCore: zero address"); + require(feeTier == 500 || feeTier == 3000 || feeTier == 10000, "MockUniversalCore: invalid fee tier"); + defaultFeeTier[token] = feeTier; + emit SetDefaultFeeTier(token, feeTier); + } + + function setSlippageTolerance(address token, uint256 tolerance) external onlyOwner { + require(token != address(0), "MockUniversalCore: zero address"); + require(tolerance <= 5000, "MockUniversalCore: invalid slippage tolerance"); // Max 50% + slippageTolerance[token] = tolerance; + emit SetSlippageTolerance(token, tolerance); + } + + function setDefaultDeadlineMins(uint256 minutesValue) external onlyOwner { + defaultDeadlineMins = minutesValue; + emit SetDefaultDeadlineMins(minutesValue); + } + + function pause() external onlyOwner { + _paused = true; + } + + function unpause() external onlyOwner { + _paused = false; + } + + // ========= View Functions ========= + function paused() external view returns (bool) { + return _paused; + } + + function getSwapQuote(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn) public returns (uint256) { + // Simplified implementation for testing + return amountIn; + } + + function withdrawGasFee(address _prc20) public view returns (address gasToken, uint256 gasFee) { + string memory chainID = IPRC20(_prc20).SOURCE_CHAIN_ID(); + + gasToken = gasTokenPRC20ByChainId[chainID]; + require(gasToken != address(0), "MockUniversalCore: zero gas token"); + + uint256 price = gasPriceByChainId[chainID]; + require(price != 0, "MockUniversalCore: zero gas price"); + + gasFee = price * BASE_GAS_LIMIT + IPRC20(_prc20).PC_PROTOCOL_FEE(); + } + + function withdrawGasFeeWithGasLimit(address _prc20, uint256 gasLimit) public view returns (address gasToken, uint256 gasFee) { + string memory chainID = IPRC20(_prc20).SOURCE_CHAIN_ID(); + + gasToken = gasTokenPRC20ByChainId[chainID]; + require(gasToken != address(0), "MockUniversalCore: zero gas token"); + + uint256 price = gasPriceByChainId[chainID]; + require(price != 0, "MockUniversalCore: zero gas price"); + + gasFee = price * gasLimit + IPRC20(_prc20).PC_PROTOCOL_FEE(); + } + + /// @notice Update the base gas limit for the cross-chain outbound transactions. + /// @param gasLimit New base gas limit + function updateBaseGasLimit(uint256 gasLimit) external onlyRole(DEFAULT_ADMIN_ROLE) { + uint256 oldLimit = BASE_GAS_LIMIT; + BASE_GAS_LIMIT = gasLimit; + emit BaseGasLimitUpdated(oldLimit, gasLimit); + } + + // ========= Test Helper Functions ========= + function setUniversalExecutorModule(address _uem) external { + // This is just for test compatibility with the old mock + // In the real contract, UEM is immutable + } +} diff --git a/contracts/evm-gateway/test/oracle/OracleTest.t.sol b/contracts/evm-gateway/test/oracle/OracleTest.t.sol index 3f46be7..197440d 100644 --- a/contracts/evm-gateway/test/oracle/OracleTest.t.sol +++ b/contracts/evm-gateway/test/oracle/OracleTest.t.sol @@ -331,6 +331,7 @@ contract OracleTest is BaseTest { admin, pauser, tss, + address(this), // vault address 100e18, // minCapUsd 10000e18, // maxCapUsd address(0x123), // factory @@ -400,8 +401,8 @@ contract OracleTest is BaseTest { vm.prank(admin); newGateway.initialize( admin, - pauser, tss, + address(this), // vault address 1e18, // minCapUsd 10e18, // maxCapUsd address(0), // factory = address(0) diff --git a/contracts/evm-gateway/test/vault/Vault.t.sol b/contracts/evm-gateway/test/vault/Vault.t.sol new file mode 100644 index 0000000..62906c3 --- /dev/null +++ b/contracts/evm-gateway/test/vault/Vault.t.sol @@ -0,0 +1,940 @@ +// SPDX-License-Identifier: MIT +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"; + +contract VaultTest is Test { + Vault public vault; + Vault public vaultImpl; + UniversalGateway public gateway; + UniversalGateway public gatewayImpl; + MockERC20 public token; + MockERC20 public token2; + MockTokenApprovalVariants public variantToken; + MockRevertingTarget public mockRevertingTarget; + MockReentrantContract public reentrantAttacker; + MockTarget public mockTarget; + + address public admin; + address public pauser; + address public tss; + address public user1; + address public user2; + address public weth; + + // 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 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)); + + function setUp() public { + admin = makeAddr("admin"); + pauser = makeAddr("pauser"); + tss = makeAddr("tss"); + user1 = makeAddr("user1"); + user2 = makeAddr("user2"); + weth = makeAddr("weth"); + + // Deploy UniversalGateway + gatewayImpl = new UniversalGateway(); + bytes memory gatewayInitData = abi.encodeWithSelector( + UniversalGateway.initialize.selector, + admin, + tss, + address(this), // vault address (will be set to actual vault after deployment) + 1e18, // minCapUsd + 10e18, // maxCapUsd + address(0), // factory (not needed for Vault tests) + address(0), // router (not needed for Vault tests) + weth + ); + ERC1967Proxy gatewayProxy = new ERC1967Proxy(address(gatewayImpl), gatewayInitData); + gateway = UniversalGateway(payable(address(gatewayProxy))); + + // Deploy Vault implementation and proxy + vaultImpl = new Vault(); + 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(); + + // Deploy tokens + token = new MockERC20("Test Token", "TST", 18, 1_000_000e18); + token2 = new MockERC20("Test Token 2", "TST2", 6, 1_000_000e6); + variantToken = new MockTokenApprovalVariants(); + + // Deploy helper contracts + mockRevertingTarget = new MockRevertingTarget(); + reentrantAttacker = new MockReentrantContract(address(0), address(0), address(0)); + reentrantAttacker.setVault(address(vault)); + mockTarget = new MockTarget(); + + // Setup: support tokens in gateway + address[] memory tokens = new address[](3); + 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); + + // Fund vault with tokens + token.mint(address(vault), 100_000e18); + token2.mint(address(vault), 100_000e6); + variantToken.mint(address(vault), 100_000e18); + } + + // ============================================================================ + // INITIALIZATION TESTS + // ============================================================================ + + function test_Initialization_RolesAssigned() public view { + assertTrue(vault.hasRole(vault.DEFAULT_ADMIN_ROLE(), admin)); + assertTrue(vault.hasRole(vault.PAUSER_ROLE(), pauser)); + assertTrue(vault.hasRole(vault.TSS_ROLE(), tss)); + } + + function test_Initialization_GatewaySet() public view { + assertEq(address(vault.gateway()), address(gateway)); + } + + function test_Initialization_TSSAddressSet() public view { + assertEq(vault.TSS_ADDRESS(), tss); + } + + function test_Initialization_StartsUnpaused() public view { + assertFalse(vault.paused()); + } + + function test_Initialization_RevertsOnZeroAdmin() public { + Vault newImpl = new Vault(); + 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) + ); + 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) + ); + 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) + ); + vm.expectRevert(Errors.ZeroAddress.selector); + new ERC1967Proxy(address(newImpl), initData); + } + + // ============================================================================ + // ACCESS CONTROL & ROLE ROTATION TESTS + // ============================================================================ + + function test_Pause_OnlyPauserCanPause() public { + vm.prank(pauser); + vault.pause(); + assertTrue(vault.paused()); + } + + function test_Pause_NonPauserReverts() public { + vm.prank(user1); + vm.expectRevert(); + vault.pause(); + } + + function test_Unpause_OnlyPauserCanUnpause() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(pauser); + vault.unpause(); + assertFalse(vault.paused()); + } + + function test_Unpause_NonPauserReverts() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(user1); + vm.expectRevert(); + vault.unpause(); + } + + 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 + ); + 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)); + } + + function test_SetGateway_NonAdminReverts() public { + vm.prank(user1); + vm.expectRevert(); + vault.setGateway(address(gateway)); + } + + function test_SetGateway_ZeroAddressReverts() public { + vm.prank(admin); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.setGateway(address(0)); + } + + 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 + ); + 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)); + vault.setGateway(address(newGateway)); + } + + function test_SetTSS_OnlyAdminCanSet() public { + address newTSS = makeAddr("newTSS"); + + vm.prank(admin); + vault.setTSS(newTSS); + assertEq(vault.TSS_ADDRESS(), newTSS); + assertTrue(vault.hasRole(vault.TSS_ROLE(), newTSS)); + } + + function test_SetTSS_NonAdminReverts() public { + address newTSS = makeAddr("newTSS"); + + vm.prank(user1); + vm.expectRevert(); + vault.setTSS(newTSS); + } + + function test_SetTSS_ZeroAddressReverts() public { + vm.prank(admin); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.setTSS(address(0)); + } + + 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); + } + + 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); + } + + function test_SetTSS_EmitsEvent() public { + address newTSS = makeAddr("newTSS"); + + vm.prank(admin); + vm.expectEmit(true, true, false, false); + emit TSSUpdated(tss, newTSS); + vault.setTSS(newTSS); + } + + 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); + } + + function test_Withdraw_OnlyTSSCanCall() public { + vm.prank(user1); + vm.expectRevert(); + vault.withdraw(txID, user1, address(token), user1, 100e18); + } + + function test_RevertWithdraw_OnlyTSSCanCall() public { + vm.prank(user1); + vm.expectRevert(); + vault.revertWithdraw(address(token), user1, 100e18, RevertInstructions(user1, "")); + } + + // ============================================================================ + // PAUSE GATING TESTS + // ============================================================================ + + function test_Pause_BlocksWithdraw() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(tss); + vm.expectRevert(); + vault.withdraw(txID, user1, address(token), user1, 100e18); + } + + function test_Pause_BlocksRevertWithdraw() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(tss); + vm.expectRevert(); + vault.revertWithdraw(address(token), user1, 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 + ); + 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)); + } + + function test_Pause_DoublePauseReverts() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(pauser); + vm.expectRevert(); + vault.pause(); + } + + function test_Unpause_DoubleUnpauseReverts() public { + vm.prank(pauser); + vm.expectRevert(); + vault.unpause(); + } + + 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); + } + + // ============================================================================ + // TOKEN SUPPORT / GATEWAY GATING TESTS + // ============================================================================ + + 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); + } + + 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(address(unsupportedToken), user1, 100e18, RevertInstructions(user1, "")); + } + + function test_TokenSupport_TogglingReflectsImmediately() public { + // Initially supported + vm.prank(tss); + vault.withdraw(bytes32(uint256(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); + + // 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); + assertEq(token.balanceOf(user2), 100e18); + } + + function test_Withdraw_ZeroTokenAddressReverts() public { + vm.prank(tss); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.withdraw(txID, user1, address(0), user1, 100e18); + } + + function test_RevertWithdraw_ZeroTokenAddressReverts() public { + vm.prank(tss); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.revertWithdraw(address(0), user1, 100e18, RevertInstructions(user1, "")); + } + + // ============================================================================ + // WITHDRAW (SIMPLE TRANSFER) TESTS + // ============================================================================ + + 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); + vault.withdraw(txID, user1, address(token), user1, amount); + } + + function test_Withdraw_ZeroAmountReverts() public { + vm.prank(tss); + vm.expectRevert(Errors.InvalidAmount.selector); + vault.withdraw(txID, user1, address(token), user1, 0); + } + + function test_Withdraw_ZeroRecipientReverts() public { + vm.prank(tss); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.withdraw(txID, user1, address(token), address(0), 100e18); + } + + 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); + } + + function test_Withdraw_MultipleRecipients() public { + vm.prank(tss); + vault.withdraw(bytes32(uint256(1)), user1, address(token), user1, 100e18); + + vm.prank(tss); + vault.withdraw(bytes32(uint256(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); + + vm.prank(tss); + vault.withdraw(bytes32(uint256(2)), user1, address(token2), user1, 50e6); + + assertEq(token.balanceOf(user1), 100e18); + assertEq(token2.balanceOf(user1), 50e6); + } + + // ============================================================================ + // REVERTWITHDRAW TESTS + // ============================================================================ + + function test_RevertWithdraw_StandardToken_Success() public { + uint256 amount = 1000e18; + + vm.prank(tss); + vault.revertWithdraw(address(token), user1, 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(address(token), user1, amount, revertInstr); + } + + function test_RevertWithdraw_ZeroAmountReverts() public { + vm.prank(tss); + vm.expectRevert(Errors.InvalidAmount.selector); + vault.revertWithdraw(address(token), user1, 0, RevertInstructions(user1, "")); + } + + function test_RevertWithdraw_ZeroRecipientReverts() public { + vm.prank(tss); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.revertWithdraw(address(token), address(0), 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(address(token), user1, vaultBalance + 1, RevertInstructions(user1, "")); + } + + function test_RevertWithdraw_WhenPausedReverts() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(tss); + vm.expectRevert(); + vault.revertWithdraw(address(token), user1, 100e18, RevertInstructions(user1, "")); + } + + // ============================================================================ + // WITHDRAWANDEXECUTE TESTS + // ============================================================================ + + 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); + + // Verify tokens were transferred and call was executed + assertEq(mockTarget.lastCaller(), address(gateway)); + assertEq(token.balanceOf(address(vault)), initialVaultBalance - amount); + } + + 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); + } + + 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); + } + + 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); + } + + 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); + } + + 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); + } + + 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); + } + + 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); + } + + 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); + } + + 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); + + // Verify the call was executed (MockTarget stores lastCaller) + assertEq(mockTarget.lastCaller(), address(gateway)); + assertEq(mockTarget.lastToken(), address(token)); + } + + 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); + + // 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); + + // 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); + + // Verify both calls executed + assertEq(mockTarget.lastCaller(), address(gateway)); + } + + // ============================================================================ + // SWEEP TESTS + // ============================================================================ + + function test_Sweep_OnlyAdminCanCall() public { + vm.prank(admin); + vault.sweep(address(token), user1, 100e18); + assertEq(token.balanceOf(user1), 100e18); + } + + function test_Sweep_NonAdminReverts() public { + vm.prank(user1); + vm.expectRevert(); + vault.sweep(address(token), user1, 100e18); + } + + function test_Sweep_ZeroTokenReverts() public { + vm.prank(admin); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.sweep(address(0), user1, 100e18); + } + + function test_Sweep_ZeroRecipientReverts() public { + vm.prank(admin); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.sweep(address(token), address(0), 100e18); + } + + 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); + } + + // ============================================================================ + // NO NATIVE INVARIANT TESTS + // ============================================================================ + + function test_NoNative_DirectETHSendReverts() public { + vm.deal(user1, 1 ether); + + vm.prank(user1); + (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); + } + + function test_NoNative_FunctionsDoNotAcceptValue() public { + vm.deal(tss, 1 ether); + + vm.prank(tss); + (bool success,) = address(vault).call{value: 1 ether}( + abi.encodeWithSelector(vault.withdraw.selector, txID, user1, address(token), user1, 100e18) + ); + assertFalse(success); + } + + // ============================================================================ + // GATEWAY POINTER CHANGES TESTS + // ============================================================================ + + 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 + ); + 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); + } + + function test_GatewayChange_ReenableSupport() public { + // 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 + ); + 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); + + // 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); + assertEq(token.balanceOf(user1), 100e18); + } + + // ============================================================================ + // EVENTS CORRECTNESS TESTS + // ============================================================================ + + 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 + ); + 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)); + vault.setGateway(address(newGateway)); + } + + function test_Events_TSSUpdated() public { + address newTSS = makeAddr("newTSS"); + + vm.prank(admin); + vm.expectEmit(true, true, false, false); + emit TSSUpdated(tss, newTSS); + vault.setTSS(newTSS); + } + + 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); + vault.withdraw(txID, user1, address(token), user1, amount); + } + + 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(address(token), user1, 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) + ); + 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 new file mode 100644 index 0000000..6849f65 --- /dev/null +++ b/contracts/evm-gateway/test/vault/VaultPC.t.sol @@ -0,0 +1,695 @@ +// SPDX-License-Identifier: MIT +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"; + +contract VaultPCTest is Test { + VaultPC public vault; + VaultPC public vaultImpl; + MockUniversalCoreReal public universalCore; + MockPRC20 public prc20Token; + MockPRC20 public prc20Token2; + MockReentrantContract public reentrantAttacker; + + address public admin; + address public pauser; + address public fundManager; + address public uem; + address public user1; + address public user2; + + // Events + event GatewayPCUpdated(address indexed oldGatewayPC, address indexed newGatewayPC); + event FeesWithdrawn(address indexed caller, address indexed token, uint256 amount); + + function setUp() public { + admin = makeAddr("admin"); + pauser = makeAddr("pauser"); + fundManager = makeAddr("fundManager"); + user1 = makeAddr("user1"); + user2 = makeAddr("user2"); + uem = makeAddr("uem"); + + // Deploy UniversalCore mock + universalCore = new MockUniversalCoreReal(uem); + + // Deploy VaultPC implementation and proxy + vaultImpl = new VaultPC(); + bytes memory vaultInitData = abi.encodeWithSelector( + VaultPC.initialize.selector, + admin, + pauser, + fundManager, + address(universalCore) + ); + ERC1967Proxy vaultProxy = new ERC1967Proxy(address(vaultImpl), vaultInitData); + vault = VaultPC(address(vaultProxy)); + + // Deploy PRC20 tokens + prc20Token = new MockPRC20( + "Push Ethereum", + "pETH", + 18, + "1", + MockPRC20.TokenType.NATIVE, + 100e18, // protocol fee + address(universalCore), + "0x0000000000000000000000000000000000000000" + ); + + prc20Token2 = new MockPRC20( + "Push BNB", + "pBNB", + 18, + "56", + MockPRC20.TokenType.NATIVE, + 50e18, // protocol fee + address(universalCore), + "0x0000000000000000000000000000000000000000" + ); + + // Setup token support in UniversalCore + universalCore.setSupportedToken(address(prc20Token), true); + universalCore.setSupportedToken(address(prc20Token2), true); + + // Deploy reentrant attacker + reentrantAttacker = new MockReentrantContract(address(0), address(0), address(0)); + reentrantAttacker.setVaultPC(address(vault)); + + // Fund vault with tokens + prc20Token.mint(address(vault), 100_000e18); + prc20Token2.mint(address(vault), 100_000e18); + } + + // ============================================================================ + // INITIALIZATION TESTS + // ============================================================================ + + function test_Initialization_RolesAssigned() public view { + assertTrue(vault.hasRole(vault.DEFAULT_ADMIN_ROLE(), admin)); + assertTrue(vault.hasRole(vault.PAUSER_ROLE(), pauser)); + assertTrue(vault.hasRole(vault.FUND_MANAGER_ROLE(), fundManager)); + } + + function test_Initialization_UniversalCoreSet() public view { + assertEq(vault.UNIVERSAL_CORE(), address(universalCore)); + } + + function test_Initialization_StartsUnpaused() public view { + assertFalse(vault.paused()); + } + + function test_Initialization_RevertsOnZeroAdmin() public { + VaultPC newImpl = new VaultPC(); + bytes memory initData = abi.encodeWithSelector( + VaultPC.initialize.selector, + address(0), + pauser, + fundManager, + address(universalCore) + ); + 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, + address(universalCore) + ); + 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), + address(universalCore) + ); + vm.expectRevert(Errors.ZeroAddress.selector); + new ERC1967Proxy(address(newImpl), initData); + } + + function test_Initialization_RevertsOnZeroUniversalCore() public { + VaultPC newImpl = new VaultPC(); + bytes memory initData = abi.encodeWithSelector( + VaultPC.initialize.selector, + admin, + pauser, + fundManager, + address(0) + ); + vm.expectRevert(Errors.ZeroAddress.selector); + new ERC1967Proxy(address(newImpl), initData); + } + + // ============================================================================ + // ACCESS CONTROL TESTS + // ============================================================================ + + function test_Pause_OnlyPauserCanPause() public { + vm.prank(pauser); + vault.pause(); + assertTrue(vault.paused()); + } + + function test_Pause_NonPauserReverts() public { + vm.prank(user1); + vm.expectRevert(); + vault.pause(); + } + + function test_Unpause_OnlyPauserCanUnpause() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(pauser); + vault.unpause(); + assertFalse(vault.paused()); + } + + function test_Unpause_NonPauserReverts() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(user1); + vm.expectRevert(); + vault.unpause(); + } + + function test_UpdateUniversalCore_OnlyAdminCanUpdate() public { + MockUniversalCoreReal newCore = new MockUniversalCoreReal(uem); + + vm.prank(admin); + vault.updateUniversalCore(address(newCore)); + assertEq(vault.UNIVERSAL_CORE(), address(newCore)); + } + + function test_UpdateUniversalCore_NonAdminReverts() public { + MockUniversalCoreReal newCore = new MockUniversalCoreReal(uem); + + vm.prank(user1); + vm.expectRevert(); + vault.updateUniversalCore(address(newCore)); + } + + function test_UpdateUniversalCore_ZeroAddressReverts() public { + vm.prank(admin); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.updateUniversalCore(address(0)); + } + + function test_Withdraw_OnlyFundManagerCanCall() public { + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 100e18); + assertEq(prc20Token.balanceOf(user1), 100e18); + } + + function test_Withdraw_NonFundManagerReverts() public { + vm.prank(user1); + vm.expectRevert(); + vault.withdraw(address(prc20Token), user1, 100e18); + } + + function test_Sweep_OnlyFundManagerCanCall() public { + vm.prank(fundManager); + vault.sweep(address(prc20Token), user1, 100e18); + assertEq(prc20Token.balanceOf(user1), 100e18); + } + + function test_Sweep_NonFundManagerReverts() public { + vm.prank(user1); + vm.expectRevert(); + vault.sweep(address(prc20Token), user1, 100e18); + } + + // ============================================================================ + // PAUSE GATING TESTS + // ============================================================================ + + function test_Pause_BlocksWithdraw() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(fundManager); + vm.expectRevert(); + vault.withdraw(address(prc20Token), user1, 100e18); + } + + function test_Pause_DoublePauseReverts() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(pauser); + vm.expectRevert(); + vault.pause(); + } + + function test_Unpause_DoubleUnpauseReverts() public { + vm.prank(pauser); + vm.expectRevert(); + vault.unpause(); + } + + function test_Unpause_RestoresWithdrawFunctionality() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(pauser); + vault.unpause(); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 100e18); + assertEq(prc20Token.balanceOf(user1), 100e18); + } + + function test_UpdateUniversalCore_WorksWhenPaused() public { + MockUniversalCoreReal newCore = new MockUniversalCoreReal(uem); + + vm.prank(pauser); + vault.pause(); + + vm.prank(admin); + vault.updateUniversalCore(address(newCore)); + assertEq(vault.UNIVERSAL_CORE(), address(newCore)); + } + + // ============================================================================ + // TOKEN SUPPORT / UNIVERSALCORE GATING TESTS + // ============================================================================ + + function test_Withdraw_UnsupportedTokenReverts() public { + MockPRC20 unsupportedToken = new MockPRC20( + "Unsupported", + "UNS", + 18, + "999", + MockPRC20.TokenType.NATIVE, + 10e18, + address(universalCore), + "0x0000000000000000000000000000000000000000" + ); + unsupportedToken.mint(address(vault), 100e18); + + vm.prank(fundManager); + vm.expectRevert(Errors.NotSupported.selector); + vault.withdraw(address(unsupportedToken), user1, 100e18); + } + + function test_TokenSupport_TogglingReflectsImmediately() public { + // Initially supported + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 100e18); + assertEq(prc20Token.balanceOf(user1), 100e18); + + // Remove support in UniversalCore + universalCore.setSupportedToken(address(prc20Token), false); + + vm.prank(fundManager); + vm.expectRevert(Errors.NotSupported.selector); + vault.withdraw(address(prc20Token), user2, 100e18); + + // Re-add support + universalCore.setSupportedToken(address(prc20Token), true); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user2, 100e18); + assertEq(prc20Token.balanceOf(user2), 100e18); + } + + function test_UniversalCoreChange_LiveSupport() public { + // Create new UniversalCore without token support + MockUniversalCoreReal newCore = new MockUniversalCoreReal(uem); + + vm.prank(admin); + vault.updateUniversalCore(address(newCore)); + + vm.prank(fundManager); + vm.expectRevert(Errors.NotSupported.selector); + vault.withdraw(address(prc20Token), user1, 100e18); + + // Add support in new core + newCore.setSupportedToken(address(prc20Token), true); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 100e18); + assertEq(prc20Token.balanceOf(user1), 100e18); + } + + // ============================================================================ + // WITHDRAW TESTS + // ============================================================================ + + function test_Withdraw_StandardToken_Success() public { + uint256 amount = 1000e18; + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, amount); + + assertEq(prc20Token.balanceOf(user1), amount); + } + + function test_Withdraw_EmitsFeesWithdrawnEvent() public { + uint256 amount = 1000e18; + + vm.prank(fundManager); + vm.expectEmit(true, true, false, true); + emit FeesWithdrawn(fundManager, address(prc20Token), amount); + vault.withdraw(address(prc20Token), user1, amount); + } + + function test_Withdraw_ZeroAmountReverts() public { + vm.prank(fundManager); + vm.expectRevert(Errors.InvalidAmount.selector); + vault.withdraw(address(prc20Token), user1, 0); + } + + function test_Withdraw_ZeroRecipientReverts() public { + vm.prank(fundManager); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.withdraw(address(prc20Token), address(0), 100e18); + } + + function test_Withdraw_ZeroTokenAddressReverts() public { + // Token support is checked first, so we expect NotSupported for address(0) + vm.prank(fundManager); + vm.expectRevert(Errors.NotSupported.selector); + vault.withdraw(address(0), user1, 100e18); + } + + function test_Withdraw_InsufficientBalanceReverts() public { + uint256 vaultBalance = prc20Token.balanceOf(address(vault)); + + vm.prank(fundManager); + vm.expectRevert(Errors.InvalidAmount.selector); + vault.withdraw(address(prc20Token), user1, vaultBalance + 1); + } + + function test_Withdraw_MultipleRecipients() public { + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 100e18); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user2, 200e18); + + assertEq(prc20Token.balanceOf(user1), 100e18); + assertEq(prc20Token.balanceOf(user2), 200e18); + } + + function test_Withdraw_DifferentTokens() public { + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 100e18); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token2), user1, 50e18); + + assertEq(prc20Token.balanceOf(user1), 100e18); + assertEq(prc20Token2.balanceOf(user1), 50e18); + } + + function test_Withdraw_SequentialCalls_Success() public { + // Multiple sequential withdrawals should work fine + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 100e18); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 200e18); + + assertEq(prc20Token.balanceOf(user1), 300e18); + } + + // ============================================================================ + // SWEEP TESTS + // ============================================================================ + + function test_Sweep_StandardToken_Success() public { + uint256 amount = 500e18; + + vm.prank(fundManager); + vault.sweep(address(prc20Token), user1, amount); + + assertEq(prc20Token.balanceOf(user1), amount); + } + + function test_Sweep_ZeroTokenReverts() public { + vm.prank(fundManager); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.sweep(address(0), user1, 100e18); + } + + function test_Sweep_ZeroRecipientReverts() public { + vm.prank(fundManager); + vm.expectRevert(Errors.ZeroAddress.selector); + vault.sweep(address(prc20Token), address(0), 100e18); + } + + function test_Sweep_UnsupportedToken_NoRevert() public { + // Sweep should work even for unsupported tokens (emergency recovery) + MockPRC20 unsupportedToken = new MockPRC20( + "Unsupported", + "UNS", + 18, + "999", + MockPRC20.TokenType.NATIVE, + 10e18, + address(universalCore), + "0x0000000000000000000000000000000000000000" + ); + unsupportedToken.mint(address(vault), 100e18); + + vm.prank(fundManager); + vault.sweep(address(unsupportedToken), user1, 100e18); + assertEq(unsupportedToken.balanceOf(user1), 100e18); + } + + function test_Sweep_WorksWhenPaused() public { + vm.prank(pauser); + vault.pause(); + + // Sweep should work even when paused (emergency recovery) + vm.prank(fundManager); + vault.sweep(address(prc20Token), user1, 100e18); + assertEq(prc20Token.balanceOf(user1), 100e18); + } + + // ============================================================================ + // EDGE CASES AND INTEGRATION TESTS + // ============================================================================ + + function test_MultipleWithdrawals_ReducesBalance() public { + uint256 initialBalance = prc20Token.balanceOf(address(vault)); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 100e18); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user2, 200e18); + + uint256 finalBalance = prc20Token.balanceOf(address(vault)); + assertEq(finalBalance, initialBalance - 300e18); + } + + function test_Withdraw_ExactBalance_Success() public { + uint256 vaultBalance = prc20Token.balanceOf(address(vault)); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, vaultBalance); + + assertEq(prc20Token.balanceOf(user1), vaultBalance); + assertEq(prc20Token.balanceOf(address(vault)), 0); + } + + function test_UpdateUniversalCore_AffectsTokenSupport() public { + MockUniversalCoreReal newCore = new MockUniversalCoreReal(uem); + // Don't configure any token support in new core + + vm.prank(admin); + vault.updateUniversalCore(address(newCore)); + + // Should now fail because new core doesn't support the token + vm.prank(fundManager); + vm.expectRevert(Errors.NotSupported.selector); + vault.withdraw(address(prc20Token), user1, 100e18); + } + + function test_Pause_DoesNotAffectUpdateUniversalCore() public { + MockUniversalCoreReal newCore = new MockUniversalCoreReal(uem); + + vm.prank(pauser); + vault.pause(); + + vm.prank(admin); + vault.updateUniversalCore(address(newCore)); + assertEq(vault.UNIVERSAL_CORE(), address(newCore)); + } + + function test_Pause_DoesNotAffectSweep() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(fundManager); + vault.sweep(address(prc20Token), user1, 100e18); + assertEq(prc20Token.balanceOf(user1), 100e18); + } + + // ============================================================================ + // EDGE CASE & ADDITIONAL COVERAGE TESTS + // ============================================================================ + + function test_UpdateUniversalCore_SequentialUpdates() public { + MockUniversalCoreReal newCore1 = new MockUniversalCoreReal(uem); + MockUniversalCoreReal newCore2 = new MockUniversalCoreReal(uem); + + vm.prank(admin); + vault.updateUniversalCore(address(newCore1)); + assertEq(vault.UNIVERSAL_CORE(), address(newCore1)); + + vm.prank(admin); + vault.updateUniversalCore(address(newCore2)); + assertEq(vault.UNIVERSAL_CORE(), address(newCore2)); + } + + function test_Sweep_ExactBalance() public { + uint256 exactBalance = prc20Token.balanceOf(address(vault)); + + vm.prank(fundManager); + vault.sweep(address(prc20Token), user1, exactBalance); + + assertEq(prc20Token.balanceOf(address(vault)), 0); + assertEq(prc20Token.balanceOf(user1), exactBalance); + } + + function test_Withdraw_AfterUniversalCoreChange_RespectNewSupport() public { + // Create new core with different token support + MockUniversalCoreReal newCore = new MockUniversalCoreReal(uem); + newCore.setSupportedToken(address(prc20Token), false); // Disable prc20Token + newCore.setSupportedToken(address(prc20Token2), true); // Enable prc20Token2 + + vm.prank(admin); + vault.updateUniversalCore(address(newCore)); + + // prc20Token should now fail + vm.prank(fundManager); + vm.expectRevert(Errors.NotSupported.selector); + vault.withdraw(address(prc20Token), user1, 100e18); + + // prc20Token2 should succeed + vm.prank(fundManager); + vault.withdraw(address(prc20Token2), user1, 100e18); + assertEq(prc20Token2.balanceOf(user1), 100e18); + } + + function test_Sweep_WorksRegardlessOfTokenSupport() public { + // Create unsupported token + MockPRC20 unsupportedToken = new MockPRC20( + "Unsupported", + "UNS", + 18, + "999", + MockPRC20.TokenType.ERC20, + 0, + address(universalCore), + "0x0" + ); + unsupportedToken.mint(address(vault), 1000e18); + + // Sweep should work even for unsupported tokens (emergency recovery) + vm.prank(fundManager); + vault.sweep(address(unsupportedToken), user1, 500e18); + assertEq(unsupportedToken.balanceOf(user1), 500e18); + } + + function test_Withdraw_AfterPauseUnpause_WorksNormally() public { + vm.prank(pauser); + vault.pause(); + + vm.prank(pauser); + vault.unpause(); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 100e18); + assertEq(prc20Token.balanceOf(user1), 100e18); + } + + function test_Withdraw_ChecksSupportBeforeZeroAddress() public { + // Create unsupported token first + MockPRC20 unsupportedToken = new MockPRC20( + "Unsupported", + "UNS", + 18, + "888", + MockPRC20.TokenType.ERC20, + 0, + address(universalCore), + "0x0" + ); + unsupportedToken.mint(address(vault), 1000e18); + + // Should revert with NotSupported (checked first via _enforceSupportedToken) + vm.prank(fundManager); + vm.expectRevert(Errors.NotSupported.selector); + vault.withdraw(address(unsupportedToken), user1, 100e18); + } + + function test_Sweep_PartialAmount() public { + uint256 totalBalance = prc20Token.balanceOf(address(vault)); + uint256 sweepAmount = totalBalance / 3; + + vm.prank(fundManager); + vault.sweep(address(prc20Token), user1, sweepAmount); + + assertEq(prc20Token.balanceOf(address(vault)), totalBalance - sweepAmount); + assertEq(prc20Token.balanceOf(user1), sweepAmount); + } + + function test_Withdraw_MultipleSmallAmounts() public { + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 1e18); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 2e18); + + vm.prank(fundManager); + vault.withdraw(address(prc20Token), user1, 3e18); + + assertEq(prc20Token.balanceOf(user1), 6e18); + } + + // ============================================================================ + // NO NATIVE INVARIANT TESTS + // ============================================================================ + + function test_NoNative_DirectETHSendReverts() public { + vm.deal(user1, 1 ether); + + vm.prank(user1); + (bool success,) = address(vault).call{value: 1 ether}(""); + assertFalse(success); + } + + function test_NoNative_FunctionsDoNotAcceptValue() public { + vm.deal(fundManager, 1 ether); + + vm.prank(fundManager); + (bool success,) = address(vault).call{value: 1 ether}( + abi.encodeWithSelector(vault.withdraw.selector, address(prc20Token), user1, 100e18) + ); + assertFalse(success); + } +} +