diff --git a/.gas-snapshot b/.gas-snapshot index ea49b88e..4cd3686b 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,73 +1,86 @@ AccountDelegate:test_isAccountDelegate_account_doesnt_exist() (gas: 26514) -AccountDelegate:test_isAccountDelegate_false() (gas: 391946) -AccountDelegate:test_isAccountDelegate_true() (gas: 389915) -AccountOwner:test_isAccountOwner_account_doesnt_exist() (gas: 25010) -AccountOwner:test_isAccountOwner_false() (gas: 229790) -AccountOwner:test_isAccountOwner_true() (gas: 229780) -CanExecute:test_canExecute_false_nonce_used() (gas: 635986) -CanExecute:test_canExecute_false_trusted_executor() (gas: 44106) -CanExecute:test_canExecute_true() (gas: 43793) +AccountDelegate:test_isAccountDelegate_false() (gas: 391969) +AccountDelegate:test_isAccountDelegate_true() (gas: 389938) +AccountOwner:test_isAccountOwner_account_doesnt_exist() (gas: 25032) +AccountOwner:test_isAccountOwner_false() (gas: 229812) +AccountOwner:test_isAccountOwner_true() (gas: 229802) +CanExecute:test_canExecute_false_insufficent_account_credit() (gas: 414653) +CanExecute:test_canExecute_false_invalid_signature() (gas: 448261) +CanExecute:test_canExecute_false_invalid_signer() (gas: 441634) +CanExecute:test_canExecute_false_maxExecutorFee_exceeded() (gas: 397133) +CanExecute:test_canExecute_false_nonce_used() (gas: 825924) +CanExecute:test_canExecute_false_trusted_executor() (gas: 438376) +CanExecute:test_canExecute_true() (gas: 437970) CommitOrder:test_commitOrder() (gas: 382994) -CommitOrder:test_commitOrder_insufficient_collateral() (gas: 441147) -CommitOrder:test_commitOrder_invalid_market() (gas: 38351) -Conditions:test_isMarketOpen() (gas: 26586) -Conditions:test_isOrderFeeBelow() (gas: 179847) -Conditions:test_isPositionSizeAbove() (gas: 18867) -Conditions:test_isPositionSizeBelow() (gas: 18762) -Conditions:test_isPriceAbove() (gas: 19150) -Conditions:test_isPriceBelow() (gas: 19178) -Conditions:test_isTimestampAfter() (gas: 7513) -Conditions:test_isTimestampBefore() (gas: 7602) -DeploymentTest:test_deploy() (gas: 2880751) -DeploymentTest:test_deploy_oracle_zero_address() (gas: 40293) -DeploymentTest:test_deploy_perps_market_proxy_zero_address() (gas: 40211) -DeploymentTest:test_deploy_spot_market_proxy_zero_address() (gas: 40267) -DeploymentTest:test_deploy_susd_proxy_zero_address() (gas: 40280) -DeploymentTest:test_deploy_trusted_forwarder_zero_address() (gas: 40327) -DepositCollateral:test_depositCollateral() (gas: 258507) -DepositCollateral:test_depositCollateral_availableMargin() (gas: 266075) -DepositCollateral:test_depositCollateral_collateralAmount() (gas: 259083) -DepositCollateral:test_depositCollateral_insufficient_balance() (gas: 56023) -DepositCollateral:test_depositCollateral_totalCollateralValue() (gas: 263459) -EIP7412Test:test_fulfillOracleQuery(bytes) (runs: 256, μ: 142463, ~: 142365) -Execute:test_execute_CannotExecuteOrder() (gas: 37220) -Execute:test_execute_leverage_exceeded() (gas: 716391) -Execute:test_execute_order_committed() (gas: 633942) -Fee:test_fee_imposed() (gas: 640084) -Fee:test_fee_imposed_above_upper_fee_cap() (gas: 618750) -Fee:test_fee_imposed_at_upper_fee_cap() (gas: 618653) -Fee:test_fee_imposed_below_lower_fee_cap() (gas: 617168) -Fee:test_fee_imposed_below_upper_fee_cap() (gas: 619573) -Fee:test_fee_imposed_fee_cannot_be_paid() (gas: 394180) -Fee:test_fee_imposed_insufficient_collateral_for_order() (gas: 716414) +CommitOrder:test_commitOrder_insufficient_collateral() (gas: 441236) +CommitOrder:test_commitOrder_invalid_market() (gas: 38373) +Conditions:test_isMarketOpen() (gas: 26619) +Conditions:test_isOrderFeeBelow() (gas: 179759) +Conditions:test_isPositionSizeAbove() (gas: 18823) +Conditions:test_isPositionSizeBelow() (gas: 18828) +Conditions:test_isPriceAbove() (gas: 19186) +Conditions:test_isPriceBelow() (gas: 19092) +Conditions:test_isTimestampAfter() (gas: 7711) +Conditions:test_isTimestampBefore() (gas: 7645) +DeploymentTest:test_deploy() (gas: 4550522) +DeploymentTest:test_deploy_oracle_zero_address() (gas: 1681379) +DeploymentTest:test_deploy_perps_market_proxy_zero_address() (gas: 1681295) +DeploymentTest:test_deploy_spot_market_proxy_zero_address() (gas: 1681362) +DeploymentTest:test_deploy_susd_proxy_zero_address() (gas: 1681375) +DeploymentTest:test_deploy_trusted_forwarder_zero_address() (gas: 37411) +Deposit:test_depositEth() (gas: 40987) +Deposit:test_depositEth_event() (gas: 42524) +Deposit:test_depositEth_fuzz(uint256,uint128) (runs: 256, μ: 39369, ~: 40409) +Deposit:test_depositEth_via_trustedForwarder() (gas: 68274) +Deposit:test_depositEth_via_trustedForwarder_value_mismatch() (gas: 72575) +DepositCollateral:test_depositCollateral() (gas: 258530) +DepositCollateral:test_depositCollateral_availableMargin() (gas: 266098) +DepositCollateral:test_depositCollateral_collateralAmount() (gas: 259106) +DepositCollateral:test_depositCollateral_insufficient_balance() (gas: 56046) +DepositCollateral:test_depositCollateral_totalCollateralValue() (gas: 263482) +EIP7412Test:test_fulfillOracleQuery(bytes) (runs: 256, μ: 142507, ~: 142409) +Execute:test_execute_CannotExecuteOrder_invalid_acceptablePrice() (gas: 330554) +Execute:test_execute_CannotExecuteOrder_invalid_settlementStrategyId() (gas: 96428) +Execute:test_execute_CannotExecuteOrder_too_leveraged() (gas: 361127) +Execute:test_execute_event() (gas: 433535) +Execute:test_execute_order_committed() (gas: 430028) +Fee:test_fee_exceeds_account_credit() (gas: 53442) +Fee:test_fee_exceeds_maxExecutorFee() (gas: 53021) +Fee:test_fee_imposed() (gas: 465437) MathLibTest:test_abs128() (gas: 448) MathLibTest:test_abs256() (gas: 480) MathLibTest:test_castU128() (gas: 350) MathLibTest:test_castU128_overflow() (gas: 3509) MathLibTest:test_fuzz_abs128(int128) (runs: 256, μ: 576, ~: 603) -MathLibTest:test_fuzz_abs256(int256) (runs: 256, μ: 472, ~: 458) +MathLibTest:test_fuzz_abs256(int256) (runs: 256, μ: 471, ~: 458) MathLibTest:test_isSameSign() (gas: 999) -MulticallableTest:test_multicall_depositCollateral_commitOrder() (gas: 605194) -NonceBitmapTest:test_fuzz_invalidateUnorderedNonces(uint256) (runs: 256, μ: 52780, ~: 52780) -NonceBitmapTest:test_hasUnorderedNonceBeenUsed() (gas: 54136) -NonceBitmapTest:test_invalidateUnorderedNonces() (gas: 73100) -NonceBitmapTest:test_invalidateUnorderedNonces_Only_Owner_Delegate() (gas: 190344) -ReduceOnly:test_reduce_only() (gas: 635576) -ReduceOnly:test_reduce_only_same_sign() (gas: 68498) -ReduceOnly:test_reduce_only_truncate_size_down() (gas: 635706) -ReduceOnly:test_reduce_only_truncate_size_up() (gas: 610509) -ReduceOnly:test_reduce_only_zero_size() (gas: 159015) -VerifyConditions:test_max_condition_size_exceeded() (gas: 45024) -VerifyConditions:test_verifyConditions_InvalidConditionSelector() (gas: 14123) -VerifyConditions:test_verify_conditions_not_verified() (gas: 29682) -VerifyConditions:test_verify_conditions_verified() (gas: 135831) -VerifySignature:test_verifySignature() (gas: 23816) -VerifySignature:test_verifySignature_false_private_key() (gas: 26638) -VerifySigner:test_verifySigner() (gas: 25845) -VerifySigner:test_verifySigner_false() (gas: 28553) -WithdrawCollateral:test_withdrawCollateral() (gas: 353200) -WithdrawCollateral:test_withdrawCollateral_availableMargin() (gas: 354718) -WithdrawCollateral:test_withdrawCollateral_collateralAmount() (gas: 353710) -WithdrawCollateral:test_withdrawCollateral_insufficient_account_collateral_balance() (gas: 274196) -WithdrawCollateral:test_withdrawCollateral_totalCollateralValue() (gas: 354216) -WithdrawCollateral:test_withdrawCollateral_zero() (gas: 266154) \ No newline at end of file +NonceBitmapTest:test_fuzz_invalidateUnorderedNonces(uint256) (runs: 256, μ: 52725, ~: 52725) +NonceBitmapTest:test_hasUnorderedNonceBeenUsed() (gas: 54036) +NonceBitmapTest:test_invalidateUnorderedNonces() (gas: 76121) +NonceBitmapTest:test_invalidateUnorderedNonces_Only_Owner_Delegate() (gas: 190234) +NonceBitmapTest:test_invalidateUnorderedNonces_event() (gas: 53464) +ReduceOnly:test_reduce_only() (gas: 431717) +ReduceOnly:test_reduce_only_same_sign() (gas: 72073) +ReduceOnly:test_reduce_only_truncate_size_down() (gas: 431824) +ReduceOnly:test_reduce_only_truncate_size_up() (gas: 408486) +ReduceOnly:test_reduce_only_zero_size() (gas: 162568) +VerifyConditions:test_max_condition_size_exceeded() (gas: 45101) +VerifyConditions:test_verifyConditions_InvalidConditionSelector() (gas: 14132) +VerifyConditions:test_verify_conditions_not_verified() (gas: 29751) +VerifyConditions:test_verify_conditions_verified() (gas: 135901) +VerifySignature:test_verifySignature() (gas: 23931) +VerifySignature:test_verifySignature_false_private_key() (gas: 26685) +VerifySigner:test_verifySigner() (gas: 25884) +VerifySigner:test_verifySigner_false() (gas: 28636) +Withdraw:test_withdrawEth() (gas: 50332) +Withdraw:test_withdrawEth_EthTransferFailed() (gas: 78823) +Withdraw:test_withdrawEth_InsufficientEthBalance() (gas: 57013) +Withdraw:test_withdrawEth_Unauthorized() (gas: 56652) +Withdraw:test_withdrawEth_event() (gas: 49576) +Withdraw:test_withdrawEth_fuzz(uint256) (runs: 256, μ: 68193, ~: 68743) +WithdrawCollateral:test_withdrawCollateral() (gas: 353254) +WithdrawCollateral:test_withdrawCollateral_availableMargin() (gas: 354755) +WithdrawCollateral:test_withdrawCollateral_collateralAmount() (gas: 353747) +WithdrawCollateral:test_withdrawCollateral_insufficient_account_collateral_balance() (gas: 274242) +WithdrawCollateral:test_withdrawCollateral_totalCollateralValue() (gas: 354253) +WithdrawCollateral:test_withdrawCollateral_zero() (gas: 266200) \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 888d42dc..a92cdcee 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/trusted-multicall-forwarder"] + path = lib/trusted-multicall-forwarder + url = https://github.com/Synthetixio/trusted-multicall-forwarder diff --git a/README.md b/README.md index 57b25da1..24a5294e 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,7 @@ src/ │ └── SignatureCheckerLib.sol └── utils ├── EIP712.sol - ├── EIP7412.sol - ├── ERC2771Context.sol - └── Multicallable.sol + └── EIP7412.sol ``` ## Tests diff --git a/deployments/Base.json b/deployments/Base.json index e69de29b..0ce8635d 100644 --- a/deployments/Base.json +++ b/deployments/Base.json @@ -0,0 +1,4 @@ +{ + "Engine": "", + "TrustedMulticallForwarder": "" +} \ No newline at end of file diff --git a/deployments/BaseGoerli.json b/deployments/BaseGoerli.json index 153c83cc..0ce8635d 100644 --- a/deployments/BaseGoerli.json +++ b/deployments/BaseGoerli.json @@ -1,3 +1,4 @@ { - "Engine": "0x319Ae7F3a0D635eD9CCF0276dCeAF680F9C7c397" + "Engine": "", + "TrustedMulticallForwarder": "" } \ No newline at end of file diff --git a/deployments/Optimism.json b/deployments/Optimism.json index e69de29b..0ce8635d 100644 --- a/deployments/Optimism.json +++ b/deployments/Optimism.json @@ -0,0 +1,4 @@ +{ + "Engine": "", + "TrustedMulticallForwarder": "" +} \ No newline at end of file diff --git a/deployments/OptimismGoerli.json b/deployments/OptimismGoerli.json index 85ca6b3e..0ce8635d 100644 --- a/deployments/OptimismGoerli.json +++ b/deployments/OptimismGoerli.json @@ -1,3 +1,4 @@ { - "Engine": "0x16Da5d51C1DDB43C1105A253F2dE9eAB2B3b76CA" + "Engine": "", + "TrustedMulticallForwarder": "" } \ No newline at end of file diff --git a/diagrams/Conditional-Orders.png b/diagrams/Conditional-Orders.png index 46a9ab88..314f4d76 100644 Binary files a/diagrams/Conditional-Orders.png and b/diagrams/Conditional-Orders.png differ diff --git a/lcov.info b/lcov.info index 73ee62ae..927dfc35 100644 --- a/lcov.info +++ b/lcov.info @@ -1,237 +1,257 @@ TN: SF:script/Deploy.s.sol -FN:58,DeployBaseGoerli.run -FNDA:0,DeployBaseGoerli.run -DA:59,0 -DA:60,0 -DA:62,0 -DA:70,0 -FN:38,DeployBase.run -FNDA:0,DeployBase.run -DA:39,0 -DA:40,0 -DA:42,0 -DA:50,0 -FN:78,DeployOptimism.run +FN:90,DeployOptimism.run FNDA:0,DeployOptimism.run -DA:79,0 -DA:80,0 +DA:91,0 +DA:92,0 +DA:94,0 +DA:101,0 +FN:71,DeployBaseGoerli.run +FNDA:0,DeployBaseGoerli.run +DA:72,0 +DA:73,0 +DA:75,0 DA:82,0 -DA:90,0 -FN:17,Setup.deploySystem -FNDA:0,Setup.deploySystem -DA:24,0 -FN:98,DeployOptimismGoerli.run +FN:109,DeployOptimismGoerli.run FNDA:0,DeployOptimismGoerli.run -DA:99,0 -DA:100,0 -DA:102,0 DA:110,0 +DA:111,0 +DA:113,0 +DA:120,0 +FN:52,DeployBase.run +FNDA:0,DeployBase.run +DA:53,0 +DA:54,0 +DA:56,0 +DA:63,0 +FN:24,Setup.deploySystem +FNDA:5,Setup.deploySystem +DA:36,5 +DA:38,5 FNF:5 -FNH:0 -LF:17 -LH:0 +FNH:1 +LF:18 +LH:2 BRF:0 BRH:0 end_of_record TN: SF:src/Engine.sol -FN:134,Engine.isAccountOwner +FN:124,Engine.isAccountOwner FNDA:3,Engine.isAccountOwner -DA:140,10 -FN:144,Engine.isAccountDelegate +DA:130,270 +FN:134,Engine.isAccountDelegate FNDA:2,Engine.isAccountDelegate -DA:150,2 -FN:155,Engine._isAccountOwnerOrDelegate -FNDA:285,Engine._isAccountOwnerOrDelegate -DA:160,285 -FN:170,Engine.invalidateUnorderedNonces -FNDA:260,Engine.invalidateUnorderedNonces +DA:140,2 +FN:145,Engine._isAccountOwnerOrDelegate +FNDA:283,Engine._isAccountOwnerOrDelegate +DA:150,283 +FN:160,Engine.depositEth +FNDA:525,Engine.depositEth +DA:161,525 +DA:163,525 +FN:167,Engine.withdrawEth +FNDA:261,Engine.withdrawEth +DA:171,261 +DA:173,261 +BRDA:173,0,0,1 +BRDA:173,0,1,260 DA:175,260 -BRDA:175,0,0,260 -BRDA:175,0,1,- -DA:181,260 -DA:183,260 -DA:185,0 -FN:190,Engine.hasUnorderedNonceBeenUsed +DA:177,258 +FN:184,Engine._withdrawEth +FNDA:272,Engine._withdrawEth +DA:189,272 +BRDA:189,1,0,1 +BRDA:189,1,1,271 +DA:192,271 +DA:194,271 +DA:196,271 +BRDA:196,2,0,1 +BRDA:196,2,1,270 +FN:204,Engine.invalidateUnorderedNonces +FNDA:261,Engine.invalidateUnorderedNonces +DA:209,261 +BRDA:209,3,0,261 +BRDA:209,3,1,- +DA:215,261 +DA:217,261 +DA:219,0 +FN:224,Engine.hasUnorderedNonceBeenUsed FNDA:260,Engine.hasUnorderedNonceBeenUsed -DA:196,281 -DA:200,281 -DA:214,281 -FN:224,Engine._bitmapPositions -FNDA:296,Engine._bitmapPositions -DA:231,296 -DA:235,296 -FN:241,Engine._useUnorderedNonce -FNDA:15,Engine._useUnorderedNonce -DA:242,15 -DA:246,15 -DA:253,15 -DA:273,15 -BRDA:273,1,0,- -BRDA:273,1,1,15 -FN:281,Engine.modifyCollateral -FNDA:19,Engine.modifyCollateral -DA:286,19 -DA:288,19 -DA:290,19 -BRDA:290,2,0,12 -BRDA:290,2,1,7 -DA:291,12 -DA:295,7 -BRDA:295,3,0,- -BRDA:295,3,1,7 -DA:296,7 -FN:302,Engine._depositCollateral -FNDA:12,Engine._depositCollateral -DA:310,12 -DA:312,11 -DA:314,11 -FN:317,Engine._withdrawCollateral -FNDA:20,Engine._withdrawCollateral -DA:324,20 -DA:327,17 -FN:333,Engine._getSynthAddress -FNDA:19,Engine._getSynthAddress -DA:338,19 -FN:348,Engine.commitOrder -FNDA:4,Engine.commitOrder -DA:362,4 -BRDA:362,4,0,4 -BRDA:362,4,1,- -DA:363,4 -DA:373,0 -FN:377,Engine._commitOrder -FNDA:16,Engine._commitOrder -DA:386,16 -FN:404,Engine.execute -FNDA:16,Engine.execute -DA:417,16 -BRDA:417,5,0,1 -BRDA:417,5,1,15 -DA:420,15 -DA:424,15 -DA:427,15 -BRDA:427,6,0,2 -BRDA:427,6,1,3 -DA:428,5 -DA:433,5 -BRDA:433,7,0,1 -BRDA:433,7,1,4 -DA:434,1 -DA:438,4 -BRDA:438,8,0,1 -BRDA:438,8,1,3 -DA:439,1 -DA:445,3 -BRDA:445,9,0,2 -BRDA:445,9,1,3 -DA:455,2 -DA:461,13 -DA:467,13 -DA:470,13 -BRDA:470,10,0,6 -BRDA:470,10,1,7 -DA:471,6 -DA:472,7 -BRDA:472,11,0,2 -BRDA:472,11,1,7 -DA:473,2 -DA:477,13 -DA:486,12 -FN:498,Engine.canExecute -FNDA:5,Engine.canExecute -DA:503,21 -BRDA:503,12,0,2 -BRDA:503,12,1,19 -DA:504,2 -DA:508,19 -BRDA:508,13,0,1 -BRDA:508,13,1,18 -DA:511,18 -BRDA:511,14,0,- -BRDA:511,14,1,18 -DA:514,18 -BRDA:514,15,0,- -BRDA:514,15,1,- -DA:517,0 -BRDA:517,16,0,- -BRDA:517,16,1,- -DA:521,18 -BRDA:521,17,0,1 -BRDA:521,17,1,17 -DA:524,17 -FN:532,Engine.verifySigner +DA:230,279 +DA:234,279 +DA:248,279 +FN:258,Engine._bitmapPositions +FNDA:291,Engine._bitmapPositions +DA:265,291 +DA:269,291 +FN:275,Engine._useUnorderedNonce +FNDA:12,Engine._useUnorderedNonce +DA:276,12 +DA:280,12 +DA:287,12 +DA:307,12 +BRDA:307,4,0,- +BRDA:307,4,1,12 +FN:315,Engine.modifyCollateral +FNDA:17,Engine.modifyCollateral +DA:320,17 +DA:322,17 +DA:324,17 +BRDA:324,5,0,11 +BRDA:324,5,1,6 +DA:325,11 +DA:329,6 +BRDA:329,6,0,- +BRDA:329,6,1,6 +DA:330,6 +FN:336,Engine._depositCollateral +FNDA:11,Engine._depositCollateral +DA:344,11 +DA:346,10 +DA:348,10 +FN:351,Engine._withdrawCollateral +FNDA:6,Engine._withdrawCollateral +DA:358,6 +DA:361,4 +FN:367,Engine._getSynthAddress +FNDA:17,Engine._getSynthAddress +DA:372,17 +FN:382,Engine.commitOrder +FNDA:3,Engine.commitOrder +DA:396,3 +BRDA:396,7,0,3 +BRDA:396,7,1,- +DA:397,3 +DA:407,0 +FN:411,Engine._commitOrder +FNDA:13,Engine._commitOrder +DA:420,13 +FN:438,Engine.execute +FNDA:14,Engine.execute +DA:453,14 +BRDA:453,8,0,2 +BRDA:453,8,1,12 +DA:456,12 +DA:462,12 +DA:466,12 +DA:469,12 +BRDA:469,9,0,2 +BRDA:469,9,1,3 +DA:470,5 +DA:475,5 +BRDA:475,10,0,1 +BRDA:475,10,1,4 +DA:476,1 +DA:480,4 +BRDA:480,11,0,1 +BRDA:480,11,1,3 +DA:481,1 +DA:487,3 +BRDA:487,12,0,2 +BRDA:487,12,1,3 +DA:497,2 +DA:502,10 +DA:512,7 +FN:520,Engine.canExecute +FNDA:9,Engine.canExecute +DA:526,23 +BRDA:526,13,0,2 +BRDA:526,13,1,21 +DA:529,21 +BRDA:529,14,0,2 +BRDA:529,14,1,19 +DA:532,19 +BRDA:532,15,0,2 +BRDA:532,15,1,17 +DA:533,2 +DA:537,17 +BRDA:537,16,0,1 +BRDA:537,16,1,16 +DA:540,16 +BRDA:540,17,0,1 +BRDA:540,17,1,15 +DA:543,15 +BRDA:543,18,0,- +BRDA:543,18,1,- +DA:546,0 +BRDA:546,19,0,- +BRDA:546,19,1,- +DA:550,15 +BRDA:550,20,0,1 +BRDA:550,20,1,14 +DA:553,14 +FN:561,Engine.verifySigner FNDA:2,Engine.verifySigner -DA:538,21 -FN:542,Engine.verifySignature +DA:567,19 +FN:571,Engine.verifySignature FNDA:2,Engine.verifySignature -DA:546,20 -FN:552,Engine.verifyConditions +DA:575,18 +FN:581,Engine.verifyConditions FNDA:4,Engine.verifyConditions -DA:558,4 -DA:559,4 -BRDA:559,18,0,1 -BRDA:559,18,1,3 -DA:560,1 -DA:563,3 -DA:564,13 -DA:565,13 -DA:568,13 -DA:573,13 -DA:574,11 -DA:575,9 -DA:576,7 -DA:577,5 -DA:578,4 -DA:579,3 -DA:580,2 -BRDA:572,19,0,1 -BRDA:572,19,1,11 -DA:583,12 -DA:586,12 -BRDA:586,20,0,1 -BRDA:586,20,1,11 -DA:589,11 -DA:592,1 -DA:596,1 -FN:604,Engine.isTimestampAfter +DA:587,4 +DA:588,4 +BRDA:588,21,0,1 +BRDA:588,21,1,3 +DA:589,1 +DA:592,3 +DA:593,13 +DA:594,13 +DA:597,13 +DA:602,13 +DA:603,11 +DA:604,9 +DA:605,7 +DA:606,5 +DA:607,4 +DA:608,3 +DA:609,2 +BRDA:601,22,0,1 +BRDA:601,22,1,11 +DA:612,12 +DA:615,12 +BRDA:615,23,0,1 +BRDA:615,23,1,11 +DA:618,11 +DA:621,1 +DA:625,1 +FN:633,Engine.isTimestampAfter FNDA:5,Engine.isTimestampAfter -DA:610,5 -FN:614,Engine.isTimestampBefore +DA:639,5 +FN:643,Engine.isTimestampBefore FNDA:5,Engine.isTimestampBefore -DA:620,5 -FN:624,Engine.isPriceAbove +DA:649,5 +FN:653,Engine.isPriceAbove FNDA:6,Engine.isPriceAbove -DA:629,6 -DA:634,6 -FN:638,Engine.isPriceBelow +DA:658,6 +DA:663,6 +FN:667,Engine.isPriceBelow FNDA:6,Engine.isPriceBelow -DA:643,6 -DA:648,6 -FN:652,Engine.isMarketOpen +DA:672,6 +DA:677,6 +FN:681,Engine.isMarketOpen FNDA:3,Engine.isMarketOpen -DA:658,3 -FN:662,Engine.isPositionSizeAbove +DA:687,3 +FN:691,Engine.isPositionSizeAbove FNDA:4,Engine.isPositionSizeAbove -DA:667,4 -DA:668,4 -DA:670,4 -FN:674,Engine.isPositionSizeBelow +DA:696,4 +DA:697,4 +DA:699,4 +FN:703,Engine.isPositionSizeBelow FNDA:4,Engine.isPositionSizeBelow -DA:679,4 -DA:680,4 -DA:682,4 -FN:686,Engine.isOrderFeeBelow +DA:708,4 +DA:709,4 +DA:711,4 +FN:715,Engine.isOrderFeeBelow FNDA:4,Engine.isOrderFeeBelow -DA:692,4 -DA:697,4 -FNF:26 -FNH:26 -LF:96 -LH:93 -BRF:42 -BRH:33 +DA:721,4 +DA:726,4 +FNF:29 +FNH:29 +LF:103 +LH:100 +BRF:48 +BRH:40 end_of_record TN: SF:src/libraries/ConditionalOrderHashLib.sol @@ -295,20 +315,20 @@ FNDA:0,EIP712._domainNameAndVersion DA:74,0 DA:75,0 FN:80,EIP712.DOMAIN_SEPARATOR -FNDA:21,EIP712.DOMAIN_SEPARATOR -DA:81,21 -DA:82,21 +FNDA:26,EIP712.DOMAIN_SEPARATOR +DA:81,26 +DA:82,26 BRDA:82,0,0,- -BRDA:82,0,1,21 +BRDA:82,0,1,26 DA:83,0 FN:100,EIP712._hashTypedData -FNDA:20,EIP712._hashTypedData -DA:105,20 -DA:106,20 +FNDA:18,EIP712._hashTypedData +DA:105,18 +DA:106,18 BRDA:106,1,0,- -BRDA:106,1,1,20 +BRDA:106,1,1,18 DA:107,0 -DA:115,20 +DA:115,18 FN:127,EIP712.eip712Domain FNDA:0,EIP712.eip712Domain DA:140,0 @@ -323,10 +343,10 @@ DA:154,0 DA:155,0 DA:164,0 FN:169,EIP712._cachedDomainSeparatorInvalidated -FNDA:41,EIP712._cachedDomainSeparatorInvalidated -DA:174,41 -DA:175,41 -DA:178,41 +FNDA:44,EIP712._cachedDomainSeparatorInvalidated +DA:174,44 +DA:175,44 +DA:178,44 FNF:6 FNH:3 LF:21 @@ -347,55 +367,6 @@ BRF:0 BRH:0 end_of_record TN: -SF:src/utils/ERC2771Context.sol -FN:52,ERC2771Context.trustedForwarder -FNDA:0,ERC2771Context.trustedForwarder -DA:53,314 -FN:59,ERC2771Context.isTrustedForwarder -FNDA:0,ERC2771Context.isTrustedForwarder -DA:60,314 -FN:68,ERC2771Context._msgSender -FNDA:314,ERC2771Context._msgSender -DA:69,314 -BRDA:69,0,0,- -BRDA:69,0,1,314 -DA:73,0 -DA:76,314 -FN:85,ERC2771Context._msgData -FNDA:0,ERC2771Context._msgData -DA:86,0 -BRDA:86,1,0,- -BRDA:86,1,1,- -DA:87,0 -DA:89,0 -FN:16,Context._msgSender -FNDA:314,Context._msgSender -DA:17,314 -FN:20,Context._msgData -FNDA:0,Context._msgData -DA:21,0 -FNF:6 -FNH:2 -LF:10 -LH:5 -BRF:4 -BRH:1 -end_of_record -TN: -SF:src/utils/Multicallable.sol -FN:19,Multicallable.multicall -FNDA:1,Multicallable.multicall -DA:24,1 -BRDA:24,0,0,- -DA:34,1 -FNF:1 -FNH:1 -LF:2 -LH:2 -BRF:1 -BRH:0 -end_of_record -TN: SF:test/AsyncOrder.t.sol FN:8,AsyncOrderTest.setUp FNDA:0,AsyncOrderTest.setUp @@ -463,56 +434,73 @@ BRF:0 BRH:0 end_of_record TN: +SF:test/EthManagement.t.sol +FN:11,EthManagementTest.setUp +FNDA:0,EthManagementTest.setUp +DA:12,0 +DA:13,0 +FNF:1 +FNH:0 +LF:2 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: SF:test/utils/Bootstrap.sol -FN:134,BootstrapOptimismGoerli.init -FNDA:0,BootstrapOptimismGoerli.init -DA:138,0 -DA:146,0 -DA:154,0 -FN:102,BootstrapOptimism.init -FNDA:0,BootstrapOptimism.init -DA:106,0 -DA:114,0 -DA:122,0 -FN:36,Bootstrap.initializeOptimismGoerli +FN:41,Bootstrap.initializeOptimismGoerli FNDA:0,Bootstrap.initializeOptimismGoerli -DA:37,0 -DA:38,0 -DA:45,0 -DA:47,0 -DA:48,0 -DA:49,0 -DA:50,0 +DA:42,0 +DA:43,0 DA:51,0 -DA:52,0 DA:53,0 DA:54,0 -DA:56,0 +DA:55,0 DA:57,0 DA:58,0 -DA:63,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:64,0 DA:65,0 -FN:68,Bootstrap.initializeOptimism +DA:66,0 +DA:71,0 +DA:73,0 +FN:76,Bootstrap.initializeOptimism FNDA:0,Bootstrap.initializeOptimism -DA:69,0 -DA:70,0 DA:77,0 -DA:79,0 -DA:80,0 -DA:81,0 -DA:82,0 -DA:83,0 -DA:84,0 -DA:85,0 +DA:78,0 DA:86,0 DA:88,0 DA:89,0 DA:90,0 +DA:92,0 +DA:93,0 +DA:94,0 DA:95,0 +DA:96,0 DA:97,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:106,0 +DA:108,0 +FN:146,BootstrapOptimismGoerli.init +FNDA:0,BootstrapOptimismGoerli.init +DA:150,0 +DA:151,0 +DA:158,0 +DA:166,0 +FN:113,BootstrapOptimism.init +FNDA:0,BootstrapOptimism.init +DA:117,0 +DA:118,0 +DA:125,0 +DA:133,0 FNF:4 FNH:0 -LF:38 +LF:42 LH:0 BRF:0 BRH:0 @@ -585,55 +573,14 @@ BRF:0 BRH:0 end_of_record TN: -SF:test/utils/analysis/ConditionalOrderFees.sol -FN:7,ConditionalOrderTest.setUp -FNDA:0,ConditionalOrderTest.setUp -DA:8,0 -DA:9,0 -FN:12,ConditionalOrderTest.plot_chart -FNDA:0,ConditionalOrderTest.plot_chart -DA:13,0 -DA:14,0 -DA:15,0 -DA:20,0 -DA:21,0 -DA:23,0 -BRDA:23,0,0,- -BRDA:23,0,1,- -DA:24,0 -DA:26,0 -BRDA:25,1,0,- -BRDA:25,1,1,- -DA:28,0 -DA:30,0 -FNF:2 -FNH:0 -LF:12 -LH:0 -BRF:4 -BRH:0 -end_of_record -TN: SF:test/utils/exposed/EngineExposed.sol FN:25,EngineExposed.getSynthAddress FNDA:0,EngineExposed.getSynthAddress DA:30,0 -FN:33,EngineExposed.expose_UPPER_FEE_CAP -FNDA:9,EngineExposed.expose_UPPER_FEE_CAP -DA:34,9 -FN:37,EngineExposed.expose_LOWER_FEE_CAP -FNDA:2,EngineExposed.expose_LOWER_FEE_CAP -DA:38,2 -FN:41,EngineExposed.expose_FEE_SCALING_FACTOR -FNDA:7,EngineExposed.expose_FEE_SCALING_FACTOR -DA:42,7 -FN:45,EngineExposed.expose_MAX_BPS -FNDA:0,EngineExposed.expose_MAX_BPS -DA:46,0 -FNF:5 -FNH:3 -LF:5 -LH:3 +FNF:1 +FNH:0 +LF:1 +LH:0 BRF:0 BRH:0 end_of_record @@ -663,9 +610,12 @@ DA:45,0 FN:54,SynthetixMock.mock_fulfillOracleQuery FNDA:0,SynthetixMock.mock_fulfillOracleQuery DA:58,0 -FNF:4 +FN:67,SynthetixMock.mock_getAccountOwner +FNDA:0,SynthetixMock.mock_getAccountOwner +DA:72,0 +FNF:5 FNH:0 -LF:4 +LF:5 LH:0 BRF:0 BRH:0 diff --git a/lib/trusted-multicall-forwarder b/lib/trusted-multicall-forwarder new file mode 160000 index 00000000..1ccd1853 --- /dev/null +++ b/lib/trusted-multicall-forwarder @@ -0,0 +1 @@ +Subproject commit 1ccd185367d1cdbb66b337366d3859094b4c9c70 diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index da1279bf..b4b331c2 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -1,7 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.18; +// contracts import {Engine} from "src/Engine.sol"; +import {TrustedMulticallForwarder} from + "lib/trusted-multicall-forwarder/src/TrustedMulticallForwarder.sol"; + +// parameters import {BaseGoerliParameters} from "script/utils/parameters/BaseGoerliParameters.sol"; import {BaseParameters} from "script/utils/parameters/BaseParameters.sol"; @@ -9,6 +14,8 @@ import {OptimismGoerliParameters} from "script/utils/parameters/OptimismGoerliParameters.sol"; import {OptimismParameters} from "script/utils/parameters/OptimismParameters.sol"; + +// forge utils import {Script} from "lib/forge-std/src/Script.sol"; /// @title Kwenta Smart Margin v3 deployment script @@ -18,15 +25,22 @@ contract Setup is Script { address perpsMarketProxy, address spotMarketProxy, address sUSDProxy, - address oracle, - address trustedForwarder - ) public returns (Engine engine) { + address oracle + ) + public + returns ( + Engine engine, + TrustedMulticallForwarder trustedForwarderContract + ) + { + trustedForwarderContract = new TrustedMulticallForwarder(); + engine = new Engine({ _perpsMarketProxy: perpsMarketProxy, _spotMarketProxy: spotMarketProxy, _sUSDProxy: sUSDProxy, _oracle: oracle, - _trustedForwarder: trustedForwarder + _trustedForwarder: address(trustedForwarderContract) }); } } @@ -43,8 +57,7 @@ contract DeployBase is Setup, BaseParameters { perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, sUSDProxy: USD_PROXY, - oracle: PYTH, - trustedForwarder: TRUSTED_FORWARDER + oracle: PYTH }); vm.stopBroadcast(); @@ -63,8 +76,7 @@ contract DeployBaseGoerli is Setup, BaseGoerliParameters { perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, sUSDProxy: USD_PROXY, - oracle: PYTH, - trustedForwarder: TRUSTED_FORWARDER + oracle: PYTH }); vm.stopBroadcast(); @@ -83,8 +95,7 @@ contract DeployOptimism is Setup, OptimismParameters { perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, sUSDProxy: USD_PROXY, - oracle: PYTH, - trustedForwarder: TRUSTED_FORWARDER + oracle: PYTH }); vm.stopBroadcast(); @@ -103,8 +114,7 @@ contract DeployOptimismGoerli is Setup, OptimismGoerliParameters { perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, sUSDProxy: USD_PROXY, - oracle: PYTH, - trustedForwarder: TRUSTED_FORWARDER + oracle: PYTH }); vm.stopBroadcast(); diff --git a/script/utils/parameters/BaseGoerliParameters.sol b/script/utils/parameters/BaseGoerliParameters.sol index 72f453de..b367e3a4 100644 --- a/script/utils/parameters/BaseGoerliParameters.sol +++ b/script/utils/parameters/BaseGoerliParameters.sol @@ -12,7 +12,4 @@ contract BaseGoerliParameters { 0x579c612E4Bf390f5504DB9f76b6F5759A3172279; address public constant PYTH = 0x5955C1478F0dAD753C7E2B4dD1b4bC530C64749f; - - address public constant TRUSTED_FORWARDER = - 0xAE788aaf52780741E12BF79Ad684B91Bb0EF4D92; } diff --git a/script/utils/parameters/BaseParameters.sol b/script/utils/parameters/BaseParameters.sol index 19fb2717..01ea1811 100644 --- a/script/utils/parameters/BaseParameters.sol +++ b/script/utils/parameters/BaseParameters.sol @@ -9,7 +9,4 @@ contract BaseParameters { address public constant USD_PROXY = address(0); address public constant PYTH = 0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a; - - /// @custom:todo Base doesn't have a trusted forwarder yet - address public constant TRUSTED_FORWARDER = address(0); } diff --git a/script/utils/parameters/OptimismGoerliParameters.sol b/script/utils/parameters/OptimismGoerliParameters.sol index d299dc7c..d22dd024 100644 --- a/script/utils/parameters/OptimismGoerliParameters.sol +++ b/script/utils/parameters/OptimismGoerliParameters.sol @@ -12,8 +12,4 @@ contract OptimismGoerliParameters { 0xe487Ad4291019b33e2230F8E2FB1fb6490325260; address public constant PYTH = 0xff1a0f4744e8582DF1aE09D5611b887B6a12925C; - - /// @custom:todo Optimism Goerli doesn't have a trusted forwarder yet - address public constant TRUSTED_FORWARDER = - 0xAE788aaf52780741E12BF79Ad684B91Bb0EF4D92; } diff --git a/script/utils/parameters/OptimismParameters.sol b/script/utils/parameters/OptimismParameters.sol index 10679c61..d6e80c34 100644 --- a/script/utils/parameters/OptimismParameters.sol +++ b/script/utils/parameters/OptimismParameters.sol @@ -11,7 +11,4 @@ contract OptimismParameters { 0xb2F30A7C980f052f02563fb518dcc39e6bf38175; address public constant PYTH = 0xff1a0f4744e8582DF1aE09D5611b887B6a12925C; - - /// @custom:todo Optimism doesn't have a trusted forwarder yet - address public constant TRUSTED_FORWARDER = address(0); } diff --git a/src/Engine.sol b/src/Engine.sol index aaf5d72e..8c54228f 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -5,19 +5,19 @@ import {ConditionalOrderHashLib} from "src/libraries/ConditionalOrderHashLib.sol"; import {EIP712} from "src/utils/EIP712.sol"; import {EIP7412} from "src/utils/EIP7412.sol"; -import {ERC2771Context} from "src/utils/ERC2771Context.sol"; +import {ERC2771Context} from + "lib/trusted-multicall-forwarder/lib/openzeppelin-contracts/contracts/metatx/ERC2771Context.sol"; import {IEngine, IPerpsMarketProxy} from "src/interfaces/IEngine.sol"; import {IERC20} from "src/interfaces/tokens/IERC20.sol"; import {IPyth, PythStructs} from "src/interfaces/oracles/IPyth.sol"; import {ISpotMarketProxy} from "src/interfaces/synthetix/ISpotMarketProxy.sol"; import {MathLib} from "src/libraries/MathLib.sol"; -import {Multicallable} from "src/utils/Multicallable.sol"; import {SignatureCheckerLib} from "src/libraries/SignatureCheckerLib.sol"; /// @title Kwenta Smart Margin v3: Engine contract /// @notice Responsible for interacting with Synthetix v3 perps markets /// @author JaredBorders (jaredborders@pm.me) -contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context { +contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { using MathLib for int128; using MathLib for int256; using MathLib for uint256; @@ -36,21 +36,6 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context { /// @notice "0" synthMarketId represents sUSD in Synthetix v3 uint128 internal constant USD_SYNTH_ID = 0; - /// @notice max fee that can be charged for a conditional order execution - /// @dev 50 USD - uint256 internal constant UPPER_FEE_CAP = 50 ether; - - /// @notice min fee that can be charged for a conditional order execution - /// @dev 2 USD - uint256 internal constant LOWER_FEE_CAP = 2 ether; - - /// @notice percentage of the simulated order fee that is charged for a conditional order execution - /// @dev denoted in BPS (basis points) where 1% = 100 BPS and 100% = 10000 BPS - uint256 internal constant FEE_SCALING_FACTOR = 1000; - - /// @notice max BPS - uint256 internal constant MAX_BPS = 10_000; - /// @notice max number of conditions that can be defined for a conditional order uint256 internal constant MAX_CONDITIONS = 8; @@ -97,6 +82,11 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context { mapping(uint128 accountId => mapping(uint256 index => uint256 bitmap)) public nonceBitmap; + /// @notice mapping of account id to ETH balance + /// @dev ETH can be deposited/withdrawn from the + /// Engine contract to pay for fee(s) (conditional order execution, etc.) + mapping(uint128 accountId => uint256 ethBalance) public ethBalances; + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -162,6 +152,50 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context { ); } + /*////////////////////////////////////////////////////////////// + ETH MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IEngine + function depositEth(uint128 _accountId) external payable override { + ethBalances[_accountId] += msg.value; + + emit EthDeposit(_accountId, msg.value); + } + + /// @inheritdoc IEngine + function withdrawEth(uint128 _accountId, uint256 _amount) + external + override + { + address payable caller = payable(_msgSender()); + + if (!isAccountOwner(_accountId, caller)) revert Unauthorized(); + + _withdrawEth(caller, _accountId, _amount); + + emit EthWithdraw(_accountId, _amount); + } + + /// @notice debit ETH from the account and transfer it to the caller + /// @dev UNSAFE to call directly; use `withdrawEth` instead + /// @param _caller the caller of the function + /// @param _accountId the account id to debit ETH from + function _withdrawEth( + address payable _caller, + uint128 _accountId, + uint256 _amount + ) internal { + if (_amount > ethBalances[_accountId]) revert InsufficientEthBalance(); + + // decrement the ETH balance of the account prior to transferring ETH to the caller + ethBalances[_accountId] -= _amount; + + (bool sent,) = _caller.call{value: _amount}(""); + + if (!sent) revert EthTransferFailed(); + } + /*////////////////////////////////////////////////////////////// NONCE MANAGEMENT //////////////////////////////////////////////////////////////*/ @@ -401,24 +435,32 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context { //////////////////////////////////////////////////////////////*/ /// @inheritdoc IEngine - function execute(ConditionalOrder calldata _co, bytes calldata _signature) + function execute( + ConditionalOrder calldata _co, + bytes calldata _signature, + uint256 _fee + ) external override - returns ( - IPerpsMarketProxy.Data memory retOrder, - uint256 fees, - uint256 conditionalOrderFee - ) + returns (IPerpsMarketProxy.Data memory retOrder, uint256 synthetixFees) { - /// @dev check: (1) nonce has not been executed before - /// @dev check: (2) signer is authorized to interact with the account - /// @dev check: (3) signature for the order was signed by the signer - /// @dev check: (4) conditions are met || trusted executor is msg sender - if (!canExecute(_co, _signature)) revert CannotExecuteOrder(); + /// @dev check: (1) fee does not exceed the max fee set by the conditional order + /// @dev check: (2) fee does not exceed balance credited to the account + /// @dev check: (3) nonce has not been executed before + /// @dev check: (4) signer is authorized to interact with the account + /// @dev check: (5) signature for the order was signed by the signer + /// @dev check: (6) conditions are met || trusted executor is msg sender + if (!canExecute(_co, _signature, _fee)) revert CannotExecuteOrder(); /// @dev spend the nonce associated with the order; this prevents replay _useUnorderedNonce(_co.orderDetails.accountId, _co.nonce); + /// @dev impose a fee for executing the conditional order + /// @dev the fee is denoted in ETH and is paid to the caller (conditional order executor) + /// @dev the fee does not exceed the max fee set by the conditional order and + /// this is enforced by the `canExecute` function + _withdrawEth(payable(_msgSender()), _co.orderDetails.accountId, _fee); + /// @notice get size delta from order details /// @dev up to the caller to not waste gas by passing in a size delta of zero int128 sizeDelta = _co.orderDetails.sizeDelta; @@ -431,12 +473,12 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context { // ensure position exists; reduce only orders cannot increase position size if (positionSize == 0) { - return (retOrder, 0, 0); + return (retOrder, 0); } // ensure incoming size delta is NOT the same sign; i.e. reduce only orders cannot increase position size if (positionSize.isSameSign(sizeDelta)) { - return (retOrder, 0, 0); + return (retOrder, 0); } // ensure incoming size delta is not larger than current position size @@ -456,34 +498,8 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context { } } - /// @dev fetch estimated order fees to be used to - /// calculate conditional order fee - (uint256 orderFees,) = PERPS_MARKET_PROXY.computeOrderFees({ - marketId: _co.orderDetails.marketId, - sizeDelta: sizeDelta - }); - - /// @dev calculate conditional order fee based on scaled order fees - conditionalOrderFee = (orderFees * FEE_SCALING_FACTOR) / MAX_BPS; - - /// @dev ensure conditional order fee is within bounds - if (conditionalOrderFee < LOWER_FEE_CAP) { - conditionalOrderFee = LOWER_FEE_CAP; - } else if (conditionalOrderFee > UPPER_FEE_CAP) { - conditionalOrderFee = UPPER_FEE_CAP; - } - - /// @dev withdraw conditional order fee from account prior to executing order - _withdrawCollateral({ - _to: _msgSender(), - _synth: SUSD, - _accountId: _co.orderDetails.accountId, - _synthMarketId: USD_SYNTH_ID, - _amount: -int256(conditionalOrderFee) - }); - /// @dev execute the order - (retOrder, fees) = _commitOrder({ + (retOrder, synthetixFees) = _commitOrder({ _perpsMarketId: _co.orderDetails.marketId, _accountId: _co.orderDetails.accountId, _sizeDelta: sizeDelta, @@ -492,13 +508,26 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context { _trackingCode: _co.orderDetails.trackingCode, _referrer: _co.orderDetails.referrer }); + + emit ConditionalOrderExecuted({ + order: retOrder, + synthetixFees: synthetixFees, + executorFee: _fee + }); } /// @inheritdoc IEngine function canExecute( ConditionalOrder calldata _co, - bytes calldata _signature + bytes calldata _signature, + uint256 _fee ) public view override returns (bool) { + // verify fee does not exceed the max fee set by the conditional order + if (_fee > _co.maxExecutorFee) return false; + + // verify account has enough credit (ETH) to pay the fee + if (_fee > ethBalances[_co.orderDetails.accountId]) return false; + // verify nonce has not been executed before if (hasUnorderedNonceBeenUsed(_co.orderDetails.accountId, _co.nonce)) { return false; diff --git a/src/interfaces/IEngine.sol b/src/interfaces/IEngine.sol index bcd4ad4b..d9b9db2d 100644 --- a/src/interfaces/IEngine.sol +++ b/src/interfaces/IEngine.sol @@ -42,6 +42,8 @@ interface IEngine { bool requireVerified; // address that can execute the order if requireVerified is false address trustedExecutor; + // max fee denominated in ETH that can be paid to the executor + uint256 maxExecutorFee; // array of extra conditions to be met bytes[] conditions; } @@ -69,15 +71,42 @@ interface IEngine { /// @notice thrown when attempting to verify a condition identified by an invalid selector error InvalidConditionSelector(bytes4 selector); + /// @notice thrown when attempting to debit an account with insufficient balance + error InsufficientEthBalance(); + + /// @notice thrown when attempting to transfer eth fails + error EthTransferFailed(); + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ /// @notice emitted when the account owner or delegate successfully invalidates an unordered nonce + /// @param accountId the id of the account that was invalidated + /// @param word the word position of the bitmap that was invalidated + /// @param mask the mask used to invalidate the bitmap event UnorderedNonceInvalidation( uint128 indexed accountId, uint256 word, uint256 mask ); + /// @notice emitted when eth is deposited into the engine and credited to an account + /// @param accountId the id of the account that was credited + /// @param amount the amount of eth deposited + event EthDeposit(uint128 indexed accountId, uint256 amount); + + /// @notice emitted when eth is withdrawn from the engine and debited from an account + /// @param accountId the id of the account that was debited + /// @param amount the amount of eth withdrawn + event EthWithdraw(uint128 indexed accountId, uint256 amount); + + /// @notice emitted when a conditional order is executed + /// @param order the order commited to the perps market + /// that was defined in the conditional order + /// @param executorFee the fee paid to the executor for executing the conditional order + event ConditionalOrderExecuted( + IPerpsMarketProxy.Data order, uint256 synthetixFees, uint256 executorFee + ); + /*////////////////////////////////////////////////////////////// AUTHENTICATION //////////////////////////////////////////////////////////////*/ @@ -104,6 +133,19 @@ interface IEngine { view returns (bool); + /*////////////////////////////////////////////////////////////// + ETH MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /// @notice deposit eth into the engine and credit the account identified by the accountId + /// @param _accountId the id of the account to credit + function depositEth(uint128 _accountId) external payable; + + /// @notice withdraw eth from the engine and debit the account identified by the accountId + /// @param _accountId the id of the account to debit + /// @param _amount the amount of eth to withdraw + function withdrawEth(uint128 _accountId, uint256 _amount) external; + /*////////////////////////////////////////////////////////////// NONCE MANAGEMENT //////////////////////////////////////////////////////////////*/ @@ -171,41 +213,114 @@ interface IEngine { CONDITIONAL ORDER MANAGEMENT //////////////////////////////////////////////////////////////*/ - /// In order for a conditional order to be committed and then executed there are a number of requirements that need to be met: + /// Conditional Orders + /// + /// tldr: + /// Conditional Orders (co's) are signed objects that define an async order + /// and the conditions that must be met for the order to be executed. + /// + /// deep dive: + /// co's are composed of 8 main parts: + /// 1. The async order details which are defined in the OrderDetails struct + /// (the order that is being submitted to Synthetix perps v3 market) + /// 2. isReduceOnly flag which indicates if the order can only reduce + /// the position size and is also defined in the OrderDetails struct + /// 3. The signer of the co which must be the account owner or delegate + /// and is included in the ConditionalOrder struct. + /// THIS DATA IS ALWAYS CHECKED ON-CHAIN + /// 4. The nonce of the co which is included in the ConditionalOrder struct. + /// The nonce is specific to the account id and is used to prevent replay attacks. + /// The nonce is not specific to an address, but rather an account id. + /// THIS DATA IS ALWAYS CHECKED ON-CHAIN + /// 5. The requireVerified flag which is included in the ConditionalOrder struct. + /// If requireVerified is true, all conditions defined in the co must be satisfied on-chain. + /// If requireVerified is false, the co can ONLY be executed by the trustedExecutor. + /// Notice that the conditions are not checked on-chain if requireVerified is false but are + /// expected to be checked off-chain by the trustedExecutor. This saves a significant amount gas. + /// 6. The trustedExecutor address which is included in the ConditionalOrder struct. + /// The trustedExecutor is the address that can execute the co if requireVerified is false. + /// If requireVerified is true, the trustedExecutor is ignored/not used. + /// 7. The maxExecutorFee which is included in the ConditionalOrder struct. + /// The maxExecutorFee is the maximum fee that can be imposed by the address that + /// successfully executes the co (trustedExecutor or not). This max fee is denominated in ETH and is + /// enforced on-chain. If the maxExecutorFee is greater than the fee specified + /// by the executor, the co will *not* be executed. + /// 8. The conditions which are included in the ConditionalOrder struct. + /// Conditions are encoded function selectors and parameters that are used to determine + /// if the co can be executed. Conditions are checked on-chain if requireVerified is true. + /// If requireVerified is false, conditions are expected to be checked off-chain by the trustedExecutor. + /// Conditions are stictly limited selectors defined in the Engine contract + /// (ex: isTimestampBeforeSelector, isPriceAboveSelector, etc.) /// - /// (1) The account must have sufficient snxUSD collateral to handle the order - /// (2) The account must not have another order committed - /// (3) The order’s set `acceptablePrice` needs to be met both on committing the order and when it gets executed - /// (users should choose a value for this that is likely to execute based on the conditions set) - /// (4) The order can only be executed within Synthetix’s set settlement window - /// (5) There must be a keeper that executes a conditional order + /// co's are not creaed on-chain. They are composed and signed off-chain. The signature + /// is then passed to the Engine contract along with the co. The Engine contract then + /// verifies the signature along with many other "things" to determine if the co can be executed. /// - /// @notice There is no guarantee a conditional order will be executed + /// Checklist: + /// In *every* case of co execution, the logic of validating the co is: + /// + /// 1. Check if the fee specified by the executor is less than or equal to the maxExecutorFee + /// 2. Check if the account has sufficient ETH credit to pay the fee + /// (see ETH MANAGEMENT for how that can be accomplished) + /// 3. Check if the nonce has been used (see NONCE MANAGEMENT for how that can be accomplished) + /// 4. Check if the signer is the owner or delegate of the account + /// 5. Check if the signature is valid for the given co and signer + /// 6. IF requireVerified is true, check if all conditions are met + /// ELSE IF requireVerified is false, check if the msg.sender is the trustedExecutor + /// + /// All of these checks are carried out via a call to the Engine's canExecute function + /// that returns true or false. If canExecute returns true, the co can be executed. + /// If canExecute returns false, the co cannot be executed. + /// This function is expected to be used off-chain to determine if the co can be executed. + /// It will be called within the Engine's execute function to determine if the co can be executed + /// and if it returns true, the co will be executed. If it returns false, the co will not be executed + /// and the transaction will revert with CannotExecuteOrder(). + /// + /// The Engine contract does not store co's. It only stores the nonceBitmaps for each account. + /// The Engine does hold and account for ETH credit and can modify the ETH credit of an account. + /// + /// ETH Management: + /// With the introduction of co's, the Engine contract now holds ETH credit for accounts. + /// Using collateral to pay for fees is not ideal due to accounting risks associated with + /// orders that are close to max leverage. To mitigate this risk, the Engine contract + /// holds ETH credit for accounts. This ETH credit is used to pay for fees. + /// Furthermore, given the multi-colateral nature of the protocol, the Engine contract + /// does not need to handle scenarios where an account does not have sufficient + /// snxUSD collateral to pay the fee. + /// + /// Finally, the current approach to implementing Account Abstraction via ERC-4337 + /// requires traders deposit ETH to the "protocol" prior to trading. This ETH can be + /// multipurposed to pay for fees. This is the approach taken by the Engine contract. + + /// @custom:docs for more in-depth documentation of conditional order mechanism, + /// please refer to https://github.com/Kwenta/smart-margin-v3/wiki/Conditional-Orders /// @notice execute a conditional order /// @param _co the conditional order /// @param _signature the signature of the conditional order + /// @param _fee the fee paid to executor for the conditional order /// @return retOrder the order committed - /// @return fees the fees paid for the order to Synthetix - /// @return conditionalOrderFee the fee paid to executor for the conditional order - function execute(ConditionalOrder calldata _co, bytes calldata _signature) + /// @return synthetixFees the fees paid for the order to Synthetix + /// and *NOT* the fees paid to the executor + function execute( + ConditionalOrder calldata _co, + bytes calldata _signature, + uint256 _fee + ) external - returns ( - IPerpsMarketProxy.Data memory retOrder, - uint256 fees, - uint256 conditionalOrderFee - ); - - /// @notice checks if the order can be executed based on defined conditions - /// @dev this function does NOT check if the order can be executed based on the account's balance - /// (i.e. does not check if enough USD is available to pay for the order fee nor does it check - /// if enough collateral is available to cover the order) - /// @param _co the conditional order + returns (IPerpsMarketProxy.Data memory retOrder, uint256 synthetixFees); + + /// @notice checks if the conditional order can be executed + /// @param _co the conditional order which details the order to be executed and the conditions to be met /// @param _signature the signature of the conditional order - /// @return true if the order can be executed based on defined conditions, false otherwise + /// @param _fee the executor specified fee for the executing the conditional order + /// @dev if the fee is greater than the maxExecutorFee defined in the conditional order, + /// or if the account lacks sufficient ETH credit to pay the fee, canExecute will return false + /// @return true if the order can be executed, false otherwise function canExecute( ConditionalOrder calldata _co, - bytes calldata _signature + bytes calldata _signature, + uint256 _fee ) external view returns (bool); /// @notice verify the conditional order signer is the owner or delegate of the account diff --git a/src/libraries/ConditionalOrderHashLib.sol b/src/libraries/ConditionalOrderHashLib.sol index fd62c47a..5956c663 100644 --- a/src/libraries/ConditionalOrderHashLib.sol +++ b/src/libraries/ConditionalOrderHashLib.sol @@ -14,7 +14,7 @@ library ConditionalOrderHashLib { /// @notice pre-computed keccak256(ConditionalOrder struct) bytes32 public constant _CONDITIONAL_ORDER_TYPEHASH = keccak256( - "ConditionalOrder(OrderDetails orderDetails,address signer,uint128 nonce,bool requireVerified,address trustedExecutor,bytes[] conditions)OrderDetails(uint128 marketId,uint128 accountId,int128 sizeDelta,uint128 settlementStrategyId,uint256 acceptablePrice,bool isReduceOnly,bytes32 trackingCode,address referrer)" + "ConditionalOrder(OrderDetails orderDetails,address signer,uint128 nonce,bool requireVerified,address trustedExecutor,uint256 maxExecutorFee,bytes[] conditions)OrderDetails(uint128 marketId,uint128 accountId,int128 sizeDelta,uint128 settlementStrategyId,uint256 acceptablePrice,bool isReduceOnly,bytes32 trackingCode,address referrer)" ); /// @notice hash the OrderDetails struct diff --git a/src/utils/ERC2771Context.sol b/src/utils/ERC2771Context.sol deleted file mode 100644 index d70b3edd..00000000 --- a/src/utils/ERC2771Context.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (metatx/ERC2771Context.sol) -pragma solidity 0.8.18; - -/** - * @dev Provides information about the current execution context, including the - * sender of the transaction and its data. While these are generally available - * via msg.sender and msg.data, they should not be accessed in such a direct - * manner, since when dealing with meta-transactions the account sending and - * paying for execution may not be the actual sender (as far as an application - * is concerned). - * - * This contract is only required for intermediate, library-like contracts. - */ -abstract contract Context { - function _msgSender() internal view virtual returns (address) { - return msg.sender; - } - - function _msgData() internal view virtual returns (bytes calldata) { - return msg.data; - } -} - -/** - * @dev Context variant with ERC2771 support. - * - * WARNING: Avoid using this pattern in contracts that rely in a specific calldata length as they'll - * be affected by any forwarder whose `msg.data` is suffixed with the `from` address according to the ERC2771 - * specification adding the address size in bytes (20) to the calldata size. An example of an unexpected - * behavior could be an unintended fallback (or another function) invocation while trying to invoke the `receive` - * function only accessible if `msg.data.length == 0`. - */ -contract ERC2771Context is Context { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address private immutable _trustedForwarder; - - /** - * @dev Initializes the contract with a trusted forwarder, which will be able to - * invoke functions on this contract on behalf of other accounts. - * - * NOTE: The trusted forwarder can be replaced by overriding {trustedForwarder}. - */ - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address trustedForwarder_) { - _trustedForwarder = trustedForwarder_; - } - - /** - * @dev Returns the address of the trusted forwarder. - */ - function trustedForwarder() public view returns (address) { - return _trustedForwarder; - } - - /** - * @dev Indicates whether any particular address is the trusted forwarder. - */ - function isTrustedForwarder(address forwarder) public view returns (bool) { - return forwarder == trustedForwarder(); - } - - /** - * @dev Override for `msg.sender`. Defaults to the original `msg.sender` whenever - * a call is not performed by the trusted forwarder or the calldata length is less than - * 20 bytes (an address length). - */ - function _msgSender() internal view override returns (address sender) { - if (isTrustedForwarder(msg.sender) && msg.data.length >= 20) { - // The assembly code is more direct than the Solidity version using `abi.decode`. - /// @solidity memory-safe-assembly - assembly { - sender := shr(96, calldataload(sub(calldatasize(), 20))) - } - } else { - return super._msgSender(); - } - } - - /** - * @dev Override for `msg.data`. Defaults to the original `msg.data` whenever - * a call is not performed by the trusted forwarder or the calldata length is less than - * 20 bytes (an address length). - */ - function _msgData() internal view override returns (bytes calldata) { - if (isTrustedForwarder(msg.sender) && msg.data.length >= 20) { - return msg.data[:msg.data.length - 20]; - } else { - return super._msgData(); - } - } -} diff --git a/src/utils/Multicallable.sol b/src/utils/Multicallable.sol deleted file mode 100644 index 7976ef3b..00000000 --- a/src/utils/Multicallable.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.18; - -/// @notice Contract that enables a single call to call multiple methods on itself. -/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/Multicallable.sol) -/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/Multicallable.sol) -contract Multicallable { - /// @dev Apply `DELEGATECALL` with the current contract to each calldata in `data`, - /// and store the `abi.encode` formatted results of each `DELEGATECALL` into `results`. - /// If any of the `DELEGATECALL`s reverts, the entire context is reverted, - /// and the error is bubbled up. - /// - /// This function is deliberately made non-payable to guard against double-spending. - /// (See: https://www.paradigm.xyz/2021/08/two-rights-might-make-a-wrong) - /// - /// For efficiency, this function will directly return the results, terminating the context. - /// If called internally, it must be called at the end of a function - /// that returns `(bytes[] memory)`. - function multicall(bytes[] calldata data) public returns (bytes[] memory) { - assembly { - mstore(0x00, 0x20) - mstore(0x20, data.length) // Store `data.length` into `results`. - // Early return if no data. - if iszero(data.length) { return(0x00, 0x40) } - - let results := 0x40 - // `shl` 5 is equivalent to multiplying by 0x20. - let end := shl(5, data.length) - // Copy the offsets from calldata into memory. - calldatacopy(0x40, data.offset, end) - // Offset into `results`. - let resultsOffset := end - // Pointer to the end of `results`. - end := add(results, end) - - for {} 1 {} { - // The offset of the current bytes in the calldata. - let o := add(data.offset, mload(results)) - let memPtr := add(resultsOffset, 0x40) - // Copy the current bytes from calldata to the memory. - calldatacopy( - memPtr, - add(o, 0x20), // The offset of the current bytes' bytes. - calldataload(o) // The length of the current bytes. - ) - if iszero( - delegatecall( - gas(), address(), memPtr, calldataload(o), 0x00, 0x00 - ) - ) { - // Bubble up the revert if the delegatecall reverts. - returndatacopy(0x00, 0x00, returndatasize()) - revert(0x00, returndatasize()) - } - // Append the current `resultsOffset` into `results`. - mstore(results, resultsOffset) - results := add(results, 0x20) - // Append the `returndatasize()`, and the return data. - mstore(memPtr, returndatasize()) - returndatacopy(add(memPtr, 0x20), 0x00, returndatasize()) - // Advance the `resultsOffset` by `returndatasize() + 0x20`, - // rounded up to the next multiple of 32. - resultsOffset := - and( - add(add(resultsOffset, returndatasize()), 0x3f), - 0xffffffffffffffe0 - ) - if iszero(lt(results, end)) { break } - } - return(0x00, add(resultsOffset, 0x40)) - } - } -} diff --git a/test/AsyncOrder.t.sol b/test/AsyncOrder.t.sol index e2da7446..c991e7dd 100644 --- a/test/AsyncOrder.t.sol +++ b/test/AsyncOrder.t.sol @@ -32,7 +32,7 @@ contract CommitOrder is AsyncOrderTest { _perpsMarketId: SETH_PERPS_MARKET_ID, _accountId: accountId, _sizeDelta: 1 ether, - _settlementStrategyId: 0, + _settlementStrategyId: SETTLEMENT_STRATEGY_ID, _acceptablePrice: type(uint256).max, _trackingCode: TRACKING_CODE, _referrer: REFERRER @@ -65,7 +65,7 @@ contract CommitOrder is AsyncOrderTest { _perpsMarketId: INVALID_PERPS_MARKET_ID, _accountId: accountId, _sizeDelta: 1 ether, - _settlementStrategyId: 0, + _settlementStrategyId: SETTLEMENT_STRATEGY_ID, _acceptablePrice: type(uint256).max, _trackingCode: TRACKING_CODE, _referrer: REFERRER @@ -95,7 +95,7 @@ contract CommitOrder is AsyncOrderTest { _perpsMarketId: SETH_PERPS_MARKET_ID, _accountId: accountId, _sizeDelta: sizeDelta, - _settlementStrategyId: 0, + _settlementStrategyId: SETTLEMENT_STRATEGY_ID, _acceptablePrice: type(uint256).max, _trackingCode: TRACKING_CODE, _referrer: REFERRER diff --git a/test/ConditionalOrder.t.sol b/test/ConditionalOrder.t.sol index ba6a1bb2..92bdb326 100644 --- a/test/ConditionalOrder.t.sol +++ b/test/ConditionalOrder.t.sol @@ -54,103 +54,123 @@ contract ConditionalOrderTest is } contract CanExecute is ConditionalOrderTest { - function test_canExecute_true() public { - IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ + IEngine.OrderDetails public orderDetails; + IEngine.ConditionalOrder public co; + bytes public signature; + + function _defineConditionalOrder() internal { + orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: false, trackingCode: TRACKING_CODE, referrer: REFERRER }); - IEngine.ConditionalOrder memory co = IEngine.ConditionalOrder({ + co = IEngine.ConditionalOrder({ orderDetails: orderDetails, signer: signer, nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); - bytes memory signature = getConditionalOrderSignature({ + signature = getConditionalOrderSignature({ co: co, privateKey: signerPrivateKey, domainSeparator: engine.DOMAIN_SEPARATOR() }); + } - bool canExec = engine.canExecute(co, signature); + function test_canExecute_true() public { + _defineConditionalOrder(); + + bool canExec = engine.canExecute(co, signature, ZERO_CO_FEE); assertTrue(canExec); } - function test_canExecute_false_nonce_used() public { - IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ - marketId: SETH_PERPS_MARKET_ID, - accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, - isReduceOnly: false, - trackingCode: TRACKING_CODE, - referrer: REFERRER - }); + function test_canExecute_false_maxExecutorFee_exceeded() public { + _defineConditionalOrder(); - IEngine.ConditionalOrder memory co = IEngine.ConditionalOrder({ - orderDetails: orderDetails, - signer: signer, - nonce: 0, - requireVerified: false, - trustedExecutor: address(this), - conditions: new bytes[](0) - }); + co.maxExecutorFee = 0; // 0 max fee (i.e. any non-zero fee is too high) - bytes memory signature = getConditionalOrderSignature({ + signature = getConditionalOrderSignature({ co: co, privateKey: signerPrivateKey, domainSeparator: engine.DOMAIN_SEPARATOR() }); - engine.execute(co, signature); + // CO_FEE is non-zero, so it exceeds the maxExecutorFee + bool canExec = engine.canExecute(co, signature, CO_FEE); + + assertFalse(canExec); + } + + function test_canExecute_false_insufficent_account_credit() public { + _defineConditionalOrder(); + + // ensure the account has no credit + assertEq(engine.ethBalances(accountId), 0); + + // CO_FEE is non-zero, and the account has no credit + bool canExec = engine.canExecute(co, signature, CO_FEE); + + assertFalse(canExec); + } + + function test_canExecute_false_nonce_used() public { + _defineConditionalOrder(); + + engine.execute(co, signature, ZERO_CO_FEE); // nonce is now used; cannot execute again - bool canExec = engine.canExecute(co, signature); + bool canExec = engine.canExecute(co, signature, ZERO_CO_FEE); assertFalse(canExec); } - function test_canExecute_false_trusted_executor() public { - IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ - marketId: SETH_PERPS_MARKET_ID, - accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, - isReduceOnly: false, - trackingCode: TRACKING_CODE, - referrer: REFERRER - }); + function test_canExecute_false_invalid_signer() public { + _defineConditionalOrder(); - IEngine.ConditionalOrder memory co = IEngine.ConditionalOrder({ - orderDetails: orderDetails, - signer: signer, - nonce: 0, - requireVerified: false, - trustedExecutor: address(this), - conditions: new bytes[](0) - }); + co.signer = BAD_ACTOR; - bytes memory signature = getConditionalOrderSignature({ + signature = getConditionalOrderSignature({ co: co, privateKey: signerPrivateKey, domainSeparator: engine.DOMAIN_SEPARATOR() }); + bool canExec = engine.canExecute(co, signature, ZERO_CO_FEE); + + assertFalse(canExec); + } + + function test_canExecute_false_invalid_signature() public { + _defineConditionalOrder(); + + signature = getConditionalOrderSignature({ + co: co, + privateKey: bad_signerPrivateKey, + domainSeparator: engine.DOMAIN_SEPARATOR() + }); + + bool canExec = engine.canExecute(co, signature, ZERO_CO_FEE); + + assertFalse(canExec); + } + + function test_canExecute_false_trusted_executor() public { + _defineConditionalOrder(); + vm.prank(BAD_ACTOR); - bool canExec = engine.canExecute(co, signature); + bool canExec = engine.canExecute(co, signature, ZERO_CO_FEE); assertFalse(canExec); } @@ -162,7 +182,7 @@ contract VerifySigner is ConditionalOrderTest { marketId: 0, accountId: accountId, sizeDelta: 0, - settlementStrategyId: 0, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, acceptablePrice: 0, isReduceOnly: false, trackingCode: TRACKING_CODE, @@ -175,6 +195,7 @@ contract VerifySigner is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -188,7 +209,7 @@ contract VerifySigner is ConditionalOrderTest { marketId: 0, accountId: accountId, sizeDelta: 0, - settlementStrategyId: 0, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, acceptablePrice: 0, isReduceOnly: false, trackingCode: TRACKING_CODE, @@ -201,6 +222,7 @@ contract VerifySigner is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -216,7 +238,7 @@ contract VerifySignature is ConditionalOrderTest { marketId: 0, accountId: 0, sizeDelta: 0, - settlementStrategyId: 0, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, acceptablePrice: 0, isReduceOnly: false, trackingCode: TRACKING_CODE, @@ -229,6 +251,7 @@ contract VerifySignature is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -248,7 +271,7 @@ contract VerifySignature is ConditionalOrderTest { marketId: 0, accountId: 0, sizeDelta: 0, - settlementStrategyId: 0, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, acceptablePrice: 0, isReduceOnly: false, trackingCode: TRACKING_CODE, @@ -261,6 +284,7 @@ contract VerifySignature is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -288,6 +312,7 @@ contract VerifyConditions is ConditionalOrderTest { nonce: 0, requireVerified: true, trustedExecutor: address(0), + maxExecutorFee: type(uint256).max, conditions: conditions }); @@ -339,6 +364,7 @@ contract VerifyConditions is ConditionalOrderTest { nonce: 0, requireVerified: true, trustedExecutor: address(0), + maxExecutorFee: type(uint256).max, conditions: conditions }); @@ -373,6 +399,7 @@ contract VerifyConditions is ConditionalOrderTest { nonce: 0, requireVerified: true, trustedExecutor: address(0), + maxExecutorFee: type(uint256).max, conditions: conditions }); @@ -395,6 +422,7 @@ contract VerifyConditions is ConditionalOrderTest { nonce: 0, requireVerified: true, trustedExecutor: address(0), + maxExecutorFee: type(uint256).max, conditions: conditions }); @@ -409,13 +437,17 @@ contract VerifyConditions is ConditionalOrderTest { } contract Execute is ConditionalOrderTest { + event ConditionalOrderExecuted( + IPerpsMarketProxy.Data order, uint256 synthetixFees, uint256 executorFee + ); + function test_execute_order_committed() public { IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: false, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -427,6 +459,7 @@ contract Execute is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -436,11 +469,8 @@ contract Execute is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - ( - IPerpsMarketProxy.Data memory retOrder, - uint256 fees, - uint256 conditionalOrderFee - ) = engine.execute(co, signature); + (IPerpsMarketProxy.Data memory retOrder, uint256 fees) = + engine.execute(co, signature, ZERO_CO_FEE); // retOrder assertTrue(retOrder.settlementTime != 0); @@ -454,18 +484,15 @@ contract Execute is ConditionalOrderTest { // fees assertTrue(fees != 0); - - // conditionalOrderFee - assertTrue(conditionalOrderFee != 0); } - function test_execute_leverage_exceeded() public { + function test_execute_event() public { IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 50 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: false, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -477,6 +504,7 @@ contract Execute is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -486,77 +514,22 @@ contract Execute is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (uint256 orderFees,) = perpsMarketProxy.computeOrderFees({ - marketId: SETH_PERPS_MARKET_ID, - sizeDelta: 50 ether - }); - - uint256 requiredMargin = perpsMarketProxy.requiredMarginForOrder( - accountId, SETH_PERPS_MARKET_ID, 50 ether - ); - - uint256 marginPostConditionalOrderFee = AMOUNT - - ( - orderFees = - orderFees * engineExposed.expose_FEE_SCALING_FACTOR() / 10_000 - ); - - vm.expectRevert( - abi.encodeWithSelector( - InsufficientMargin.selector, - marginPostConditionalOrderFee, - requiredMargin - ) - ); - - engine.execute(co, signature); - } - - function test_execute_CannotExecuteOrder() public { - IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ - marketId: SETH_PERPS_MARKET_ID, - accountId: accountId, - sizeDelta: 50 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, - isReduceOnly: false, - trackingCode: TRACKING_CODE, - referrer: REFERRER - }); - - IEngine.ConditionalOrder memory co = IEngine.ConditionalOrder({ - orderDetails: orderDetails, - signer: address(0), - nonce: 0, - requireVerified: false, - trustedExecutor: address(this), - conditions: new bytes[](0) - }); - - bytes memory signature = getConditionalOrderSignature({ - co: co, - privateKey: signerPrivateKey, - domainSeparator: engine.DOMAIN_SEPARATOR() - }); + IPerpsMarketProxy.Data memory emptyOrder; - vm.expectRevert( - abi.encodeWithSelector(IEngine.CannotExecuteOrder.selector) - ); + // only checking that the event was emitted and not the values + vm.expectEmit(true, true, true, false); + emit ConditionalOrderExecuted(emptyOrder, 0, 0); - engine.execute(co, signature); + engine.execute(co, signature, ZERO_CO_FEE); } -} - -contract Fee is ConditionalOrderTest { - function test_fee_imposed() public { - assertEq(0, sUSD.balanceOf(address(this))); + function test_execute_CannotExecuteOrder_too_leveraged() public { IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 10 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: INVALID_SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: false, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -568,6 +541,7 @@ contract Fee is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -577,29 +551,19 @@ contract Fee is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (,, uint256 conditionalOrderFee) = engine.execute(co, signature); - - assertEq(sUSD.balanceOf(address(this)), conditionalOrderFee); + try engine.execute(co, signature, ZERO_CO_FEE) {} + catch (bytes memory reason) { + assertEq(bytes4(reason), InsufficientMargin.selector); + } } - function test_fee_imposed_at_upper_fee_cap() public { - uint256 mocked_order_fees = engineExposed.expose_UPPER_FEE_CAP() - * engineExposed.expose_FEE_SCALING_FACTOR(); - - mock_computeOrderFees({ - perpsMarketProxy: address(perpsMarketProxy), - marketId: SETH_PERPS_MARKET_ID, - sizeDelta: 1 ether, - orderFees: mocked_order_fees, - fillPrice: 1 ether - }); - + function test_execute_CannotExecuteOrder_invalid_acceptablePrice() public { IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: INVALID_ACCEPTABLE_PRICE, isReduceOnly: false, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -611,6 +575,7 @@ contract Fee is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -620,32 +585,21 @@ contract Fee is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (,, uint256 conditionalOrderFee) = engine.execute(co, signature); - - assertEq(engineExposed.expose_UPPER_FEE_CAP(), conditionalOrderFee); - assertEq( - engineExposed.expose_UPPER_FEE_CAP(), sUSD.balanceOf(address(this)) - ); + try engine.execute(co, signature, ZERO_CO_FEE) {} + catch (bytes memory reason) { + assertEq(bytes4(reason), AcceptablePriceExceeded.selector); + } } - function test_fee_imposed_above_upper_fee_cap() public { - uint256 mocked_order_fees = engineExposed.expose_UPPER_FEE_CAP() - * (engineExposed.expose_FEE_SCALING_FACTOR() + 1); - - mock_computeOrderFees({ - perpsMarketProxy: address(perpsMarketProxy), - marketId: SETH_PERPS_MARKET_ID, - sizeDelta: 1 ether, - orderFees: mocked_order_fees, - fillPrice: 1 ether - }); - + function test_execute_CannotExecuteOrder_invalid_settlementStrategyId() + public + { IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: INVALID_SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: false, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -657,6 +611,7 @@ contract Fee is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -666,31 +621,27 @@ contract Fee is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (,, uint256 conditionalOrderFee) = engine.execute(co, signature); - - assertEq(engineExposed.expose_UPPER_FEE_CAP(), conditionalOrderFee); - assertEq( - engineExposed.expose_UPPER_FEE_CAP(), sUSD.balanceOf(address(this)) + vm.expectRevert( + abi.encodeWithSelector( + InvalidSettlementStrategy.selector, + INVALID_SETTLEMENT_STRATEGY_ID + ) ); - } - function test_fee_imposed_below_upper_fee_cap() public { - uint256 mocked_order_fees = engineExposed.expose_UPPER_FEE_CAP(); + engine.execute(co, signature, ZERO_CO_FEE); + } +} - mock_computeOrderFees({ - perpsMarketProxy: address(perpsMarketProxy), - marketId: SETH_PERPS_MARKET_ID, - sizeDelta: 1 ether, - orderFees: mocked_order_fees, - fillPrice: 1 ether - }); +contract Fee is ConditionalOrderTest { + function test_fee_imposed() public { + engine.depositEth{value: 1 ether}(accountId); IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: false, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -702,6 +653,7 @@ contract Fee is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -711,84 +663,24 @@ contract Fee is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (,, uint256 conditionalOrderFee) = engine.execute(co, signature); - - assertEq( - ( - engineExposed.expose_UPPER_FEE_CAP() - * engineExposed.expose_FEE_SCALING_FACTOR() - ) / 10_000, - conditionalOrderFee - ); - assertEq( - ( - engineExposed.expose_UPPER_FEE_CAP() - * engineExposed.expose_FEE_SCALING_FACTOR() - ) / 10_000, - sUSD.balanceOf(address(this)) - ); - } - - function test_fee_imposed_below_lower_fee_cap() public { - uint256 mocked_order_fees = 0; - - mock_computeOrderFees({ - perpsMarketProxy: address(perpsMarketProxy), - marketId: SETH_PERPS_MARKET_ID, - sizeDelta: 1 ether, - orderFees: mocked_order_fees, - fillPrice: 1 ether - }); - - IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ - marketId: SETH_PERPS_MARKET_ID, - accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, - isReduceOnly: false, - trackingCode: TRACKING_CODE, - referrer: REFERRER - }); + uint256 preExecutorBalance = address(this).balance; - IEngine.ConditionalOrder memory co = IEngine.ConditionalOrder({ - orderDetails: orderDetails, - signer: signer, - nonce: 0, - requireVerified: false, - trustedExecutor: address(this), - conditions: new bytes[](0) - }); - - bytes memory signature = getConditionalOrderSignature({ - co: co, - privateKey: signerPrivateKey, - domainSeparator: engine.DOMAIN_SEPARATOR() - }); + engine.execute(co, signature, CO_FEE); - (,, uint256 conditionalOrderFee) = engine.execute(co, signature); + uint256 postExecutorBalance = address(this).balance; - assertEq(engineExposed.expose_LOWER_FEE_CAP(), conditionalOrderFee); - assertEq( - engineExposed.expose_LOWER_FEE_CAP(), sUSD.balanceOf(address(this)) - ); + assertEq(preExecutorBalance + CO_FEE, postExecutorBalance); } - function test_fee_imposed_fee_cannot_be_paid() public { - vm.prank(signer); - - engine.modifyCollateral({ - _accountId: accountId, - _synthMarketId: SUSD_SPOT_MARKET_ID, - _amount: -int256(AMOUNT) - }); + function test_fee_exceeds_account_credit() public { + engine.depositEth{value: CO_FEE - 1}(accountId); IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 10 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: false, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -800,6 +692,7 @@ contract Fee is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -809,33 +702,20 @@ contract Fee is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (uint256 orderFees,) = perpsMarketProxy.computeOrderFees({ - marketId: SETH_PERPS_MARKET_ID, - sizeDelta: 10 ether - }); - - orderFees = - orderFees * engineExposed.expose_FEE_SCALING_FACTOR() / 10_000; - - vm.expectRevert( - abi.encodeWithSelector( - InsufficientSynthCollateral.selector, - SUSD_SPOT_MARKET_ID, - 0, - orderFees - ) - ); + vm.expectRevert(IEngine.CannotExecuteOrder.selector); - engine.execute(co, signature); + engine.execute(co, signature, CO_FEE); } - function test_fee_imposed_insufficient_collateral_for_order() public { + function test_fee_exceeds_maxExecutorFee() public { + engine.depositEth{value: 1 ether}(accountId); + IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 50 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: false, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -847,6 +727,7 @@ contract Fee is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: 0, // 0 max fee (i.e. any non-zero fee is too high) conditions: new bytes[](0) }); @@ -856,30 +737,9 @@ contract Fee is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (uint256 orderFees,) = perpsMarketProxy.computeOrderFees({ - marketId: SETH_PERPS_MARKET_ID, - sizeDelta: 50 ether - }); - - uint256 requiredMargin = perpsMarketProxy.requiredMarginForOrder( - accountId, SETH_PERPS_MARKET_ID, 50 ether - ); - - uint256 marginPostConditionalOrderFee = AMOUNT - - ( - orderFees = - orderFees * engineExposed.expose_FEE_SCALING_FACTOR() / 10_000 - ); - - vm.expectRevert( - abi.encodeWithSelector( - InsufficientMargin.selector, - marginPostConditionalOrderFee, - requiredMargin - ) - ); + vm.expectRevert(IEngine.CannotExecuteOrder.selector); - engine.execute(co, signature); + engine.execute(co, signature, CO_FEE); } } @@ -892,9 +752,9 @@ contract ReduceOnly is ConditionalOrderTest { IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: true, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -906,6 +766,7 @@ contract ReduceOnly is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -915,7 +776,7 @@ contract ReduceOnly is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (, uint256 fees,) = engine.execute(co, signature); + (, uint256 fees) = engine.execute(co, signature, ZERO_CO_FEE); // confirms that the reduce-only order was executed assertTrue(fees > 0); @@ -925,9 +786,9 @@ contract ReduceOnly is ConditionalOrderTest { IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: true, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -939,6 +800,7 @@ contract ReduceOnly is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -948,11 +810,9 @@ contract ReduceOnly is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (, uint256 fees, uint256 conditionalOrderFee) = - engine.execute(co, signature); + (, uint256 fees) = engine.execute(co, signature, ZERO_CO_FEE); assertEq(0, fees); - assertEq(0, conditionalOrderFee); } function test_reduce_only_same_sign() public { @@ -963,9 +823,9 @@ contract ReduceOnly is ConditionalOrderTest { IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: true, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -977,6 +837,7 @@ contract ReduceOnly is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -986,11 +847,9 @@ contract ReduceOnly is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (, uint256 fees, uint256 conditionalOrderFee) = - engine.execute(co, signature); + (, uint256 fees) = engine.execute(co, signature, ZERO_CO_FEE); assertEq(0, fees); - assertEq(0, conditionalOrderFee); } function test_reduce_only_truncate_size_down() public { @@ -1002,8 +861,8 @@ contract ReduceOnly is ConditionalOrderTest { marketId: SETH_PERPS_MARKET_ID, accountId: accountId, sizeDelta: type(int128).max, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: true, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -1015,6 +874,7 @@ contract ReduceOnly is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -1024,7 +884,7 @@ contract ReduceOnly is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (, uint256 fees,) = engine.execute(co, signature); + (, uint256 fees) = engine.execute(co, signature, ZERO_CO_FEE); // confirms that the reduce-only order was executed /// @dev max sizeDelta used proves prices was truncated @@ -1040,7 +900,7 @@ contract ReduceOnly is ConditionalOrderTest { marketId: SETH_PERPS_MARKET_ID, accountId: accountId, sizeDelta: -type(int128).max, - settlementStrategyId: 0, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, acceptablePrice: 0, isReduceOnly: true, trackingCode: TRACKING_CODE, @@ -1053,6 +913,7 @@ contract ReduceOnly is ConditionalOrderTest { nonce: 0, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -1062,7 +923,7 @@ contract ReduceOnly is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (, uint256 fees,) = engine.execute(co, signature); + (, uint256 fees) = engine.execute(co, signature, ZERO_CO_FEE); // confirms that the reduce-only order was executed /// @dev max sizeDelta used proves prices was truncated diff --git a/test/Deployment.t.sol b/test/Deployment.t.sol index 7cff9855..d93c09d5 100644 --- a/test/Deployment.t.sol +++ b/test/Deployment.t.sol @@ -6,75 +6,78 @@ import {IEngine} from "src/interfaces/IEngine.sol"; import {Test} from "lib/forge-std/src/Test.sol"; contract DeploymentTest is Test, Setup { + Setup setup; + + function setUp() public { + setup = new Setup(); + } + function test_deploy() public { - Engine engine = Setup.deploySystem({ + (Engine engine,) = setup.deploySystem({ perpsMarketProxy: address(0x1), spotMarketProxy: address(0x2), sUSDProxy: address(0x3), - oracle: address(0x4), - trustedForwarder: address(0x5) + oracle: address(0x4) }); assertTrue(address(engine) != address(0x0)); } function test_deploy_perps_market_proxy_zero_address() public { - vm.expectRevert(abi.encodeWithSelector(IEngine.ZeroAddress.selector)); - - Setup.deploySystem({ + try setup.deploySystem({ perpsMarketProxy: address(0), spotMarketProxy: address(0x2), sUSDProxy: address(0x3), - oracle: address(0x4), - trustedForwarder: address(0x5) - }); + oracle: address(0x4) + }) {} catch (bytes memory reason) { + assertEq(bytes4(reason), IEngine.ZeroAddress.selector); + } } function test_deploy_spot_market_proxy_zero_address() public { - vm.expectRevert(abi.encodeWithSelector(IEngine.ZeroAddress.selector)); - - Setup.deploySystem({ + try setup.deploySystem({ perpsMarketProxy: address(0x1), spotMarketProxy: address(0), sUSDProxy: address(0x3), - oracle: address(0x4), - trustedForwarder: address(0x5) - }); + oracle: address(0x4) + }) {} catch (bytes memory reason) { + assertEq(bytes4(reason), IEngine.ZeroAddress.selector); + } } function test_deploy_susd_proxy_zero_address() public { - vm.expectRevert(abi.encodeWithSelector(IEngine.ZeroAddress.selector)); - - Setup.deploySystem({ + try setup.deploySystem({ perpsMarketProxy: address(0x1), spotMarketProxy: address(0x2), sUSDProxy: address(0), - oracle: address(0x4), - trustedForwarder: address(0x5) - }); + oracle: address(0x4) + }) {} catch (bytes memory reason) { + assertEq(bytes4(reason), IEngine.ZeroAddress.selector); + } } function test_deploy_oracle_zero_address() public { - vm.expectRevert(abi.encodeWithSelector(IEngine.ZeroAddress.selector)); - - Setup.deploySystem({ + try setup.deploySystem({ perpsMarketProxy: address(0x1), spotMarketProxy: address(0x2), sUSDProxy: address(0x3), - oracle: address(0), - trustedForwarder: address(0x5) - }); + oracle: address(0) + }) {} catch (bytes memory reason) { + assertEq(bytes4(reason), IEngine.ZeroAddress.selector); + } } function test_deploy_trusted_forwarder_zero_address() public { - vm.expectRevert(abi.encodeWithSelector(IEngine.ZeroAddress.selector)); - - Setup.deploySystem({ - perpsMarketProxy: address(0x1), - spotMarketProxy: address(0x2), - sUSDProxy: address(0x3), - oracle: address(0x4), - trustedForwarder: address(0) - }); + // trusted forwarder is deployed within the deploy script thus + // this test does not use the setup.deploySystem function + try new Engine({ + _perpsMarketProxy: address(0x1), + _spotMarketProxy: address(0x2), + _sUSDProxy: address(0x3), + _oracle: address(0x4), + _trustedForwarder: address(0) + }) {} catch (bytes memory reason) { + assertEq(bytes4(reason), IEngine.ZeroAddress.selector); + } } } diff --git a/test/EthManagement.t.sol b/test/EthManagement.t.sol new file mode 100644 index 00000000..69e54e8e --- /dev/null +++ b/test/EthManagement.t.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +import {Bootstrap, TrustedMulticallForwarder} from "test/utils/Bootstrap.sol"; +import {IEngine} from "src/interfaces/IEngine.sol"; +import {SynthetixMock} from "test/utils/mocks/SynthetixMock.sol"; + +contract CantReceiveEth {} + +contract EthManagementTest is Bootstrap, SynthetixMock { + function setUp() public { + vm.rollFork(GOERLI_BLOCK_NUMBER); + initializeOptimismGoerli(); + } +} + +contract Deposit is EthManagementTest { + event EthDeposit(uint128 indexed accountId, uint256 amount); + + function test_depositEth() public { + assertEq(engine.ethBalances(accountId), 0); + assertEq(address(engine).balance, 0); + + engine.depositEth{value: AMOUNT}(accountId); + + assertEq(engine.ethBalances(accountId), AMOUNT); + assertEq(address(engine).balance, AMOUNT); + } + + function test_depositEth_via_trustedForwarder() public { + TrustedMulticallForwarder.Call3Value memory call = + TrustedMulticallForwarder.Call3Value( + address(engine), + true, + AMOUNT, + abi.encodeWithSelector(engine.depositEth.selector, accountId) + ); + + TrustedMulticallForwarder.Call3Value[] memory calls = + new TrustedMulticallForwarder.Call3Value[](2); + + calls[0] = call; + calls[1] = call; + + trustedForwarderContract.aggregate3Value{value: AMOUNT * 2}(calls); + + assertEq(engine.ethBalances(accountId), AMOUNT * 2); + } + + function test_depositEth_via_trustedForwarder_value_mismatch() public { + TrustedMulticallForwarder.Call3Value memory call = + TrustedMulticallForwarder.Call3Value( + address(engine), + true, + AMOUNT, + abi.encodeWithSelector(engine.depositEth.selector, accountId) + ); + + TrustedMulticallForwarder.Call3Value[] memory calls = + new TrustedMulticallForwarder.Call3Value[](2); + + calls[0] = call; + calls[1] = call; + + vm.expectRevert("Multicall3: value mismatch"); + + // msg.value is AMOUNT, but since two calls spend AMOUNT each, the accumulated + // msg.value is AMOUNT * 2. This should fail. + trustedForwarderContract.aggregate3Value{value: AMOUNT}(calls); + + assertEq(engine.ethBalances(accountId), 0); + } + + function test_depositEth_fuzz( + uint256 fuzzedEthAmount, + uint128 fuzzedAccountId + ) public { + vm.assume(fuzzedEthAmount < address(this).balance - 1 ether); + + engine.depositEth{value: fuzzedEthAmount}(fuzzedAccountId); + + assertEq(engine.ethBalances(fuzzedAccountId), fuzzedEthAmount); + assertEq(address(engine).balance, fuzzedEthAmount); + } + + function test_depositEth_event() public { + vm.expectEmit(true, true, true, true); + emit EthDeposit(accountId, AMOUNT); + + engine.depositEth{value: AMOUNT}(accountId); + } +} + +contract Withdraw is EthManagementTest { + event EthWithdraw(uint128 indexed accountId, uint256 amount); + + function test_withdrawEth() public { + engine.depositEth{value: AMOUNT}(accountId); + + assertEq(engine.ethBalances(accountId), AMOUNT); + assertEq(address(engine).balance, AMOUNT); + + vm.prank(ACTOR); + + engine.withdrawEth(accountId, AMOUNT); + + assertEq(engine.ethBalances(accountId), 0); + assertEq(address(engine).balance, 0); + } + + function test_withdrawEth_fuzz(uint256 fuzzedEthAmount) public { + vm.assume(fuzzedEthAmount <= AMOUNT); + + engine.depositEth{value: AMOUNT}(accountId); + + assertEq(engine.ethBalances(accountId), AMOUNT); + assertEq(address(engine).balance, AMOUNT); + + vm.prank(ACTOR); + + engine.withdrawEth(accountId, fuzzedEthAmount); + + assertEq(engine.ethBalances(accountId), AMOUNT - fuzzedEthAmount); + assertEq(address(engine).balance, AMOUNT - fuzzedEthAmount); + } + + function test_withdrawEth_Unauthorized() public { + engine.depositEth{value: AMOUNT}(accountId); + + vm.expectRevert(IEngine.Unauthorized.selector); + + vm.prank(BAD_ACTOR); + + engine.withdrawEth(accountId, AMOUNT); + } + + function test_withdrawEth_InsufficientEthBalance() public { + engine.depositEth{value: AMOUNT}(accountId); + + vm.expectRevert(IEngine.InsufficientEthBalance.selector); + + vm.prank(ACTOR); + + engine.withdrawEth(accountId, AMOUNT + 1); + } + + function test_withdrawEth_EthTransferFailed() public { + CantReceiveEth cantReceiveEth = new CantReceiveEth(); + + engine.depositEth{value: AMOUNT}(accountId); + + mock_getAccountOwner( + address(perpsMarketProxy), accountId, address(cantReceiveEth) + ); + + vm.prank(address(cantReceiveEth)); + + vm.expectRevert(IEngine.EthTransferFailed.selector); + + engine.withdrawEth(accountId, AMOUNT); + } + + function test_withdrawEth_event() public { + engine.depositEth{value: AMOUNT}(accountId); + + vm.expectEmit(true, true, true, true); + emit EthWithdraw(accountId, AMOUNT); + + vm.prank(ACTOR); + + engine.withdrawEth(accountId, AMOUNT); + } +} diff --git a/test/Multicallable.t.sol b/test/Multicallable.t.sol deleted file mode 100644 index b2715490..00000000 --- a/test/Multicallable.t.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.18; - -import {Bootstrap} from "test/utils/Bootstrap.sol"; - -contract MulticallableTest is Bootstrap { - function setUp() public { - vm.rollFork(GOERLI_BLOCK_NUMBER); - initializeOptimismGoerli(); - } - - function test_multicall_depositCollateral_commitOrder() public { - vm.startPrank(ACTOR); - - sUSD.approve(address(engine), type(uint256).max); - - bytes[] memory data = new bytes[](2); - data[0] = abi.encodeWithSelector( - engine.modifyCollateral.selector, - accountId, // _accountId - SUSD_SPOT_MARKET_ID, // _synthMarketId - int256(AMOUNT) // _amount - ); - data[1] = abi.encodeWithSelector( - engine.commitOrder.selector, - SETH_PERPS_MARKET_ID, // _perpsMarketId - accountId, // _accountId - 1 ether, // _sizeDelta - 0, // _settlementStrategyId - type(uint256).max, // _acceptablePrice - TRACKING_CODE, // _trackingCode - REFERRER // _referrer - ); - - engine.multicall(data); - - vm.stopPrank(); - } -} diff --git a/test/NonceBitmap.t.sol b/test/NonceBitmap.t.sol index b2635dd4..7959d052 100644 --- a/test/NonceBitmap.t.sol +++ b/test/NonceBitmap.t.sol @@ -10,6 +10,10 @@ contract NonceBitmapTest is Bootstrap, ConditionalOrderSignature { address signer; uint256 signerPrivateKey; + event UnorderedNonceInvalidation( + uint128 indexed accountId, uint256 word, uint256 mask + ); + function setUp() public { vm.rollFork(GOERLI_BLOCK_NUMBER); initializeOptimismGoerli(); @@ -47,9 +51,9 @@ contract NonceBitmapTest is Bootstrap, ConditionalOrderSignature { IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, - sizeDelta: 1 ether, - settlementStrategyId: 0, - acceptablePrice: type(uint256).max, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, isReduceOnly: false, trackingCode: TRACKING_CODE, referrer: REFERRER @@ -61,6 +65,7 @@ contract NonceBitmapTest is Bootstrap, ConditionalOrderSignature { nonce: nonce, requireVerified: false, trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, conditions: new bytes[](0) }); @@ -70,7 +75,7 @@ contract NonceBitmapTest is Bootstrap, ConditionalOrderSignature { domainSeparator: engine.DOMAIN_SEPARATOR() }); - bool canExec = engine.canExecute(co, signature); + bool canExec = engine.canExecute(co, signature, ZERO_CO_FEE); assertTrue(canExec); @@ -78,11 +83,20 @@ contract NonceBitmapTest is Bootstrap, ConditionalOrderSignature { engine.invalidateUnorderedNonces(accountId, uint248(nonce >> 8), mask); - canExec = engine.canExecute(co, signature); + canExec = engine.canExecute(co, signature, ZERO_CO_FEE); assertFalse(canExec); } + function test_invalidateUnorderedNonces_event() public { + vm.expectEmit(true, true, true, true); + emit UnorderedNonceInvalidation(accountId, 0, type(uint256).max); + + vm.prank(signer); + + engine.invalidateUnorderedNonces(accountId, 0, type(uint256).max); + } + function test_hasUnorderedNonceBeenUsed() public { uint256 nonce = 1; uint256 mask = type(uint256).max; diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index c5be4cec..bcb1c843 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -18,12 +18,15 @@ import {IPerpsMarketProxy} from "src/interfaces/synthetix/IPerpsMarketProxy.sol" import {ISpotMarketProxy} from "src/interfaces/synthetix/ISpotMarketProxy.sol"; import {IPyth} from "src/interfaces/oracles/IPyth.sol"; import {SynthMinter} from "test/utils/SynthMinter.sol"; +import {TrustedMulticallForwarder} from + "lib/trusted-multicall-forwarder/src/TrustedMulticallForwarder.sol"; contract Bootstrap is Test, Constants, Conditions, SynthetixV3Errors { using console2 for *; Engine public engine; EngineExposed public engineExposed; + TrustedMulticallForwarder public trustedForwarderContract; IPerpsMarketProxy public perpsMarketProxy; ISpotMarketProxy public spotMarketProxy; IERC20 public sUSD; @@ -33,6 +36,8 @@ contract Bootstrap is Test, Constants, Conditions, SynthetixV3Errors { SynthMinter public synthMinter; uint128 public accountId; + receive() external payable {} + function initializeOptimismGoerli() public { BootstrapOptimismGoerli bootstrap = new BootstrapOptimismGoerli(); ( @@ -41,11 +46,14 @@ contract Bootstrap is Test, Constants, Conditions, SynthetixV3Errors { address _perpesMarketProxyAddress, address _spotMarketProxyAddress, address _sUSDAddress, - address _pythAddress + address _pythAddress, + address _trustedForwarderAddress ) = bootstrap.init(); engine = Engine(_engineAddress); engineExposed = EngineExposed(_engineExposedAddress); + trustedForwarderContract = + TrustedMulticallForwarder(_trustedForwarderAddress); perpsMarketProxy = IPerpsMarketProxy(_perpesMarketProxyAddress); spotMarketProxy = ISpotMarketProxy(_spotMarketProxyAddress); sUSD = IERC20(_sUSDAddress); @@ -73,11 +81,14 @@ contract Bootstrap is Test, Constants, Conditions, SynthetixV3Errors { address _perpesMarketProxyAddress, address _spotMarketProxyAddress, address _sUSDAddress, - address _pythAddress + address _pythAddress, + address _trustedForwarderAddress ) = bootstrap.init(); engine = Engine(_engineAddress); engineExposed = EngineExposed(_engineExposedAddress); + trustedForwarderContract = + TrustedMulticallForwarder(_trustedForwarderAddress); perpsMarketProxy = IPerpsMarketProxy(_perpesMarketProxyAddress); spotMarketProxy = ISpotMarketProxy(_spotMarketProxyAddress); sUSD = IERC20(_sUSDAddress); @@ -101,14 +112,14 @@ contract Bootstrap is Test, Constants, Conditions, SynthetixV3Errors { contract BootstrapOptimism is Setup, OptimismParameters { function init() public - returns (address, address, address, address, address, address) + returns (address, address, address, address, address, address, address) { - Engine engine = Setup.deploySystem({ + (Engine engine, TrustedMulticallForwarder trustedForwarderContract) = + Setup.deploySystem({ perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, sUSDProxy: USD_PROXY, - oracle: PYTH, - trustedForwarder: TRUSTED_FORWARDER + oracle: PYTH }); EngineExposed engineExposed = new EngineExposed({ @@ -116,7 +127,7 @@ contract BootstrapOptimism is Setup, OptimismParameters { _spotMarketProxy: SPOT_MARKET_PROXY, _sUSDProxy: USD_PROXY, _oracle: PYTH, - _trustedForwarder: TRUSTED_FORWARDER + _trustedForwarder: address(0x1) }); return ( @@ -125,7 +136,8 @@ contract BootstrapOptimism is Setup, OptimismParameters { PERPS_MARKET_PROXY, SPOT_MARKET_PROXY, USD_PROXY, - PYTH + PYTH, + address(trustedForwarderContract) ); } } @@ -133,14 +145,14 @@ contract BootstrapOptimism is Setup, OptimismParameters { contract BootstrapOptimismGoerli is Setup, OptimismGoerliParameters { function init() public - returns (address, address, address, address, address, address) + returns (address, address, address, address, address, address, address) { - Engine engine = Setup.deploySystem({ + (Engine engine, TrustedMulticallForwarder trustedForwarderContract) = + Setup.deploySystem({ perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, sUSDProxy: USD_PROXY, - oracle: PYTH, - trustedForwarder: TRUSTED_FORWARDER + oracle: PYTH }); EngineExposed engineExposed = new EngineExposed({ @@ -148,7 +160,7 @@ contract BootstrapOptimismGoerli is Setup, OptimismGoerliParameters { _spotMarketProxy: SPOT_MARKET_PROXY, _sUSDProxy: USD_PROXY, _oracle: PYTH, - _trustedForwarder: TRUSTED_FORWARDER + _trustedForwarder: address(0x1) }); return ( @@ -157,7 +169,8 @@ contract BootstrapOptimismGoerli is Setup, OptimismGoerliParameters { PERPS_MARKET_PROXY, SPOT_MARKET_PROXY, USD_PROXY, - PYTH + PYTH, + address(trustedForwarderContract) ); } } diff --git a/test/utils/ConditionalOrderSignature.sol b/test/utils/ConditionalOrderSignature.sol index 282e5cef..82585d10 100644 --- a/test/utils/ConditionalOrderSignature.sol +++ b/test/utils/ConditionalOrderSignature.sol @@ -15,7 +15,7 @@ contract ConditionalOrderSignature { /// @notice pre-computed keccak256(ConditionalOrder struct) bytes32 public constant _CONDITIONAL_ORDER_TYPEHASH = keccak256( - "ConditionalOrder(OrderDetails orderDetails,address signer,uint128 nonce,bool requireVerified,address trustedExecutor,bytes[] conditions)OrderDetails(uint128 marketId,uint128 accountId,int128 sizeDelta,uint128 settlementStrategyId,uint256 acceptablePrice,bool isReduceOnly,bytes32 trackingCode,address referrer)" + "ConditionalOrder(OrderDetails orderDetails,address signer,uint128 nonce,bool requireVerified,address trustedExecutor,uint256 maxExecutorFee,bytes[] conditions)OrderDetails(uint128 marketId,uint128 accountId,int128 sizeDelta,uint128 settlementStrategyId,uint256 acceptablePrice,bool isReduceOnly,bytes32 trackingCode,address referrer)" ); function getConditionalOrderSignatureRaw( diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol index b34a9638..3cdb51c1 100644 --- a/test/utils/Constants.sol +++ b/test/utils/Constants.sol @@ -11,6 +11,18 @@ contract Constants { address internal constant REFERRER = 0xF510a2Ff7e9DD7e18629137adA4eb56B9c13E885; + int128 internal constant SIZE_DELTA = 1 ether; + + int128 internal constant INVALID_SIZE_DELTA = type(int128).max; + + uint256 internal constant ACCEPTABLE_PRICE = type(uint256).max; + + uint256 internal constant INVALID_ACCEPTABLE_PRICE = 0; + + uint128 internal constant SETTLEMENT_STRATEGY_ID = 0; + + uint128 internal constant INVALID_SETTLEMENT_STRATEGY_ID = type(uint128).max; + bytes32 internal constant ADMIN_PERMISSION = "ADMIN"; bytes32 internal constant PERPS_COMMIT_ASYNC_ORDER_PERMISSION = @@ -47,4 +59,8 @@ contract Constants { address internal constant MARKET_CONFIGURATION_MODULE = 0xE3b87A4c0E5F77504D6fa7656Cd8Caf2Ef331162; + + uint256 internal constant ZERO_CO_FEE = 0; + + uint256 internal constant CO_FEE = 100 wei; } diff --git a/test/utils/analysis/ConditionalOrderFees.sol b/test/utils/analysis/ConditionalOrderFees.sol deleted file mode 100644 index 1bf50107..00000000 --- a/test/utils/analysis/ConditionalOrderFees.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.18; - -import {Bootstrap, console2} from "test/utils/Bootstrap.sol"; - -contract ConditionalOrderTest is Bootstrap { - function setUp() public { - vm.rollFork(GOERLI_BLOCK_NUMBER); - initializeOptimismGoerli(); - } - - function plot_chart() public view { - for (uint256 i = 1; i <= 700; i++) { - uint256 sizeDelta = i * 1e17; - (uint256 orderFees,) = perpsMarketProxy.computeOrderFees({ - marketId: SETH_PERPS_MARKET_ID, - sizeDelta: int128(int256(sizeDelta)) - }); - - uint256 conditional_order_fee = - orderFees * engineExposed.expose_FEE_SCALING_FACTOR() / 10_000; - - if (conditional_order_fee < engineExposed.expose_LOWER_FEE_CAP()) { - console2.log("%s,", engineExposed.expose_LOWER_FEE_CAP() / 1e18); - } else if ( - conditional_order_fee > engineExposed.expose_UPPER_FEE_CAP() - ) { - console2.log("%s,", engineExposed.expose_UPPER_FEE_CAP() / 1e18); - } else { - console2.log("%s,", conditional_order_fee / 1e18); - } - } - } -} diff --git a/test/utils/errors/SynthetixV3Errors.sol b/test/utils/errors/SynthetixV3Errors.sol index ccc035b8..abbcf7c2 100644 --- a/test/utils/errors/SynthetixV3Errors.sol +++ b/test/utils/errors/SynthetixV3Errors.sol @@ -12,7 +12,7 @@ contract SynthetixV3Errors { bytes extraData ); error InvalidVerificationResponse(); - error InvalidSettlementStrategy(SettlementStrategyType strategyType); + error InvalidSettlementStrategy(uint128 settlementStrategyId); error MinimumSettlementAmountNotMet(uint256 minimum, uint256 actual); error SettlementStrategyNotFound(SettlementStrategyType strategyType); error InvalidFeeCollectorInterface(address invalidFeeCollector); diff --git a/test/utils/exposed/EngineExposed.sol b/test/utils/exposed/EngineExposed.sol index ae72e387..9cd52328 100644 --- a/test/utils/exposed/EngineExposed.sol +++ b/test/utils/exposed/EngineExposed.sol @@ -29,20 +29,4 @@ contract EngineExposed is Engine { { return _getSynthAddress(synthMarketId); } - - function expose_UPPER_FEE_CAP() public pure returns (uint256) { - return UPPER_FEE_CAP; - } - - function expose_LOWER_FEE_CAP() public pure returns (uint256) { - return LOWER_FEE_CAP; - } - - function expose_FEE_SCALING_FACTOR() public pure returns (uint256) { - return FEE_SCALING_FACTOR; - } - - function expose_MAX_BPS() public pure returns (uint256) { - return MAX_BPS; - } } diff --git a/test/utils/mocks/SynthetixMock.sol b/test/utils/mocks/SynthetixMock.sol index e8cf00f2..ebf458de 100644 --- a/test/utils/mocks/SynthetixMock.sol +++ b/test/utils/mocks/SynthetixMock.sol @@ -63,4 +63,18 @@ contract SynthetixMock is Test { abi.encode() ); } + + function mock_getAccountOwner( + address perpsMarketProxy, + uint128 accountId, + address owner + ) public { + vm.mockCall( + perpsMarketProxy, + abi.encodeWithSelector( + IPerpsMarketProxy.getAccountOwner.selector, accountId + ), + abi.encode(owner) + ); + } }