diff --git a/.gas-snapshot b/.gas-snapshot index 92d56bdd..9530acf6 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,110 +1,99 @@ -AccountDelegate:test_isAccountDelegate_account_doesnt_exist() (gas: 26514) -AccountDelegate:test_isAccountDelegate_false() (gas: 391969) -AccountDelegate:test_isAccountDelegate_true() (gas: 389938) -AccountOwner:test_isAccountOwner_account_doesnt_exist() (gas: 25010) -AccountOwner:test_isAccountOwner_false() (gas: 229790) -AccountOwner:test_isAccountOwner_true() (gas: 229780) -CanExecute:test_canExecute_false_insufficent_account_credit() (gas: 414871) -CanExecute:test_canExecute_false_invalid_signature() (gas: 448904) -CanExecute:test_canExecute_false_invalid_signer() (gas: 442063) -CanExecute:test_canExecute_false_maxExecutorFee_exceeded() (gas: 397562) -CanExecute:test_canExecute_false_nonce_used() (gas: 826365) -CanExecute:test_canExecute_false_trusted_executor() (gas: 438807) -CanExecute:test_canExecute_true() (gas: 438401) -CommitOrder:test_commitOrder() (gas: 382994) -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: 18762) -Conditions:test_isPriceAbove() (gas: 19098) -Conditions:test_isPriceBelow() (gas: 19092) -Conditions:test_isTimestampAfter() (gas: 7711) -Conditions:test_isTimestampBefore() (gas: 7579) -DeploymentTest:test_deploy() (gas: 4890031) -DeploymentTest:test_deploy_oracle_zero_address() (gas: 1903033) -DeploymentTest:test_deploy_perps_market_proxy_zero_address() (gas: 1902949) -DeploymentTest:test_deploy_spot_market_proxy_zero_address() (gas: 1903016) -DeploymentTest:test_deploy_susd_proxy_zero_address() (gas: 1903029) -DeploymentTest:test_deploy_trusted_forwarder_zero_address() (gas: 37562) -Deposit:test_depositEth() (gas: 55255) -Deposit:test_depositEth_Account_Doesnt_Exist() (gas: 34271) -Deposit:test_depositEth_event() (gas: 56815) -Deposit:test_depositEth_fuzz(uint256,uint128) (runs: 256, μ: 36765, ~: 34889) -Deposit:test_depositEth_via_trustedForwarder() (gas: 85374) -Deposit:test_depositEth_via_trustedForwarder_value_mismatch() (gas: 87469) -Deposit:test_depositEth_via_trustedForwarder_value_mismatch_require_success() (gas: 86713) -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, μ: 149614, ~: 149516) -Execute:test_execute_CannotExecuteOrder_invalid_acceptablePrice() (gas: 330951) -Execute:test_execute_CannotExecuteOrder_invalid_settlementStrategyId() (gas: 96827) -Execute:test_execute_CannotExecuteOrder_too_leveraged() (gas: 361524) -Execute:test_execute_event() (gas: 433933) -Execute:test_execute_order_committed() (gas: 430425) -ExecuteBatch:test_executeBatch() (gas: 95256) -ExecuteBatch:test_executeBatch_invalid_signature() (gas: 48173) -Fee:test_fee_exceeds_account_credit() (gas: 67907) -Fee:test_fee_exceeds_maxExecutorFee() (gas: 67486) -Fee:test_fee_imposed() (gas: 468614) -MathLibTest:test_abs128() (gas: 448) +AccountDelegate:test_isAccountDelegate_account_doesnt_exist() (gas: 26492) +AccountDelegate:test_isAccountDelegate_false() (gas: 392017) +AccountDelegate:test_isAccountDelegate_true() (gas: 389964) +AccountDelegate:test_isAccountDelegate_zero_address_caller() (gas: 23181) +AccountOwner:test_isAccountOwner_account_doesnt_exist() (gas: 25044) +AccountOwner:test_isAccountOwner_false() (gas: 229891) +AccountOwner:test_isAccountOwner_true() (gas: 229814) +AccountOwner:test_isAccountOwner_zero_address_caller() (gas: 10785) +CanExecute:test_canExecute_false_insufficent_account_credit() (gas: 414884) +CanExecute:test_canExecute_false_invalid_signature() (gas: 446810) +CanExecute:test_canExecute_false_invalid_signer() (gas: 442163) +CanExecute:test_canExecute_false_maxExecutorFee_exceeded() (gas: 397594) +CanExecute:test_canExecute_false_nonce_used() (gas: 825780) +CanExecute:test_canExecute_false_require_verify_condition_not_met() (gas: 532786) +CanExecute:test_canExecute_false_trusted_executor() (gas: 439360) +CanExecute:test_canExecute_true() (gas: 438999) +CommitOrder:test_commitOrder() (gas: 382992) +CommitOrder:test_commitOrder_Unauthorized() (gas: 33029) +CommitOrder:test_commitOrder_insufficient_collateral() (gas: 441149) +CommitOrder:test_commitOrder_invalid_market() (gas: 38262) +Conditions:test_isMarketOpen() (gas: 26596) +Conditions:test_isOrderFeeBelow() (gas: 179828) +Conditions:test_isPositionSizeAbove() (gas: 18893) +Conditions:test_isPositionSizeBelow() (gas: 18863) +Conditions:test_isPriceAbove() (gas: 196473) +Conditions:test_isPriceBelow() (gas: 196438) +Conditions:test_isTimestampAfter() (gas: 7668) +Conditions:test_isTimestampBefore() (gas: 7645) +Credit:test_credit(uint256) (runs: 256, μ: 103044, ~: 112702) +Credit:test_credit_AccountDoesNotExist() (gas: 28011) +Credit:test_credit_event() (gas: 97230) +Debit:test_debit(uint256) (runs: 256, μ: 132589, ~: 142379) +Debit:test_debit_InsufficientBalance() (gas: 119995) +Debit:test_debit_Unauthorized() (gas: 120025) +Debit:test_debit_event() (gas: 102161) +DeploymentTest:test_deploy() (gas: 3195810) +DeploymentTest:test_deploy_perps_market_proxy_zero_address() (gas: 42676) +DeploymentTest:test_deploy_spot_market_proxy_zero_address() (gas: 42743) +DeploymentTest:test_deploy_susd_proxy_zero_address() (gas: 42734) +DepositCollateral:test_depositCollateral() (gas: 258395) +DepositCollateral:test_depositCollateral_availableMargin() (gas: 265963) +DepositCollateral:test_depositCollateral_collateralAmount() (gas: 259016) +DepositCollateral:test_depositCollateral_insufficient_balance() (gas: 55978) +DepositCollateral:test_depositCollateral_totalCollateralValue() (gas: 263325) +Execute:test_execute_CannotExecuteOrder_invalid_acceptablePrice() (gas: 330290) +Execute:test_execute_CannotExecuteOrder_invalid_settlementStrategyId() (gas: 96211) +Execute:test_execute_CannotExecuteOrder_too_leveraged() (gas: 360885) +Execute:test_execute_event() (gas: 433339) +Execute:test_execute_order_committed() (gas: 429786) +Fee:test_fee_exceeds_account_credit() (gas: 133066) +Fee:test_fee_exceeds_maxExecutorFee() (gas: 132599) +Fee:test_fee_imposed() (gas: 519317) +FulfillOracleQuery:test_fulfillOracleQuery(bytes) (runs: 256, μ: 26404, ~: 26400) +FulfillOracleQuery:test_fulfillOracleQuery_refund(bytes) (runs: 256, μ: 35041, ~: 35037) +FulfillOracleQuery:test_fulfillOracleQuery_revert(bytes) (runs: 256, μ: 28105, ~: 28101) +MathLibTest:test_abs128() (gas: 425) MathLibTest:test_abs256() (gas: 480) -MathLibTest:test_castU128() (gas: 350) -MathLibTest:test_castU128_overflow() (gas: 3509) MathLibTest:test_fuzz_abs128(int128) (runs: 256, μ: 577, ~: 603) MathLibTest:test_fuzz_abs256(int256) (runs: 256, μ: 472, ~: 458) MathLibTest:test_isSameSign() (gas: 999) -NonceBitmapTest:test_fuzz_invalidateUnorderedNonces(uint256) (runs: 256, μ: 52770, ~: 52770) -NonceBitmapTest:test_hasUnorderedNonceBeenUsed() (gas: 54126) -NonceBitmapTest:test_invalidateUnorderedNonces() (gas: 76509) -NonceBitmapTest:test_invalidateUnorderedNonces_Only_Owner_Delegate() (gas: 190324) -NonceBitmapTest:test_invalidateUnorderedNonces_event() (gas: 53464) -ReduceOnly:test_reduce_only() (gas: 432125) -ReduceOnly:test_reduce_only_same_sign() (gas: 72481) -ReduceOnly:test_reduce_only_truncate_size_down() (gas: 432232) -ReduceOnly:test_reduce_only_truncate_size_up() (gas: 408894) -ReduceOnly:test_reduce_only_zero_size() (gas: 162976) -TrustedMulticallForwarderTest:testAggregate3() (gas: 24779) -TrustedMulticallForwarderTest:testAggregate3Unsuccessful() (gas: 21395) -TrustedMulticallForwarderTest:testAggregate3Value() (gas: 49261) -TrustedMulticallForwarderTest:testAggregate3ValueUnsuccessful() (gas: 87877) -TrustedMulticallForwarderTest:testAggregation() (gas: 15186) -TrustedMulticallForwarderTest:testBlockAndAggregateUnsuccessful() (gas: 21080) -TrustedMulticallForwarderTest:testGetBasefee() (gas: 5486) -TrustedMulticallForwarderTest:testGetBlockHash(uint256) (runs: 256, μ: 5674, ~: 5674) -TrustedMulticallForwarderTest:testGetBlockNumber() (gas: 5487) -TrustedMulticallForwarderTest:testGetChainId() (gas: 5486) -TrustedMulticallForwarderTest:testGetCurrentBlockCoinbase() (gas: 5579) -TrustedMulticallForwarderTest:testGetCurrentBlockGasLimit() (gas: 5463) -TrustedMulticallForwarderTest:testGetCurrentBlockTimestamp() (gas: 5433) -TrustedMulticallForwarderTest:testGetEthBalance(address) (runs: 256, μ: 8421, ~: 8451) -TrustedMulticallForwarderTest:testGetLastBlockHash() (gas: 5670) -TrustedMulticallForwarderTest:testTryAggregate() (gas: 19577) -TrustedMulticallForwarderTest:testTryAggregateUnsuccessful() (gas: 21084) -TrustedMulticallForwarderTest:testTryBlockAndAggregate() (gas: 19879) -TrustedMulticallForwarderTest:testTryBlockAndAggregateUnsuccessful() (gas: 21185) -TrustedMulticallForwarderTest:testUnsuccessfulAggregation() (gas: 20818) -VerifyConditions:test_max_condition_size_exceeded() (gas: 45101) -VerifyConditions:test_verifyConditions_InvalidConditionSelector() (gas: 14132) -VerifyConditions:test_verify_conditions_not_verified() (gas: 29707) -VerifyConditions:test_verify_conditions_verified() (gas: 135835) -VerifySignature:test_verifySignature(uint256) (runs: 256, μ: 24402, ~: 24402) -VerifySignature:test_verifySignature_false_private_key() (gas: 27079) -VerifySigner:test_verifySigner() (gas: 25862) -VerifySigner:test_verifySigner_false() (gas: 28614) -Withdraw:test_withdrawEth() (gas: 52547) -Withdraw:test_withdrawEth_EthTransferFailed() (gas: 93091) -Withdraw:test_withdrawEth_InsufficientEthBalance() (gas: 59781) -Withdraw:test_withdrawEth_Unauthorized() (gas: 59420) -Withdraw:test_withdrawEth_event() (gas: 51791) -Withdraw:test_withdrawEth_fuzz(uint256) (runs: 256, μ: 70862, ~: 71511) -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 +MulticallFulfillOracleQuery:test_fulfillOracleQuery_multicall(bytes) (runs: 256, μ: 29315, ~: 29258) +MulticallFulfillOracleQuery:test_fulfillOracleQuery_multicall_double_spend(bytes) (runs: 256, μ: 41932, ~: 41822) +MulticallablePayableTest:testMulticallableBenchmark() (gas: 29229) +MulticallablePayableTest:testMulticallableOriginalBenchmark() (gas: 38407) +MulticallablePayableTest:testMulticallablePreservesMsgSender() (gas: 11065) +MulticallablePayableTest:testMulticallableReturnDataIsProperlyEncoded() (gas: 11548) +MulticallablePayableTest:testMulticallableReturnDataIsProperlyEncoded(string,string,uint256) (runs: 256, μ: 9837, ~: 11626) +MulticallablePayableTest:testMulticallableReturnDataIsProperlyEncoded(uint256,uint256,uint256,uint256) (runs: 256, μ: 11694, ~: 11694) +MulticallablePayableTest:testMulticallableRevertWithCustomError() (gas: 11767) +MulticallablePayableTest:testMulticallableRevertWithMessage() (gas: 13406) +MulticallablePayableTest:testMulticallableRevertWithMessage(string) (runs: 256, μ: 14013, ~: 13861) +MulticallablePayableTest:testMulticallableRevertWithNothing() (gas: 11633) +MulticallablePayableTest:testMulticallableWithNoData() (gas: 6266) +NonceBitmapTest:test_fuzz_invalidateUnorderedNonces(uint256) (runs: 256, μ: 52689, ~: 52689) +NonceBitmapTest:test_hasUnorderedNonceBeenUsed() (gas: 53955) +NonceBitmapTest:test_invalidateUnorderedNonces() (gas: 77151) +NonceBitmapTest:test_invalidateUnorderedNonces_Only_Owner_Delegate() (gas: 190184) +NonceBitmapTest:test_invalidateUnorderedNonces_Unauthorized() (gas: 30607) +NonceBitmapTest:test_invalidateUnorderedNonces_event() (gas: 53396) +ReduceOnly:test_reduce_only() (gas: 431521) +ReduceOnly:test_reduce_only_same_sign() (gas: 72722) +ReduceOnly:test_reduce_only_truncate_size_down() (gas: 431607) +ReduceOnly:test_reduce_only_truncate_size_up() (gas: 408356) +ReduceOnly:test_reduce_only_when_position_doesnt_exist() (gas: 163109) +ReduceOnly:test_reduce_only_zero_size_delta() (gas: 163166) +VerifyConditions:test_max_condition_size_exceeded() (gas: 45057) +VerifyConditions:test_verifyConditions_InvalidConditionSelector() (gas: 14089) +VerifyConditions:test_verify_conditions_not_verified() (gas: 129563) +VerifyConditions:test_verify_conditions_verified() (gas: 184166) +VerifySignature:test_verifySignature(uint256) (runs: 256, μ: 25033, ~: 25033) +VerifySignature:test_verifySignature_false_private_key() (gas: 25009) +VerifySigner:test_verifySigner() (gas: 25917) +VerifySigner:test_verifySigner_false() (gas: 28647) +WithdrawCollateral:test_withdrawCollateral() (gas: 353029) +WithdrawCollateral:test_withdrawCollateral_availableMargin() (gas: 354583) +WithdrawCollateral:test_withdrawCollateral_collateralAmount() (gas: 353557) +WithdrawCollateral:test_withdrawCollateral_insufficient_account_collateral_balance() (gas: 274007) +WithdrawCollateral:test_withdrawCollateral_totalCollateralValue() (gas: 354046) +WithdrawCollateral:test_withdrawCollateral_zero() (gas: 265965) \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index e80ffd88..888d42dc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts - url = https://github.com/openzeppelin/openzeppelin-contracts diff --git a/README.md b/README.md index d7e9fead..5f6ffd9c 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,6 @@ src/ ├── Engine.sol ├── interfaces │ ├── IEngine.sol -│ ├── oracles -│ │ └── IPyth.sol │ ├── synthetix │ │ ├── IERC7412.sol │ │ ├── IPerpsMarketProxy.sol @@ -39,7 +37,7 @@ src/ └── utils ├── EIP712.sol ├── EIP7412.sol - └── TrustedMulticallForwarder.sol + └── Multicallable.sol ``` ## Tests @@ -81,7 +79,7 @@ npm run decode-custom-error -- > project must be compiled first (see step 2) ``` -npx hardhat test +npm run test:hh ``` ## Deployment Addresses diff --git a/deployments/Base.json b/deployments/Base.json index f2f97b1c..7dd9b5af 100644 --- a/deployments/Base.json +++ b/deployments/Base.json @@ -1,14 +1,11 @@ { "Synthetix": { - "Engine": "", - "TrustedMulticallForwarder": "" + "Engine": "" }, "Andromeda": { - "Engine": "", - "TrustedMulticallForwarder": "" + "Engine": "" }, "Kwenta": { - "Engine": "", - "TrustedMulticallForwarder": "" + "Engine": "" } } \ No newline at end of file diff --git a/deployments/BaseGoerli.json b/deployments/BaseGoerli.json index c45d9fe5..76824e1b 100644 --- a/deployments/BaseGoerli.json +++ b/deployments/BaseGoerli.json @@ -1,14 +1,11 @@ { "Synthetix": { - "Engine": "0x500A139459fA3628C416A6b19BFADd83B20e5D0b", - "TrustedMulticallForwarder": "0x14aE2D8fA531A9e77aE434d2d700218C2845Bc83" + "Engine": "" }, "Andromeda": { - "Engine": "0x3617154844291712cBD2148D912b61d6641229a4", - "TrustedMulticallForwarder": "0xb5dCFb08a2CB07399b75B650B980732340c5Ed90" + "Engine": "" }, "Kwenta": { - "Engine": "0x0b5456EB6eE169C533a931aD2a420237ADf3Da49", - "TrustedMulticallForwarder": "0xe68Faa95D54bf335E67128c4528A551eaB3AF9fE" + "Engine": "" } } diff --git a/deployments/Optimism.json b/deployments/Optimism.json index f2f97b1c..7dd9b5af 100644 --- a/deployments/Optimism.json +++ b/deployments/Optimism.json @@ -1,14 +1,11 @@ { "Synthetix": { - "Engine": "", - "TrustedMulticallForwarder": "" + "Engine": "" }, "Andromeda": { - "Engine": "", - "TrustedMulticallForwarder": "" + "Engine": "" }, "Kwenta": { - "Engine": "", - "TrustedMulticallForwarder": "" + "Engine": "" } } \ No newline at end of file diff --git a/deployments/OptimismGoerli.json b/deployments/OptimismGoerli.json index 179fcea1..7dd9b5af 100644 --- a/deployments/OptimismGoerli.json +++ b/deployments/OptimismGoerli.json @@ -1,14 +1,11 @@ { "Synthetix": { - "Engine": "0x06198fCE58194C0cB7AB420D6da791789cE01F9d", - "TrustedMulticallForwarder": "0x9Fb8Fa76d34ad7b03A945f8335D3Ab24637091d6" + "Engine": "" }, "Andromeda": { - "Engine": "", - "TrustedMulticallForwarder": "" + "Engine": "" }, "Kwenta": { - "Engine": "", - "TrustedMulticallForwarder": "" + "Engine": "" } } \ No newline at end of file diff --git a/lcov.info b/lcov.info index bab7ec11..0123be33 100644 --- a/lcov.info +++ b/lcov.info @@ -1,273 +1,272 @@ TN: SF:script/Deploy.s.sol -FN:133,DeployOptimism_Synthetix.run +FN:24,Setup.deploySystem +FNDA:4,Setup.deploySystem +DA:29,4 +FN:116,DeployOptimism_Synthetix.run FNDA:0,DeployOptimism_Synthetix.run -DA:134,0 +DA:117,0 +DA:118,0 +DA:120,0 +DA:126,0 +FN:98,DeployBaseGoerli_Andromeda.run +FNDA:0,DeployBaseGoerli_Andromeda.run +DA:99,0 +DA:100,0 +DA:102,0 +DA:108,0 +FN:59,DeployBaseGoerli_Synthetix.run +FNDA:0,DeployBaseGoerli_Synthetix.run +DA:60,0 +DA:61,0 +DA:63,0 +DA:69,0 +FN:41,DeployBase_Synthetix.run +FNDA:0,DeployBase_Synthetix.run +DA:42,0 +DA:43,0 +DA:45,0 +DA:51,0 +FN:134,DeployOptimismGoerli_Synthetix.run +FNDA:0,DeployOptimismGoerli_Synthetix.run DA:135,0 -DA:137,0 +DA:136,0 +DA:138,0 DA:144,0 -FN:73,DeployBaseGoerli_Synthetix.run -FNDA:0,DeployBaseGoerli_Synthetix.run -DA:74,0 -DA:75,0 -DA:77,0 -DA:84,0 -FN:95,DeployBaseGoerli_KwentaFork.run +FN:80,DeployBaseGoerli_KwentaFork.run FNDA:0,DeployBaseGoerli_KwentaFork.run -DA:96,0 -DA:97,0 -DA:99,0 -DA:106,0 -FN:26,Setup.deploySystem -FNDA:5,Setup.deploySystem -DA:38,5 -DA:40,5 -FN:152,DeployOptimismGoerli_Synthetix.run -FNDA:0,DeployOptimismGoerli_Synthetix.run -DA:153,0 -DA:154,0 -DA:156,0 -DA:163,0 -FN:54,DeployBase_Synthetix.run -FNDA:0,DeployBase_Synthetix.run -DA:55,0 -DA:56,0 -DA:58,0 -DA:65,0 -FN:114,DeployBaseGoerli_Andromeda.run -FNDA:0,DeployBaseGoerli_Andromeda.run -DA:115,0 -DA:116,0 -DA:118,0 -DA:125,0 +DA:81,0 +DA:82,0 +DA:84,0 +DA:90,0 FNF:7 FNH:1 -LF:26 -LH:2 +LF:25 +LH:1 BRF:0 BRH:0 end_of_record TN: SF:src/Engine.sol -FN:126,Engine.isAccountOwner -FNDA:3,Engine.isAccountOwner -DA:132,270 -FN:136,Engine.isAccountDelegate -FNDA:2,Engine.isAccountDelegate -DA:142,2 -FN:147,Engine._isAccountOwnerOrDelegate -FNDA:283,Engine._isAccountOwnerOrDelegate -DA:152,283 -FN:162,Engine.depositEth -FNDA:527,Engine.depositEth -DA:165,527 -BRDA:165,0,0,239 -BRDA:165,0,1,288 -DA:166,239 -DA:169,288 -DA:171,288 -FN:175,Engine.withdrawEth -FNDA:261,Engine.withdrawEth -DA:179,261 -DA:181,261 -BRDA:181,1,0,1 -BRDA:181,1,1,260 -DA:183,260 -DA:185,258 -FN:192,Engine._withdrawEth -FNDA:272,Engine._withdrawEth -DA:195,272 -BRDA:195,2,0,1 -BRDA:195,2,1,271 -DA:198,271 -DA:200,271 -DA:202,271 -BRDA:202,3,0,1 -BRDA:202,3,1,270 -FN:210,Engine.invalidateUnorderedNonces -FNDA:261,Engine.invalidateUnorderedNonces -DA:215,261 -BRDA:215,4,0,261 -BRDA:215,4,1,- -DA:221,261 -DA:223,261 -DA:225,0 -FN:230,Engine.hasUnorderedNonceBeenUsed +FN:95,Engine.isAccountOwner +FNDA:4,Engine.isAccountOwner +DA:101,269 +DA:102,268 +FN:106,Engine.isAccountDelegate +FNDA:3,Engine.isAccountDelegate +DA:112,3 +FN:117,Engine._isAccountOwnerOrDelegate +FNDA:287,Engine._isAccountOwnerOrDelegate +DA:122,287 +DA:123,287 +FN:133,Engine.invalidateUnorderedNonces +FNDA:262,Engine.invalidateUnorderedNonces +DA:138,262 +BRDA:138,0,0,261 +BRDA:138,0,1,1 +DA:144,261 +DA:146,261 +DA:148,1 +FN:153,Engine.hasUnorderedNonceBeenUsed FNDA:260,Engine.hasUnorderedNonceBeenUsed -DA:236,279 -DA:240,279 -DA:254,279 -FN:264,Engine._bitmapPositions -FNDA:291,Engine._bitmapPositions -DA:271,291 -DA:275,291 -FN:281,Engine._useUnorderedNonce -FNDA:12,Engine._useUnorderedNonce -DA:282,12 -DA:286,12 -DA:293,12 -DA:313,12 -BRDA:313,5,0,- -BRDA:313,5,1,12 -FN:321,Engine.modifyCollateral +DA:159,281 +DA:163,281 +DA:177,281 +FN:187,Engine._bitmapPositions +FNDA:294,Engine._bitmapPositions +DA:194,294 +DA:198,294 +FN:204,Engine._useUnorderedNonce +FNDA:13,Engine._useUnorderedNonce +DA:205,13 +DA:209,13 +DA:216,13 +DA:236,13 +BRDA:236,1,0,- +BRDA:236,1,1,13 +FN:244,Engine.modifyCollateral FNDA:17,Engine.modifyCollateral -DA:326,17 -DA:328,17 -DA:330,17 -BRDA:330,6,0,11 -BRDA:330,6,1,6 -DA:331,11 -DA:335,6 -BRDA:335,7,0,- -BRDA:335,7,1,6 -DA:336,6 -FN:342,Engine._depositCollateral +DA:249,17 +DA:251,17 +BRDA:251,2,0,11 +BRDA:251,2,1,6 +DA:252,11 +DA:256,6 +BRDA:256,3,0,- +BRDA:256,3,1,6 +DA:258,6 +FN:264,Engine._depositCollateral FNDA:11,Engine._depositCollateral -DA:350,11 -DA:352,10 -DA:354,10 -FN:357,Engine._withdrawCollateral +DA:272,11 +DA:274,10 +DA:276,10 +FN:279,Engine._withdrawCollateral FNDA:6,Engine._withdrawCollateral -DA:364,6 -DA:367,4 -FN:373,Engine._getSynthAddress +DA:286,6 +DA:289,4 +FN:295,Engine._getSynthAddress FNDA:17,Engine._getSynthAddress -DA:378,17 -FN:388,Engine.commitOrder -FNDA:3,Engine.commitOrder -DA:402,3 -BRDA:402,8,0,3 -BRDA:402,8,1,- -DA:403,3 -DA:413,0 -FN:417,Engine._commitOrder +DA:300,17 +FN:310,Engine.commitOrder +FNDA:4,Engine.commitOrder +DA:324,4 +BRDA:324,4,0,3 +BRDA:324,4,1,1 +DA:325,3 +DA:335,1 +FN:339,Engine._commitOrder FNDA:13,Engine._commitOrder -DA:426,13 -FN:444,Engine.execute -FNDA:14,Engine.execute -DA:459,14 -BRDA:459,9,0,2 -BRDA:459,9,1,12 -DA:462,12 -DA:468,12 -DA:472,12 -DA:475,12 -BRDA:475,10,0,2 -BRDA:475,10,1,3 -DA:476,5 -DA:481,5 -BRDA:481,11,0,1 -BRDA:481,11,1,4 -DA:482,1 -DA:486,4 -BRDA:486,12,0,1 -BRDA:486,12,1,3 -DA:487,1 -DA:493,3 -BRDA:493,13,0,2 -BRDA:493,13,1,3 -DA:503,2 -DA:508,10 -DA:518,7 -FN:526,Engine.canExecute -FNDA:9,Engine.canExecute -DA:532,23 -BRDA:532,14,0,2 -BRDA:532,14,1,21 -DA:535,21 -BRDA:535,15,0,2 -BRDA:535,15,1,19 -DA:538,19 -BRDA:538,16,0,2 -BRDA:538,16,1,17 -DA:539,2 -DA:543,17 -BRDA:543,17,0,1 -BRDA:543,17,1,16 -DA:546,16 -BRDA:546,18,0,1 -BRDA:546,18,1,15 -DA:549,15 -BRDA:549,19,0,- -BRDA:549,19,1,- -DA:552,0 -BRDA:552,20,0,- -BRDA:552,20,1,- -DA:556,15 -BRDA:556,21,0,1 -BRDA:556,21,1,14 -DA:559,14 -FN:567,Engine.verifySigner +DA:348,13 +FN:366,Engine.creditAccount +FNDA:520,Engine.creditAccount +DA:372,520 +BRDA:372,5,0,1 +BRDA:372,5,1,519 +DA:373,1 +DA:376,519 +DA:379,519 +DA:381,415 +FN:385,Engine.debitAccount +FNDA:259,Engine.debitAccount +DA:389,259 +BRDA:389,6,0,1 +BRDA:389,6,1,258 +DA:391,258 +DA:393,141 +FN:400,Engine._debit +FNDA:259,Engine._debit +DA:403,259 +BRDA:403,7,0,106 +BRDA:403,7,1,153 +DA:406,153 +DA:409,153 +FN:417,Engine.execute +FNDA:15,Engine.execute +DA:432,15 +BRDA:432,8,0,2 +BRDA:432,8,1,13 +DA:435,13 +DA:441,13 +BRDA:441,9,0,1 +BRDA:441,9,1,13 +DA:444,13 +DA:447,13 +BRDA:447,10,0,2 +BRDA:447,10,1,3 +DA:448,6 +DA:453,6 +BRDA:453,11,0,2 +BRDA:453,11,1,4 +DA:454,2 +DA:459,4 +BRDA:459,12,0,1 +BRDA:459,12,1,3 +DA:460,1 +DA:466,3 +BRDA:466,13,0,2 +BRDA:466,13,1,3 +DA:476,2 +DA:481,10 +DA:491,7 +FN:499,Engine.canExecute +FNDA:10,Engine.canExecute +DA:505,25 +BRDA:505,14,0,2 +BRDA:505,14,1,23 +DA:508,23 +BRDA:508,15,0,2 +BRDA:508,15,1,21 +DA:511,21 +BRDA:511,16,0,2 +BRDA:511,16,1,19 +DA:512,2 +DA:516,19 +BRDA:516,17,0,1 +BRDA:516,17,1,18 +DA:519,18 +BRDA:519,18,0,1 +BRDA:519,18,1,17 +DA:522,17 +BRDA:522,19,0,1 +BRDA:522,19,1,- +DA:525,1 +BRDA:525,20,0,1 +BRDA:525,20,1,- +DA:529,16 +BRDA:529,21,0,1 +BRDA:529,21,1,15 +DA:532,15 +FN:540,Engine.verifySigner FNDA:2,Engine.verifySigner -DA:573,19 -FN:577,Engine.verifySignature +DA:546,21 +FN:550,Engine.verifySignature FNDA:257,Engine.verifySignature -DA:581,273 -FN:587,Engine.verifyConditions +DA:554,275 +FN:558,Engine.verifyConditions FNDA:4,Engine.verifyConditions -DA:593,4 -DA:594,4 -BRDA:594,22,0,1 -BRDA:594,22,1,3 -DA:595,1 -DA:598,3 -DA:599,13 -DA:600,13 -DA:603,13 -DA:608,13 -DA:609,11 -DA:610,9 -DA:611,7 -DA:612,5 -DA:613,4 -DA:614,3 -DA:615,2 -BRDA:607,23,0,1 -BRDA:607,23,1,11 -DA:618,12 -DA:621,12 -BRDA:621,24,0,1 -BRDA:621,24,1,11 -DA:624,11 -DA:627,1 -DA:631,1 -FN:639,Engine.isTimestampAfter -FNDA:5,Engine.isTimestampAfter -DA:645,5 -FN:649,Engine.isTimestampBefore +DA:564,5 +DA:565,5 +BRDA:565,22,0,1 +BRDA:565,22,1,4 +DA:566,1 +DA:569,4 +DA:570,14 +DA:571,14 +DA:574,14 +DA:579,14 +DA:580,12 +DA:581,10 +DA:582,7 +DA:583,5 +DA:584,4 +DA:585,3 +DA:586,2 +BRDA:578,23,0,2 +BRDA:578,23,1,11 +DA:589,13 +DA:592,13 +BRDA:592,24,0,2 +BRDA:592,24,1,11 +DA:595,11 +DA:598,1 +DA:602,1 +FN:610,Engine.isTimestampAfter +FNDA:6,Engine.isTimestampAfter +DA:616,6 +FN:620,Engine.isTimestampBefore FNDA:5,Engine.isTimestampBefore -DA:655,5 -FN:659,Engine.isPriceAbove +DA:626,5 +FN:630,Engine.isPriceAbove FNDA:6,Engine.isPriceAbove -DA:664,6 -DA:669,6 -FN:673,Engine.isPriceBelow +DA:636,6 +DA:641,6 +FN:645,Engine.isPriceBelow FNDA:6,Engine.isPriceBelow -DA:678,6 -DA:683,6 -FN:687,Engine.isMarketOpen +DA:651,6 +DA:656,6 +FN:660,Engine.isMarketOpen FNDA:3,Engine.isMarketOpen -DA:693,3 -FN:697,Engine.isPositionSizeAbove +DA:666,3 +FN:670,Engine.isPositionSizeAbove FNDA:4,Engine.isPositionSizeAbove -DA:702,4 -DA:703,4 -DA:705,4 -FN:709,Engine.isPositionSizeBelow +DA:675,4 +DA:676,4 +DA:678,4 +FN:682,Engine.isPositionSizeBelow FNDA:4,Engine.isPositionSizeBelow -DA:714,4 -DA:715,4 -DA:717,4 -FN:721,Engine.isOrderFeeBelow +DA:687,4 +DA:688,4 +DA:690,4 +FN:694,Engine.isOrderFeeBelow FNDA:4,Engine.isOrderFeeBelow -DA:727,4 -DA:732,4 +DA:700,4 +DA:705,4 FNF:29 FNH:29 LF:105 -LH:102 +LH:105 BRF:50 -BRH:42 +BRH:46 end_of_record TN: SF:src/libraries/ConditionalOrderHashLib.sol @@ -296,35 +295,75 @@ DA:23,0 FN:30,MathLib.abs256 FNDA:0,MathLib.abs256 DA:44,0 -FN:52,MathLib.castU128 -FNDA:0,MathLib.castU128 -DA:53,0 -BRDA:53,0,0,- -BRDA:53,0,1,- -DA:54,0 -DA:56,0 -FN:64,MathLib.isSameSign +FN:53,MathLib.isSameSign FNDA:0,MathLib.isSameSign -DA:65,0 -BRDA:65,1,0,- -BRDA:65,1,1,- -DA:66,0 -FNF:4 +DA:54,0 +BRDA:54,0,0,- +BRDA:54,0,1,- +DA:55,0 +FNF:3 FNH:0 -LF:7 +LF:4 LH:0 -BRF:4 +BRF:2 BRH:0 end_of_record TN: SF:src/libraries/SignatureCheckerLib.sol -FN:22,SignatureCheckerLib.isValidSignatureNowCalldata -FNDA:0,SignatureCheckerLib.isValidSignatureNowCalldata -FNF:1 +FN:46,SignatureCheckerLib.tryRecover +FNDA:0,SignatureCheckerLib.tryRecover +DA:51,0 +BRDA:51,0,0,- +BRDA:51,0,1,- +DA:52,0 +DA:53,0 +DA:54,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:63,0 +DA:65,0 +FN:85,SignatureCheckerLib.recover +FNDA:0,SignatureCheckerLib.recover +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +FN:98,SignatureCheckerLib.tryRecover +FNDA:0,SignatureCheckerLib.tryRecover +DA:113,0 +BRDA:112,1,0,- +BRDA:112,1,1,- +DA:116,0 +DA:120,0 +DA:121,0 +BRDA:121,2,0,- +BRDA:121,2,1,- +DA:122,0 +DA:125,0 +FN:129,SignatureCheckerLib._throwError +FNDA:0,SignatureCheckerLib._throwError +DA:130,0 +BRDA:130,3,0,- +BRDA:130,3,1,- +DA:131,0 +DA:132,0 +BRDA:132,4,0,- +BRDA:132,4,1,- +DA:133,0 +DA:134,0 +BRDA:134,5,0,- +BRDA:134,5,1,- +DA:135,0 +DA:136,0 +BRDA:136,6,0,- +BRDA:136,6,1,- +DA:137,0 +FNF:4 FNH:0 -LF:0 +LF:27 LH:0 -BRF:0 +BRF:14 BRH:0 end_of_record TN: @@ -334,38 +373,38 @@ FNDA:0,EIP712._domainNameAndVersion DA:74,0 DA:75,0 FN:80,EIP712.DOMAIN_SEPARATOR -FNDA:281,EIP712.DOMAIN_SEPARATOR -DA:81,281 -DA:82,281 +FNDA:283,EIP712.DOMAIN_SEPARATOR +DA:81,283 +DA:82,283 BRDA:82,0,0,- -BRDA:82,0,1,281 +BRDA:82,0,1,283 DA:83,0 FN:100,EIP712._hashTypedData -FNDA:273,EIP712._hashTypedData -DA:105,273 -DA:106,273 -BRDA:106,1,0,- -BRDA:106,1,1,273 -DA:107,0 -DA:115,273 -FN:127,EIP712.eip712Domain +FNDA:275,EIP712._hashTypedData +DA:106,275 +DA:107,275 +BRDA:107,1,0,- +BRDA:107,1,1,275 +DA:108,0 +DA:116,275 +FN:128,EIP712.eip712Domain FNDA:0,EIP712.eip712Domain -DA:140,0 DA:141,0 DA:142,0 DA:143,0 DA:144,0 DA:145,0 -FN:153,EIP712._buildDomainSeparator +DA:146,0 +FN:154,EIP712._buildDomainSeparator FNDA:0,EIP712._buildDomainSeparator -DA:154,0 -DA:155,0 -DA:164,0 -FN:169,EIP712._cachedDomainSeparatorInvalidated -FNDA:554,EIP712._cachedDomainSeparatorInvalidated -DA:174,554 -DA:175,554 -DA:178,554 +DA:156,0 +DA:157,0 +DA:166,0 +FN:171,EIP712._cachedDomainSeparatorInvalidated +FNDA:558,EIP712._cachedDomainSeparatorInvalidated +DA:176,558 +DA:177,558 +DA:180,558 FNF:6 FNH:3 LF:21 @@ -375,9 +414,9 @@ BRH:2 end_of_record TN: SF:src/utils/EIP7412.sol -FN:14,EIP7412.fulfillOracleQuery -FNDA:256,EIP7412.fulfillOracleQuery -DA:21,256 +FN:15,EIP7412.fulfillOracleQuery +FNDA:1536,EIP7412.fulfillOracleQuery +DA:21,1536 FNF:1 FNH:1 LF:1 @@ -386,165 +425,29 @@ BRF:0 BRH:0 end_of_record TN: -SF:src/utils/TrustedMulticallForwarder.sol -FN:56,TrustedMulticallForwarder.aggregate -FNDA:2,TrustedMulticallForwarder.aggregate -DA:60,2 -DA:61,2 -DA:62,2 -DA:63,2 -DA:64,2 -DA:65,3 -DA:66,3 -DA:67,3 -DA:69,3 -BRDA:69,0,0,- -BRDA:69,0,1,1 -DA:70,1 -DA:71,1 -DA:78,2 -FN:88,TrustedMulticallForwarder.tryAggregate -FNDA:2,TrustedMulticallForwarder.tryAggregate -DA:92,5 -DA:93,5 -DA:94,5 -DA:95,5 -DA:96,10 -DA:97,10 -DA:98,10 -DA:100,10 -BRDA:100,1,0,3 -BRDA:100,1,1,7 -DA:101,3 -DA:102,3 -DA:108,7 -FN:119,TrustedMulticallForwarder.tryBlockAndAggregate -FNDA:2,TrustedMulticallForwarder.tryBlockAndAggregate -DA:128,3 -DA:129,3 -DA:130,3 -FN:139,TrustedMulticallForwarder.blockAndAggregate -FNDA:1,TrustedMulticallForwarder.blockAndAggregate -DA:148,1 -FN:154,TrustedMulticallForwarder.aggregate3 -FNDA:2,TrustedMulticallForwarder.aggregate3 -DA:159,2 -DA:160,2 -DA:161,2 -DA:162,2 -DA:163,5 -DA:164,5 -DA:165,5 -DA:167,5 -BRDA:167,2,0,1 -BRDA:167,2,1,4 -DA:168,1 -DA:169,1 -DA:175,4 -FN:184,TrustedMulticallForwarder.aggregate3Value -FNDA:7,TrustedMulticallForwarder.aggregate3Value -DA:189,7 -DA:190,7 -DA:191,7 -DA:192,7 -DA:193,7 -DA:194,15 -DA:195,15 -DA:196,15 -DA:200,15 -DA:202,15 -DA:205,15 -BRDA:205,3,0,2 -BRDA:205,3,1,13 -DA:206,2 -DA:207,2 -DA:213,13 -DA:217,5 -BRDA:217,4,0,2 -BRDA:217,4,1,3 -DA:218,2 -FN:227,TrustedMulticallForwarder.executeBatch -FNDA:2,TrustedMulticallForwarder.executeBatch -DA:232,2 -DA:233,2 -DA:235,2 -DA:237,2 -DA:238,2 -DA:240,2 -DA:241,2 -DA:243,2 -DA:244,2 -DA:246,2 -DA:251,2 -DA:253,2 -BRDA:253,5,0,- -BRDA:253,5,1,1 -DA:255,1 -DA:257,1 -DA:263,1 -BRDA:263,6,0,- -BRDA:263,6,1,1 -DA:269,1 -DA:276,2 -BRDA:276,7,0,- -BRDA:276,7,1,1 -DA:277,1 -DA:281,2 -DA:287,2 -BRDA:287,8,0,- -BRDA:287,8,1,2 -DA:288,0 -DA:293,2 -BRDA:293,9,0,1 -BRDA:293,9,1,1 -DA:297,1 -FN:303,TrustedMulticallForwarder.getBlockHash -FNDA:256,TrustedMulticallForwarder.getBlockHash -DA:308,256 -FN:312,TrustedMulticallForwarder.getBlockNumber -FNDA:1,TrustedMulticallForwarder.getBlockNumber -DA:313,1 -FN:317,TrustedMulticallForwarder.getCurrentBlockCoinbase -FNDA:1,TrustedMulticallForwarder.getCurrentBlockCoinbase -DA:318,1 -FN:322,TrustedMulticallForwarder.getPrevRandao -FNDA:0,TrustedMulticallForwarder.getPrevRandao -DA:323,0 -FN:327,TrustedMulticallForwarder.getCurrentBlockGasLimit -FNDA:1,TrustedMulticallForwarder.getCurrentBlockGasLimit -DA:328,1 -FN:332,TrustedMulticallForwarder.getCurrentBlockTimestamp -FNDA:2,TrustedMulticallForwarder.getCurrentBlockTimestamp -DA:337,2 -FN:341,TrustedMulticallForwarder.getEthBalance -FNDA:256,TrustedMulticallForwarder.getEthBalance -DA:346,256 -FN:350,TrustedMulticallForwarder.getLastBlockHash -FNDA:1,TrustedMulticallForwarder.getLastBlockHash -DA:352,1 -FN:358,TrustedMulticallForwarder.getBasefee -FNDA:1,TrustedMulticallForwarder.getBasefee -DA:359,1 -FN:363,TrustedMulticallForwarder.getChainId -FNDA:1,TrustedMulticallForwarder.getChainId -DA:364,1 -FNF:17 -FNH:16 -LF:87 -LH:85 -BRF:20 -BRH:15 +SF:src/utils/MulticallablePayable.sol +FN:24,MulticallablePayable.multicall +FNDA:1287,MulticallablePayable.multicall +DA:33,1287 +BRDA:33,0,0,132 +DA:43,1155 +FNF:1 +FNH:1 +LF:2 +LH:2 +BRF:1 +BRH:1 end_of_record TN: SF:test/AsyncOrder.t.sol -FN:8,AsyncOrderTest.setUp +FN:10,AsyncOrderTest.setUp FNDA:0,AsyncOrderTest.setUp -DA:9,0 -DA:10,0 +DA:11,0 DA:12,0 DA:14,0 DA:16,0 -DA:22,0 +DA:18,0 +DA:24,0 FNF:1 FNH:0 LF:6 @@ -580,21 +483,21 @@ BRH:0 end_of_record TN: SF:test/ConditionalOrder.t.sol -FN:22,ConditionalOrderTest.setUp +FN:21,ConditionalOrderTest.setUp FNDA:0,ConditionalOrderTest.setUp +DA:22,0 DA:23,0 -DA:24,0 +DA:25,0 DA:26,0 -DA:27,0 +DA:28,0 DA:29,0 -DA:30,0 -DA:32,0 -DA:34,0 -DA:36,0 -DA:38,0 -DA:44,0 -DA:46,0 -DA:52,0 +DA:31,0 +DA:33,0 +DA:35,0 +DA:37,0 +DA:43,0 +DA:45,0 +DA:51,0 FNF:1 FNH:0 LF:13 @@ -603,11 +506,11 @@ BRF:0 BRH:0 end_of_record TN: -SF:test/EthManagement.t.sol -FN:13,EthManagementTest.setUp -FNDA:0,EthManagementTest.setUp -DA:14,0 -DA:15,0 +SF:test/Credit.t.sol +FN:11,CreditTest.setUp +FNDA:0,CreditTest.setUp +DA:12,0 +DA:13,0 FNF:1 FNH:0 LF:2 @@ -616,101 +519,90 @@ BRF:0 BRH:0 end_of_record TN: -SF:test/TrustedMulticallForwarder.t.sol -FN:14,ERC2771Example.isTrustedForwarder -FNDA:2,ERC2771Example.isTrustedForwarder -DA:19,2 -FN:22,ERC2771Example.ping -FNDA:1,ERC2771Example.ping -DA:23,1 -FN:38,TrustedMulticallForwarderTest.setUp -FNDA:0,TrustedMulticallForwarderTest.setUp -DA:40,0 -DA:41,0 -DA:44,0 -DA:45,0 -FNF:3 -FNH:2 -LF:6 -LH:2 +SF:test/EIP7412.t.sol +FN:17,EIP7412Test.setUp +FNDA:0,EIP7412Test.setUp +DA:18,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:23,0 +FNF:1 +FNH:0 +LF:5 +LH:0 BRF:0 BRH:0 end_of_record TN: SF:test/utils/Bootstrap.sol -FN:113,BootstrapOptimism.init +FN:119,BootstrapOptimism.init FNDA:0,BootstrapOptimism.init -DA:117,0 -DA:118,0 -DA:125,0 -DA:133,0 -FN:146,BootstrapOptimismGoerli.init -FNDA:0,BootstrapOptimismGoerli.init -DA:150,0 -DA:151,0 -DA:158,0 -DA:166,0 -FN:41,Bootstrap.initializeOptimismGoerli +DA:123,0 +DA:129,0 +DA:135,0 +FN:57,Bootstrap.initializeOptimismGoerli FNDA:0,Bootstrap.initializeOptimismGoerli -DA:42,0 -DA:43,0 -DA:51,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:57,0 DA:58,0 DA:59,0 -DA:60,0 -DA:61,0 -DA:62,0 -DA:64,0 DA:65,0 -DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 DA:71,0 +DA:72,0 DA:73,0 -FN:76,Bootstrap.initializeOptimism -FNDA:0,Bootstrap.initializeOptimism +DA:75,0 +DA:76,0 DA:77,0 -DA:78,0 -DA:86,0 +DA:82,0 +DA:84,0 +FN:87,Bootstrap.initializeOptimism +FNDA:0,Bootstrap.initializeOptimism 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:98,0 DA:99,0 DA:100,0 DA:101,0 +DA:102,0 +DA:103,0 +DA:105,0 DA:106,0 -DA:108,0 +DA:107,0 +DA:112,0 +DA:114,0 +FN:146,BootstrapOptimismGoerli.init +FNDA:0,BootstrapOptimismGoerli.init +DA:150,0 +DA:156,0 +DA:162,0 FNF:4 FNH:0 -LF:42 +LF:36 LH:0 BRF:0 BRH:0 end_of_record TN: SF:test/utils/ConditionalOrderSignature.sol -FN:21,ConditionalOrderSignature.getConditionalOrderSignatureRaw +FN:24,ConditionalOrderSignature.getConditionalOrderSignatureRaw FNDA:0,ConditionalOrderSignature.getConditionalOrderSignatureRaw -DA:26,0 -DA:42,0 -DA:43,0 -DA:44,0 +DA:29,0 +DA:45,0 +DA:46,0 DA:47,0 -DA:60,0 -DA:64,0 -FN:67,ConditionalOrderSignature.getConditionalOrderSignature +DA:50,0 +DA:63,0 +DA:67,0 +FN:70,ConditionalOrderSignature.getConditionalOrderSignature FNDA:0,ConditionalOrderSignature.getConditionalOrderSignature -DA:72,0 -DA:73,0 -DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 FNF:2 FNH:0 LF:10 @@ -720,32 +612,32 @@ BRH:0 end_of_record TN: SF:test/utils/Conditions.sol -FN:7,Conditions.isTimestampAfter +FN:10,Conditions.isTimestampAfter FNDA:0,Conditions.isTimestampAfter -DA:12,0 -DA:13,0 -FN:16,Conditions.isTimestampBefore +DA:15,0 +DA:16,0 +FN:19,Conditions.isTimestampBefore FNDA:0,Conditions.isTimestampBefore -DA:21,0 -DA:22,0 -FN:25,Conditions.isPriceAbove +DA:24,0 +DA:25,0 +FN:28,Conditions.isPriceAbove FNDA:0,Conditions.isPriceAbove -DA:30,0 -FN:35,Conditions.isPriceBelow +DA:33,0 +FN:38,Conditions.isPriceBelow FNDA:0,Conditions.isPriceBelow -DA:40,0 -FN:45,Conditions.isMarketOpen +DA:43,0 +FN:48,Conditions.isMarketOpen FNDA:0,Conditions.isMarketOpen -DA:50,0 -FN:53,Conditions.isPositionSizeAbove +DA:53,0 +FN:56,Conditions.isPositionSizeAbove FNDA:0,Conditions.isPositionSizeAbove -DA:58,0 -FN:63,Conditions.isPositionSizeBelow +DA:61,0 +FN:66,Conditions.isPositionSizeBelow FNDA:0,Conditions.isPositionSizeBelow -DA:68,0 -FN:73,Conditions.isOrderFeeBelow +DA:71,0 +FN:76,Conditions.isOrderFeeBelow FNDA:0,Conditions.isOrderFeeBelow -DA:78,0 +DA:81,0 FNF:8 FNH:0 LF:10 @@ -755,9 +647,9 @@ BRH:0 end_of_record TN: SF:test/utils/SynthMinter.sol -FN:22,SynthMinter.mint_sUSD +FN:24,SynthMinter.mint_sUSD FNDA:0,SynthMinter.mint_sUSD -DA:23,0 +DA:25,0 FNF:1 FNH:0 LF:1 @@ -767,9 +659,9 @@ BRH:0 end_of_record TN: SF:test/utils/exposed/EngineExposed.sol -FN:25,EngineExposed.getSynthAddress +FN:17,EngineExposed.getSynthAddress FNDA:0,EngineExposed.getSynthAddress -DA:30,0 +DA:22,0 FNF:1 FNH:0 LF:1 @@ -778,58 +670,91 @@ BRF:0 BRH:0 end_of_record TN: -SF:test/utils/mocks/MockCallee.sol -FN:14,MockCallee.getBlockHash -FNDA:12,MockCallee.getBlockHash -DA:19,12 -FN:23,MockCallee.thisMethodReverts -FNDA:11,MockCallee.thisMethodReverts -DA:24,11 -FN:28,MockCallee.sendBackValue -FNDA:2,MockCallee.sendBackValue -DA:29,2 -DA:30,2 -BRDA:30,0,0,- -BRDA:30,0,1,2 +SF:test/utils/mocks/EIP7412Mock.sol +FN:24,EIP7412MockRevert.fulfillOracleQuery +FNDA:256,EIP7412MockRevert.fulfillOracleQuery +DA:25,256 +FN:16,EIP7412MockRefund.fulfillOracleQuery +FNDA:256,EIP7412MockRefund.fulfillOracleQuery +DA:17,256 +BRDA:17,0,0,- +BRDA:17,0,1,256 +DA:18,256 +DA:19,256 +BRDA:19,1,0,256 +BRDA:19,1,1,- +FN:9,EIP7412Mock.fulfillOracleQuery +FNDA:768,EIP7412Mock.fulfillOracleQuery +DA:10,768 +BRDA:10,0,0,- +BRDA:10,0,1,768 +DA:11,768 FNF:3 FNH:3 -LF:4 -LH:4 -BRF:2 -BRH:1 +LF:6 +LH:6 +BRF:6 +BRH:3 end_of_record TN: -SF:test/utils/mocks/PythMock.sol -FN:8,PythMock.mock_pyth_getPrice -FNDA:0,PythMock.mock_pyth_getPrice -DA:15,0 -FNF:1 -FNH:0 -LF:1 -LH:0 -BRF:0 +SF:test/utils/mocks/MockMulticallablePayable.sol +FN:17,MockMulticallablePayable.revertsWithString +FNDA:257,MockMulticallablePayable.revertsWithString +DA:18,257 +FN:21,MockMulticallablePayable.revertsWithCustomError +FNDA:1,MockMulticallablePayable.revertsWithCustomError +DA:22,1 +FN:25,MockMulticallablePayable.revertsWithNothing +FNDA:1,MockMulticallablePayable.revertsWithNothing +DA:26,1 +FN:29,MockMulticallablePayable.returnsTuple +FNDA:534,MockMulticallablePayable.returnsTuple +DA:34,534 +FN:37,MockMulticallablePayable.returnsString +FNDA:125,MockMulticallablePayable.returnsString +DA:42,125 +FN:47,MockMulticallablePayable.pay +FNDA:0,MockMulticallablePayable.pay +DA:48,0 +FN:51,MockMulticallablePayable.returnsSender +FNDA:1,MockMulticallablePayable.returnsSender +DA:52,1 +FN:55,MockMulticallablePayable.multicallOriginal +FNDA:1,MockMulticallablePayable.multicallOriginal +DA:61,1 +DA:62,1 +DA:63,10 +DA:64,10 +DA:65,10 +BRDA:65,0,0,- +BRDA:65,0,1,- +DA:67,0 +BRDA:67,1,0,- +BRDA:67,1,1,- +DA:70,0 +DA:72,0 +DA:74,10 +FNF:8 +FNH:7 +LF:16 +LH:12 +BRF:4 BRH:0 end_of_record TN: SF:test/utils/mocks/SynthetixMock.sol -FN:9,SynthetixMock.mock_computeOrderFees -FNDA:0,SynthetixMock.mock_computeOrderFees -DA:16,0 -FN:25,SynthetixMock.mock_getOpenPosition +FN:10,SynthetixMock.mock_getOpenPosition FNDA:0,SynthetixMock.mock_getOpenPosition -DA:31,0 -FN:40,SynthetixMock.mock_getMaxMarketSize +DA:16,0 +FN:25,SynthetixMock.mock_getMaxMarketSize FNDA:0,SynthetixMock.mock_getMaxMarketSize -DA:45,0 -FN:54,SynthetixMock.mock_fulfillOracleQuery -FNDA:0,SynthetixMock.mock_fulfillOracleQuery -DA:58,0 -FN:67,SynthetixMock.mock_getAccountOwner +DA:30,0 +FN:39,SynthetixMock.mock_getAccountOwner FNDA:0,SynthetixMock.mock_getAccountOwner -DA:72,0 -FNF:5 +DA:44,0 +FNF:3 FNH:0 -LF:5 +LF:3 LH:0 BRF:0 BRH:0 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts deleted file mode 160000 index 932fddf6..00000000 --- a/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 932fddf69a699a9a80fd2396fd1a2ab91cdda123 diff --git a/package.json b/package.json index 4da9eae7..c1bf0f66 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "compile": "forge build", "test": "forge test --fork-url $(grep OPTIMISM_GOERLI_RPC_URL .env | cut -d '=' -f2) --etherscan-api-key $(grep OPTIMISM_ETHERSCAN_API_KEY .env | cut -d '=' -f2) --gas-report -vvv", + "test:hh": "npx hardhat test", "format": "forge fmt", "coverage": "forge coverage --fork-url $(grep OPTIMISM_GOERLI_RPC_URL .env | cut -d '=' -f2)", "coverage:generate-lcov": "forge coverage --fork-url $(grep OPTIMISM_GOERLI_RPC_URL .env | cut -d '=' -f2) --report lcov", diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 053659a3..1fd45b24 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -3,8 +3,6 @@ pragma solidity 0.8.20; // contracts import {Engine} from "src/Engine.sol"; -import {TrustedMulticallForwarder} from - "src/utils/TrustedMulticallForwarder.sol"; // parameters import {BaseGoerliParameters} from @@ -26,23 +24,12 @@ contract Setup is Script { function deploySystem( address perpsMarketProxy, address spotMarketProxy, - address sUSDProxy, - address oracle - ) - public - returns ( - Engine engine, - TrustedMulticallForwarder trustedForwarderContract - ) - { - trustedForwarderContract = new TrustedMulticallForwarder(); - + address sUSDProxy + ) public returns (Engine engine) { engine = new Engine({ _perpsMarketProxy: perpsMarketProxy, _spotMarketProxy: spotMarketProxy, - _sUSDProxy: sUSDProxy, - _oracle: oracle, - _trustedForwarder: address(trustedForwarderContract) + _sUSDProxy: sUSDProxy }); } } @@ -58,8 +45,7 @@ contract DeployBase_Synthetix is Setup, BaseParameters { Setup.deploySystem({ perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, - sUSDProxy: USD_PROXY, - oracle: PYTH + sUSDProxy: USD_PROXY }); vm.stopBroadcast(); @@ -77,8 +63,7 @@ contract DeployBaseGoerli_Synthetix is Setup, BaseGoerliParameters { Setup.deploySystem({ perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, - sUSDProxy: USD_PROXY, - oracle: PYTH + sUSDProxy: USD_PROXY }); vm.stopBroadcast(); @@ -99,8 +84,7 @@ contract DeployBaseGoerli_KwentaFork is Setup.deploySystem({ perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, - sUSDProxy: USD_PROXY, - oracle: PYTH + sUSDProxy: USD_PROXY }); vm.stopBroadcast(); @@ -118,8 +102,7 @@ contract DeployBaseGoerli_Andromeda is Setup, BaseGoerliParameters { Setup.deploySystem({ perpsMarketProxy: PERPS_MARKET_PROXY_ANDROMEDA, spotMarketProxy: SPOT_MARKET_PROXY_ANDROMEDA, - sUSDProxy: USD_PROXY_ANDROMEDA, - oracle: PYTH + sUSDProxy: USD_PROXY_ANDROMEDA }); vm.stopBroadcast(); @@ -137,8 +120,7 @@ contract DeployOptimism_Synthetix is Setup, OptimismParameters { Setup.deploySystem({ perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, - sUSDProxy: USD_PROXY, - oracle: PYTH + sUSDProxy: USD_PROXY }); vm.stopBroadcast(); @@ -156,8 +138,7 @@ contract DeployOptimismGoerli_Synthetix is Setup, OptimismGoerliParameters { Setup.deploySystem({ perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, - sUSDProxy: USD_PROXY, - oracle: PYTH + sUSDProxy: USD_PROXY }); vm.stopBroadcast(); diff --git a/script/utils/parameters/BaseGoerliKwentaForkParameters.sol b/script/utils/parameters/BaseGoerliKwentaForkParameters.sol index fc106902..f50cdf9d 100644 --- a/script/utils/parameters/BaseGoerliKwentaForkParameters.sol +++ b/script/utils/parameters/BaseGoerliKwentaForkParameters.sol @@ -10,6 +10,4 @@ contract BaseGoerliKwentaForkParameters { address public constant USD_PROXY = 0xD3bcDae94B0c2EF16d1c43d29c23b1735d864fC6; - - address public constant PYTH = 0x5955C1478F0dAD753C7E2B4dD1b4bC530C64749f; } diff --git a/script/utils/parameters/BaseGoerliParameters.sol b/script/utils/parameters/BaseGoerliParameters.sol index d1da4175..88325dc3 100644 --- a/script/utils/parameters/BaseGoerliParameters.sol +++ b/script/utils/parameters/BaseGoerliParameters.sol @@ -11,8 +11,6 @@ contract BaseGoerliParameters { address public constant USD_PROXY = 0x579c612E4Bf390f5504DB9f76b6F5759A3172279; - address public constant PYTH = 0x5955C1478F0dAD753C7E2B4dD1b4bC530C64749f; - // https://usecannon.com/packages/synthetix-omnibus/latest/84531-andromeda address public constant PERPS_MARKET_PROXY_ANDROMEDA = 0x75c43165ea38cB857C45216a37C5405A7656673c; diff --git a/script/utils/parameters/BaseParameters.sol b/script/utils/parameters/BaseParameters.sol index 43d470c9..88d50a6f 100644 --- a/script/utils/parameters/BaseParameters.sol +++ b/script/utils/parameters/BaseParameters.sol @@ -7,6 +7,4 @@ contract BaseParameters { address public constant SPOT_MARKET_PROXY = address(0); address public constant USD_PROXY = address(0); - - address public constant PYTH = 0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a; } diff --git a/script/utils/parameters/OptimismGoerliParameters.sol b/script/utils/parameters/OptimismGoerliParameters.sol index b3f2c52b..065ea18a 100644 --- a/script/utils/parameters/OptimismGoerliParameters.sol +++ b/script/utils/parameters/OptimismGoerliParameters.sol @@ -10,6 +10,4 @@ contract OptimismGoerliParameters { address public constant USD_PROXY = 0xe487Ad4291019b33e2230F8E2FB1fb6490325260; - - address public constant PYTH = 0xff1a0f4744e8582DF1aE09D5611b887B6a12925C; } diff --git a/script/utils/parameters/OptimismParameters.sol b/script/utils/parameters/OptimismParameters.sol index 05670010..4ea216fe 100644 --- a/script/utils/parameters/OptimismParameters.sol +++ b/script/utils/parameters/OptimismParameters.sol @@ -9,6 +9,4 @@ contract OptimismParameters { address public constant USD_PROXY = 0xb2F30A7C980f052f02563fb518dcc39e6bf38175; - - address public constant PYTH = 0xff1a0f4744e8582DF1aE09D5611b887B6a12925C; } diff --git a/src/Engine.sol b/src/Engine.sol index 872f0a94..19d4033a 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -5,25 +5,23 @@ import {ConditionalOrderHashLib} from "src/libraries/ConditionalOrderHashLib.sol"; import {EIP712} from "src/utils/EIP712.sol"; import {EIP7412} from "src/utils/EIP7412.sol"; -import {ERC2771Context} from - "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 {MulticallablePayable} from "src/utils/MulticallablePayable.sol"; import {SignatureCheckerLib} from "src/libraries/SignatureCheckerLib.sol"; /// @title Kwenta Smart Margin v3: Engine contract /// @notice Responsible for interacting with Synthetix v3 perps markets +/// @custom:caution Engine should never hold an ETH balance so long as it is MulticallablePayable +/// @custom:caution Add payable functions to the Engine with extreme caution /// @author JaredBorders (jaredborders@pm.me) -/// @custom:auditor this contract does not prevent reentrancy. -/// Please review the contract carefully. -contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { +contract Engine is IEngine, EIP712, EIP7412, MulticallablePayable { using MathLib for int128; using MathLib for int256; using MathLib for uint256; - using SignatureCheckerLib for bytes; + using SignatureCheckerLib for bytes32; using ConditionalOrderHashLib for ConditionalOrder; /*////////////////////////////////////////////////////////////// @@ -41,31 +39,10 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { /// @notice max number of conditions that can be defined for a conditional order uint256 internal constant MAX_CONDITIONS = 8; - /// @notice condition selector constant(s) - bytes4 internal constant isTimestampAfterSelector = - IEngine.isTimestampAfter.selector; - bytes4 internal constant isTimestampBeforeSelector = - IEngine.isTimestampBefore.selector; - bytes4 internal constant isPriceAboveSelector = - IEngine.isPriceAbove.selector; - bytes4 internal constant isPriceBelowSelector = - IEngine.isPriceBelow.selector; - bytes4 internal constant isMarketOpenSelector = - IEngine.isMarketOpen.selector; - bytes4 internal constant isPositionSizeAboveSelector = - IEngine.isPositionSizeAbove.selector; - bytes4 internal constant isPositionSizeBelowSelector = - IEngine.isPositionSizeBelow.selector; - bytes4 internal constant isOrderFeeBelowSelector = - IEngine.isOrderFeeBelow.selector; - /*////////////////////////////////////////////////////////////// IMMUTABLES //////////////////////////////////////////////////////////////*/ - /// @notice pyth oracle contract used to get asset prices - IPyth internal immutable ORACLE; - /// @notice Synthetix v3 perps market proxy contract IPerpsMarketProxy internal immutable PERPS_MARKET_PROXY; @@ -84,10 +61,9 @@ contract Engine is IEngine, 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; + /// @notice mapping of account id to sUSD balance (i.e. credit available to pay for fee(s)) + /// @dev sUSD can be credited to the Engine to pay for fee(s) + mapping(uint128 accountId => uint256) public credit; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -97,25 +73,18 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { /// @param _perpsMarketProxy Synthetix v3 perps market proxy contract /// @param _spotMarketProxy Synthetix v3 spot market proxy contract /// @param _sUSDProxy Synthetix v3 sUSD contract - /// @param _oracle pyth oracle contract used to get asset prices - /// @param _trustedForwarder trusted forwarder contract used for meta transactions constructor( address _perpsMarketProxy, address _spotMarketProxy, - address _sUSDProxy, - address _oracle, - address _trustedForwarder - ) ERC2771Context(_trustedForwarder) { + address _sUSDProxy + ) { if (_perpsMarketProxy == address(0)) revert ZeroAddress(); if (_spotMarketProxy == address(0)) revert ZeroAddress(); if (_sUSDProxy == address(0)) revert ZeroAddress(); - if (_oracle == address(0)) revert ZeroAddress(); - if (_trustedForwarder == address(0)) revert ZeroAddress(); PERPS_MARKET_PROXY = IPerpsMarketProxy(_perpsMarketProxy); SPOT_MARKET_PROXY = ISpotMarketProxy(_spotMarketProxy); SUSD = IERC20(_sUSDProxy); - ORACLE = IPyth(_oracle); } /*////////////////////////////////////////////////////////////// @@ -129,7 +98,8 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { override returns (bool) { - return PERPS_MARKET_PROXY.getAccountOwner(_accountId) == _caller; + return _caller != address(0) + && PERPS_MARKET_PROXY.getAccountOwner(_accountId) == _caller; } /// @inheritdoc IEngine @@ -149,57 +119,10 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { view returns (bool) { - return PERPS_MARKET_PROXY.isAuthorized( - _accountId, PERPS_COMMIT_ASYNC_ORDER_PERMISSION, _caller - ); - } - - /*////////////////////////////////////////////////////////////// - ETH MANAGEMENT - //////////////////////////////////////////////////////////////*/ - - /// @inheritdoc IEngine - function depositEth(uint128 _accountId) external payable override { - // ensure account exists (i.e. owner is not the zero address) - /// @notice this does not check if the caller is the account owner - if (PERPS_MARKET_PROXY.getAccountOwner(_accountId) == address(0)) { - revert AccountDoesNotExist(); - } - - ethBalances[_accountId] += msg.value; - - emit EthDeposit(_accountId, msg.value); - } - - /// @inheritdoc IEngine - function withdrawEth(uint128 _accountId, uint256 _amount) - external - override - { - address caller = _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 _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(); + return _caller != address(0) + && PERPS_MARKET_PROXY.isAuthorized( + _accountId, PERPS_COMMIT_ASYNC_ORDER_PERMISSION, _caller + ); } /*////////////////////////////////////////////////////////////// @@ -212,7 +135,7 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { uint256 _wordPos, uint256 _mask ) external override { - if (_isAccountOwnerOrDelegate(_accountId, _msgSender())) { + if (_isAccountOwnerOrDelegate(_accountId, msg.sender)) { /// @dev using bitwise OR to set the bit at the bit position /// bitmap = .......10001 /// mask = .......00110 @@ -268,7 +191,7 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { { // shift _nonce to the right by 8 bits and cast to uint248 /// @dev wordPos == 0 if 0 <= _nonce <= 255, 1 if 256 <= _nonce <= 511, etc. - wordPos = uint248(_nonce >> 8); + wordPos = _nonce >> 8; // cast the last 8 bits of _nonce to uint8 /// @dev 0 <= bitPos <= 255 @@ -325,16 +248,15 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { ) external override { IERC20 synth = IERC20(_getSynthAddress(_synthMarketId)); - address caller = _msgSender(); - if (_amount > 0) { _depositCollateral( - caller, synth, _accountId, _synthMarketId, _amount + msg.sender, synth, _accountId, _synthMarketId, _amount ); } else { - if (!isAccountOwner(_accountId, caller)) revert Unauthorized(); + if (!isAccountOwner(_accountId, msg.sender)) revert Unauthorized(); + _withdrawCollateral( - caller, synth, _accountId, _synthMarketId, _amount + msg.sender, synth, _accountId, _synthMarketId, _amount ); } } @@ -398,8 +320,8 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { override returns (IPerpsMarketProxy.Data memory retOrder, uint256 fees) { - /// @dev only the account owner can withdraw collateral - if (_isAccountOwnerOrDelegate(_accountId, _msgSender())) { + /// @dev the account owner or the delegate may commit async orders + if (_isAccountOwnerOrDelegate(_accountId, msg.sender)) { (retOrder, fees) = _commitOrder({ _perpsMarketId: _perpsMarketId, _accountId: _accountId, @@ -436,6 +358,57 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { ); } + /*////////////////////////////////////////////////////////////// + CREDIT MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IEngine + function creditAccount(uint128 _accountId, uint256 _amount) + external + override + { + // ensure account exists (i.e. owner is not the zero address) + /// @notice this does not check if the caller is the account owner + if (PERPS_MARKET_PROXY.getAccountOwner(_accountId) == address(0)) { + revert AccountDoesNotExist(); + } + + credit[_accountId] += _amount; + + /// @dev sUSD transfers that fail will revert + SUSD.transferFrom(msg.sender, address(this), _amount); + + emit Credited(_accountId, _amount); + } + + /// @inheritdoc IEngine + function debitAccount(uint128 _accountId, uint256 _amount) + external + override + { + if (!isAccountOwner(_accountId, msg.sender)) revert Unauthorized(); + + _debit(msg.sender, _accountId, _amount); + + emit Debited(_accountId, _amount); + } + + /// @notice debit sUSD from the account and transfer it to the caller + /// @dev UNSAFE to call directly; use `debit` instead + /// @param _caller the caller of the function + /// @param _accountId the account id to debit sUSD from + function _debit(address _caller, uint128 _accountId, uint256 _amount) + internal + { + if (_amount > credit[_accountId]) revert InsufficientCredit(); + + // decrement the sUSD balance of the account prior to transferring sUSD to the caller + credit[_accountId] -= _amount; + + /// @dev sUSD transfers that fail will revert + SUSD.transfer(_caller, _amount); + } + /*////////////////////////////////////////////////////////////// CONDITIONAL ORDER MANAGEMENT //////////////////////////////////////////////////////////////*/ @@ -462,13 +435,12 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { _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 is denoted in sUSD 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(_msgSender(), _co.orderDetails.accountId, _fee); + if (_fee > 0) _debit(msg.sender, _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; /// @notice handle reduce only orders @@ -479,12 +451,13 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { // ensure position exists; reduce only orders cannot increase position size if (positionSize == 0) { - return (retOrder, 0); + revert CannotExecuteOrder(); } - // 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); + // ensure incoming size delta is non-zero and NOT the same sign; + // i.e. reduce only orders cannot increase position size + if (sizeDelta == 0 || positionSize.isSameSign(sizeDelta)) { + revert CannotExecuteOrder(); } // ensure incoming size delta is not larger than current position size @@ -504,7 +477,7 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { } } - /// @dev execute the order + /// @dev commit async order (retOrder, synthetixFees) = _commitOrder({ _perpsMarketId: _co.orderDetails.marketId, _accountId: _co.orderDetails.accountId, @@ -531,8 +504,8 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { // 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 account has enough credit to pay the fee + if (_fee > credit[_co.orderDetails.accountId]) return false; // verify nonce has not been executed before if (hasUnorderedNonceBeenUsed(_co.orderDetails.accountId, _co.nonce)) { @@ -553,7 +526,7 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { } else { // if the order does not require verification, then the caller // must be the trusted executor defined by "trustedExecutor" - if (_msgSender() != _co.trustedExecutor) return false; + if (msg.sender != _co.trustedExecutor) return false; } return true; @@ -578,9 +551,7 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { ConditionalOrder calldata _co, bytes calldata _signature ) public view override returns (bool) { - return _signature.isValidSignatureNowCalldata( - _hashTypedData(_co.hash()), _co.signer - ); + return _hashTypedData(_co.hash()).recover(_signature) == _co.signer; } /// @inheritdoc IEngine @@ -605,14 +576,14 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { /// @dev checking if the selector is valid prevents the possibility of /// a malicious condition from griefing the executor if ( - selector == isTimestampAfterSelector - || selector == isTimestampBeforeSelector - || selector == isPriceAboveSelector - || selector == isPriceBelowSelector - || selector == isMarketOpenSelector - || selector == isPositionSizeAboveSelector - || selector == isPositionSizeBelowSelector - || selector == isOrderFeeBelowSelector + selector == this.isPriceAbove.selector + || selector == this.isPriceBelow.selector + || selector == this.isTimestampAfter.selector + || selector == this.isTimestampBefore.selector + || selector == this.isMarketOpen.selector + || selector == this.isPositionSizeAbove.selector + || selector == this.isPositionSizeBelow.selector + || selector == this.isOrderFeeBelow.selector ) { // @dev staticcall to prevent state changes in the case a condition is malicious (success, response) = @@ -656,31 +627,33 @@ contract Engine is IEngine, EIP712, EIP7412, ERC2771Context { } /// @inheritdoc IEngine - function isPriceAbove( - bytes32 _assetId, - int64 _price, - uint64 _confidenceInterval - ) public view override returns (bool) { - PythStructs.Price memory priceData = ORACLE.getPrice(_assetId); + function isPriceAbove(uint128 _marketId, uint256 _price, int128 _size) + public + view + override + returns (bool) + { + (, uint256 fillPrice) = PERPS_MARKET_PROXY.computeOrderFees({ + marketId: _marketId, + sizeDelta: _size + }); - /// @dev although counterintuitive, a smaller confidence interval is more accurate. - /// The Engine must ensure the current confidence interval is not - /// greater (i.e. less accurate) than the confidence interval defined by the condition. - return priceData.price > _price && priceData.conf <= _confidenceInterval; + return fillPrice > _price; } /// @inheritdoc IEngine - function isPriceBelow( - bytes32 _assetId, - int64 _price, - uint64 _confidenceInterval - ) public view override returns (bool) { - PythStructs.Price memory priceData = ORACLE.getPrice(_assetId); + function isPriceBelow(uint128 _marketId, uint256 _price, int128 _size) + public + view + override + returns (bool) + { + (, uint256 fillPrice) = PERPS_MARKET_PROXY.computeOrderFees({ + marketId: _marketId, + sizeDelta: _size + }); - /// @dev although counterintuitive, a smaller confidence interval is more accurate. - /// The Engine must ensure the current confidence interval is not - /// greater (i.e. less accurate) than the confidence interval defined by the condition. - return priceData.price < _price && priceData.conf <= _confidenceInterval; + return fillPrice < _price; } /// @inheritdoc IEngine diff --git a/src/interfaces/IEngine.sol b/src/interfaces/IEngine.sol index 902807db..1b079dec 100644 --- a/src/interfaces/IEngine.sol +++ b/src/interfaces/IEngine.sol @@ -3,14 +3,15 @@ pragma solidity 0.8.20; import {IPerpsMarketProxy} from "src/interfaces/synthetix/IPerpsMarketProxy.sol"; -/// @title Kwenta Smart Margin v3: Engine Interface +/// @title Kwenta Smart Margin v3: Engine Interface] +/// @notice Conditional Order -> "co" /// @author JaredBorders (jaredborders@pm.me) interface IEngine { /*////////////////////////////////////////////////////////////// TYPES //////////////////////////////////////////////////////////////*/ - /// @notice order details used to create an order on a perps market within a conditional order + /// @notice order details used to create an order on a perps market within a co struct OrderDetails { // order market id uint128 marketId; @@ -30,21 +31,21 @@ interface IEngine { address referrer; } - /// @notice conditional order + /// @notice co struct ConditionalOrder { // order details OrderDetails orderDetails; // address of the signer of the order address signer; - // an incrementing value indexed per order + // a means to prevent replay attacks and identify the order uint256 nonce; // option to require all extra conditions to be verified on-chain bool requireVerified; - // address that can execute the order if requireVerified is false + // address that can execute the order *if* requireVerified is false address trustedExecutor; - // max fee denominated in ETH that can be paid to the executor + // max fee denominated in sUSD that can be paid to the executor uint256 maxExecutorFee; - // array of extra conditions to be met + // array of extra conditions to be met on-chain *if* requireVerified is true bytes[] conditions; } @@ -71,15 +72,12 @@ 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(); - - /// @notice thrown when attempting to deposit eth into an account that does not exist + /// @notice thrown when attempting to deposit sUSD into an account that does not exist error AccountDoesNotExist(); + /// @notice thrown when attempting to debit more sUSD from the Engine than the account has been credited + error InsufficientCredit(); + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -92,20 +90,20 @@ interface IEngine { uint128 indexed accountId, uint256 word, uint256 mask ); - /// @notice emitted when eth is deposited into the engine and credited to an account + /// @notice emitted when sUSD 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); + /// @param amount the amount of sUSD deposited + event Credited(uint128 indexed accountId, uint256 amount); - /// @notice emitted when eth is withdrawn from the engine and debited from an account + /// @notice emitted when sUSD 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); + /// @param amount the amount of sUSD withdrawn + event Debited(uint128 indexed accountId, uint256 amount); - /// @notice emitted when a conditional order is executed + /// @notice emitted when a co 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 + /// that was defined in the co + /// @param executorFee the fee paid to the executor for executing the co event ConditionalOrderExecuted( IPerpsMarketProxy.Data order, uint256 synthetixFees, uint256 executorFee ); @@ -136,19 +134,6 @@ 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 //////////////////////////////////////////////////////////////*/ @@ -212,6 +197,19 @@ interface IEngine { address _referrer ) external returns (IPerpsMarketProxy.Data memory retOrder, uint256 fees); + /*////////////////////////////////////////////////////////////// + CREDIT MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + /// @notice tranfer sUSD into the engine and credit the account identified by the accountId + /// @param _accountId the id of the account to credit + function creditAccount(uint128 _accountId, uint256 _amount) external; + + /// @notice withdraw sUSD 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 sUSD to withdraw + function debitAccount(uint128 _accountId, uint256 _amount) external; + /*////////////////////////////////////////////////////////////// CONDITIONAL ORDER MANAGEMENT //////////////////////////////////////////////////////////////*/ @@ -219,14 +217,14 @@ interface IEngine { /// 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. + /// co's are signed objects that define an async order + /// and several 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 + /// 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. @@ -236,16 +234,21 @@ interface IEngine { /// 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. + /// If requireVerified is true, all conditions defined in the co must be satisfied *on-chain* + /// at the time of execution. + /// If requireVerified is false, the co can ONLY be executed by the trustedExecutor and the conditions + /// array is effectively unused (in the on-chain context). /// 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. + /// expected to be checked off-chain by the trustedExecutor. This saves a significant amount gas + /// and allows the trusted executor to employ additional sophisticated methods of ensuring + /// best trade execution. /// 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. + /// If requireVerified is true, the trustedExecutor is ignored/not used and + /// the conditions array becomes the source of verification imposed on-chain. /// 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 + /// successfully executes the co (trustedExecutor or not). This max fee is denominated in sUSD 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. @@ -255,6 +258,7 @@ interface IEngine { /// Conditions are stictly limited selectors defined in the Engine contract /// (ex: isTimestampBeforeSelector, isPriceAboveSelector, etc.) /// + /// /// 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. @@ -263,8 +267,8 @@ interface IEngine { /// 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) + /// 2. Check if the account has sufficient sUSD credit to pay the fee + /// (see CREDIT 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 @@ -272,36 +276,37 @@ interface IEngine { /// 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. + /// that returns true or false. If canExecute returns true, the co can be executed assuming the context of + /// the check(s) is/are reliable. /// 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(). /// + /// note: It is recommended to attempt simulating the co execution prior to submission + /// or employ some other sophisticated strategy to mitigate the risk of submitting a co that + /// cannot be executed due to internal Synthetix v3 scenarios/contexts that are *unpredictable*. + /// /// 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. + /// The Engine does hold and account for sUSD credit and can modify the sUSD credit of an account. /// - /// ETH Management: - /// With the introduction of co's, the Engine contract now holds ETH credit for accounts. + /// Credit Management: + /// With the introduction of co's, the Engine contract now holds sUSD 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. + /// holds sUSD credit for accounts. This sUSD 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. + /// collateral to pay the fee. - /// @custom:docs for more in-depth documentation of conditional order mechanism, + /// @custom:docs for more in-depth documentation of co 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 + /// @notice execute a co + /// @param _co the co + /// @param _signature the signature of the co + /// @param _fee the fee paid to executor for the co /// @return retOrder the order committed /// @return synthetixFees the fees paid for the order to Synthetix /// and *NOT* the fees paid to the executor @@ -313,12 +318,14 @@ interface IEngine { external 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 - /// @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 + /// @notice checks if the co can be executed + /// @param _co the co which details the order to be executed and the conditions to be met + /// @param _signature the signature of the co + /// @param _fee the executor specified fee for the executing the co + /// @dev if the fee is greater than the maxExecutorFee defined in the co, + /// or if the account lacks sufficient sUSD credit to pay the fee, canExecute will return false + /// @custom:warning this function may return false-positive results in the case the underlying Synthetix Perps v3 + /// market is in a state that is not predictable (ex: unpredictable updates to the market's simulated fill price) /// @return true if the order can be executed, false otherwise function canExecute( ConditionalOrder calldata _co, @@ -326,30 +333,30 @@ interface IEngine { uint256 _fee ) external view returns (bool); - /// @notice verify the conditional order signer is the owner or delegate of the account - /// @param _co the conditional order + /// @notice verify the co signer is the owner or delegate of the account + /// @param _co the co /// @return true if the signer is the owner or delegate of the account function verifySigner(ConditionalOrder calldata _co) external view returns (bool); - /// @notice verify the signature of the conditional order - /// @param _co the conditional order - /// @param _signature the signature of the conditional order + /// @notice verify the signature of the co + /// @param _co the co + /// @param _signature the signature of the co /// @return true if the signature is valid function verifySignature( ConditionalOrder calldata _co, bytes calldata _signature ) external view returns (bool); - /// @notice verify array of conditions defined in the conditional order + /// @notice verify array of conditions defined in the co /// @dev - /// 1. all conditions are defined by the conditional order creator + /// 1. all conditions are defined by the co creator /// 2. conditions are encoded function selectors and parameters /// 3. each function defined in the condition contract must return a truthy value /// 4. internally, staticcall is used to protect against malicious conditions - /// @param _co the conditional order + /// @param _co the co /// @return true if all conditions are met function verifyConditions(ConditionalOrder calldata _co) external @@ -360,6 +367,18 @@ interface IEngine { CONDITIONS //////////////////////////////////////////////////////////////*/ + /// DISCLAIMER: + /// Take note that if a trusted party is authorized to execute a co, then the trader + /// does not actually need to specify any conditions. In a contrived example, the trader + /// could simply "tell" the trusted party to execute the co if the price of BTC is above/below some number. + /// The trusted party would then check the price of BTC (via whatever method deemed necessary) + /// and execute the co. + /// This is a very simple example, but it illustrates the flexibility of the co + /// along with the degree of trust that will be placed in the trusted party. + /// Finally, it is expected that despite the conditions array being unnecessary in *this* context, + /// it will likely still be used to provide additional context to the trusted party. + /// However, *again*, it is not required. + /// @notice determine if current timestamp is after the given timestamp /// @param _timestamp the timestamp to compare against /// @return true if current timestamp is after the given `_timestamp`, false otherwise @@ -376,53 +395,29 @@ interface IEngine { view returns (bool); - /// @notice determine if the current price of an asset is above a given price - /// @dev assets price is determined by the pyth oracle - /// @param _assetId id of an asset to check the price of + /// @notice determine if the simulated fill price is above a given price + /// @dev relies on Synthetix Perps v3 market's simulated fill price + /// @param _marketId id a market used to check the price of the + /// underlying asset of that market (i.e. BTC Perp Market -> BTC) /// @param _price the price to compare against - /// @param _confidenceInterval roughly corresponds to the standard error of a normal distribution - /// - /// example: - /// given: - /// PythStructs.Price.expo (expo) = -5 - /// confidenceInterval (conf) = 1500 - /// PythStructs.Price.price (price) = 12276250 - /// - /// conf * 10^(expo) = 1500 * 10^(-5) = $0.015 - /// price * 10^(-5) = 12276250 * 10^(-5) = $122.7625 - /// - /// thus, the price of the asset is $122.7625 +/- $0.015 - /// - /// @return true if the current price of the asset is above the given `_price`, false otherwise - function isPriceAbove( - bytes32 _assetId, - int64 _price, - uint64 _confidenceInterval - ) external view returns (bool); + /// @param _size the order size to use for the simulated fill price + /// @return true if the simulated fill price is above the given `_price`, false otherwise + function isPriceAbove(uint128 _marketId, uint256 _price, int128 _size) + external + view + returns (bool); - /// @notice determine if the current price of an asset is below a given price - /// @dev assets price is determined by the pyth oracle - /// @param _assetId id of an asset to check the price of + /// @notice determine if the simulated fill price is below a given price + /// @dev relies on Synthetix Perps v3 market's simulated fill price + /// @param _marketId id a market used to check the price of the + /// underlying asset of that market (i.e. BTC Perp Market -> BTC) /// @param _price the price to compare against - /// @param _confidenceInterval roughly corresponds to the standard error of a normal distribution - /// - /// example: - /// given: - /// PythStructs.Price.expo (expo) = -5 - /// confidenceInterval (conf) = 1500 - /// PythStructs.Price.price (price) = 12276250 - /// - /// conf * 10^(expo) = 1500 * 10^(-5) = $0.015 - /// price * 10^(-5) = 12276250 * 10^(-5) = $122.7625 - /// - /// thus, the price of the asset is $122.7625 +/- $0.015 - /// - /// @return true if the current price of the asset is below the given `_price`, false otherwise - function isPriceBelow( - bytes32 _assetId, - int64 _price, - uint64 _confidenceInterval - ) external view returns (bool); + /// @param _size the order size to use for the simulated fill price + /// @return true if the simulated fill price is below the given `_price`, false otherwise + function isPriceBelow(uint128 _marketId, uint256 _price, int128 _size) + external + view + returns (bool); /// @notice can market accept non close-only orders (i.e. is the market open) /// @dev if maxMarketSize to 0, the market will be in a close-only state diff --git a/src/interfaces/oracles/IPyth.sol b/src/interfaces/oracles/IPyth.sol deleted file mode 100644 index 3376d286..00000000 --- a/src/interfaces/oracles/IPyth.sol +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.20; - -/// @title Consume prices from the Pyth Network (https://pyth.network/). -/// @dev Please refer to the guidance at https://docs.pyth.network/consumers/best-practices for how to consume prices safely. -/// @author Pyth Data Association -interface IPyth { - /// @dev Emitted when an update for price feed with `id` is processed successfully. - /// @param id The Pyth Price Feed ID. - /// @param fresh True if the price update is more recent and stored. - /// @param chainId ID of the source chain that the batch price update containing this price. - /// This value comes from Wormhole, and you can find the corresponding chains at https://docs.wormholenetwork.com/wormhole/contracts. - /// @param sequenceNumber Sequence number of the batch price update containing this price. - /// @param lastPublishTime Publish time of the previously stored price. - /// @param publishTime Publish time of the given price update. - /// @param price Price of the given price update. - /// @param conf Confidence interval of the given price update. - event PriceFeedUpdate( - bytes32 indexed id, - bool indexed fresh, - uint16 chainId, - uint64 sequenceNumber, - uint256 lastPublishTime, - uint256 publishTime, - int64 price, - uint64 conf - ); - - /// @dev Emitted when a batch price update is processed successfully. - /// @param chainId ID of the source chain that the batch price update comes from. - /// @param sequenceNumber Sequence number of the batch price update. - /// @param batchSize Number of prices within the batch price update. - /// @param freshPricesInBatch Number of prices that were more recent and were stored. - event BatchPriceFeedUpdate( - uint16 chainId, - uint64 sequenceNumber, - uint256 batchSize, - uint256 freshPricesInBatch - ); - - /// @dev Emitted when a call to `updatePriceFeeds` is processed successfully. - /// @param sender Sender of the call (`msg.sender`). - /// @param batchCount Number of batches that this function processed. - /// @param fee Amount of paid fee for updating the prices. - event UpdatePriceFeeds( - address indexed sender, uint256 batchCount, uint256 fee - ); - - /// @notice Returns the period (in seconds) that a price feed is considered valid since its publish time - function getValidTimePeriod() - external - view - returns (uint256 validTimePeriod); - - /// @notice Returns the price and confidence interval. - /// @dev Reverts if the price has not been updated within the last `getValidTimePeriod()` seconds. - /// @param id The Pyth Price Feed ID of which to fetch the price and confidence interval. - /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. - function getPrice(bytes32 id) - external - view - returns (PythStructs.Price memory price); - - /// @notice Returns the exponentially-weighted moving average price and confidence interval. - /// @dev Reverts if the EMA price is not available. - /// @param id The Pyth Price Feed ID of which to fetch the EMA price and confidence interval. - /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. - function getEmaPrice(bytes32 id) - external - view - returns (PythStructs.Price memory price); - - /// @notice Returns the price of a price feed without any sanity checks. - /// @dev This function returns the most recent price update in this contract without any recency checks. - /// This function is unsafe as the returned price update may be arbitrarily far in the past. - /// - /// Users of this function should check the `publishTime` in the price to ensure that the returned price is - /// sufficiently recent for their application. If you are considering using this function, it may be - /// safer / easier to use either `getPrice` or `getPriceNoOlderThan`. - /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. - function getPriceUnsafe(bytes32 id) - external - view - returns (PythStructs.Price memory price); - - /// @notice Returns the price that is no older than `age` seconds of the current time. - /// @dev This function is a sanity-checked version of `getPriceUnsafe` which is useful in - /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently - /// recently. - /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. - function getPriceNoOlderThan(bytes32 id, uint256 age) - external - view - returns (PythStructs.Price memory price); - - /// @notice Returns the exponentially-weighted moving average price of a price feed without any sanity checks. - /// @dev This function returns the same price as `getEmaPrice` in the case where the price is available. - /// However, if the price is not recent this function returns the latest available price. - /// - /// The returned price can be from arbitrarily far in the past; this function makes no guarantees that - /// the returned price is recent or useful for any particular application. - /// - /// Users of this function should check the `publishTime` in the price to ensure that the returned price is - /// sufficiently recent for their application. If you are considering using this function, it may be - /// safer / easier to use either `getEmaPrice` or `getEmaPriceNoOlderThan`. - /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. - function getEmaPriceUnsafe(bytes32 id) - external - view - returns (PythStructs.Price memory price); - - /// @notice Returns the exponentially-weighted moving average price that is no older than `age` seconds - /// of the current time. - /// @dev This function is a sanity-checked version of `getEmaPriceUnsafe` which is useful in - /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently - /// recently. - /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. - function getEmaPriceNoOlderThan(bytes32 id, uint256 age) - external - view - returns (PythStructs.Price memory price); - - /// @notice Update price feeds with given update messages. - /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling - /// `getUpdateFee` with the length of the `updateData` array. - /// Prices will be updated if they are more recent than the current stored prices. - /// The call will succeed even if the update is not the most recent. - /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid. - /// @param updateData Array of price update data. - function updatePriceFeeds(bytes[] calldata updateData) external payable; - - /// @notice Wrapper around updatePriceFeeds that rejects fast if a price update is not necessary. A price update is - /// necessary if the current on-chain publishTime is older than the given publishTime. It relies solely on the - /// given `publishTimes` for the price feeds and does not read the actual price update publish time within `updateData`. - /// - /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling - /// `getUpdateFee` with the length of the `updateData` array. - /// - /// `priceIds` and `publishTimes` are two arrays with the same size that correspond to senders known publishTime - /// of each priceId when calling this method. If all of price feeds within `priceIds` have updated and have - /// a newer or equal publish time than the given publish time, it will reject the transaction to save gas. - /// Otherwise, it calls updatePriceFeeds method to update the prices. - /// - /// @dev Reverts if update is not needed or the transferred fee is not sufficient or the updateData is invalid. - /// @param updateData Array of price update data. - /// @param priceIds Array of price ids. - /// @param publishTimes Array of publishTimes. `publishTimes[i]` corresponds to known `publishTime` of `priceIds[i]` - function updatePriceFeedsIfNecessary( - bytes[] calldata updateData, - bytes32[] calldata priceIds, - uint64[] calldata publishTimes - ) external payable; - - /// @notice Returns the required fee to update an array of price updates. - /// @param updateDataSize Number of price updates. - /// @return feeAmount The required fee in Wei. - function getUpdateFee(uint256 updateDataSize) - external - view - returns (uint256 feeAmount); -} - -contract PythStructs { - // A price with a degree of uncertainty, represented as a price +- a confidence interval. - // - // The confidence interval roughly corresponds to the standard error of a normal distribution. - // Both the price and confidence are stored in a fixed-point numeric representation, - // `x * (10^expo)`, where `expo` is the exponent. - // - // Please refer to the documentation at https://docs.pyth.network/consumers/best-practices for how - // to how this price safely. - struct Price { - // Price - int64 price; - // Confidence interval around the price - uint64 conf; - // Price exponent - int32 expo; - // Unix timestamp describing when the price was published - uint256 publishTime; - } - - // PriceFeed represents a current aggregate price from pyth publisher feeds. - struct PriceFeed { - // The price ID. - bytes32 id; - // Latest available price - Price price; - // Latest available exponentially-weighted moving average price - Price emaPrice; - } -} diff --git a/src/interfaces/synthetix/IERC7412.sol b/src/interfaces/synthetix/IERC7412.sol index 24ea8c9a..d4171061 100644 --- a/src/interfaces/synthetix/IERC7412.sol +++ b/src/interfaces/synthetix/IERC7412.sol @@ -19,12 +19,6 @@ interface IERC7412 { /// to the most recently posted oracle data transaction error FeeRequired(uint256 feeAmount); - /// @dev Returns a human-readable identifier of the oracle - /// contract. This should map to a URL and API - /// key on the client side. - /// @return The oracle identifier. - function oracleId() external view returns (bytes32); - /// @dev Upon resolving the oracle query, the client should /// call this function to post the data to the /// blockchain. diff --git a/src/interfaces/synthetix/IPerpsMarketProxy.sol b/src/interfaces/synthetix/IPerpsMarketProxy.sol index 21a38130..ea32851b 100644 --- a/src/interfaces/synthetix/IPerpsMarketProxy.sol +++ b/src/interfaces/synthetix/IPerpsMarketProxy.sol @@ -9,10 +9,6 @@ interface IPerpsMarketProxy { ACCOUNT MODULE //////////////////////////////////////////////////////////////*/ - /// @notice Mints an account token with an available id to `msg.sender`. - /// Emits a {AccountCreated} event. - function createAccount() external returns (uint128 accountId); - /// @notice Returns the address that owns a given account, as recorded by the system. /// @param accountId The account id whose owner is being retrieved. /// @return owner The owner of the given account id. @@ -21,37 +17,6 @@ interface IPerpsMarketProxy { view returns (address owner); - /// @notice Returns the address for the account token used by the module. - /// @return accountNftToken The address of the account token. - function getAccountTokenAddress() - external - view - returns (address accountNftToken); - - /// @notice Grants `permission` to `user` for account `accountId`. - /// @param accountId The id of the account that granted the permission. - /// @param permission The bytes32 identifier of the permission. - /// @param user The target address that received the permission. - /// @dev `msg.sender` must own the account token with ID `accountId` or have the "admin" permission. - /// @dev Emits a {PermissionGranted} event. - function grantPermission( - uint128 accountId, - bytes32 permission, - address user - ) external; - - /// @notice Revokes `permission` from `user` for account `accountId`. - /// @param accountId The id of the account that revoked the permission. - /// @param permission The bytes32 identifier of the permission. - /// @param user The target address that no longer has the permission. - /// @dev `msg.sender` must own the account token with ID `accountId` or have the "admin" permission. - /// @dev Emits a {PermissionRevoked} event. - function revokePermission( - uint128 accountId, - bytes32 permission, - address user - ) external; - /// @notice Returns `true` if `user` has been granted `permission` for account `accountId`. /// @param accountId The id of the account whose permission is being queried. /// @param permission The bytes32 identifier of the permission. @@ -108,18 +73,6 @@ interface IPerpsMarketProxy { external returns (Data memory retOrder, uint256 fees); - /// @notice For a given market, account id, and a position size, returns the required total account margin for this order to succeed - /// @dev Useful for integrators to determine if an order will succeed or fail - /// @param accountId id of the trader account. - /// @param marketId id of the market. - /// @param sizeDelta size of position. - /// @return requiredMargin margin required for the order to succeed. - function requiredMarginForOrder( - uint128 accountId, - uint128 marketId, - int128 sizeDelta - ) external view returns (uint256 requiredMargin); - /// @notice Simulates what the order fee would be for the given market with the specified size. /// @dev Note that this does not include the settlement reward fee, which is based on the strategy type used /// @param marketId id of the market. @@ -145,23 +98,6 @@ interface IPerpsMarketProxy { int256 amountDelta ) external; - /// @notice Gets the account's collateral value for a specific collateral. - /// @param accountId Id of the account. - /// @param synthMarketId Id of the synth market used as collateral. Synth market id, 0 for snxUSD. - /// @return collateralValue collateral value of the account. - function getCollateralAmount(uint128 accountId, uint128 synthMarketId) - external - view - returns (uint256); - - /// @notice Gets the account's total collateral value. - /// @param accountId Id of the account. - /// @return collateralValue total collateral value of the account. USD denominated. - function totalCollateralValue(uint128 accountId) - external - view - returns (uint256); - /// @notice Gets the details of an open position. /// @param accountId Id of the account. /// @param marketId Id of the position market. @@ -173,34 +109,6 @@ interface IPerpsMarketProxy { view returns (int256 totalPnl, int256 accruedFunding, int128 positionSize); - /// @notice Gets the available margin of an account. It can be negative due to pnl. - /// @param accountId Id of the account. - /// @return availableMargin available margin of the position. - function getAvailableMargin(uint128 accountId) - external - view - returns (int256 availableMargin); - - /// @notice Gets the exact withdrawable amount a trader has available from this account while holding the account's current positions. - /// @param accountId Id of the account. - /// @return withdrawableMargin available margin to withdraw. - function getWithdrawableMargin(uint128 accountId) - external - view - returns (int256 withdrawableMargin); - - /// @notice Gets the initial/maintenance margins across all positions that an account has open. - /// @param accountId Id of the account. - /// @return requiredInitialMargin initial margin req (used when withdrawing collateral). - /// @return requiredMaintenanceMargin maintenance margin req (used to determine liquidation threshold). - function getRequiredMargins(uint128 accountId) - external - view - returns ( - uint256 requiredInitialMargin, - uint256 requiredMaintenanceMargin - ); - /*////////////////////////////////////////////////////////////// PERPS MARKET MODULE //////////////////////////////////////////////////////////////*/ diff --git a/src/libraries/MathLib.sol b/src/libraries/MathLib.sol index fb634509..3a67ee2b 100644 --- a/src/libraries/MathLib.sol +++ b/src/libraries/MathLib.sol @@ -45,17 +45,6 @@ library MathLib { } } - /// @notice cast uint256 to uint128 - /// @dev asserts that input is not greater than uint128 max - /// @param x unsigned 256-bit number - /// @return downcasted uint128 from uint256 - function castU128(uint256 x) internal pure returns (uint128) { - if (x > type(uint128).max) { - revert OverflowU128(); - } - return uint128(x); - } - /// @notice determines if input numbers have the same sign /// @dev asserts that both numbers are not zero /// @param x signed number diff --git a/src/libraries/SignatureCheckerLib.sol b/src/libraries/SignatureCheckerLib.sol index 78cbd3bc..7256ea4d 100644 --- a/src/libraries/SignatureCheckerLib.sol +++ b/src/libraries/SignatureCheckerLib.sol @@ -1,88 +1,140 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol) pragma solidity 0.8.20; -/// @notice Signature verification helper that supports both ECDSA signatures from EOAs -/// and ERC1271 signatures from smart contract wallets like Argent and Gnosis safe. -/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/SignatureCheckerLib.sol) -/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/SignatureChecker.sol) +/// @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. /// -/// @dev Note: Unlike ECDSA signatures, contract signatures are revocable. -/// -/// WARNING! Do NOT use signatures as unique identifiers. -/// Please use EIP712 with a nonce included in the digest to prevent replay attacks. -/// This implementation does NOT check if a signature is non-malleable. +/// These functions can be used to verify that a message was signed by the holder +/// of the private keys of a given address. +/// @author OpenZeppelin library SignatureCheckerLib { - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* SIGNATURE CHECKING OPERATIONS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS + } + + /// @dev The signature derives the `address(0)`. + error ECDSAInvalidSignature(); + + /// @dev The signature has an invalid length. + error ECDSAInvalidSignatureLength(uint256 length); - /// @dev Returns whether `signature` is valid for `signer` and `hash`. - /// If `signer` is a smart contract, the signature is validated with ERC1271. - /// Otherwise, the signature is validated with `ECDSA.recover`. - function isValidSignatureNowCalldata( - bytes calldata signature, - bytes32 hash, - address signer - ) internal view returns (bool isValid) { - /// @solidity memory-safe-assembly - assembly { - // Clean the upper 96 bits of `signer` in case they are dirty. - for { signer := shr(96, shl(96, signer)) } signer {} { - let m := mload(0x40) - if eq(signature.length, 65) { - mstore(0x00, hash) - mstore( - 0x20, byte(0, calldataload(add(signature.offset, 0x40))) - ) // `v`. - calldatacopy(0x40, signature.offset, 0x40) // `r`, `s`. - let t := - staticcall( - gas(), // Amount of gas left for the transaction. - 1, // Address of `ecrecover`. - 0x00, // Start of input. - 0x80, // Size of input. - 0x01, // Start of output. - 0x20 // Size of output. - ) - // `returndatasize()` will be `0x20` upon success, and `0x00` otherwise. - if iszero( - or(iszero(returndatasize()), xor(signer, mload(t))) - ) { - isValid := 1 - mstore(0x60, 0) // Restore the zero slot. - mstore(0x40, m) // Restore the free memory pointer. - break - } - } - mstore(0x60, 0) // Restore the zero slot. - mstore(0x40, m) // Restore the free memory pointer. + /// @dev The signature has an S value that is in the upper half order. + error ECDSAInvalidSignatureS(bytes32 s); - let f := shl(224, 0x1626ba7e) - mstore(m, f) // `bytes4(keccak256("isValidSignature(bytes32,bytes)"))`. - mstore(add(m, 0x04), hash) - let d := add(m, 0x24) - mstore(d, 0x40) // The offset of the `signature` in the calldata. - mstore(add(m, 0x44), signature.length) - // Copy the `signature` over. - calldatacopy(add(m, 0x64), signature.offset, signature.length) - // forgefmt: disable-next-item - isValid := and( - // Whether the returndata is the magic value `0x1626ba7e` (left-aligned). - eq(mload(d), f), - // Whether the staticcall does not revert. - // This must be placed at the end of the `and` clause, - // as the arguments are evaluated from right to left. - staticcall( - gas(), // Remaining gas. - signer, // The `signer` address. - m, // Offset of calldata in memory. - add(signature.length, 0x64), // Length of calldata in memory. - d, // Offset of returndata. - 0x20 // Length of returndata to write. - ) - ) - break + /// @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not + /// return address(0) without also returning an error description. Errors are documented using an enum (error type) + /// and a bytes32 providing additional information about the error. + /// + /// If no error is returned, then the address can be used for verification purposes. + /// + /// The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + /// this function rejects them by requiring the `s` value to be in the lower + /// half order, and the `v` value to be either 27 or 28. + /// + /// IMPORTANT: `hash` _must_ be the result of a hash operation for the + /// verification to be secure: it is possible to craft signatures that + /// recover to arbitrary addresses for non-hashed data. A safe way to ensure + /// this is by receiving a hash of the original message (which may otherwise + /// be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + /// + /// Documentation for signature generation: + /// - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + /// - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + function tryRecover(bytes32 hash, bytes memory signature) + internal + pure + returns (address, RecoverError, bytes32) + { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + /// @solidity memory-safe-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) } + return tryRecover(hash, v, r, s); + } else { + return ( + address(0), + RecoverError.InvalidSignatureLength, + bytes32(signature.length) + ); + } + } + + /// @dev Returns the address that signed a hashed message (`hash`) with + /// `signature`. This address can then be used for verification purposes. + /// + /// The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + /// this function rejects them by requiring the `s` value to be in the lower + /// half order, and the `v` value to be either 27 or 28. + /// + /// IMPORTANT: `hash` _must_ be the result of a hash operation for the + /// verification to be secure: it is possible to craft signatures that + /// recover to arbitrary addresses for non-hashed data. A safe way to ensure + /// this is by receiving a hash of the original message (which may otherwise + /// be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + function recover(bytes32 hash, bytes memory signature) + internal + pure + returns (address) + { + (address recovered, RecoverError error, bytes32 errorArg) = + tryRecover(hash, signature); + _throwError(error, errorArg); + return recovered; + } + + /// @dev Overload of {ECDSA-tryRecover} that receives + /// the `v`, `r` and `s` signature fields separately. + function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) + internal + pure + returns (address, RecoverError, bytes32) + { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if ( + uint256(s) + > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0 + ) { + return (address(0), RecoverError.InvalidSignatureS, s); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature, bytes32(0)); + } + + return (signer, RecoverError.NoError, bytes32(0)); + } + + /// @dev Optionally reverts with the corresponding custom error according to the `error` argument provided. + function _throwError(RecoverError error, bytes32 errorArg) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert ECDSAInvalidSignature(); + } else if (error == RecoverError.InvalidSignatureLength) { + revert ECDSAInvalidSignatureLength(uint256(errorArg)); + } else if (error == RecoverError.InvalidSignatureS) { + revert ECDSAInvalidSignatureS(errorArg); } } } diff --git a/src/utils/EIP712.sol b/src/utils/EIP712.sol index 01b4b1fc..8e3cf3d5 100644 --- a/src/utils/EIP712.sol +++ b/src/utils/EIP712.sol @@ -102,15 +102,16 @@ contract EIP712 { view returns (bytes32 digest) { - bytes32 separator = _cachedDomainSeparator; + // We will use `digest` to store the domain separator to save a bit of gas. + digest = _cachedDomainSeparator; if (_cachedDomainSeparatorInvalidated()) { - separator = _buildDomainSeparator(); + digest = _buildDomainSeparator(); } /// @solidity memory-safe-assembly assembly { // Compute the digest. mstore(0x00, 0x1901000000000000) // Store "\x19\x01". - mstore(0x1a, separator) // Store the domain separator. + mstore(0x1a, digest) // Store the domain separator. mstore(0x3a, structHash) // Store the struct hash. digest := keccak256(0x18, 0x42) // Restore the part of the free memory slot that was overwritten. @@ -151,13 +152,14 @@ contract EIP712 { /// @dev Returns the EIP-712 domain separator. function _buildDomainSeparator() private view returns (bytes32 separator) { - bytes32 nameHash = _cachedNameHash; + // We will use `separator` to store the name hash to save a bit of gas. + separator = _cachedNameHash; bytes32 versionHash = _cachedVersionHash; /// @solidity memory-safe-assembly assembly { let m := mload(0x40) // Load the free memory pointer. mstore(m, _DOMAIN_TYPEHASH) - mstore(add(m, 0x20), nameHash) + mstore(add(m, 0x20), separator) mstore(add(m, 0x40), versionHash) mstore(add(m, 0x60), chainid()) mstore(add(m, 0x80), address()) diff --git a/src/utils/EIP7412.sol b/src/utils/EIP7412.sol index 0897336a..d7744067 100644 --- a/src/utils/EIP7412.sol +++ b/src/utils/EIP7412.sol @@ -8,6 +8,7 @@ import {IERC7412} from "src/interfaces/synthetix/IERC7412.sol"; /// @author JaredBorders (jaredborders@pm.me) contract EIP7412 { /// @notice Fulfill an EIP-7412 oracle query + /// @dev refunds from EIP7412Implementer are *NOT* supported /// @param EIP7412Implementer The address of the EIP-7412 implementer /// @param signedOffchainData The data that was returned /// from the off-chain interface, signed by the oracle @@ -15,9 +16,8 @@ contract EIP7412 { address payable EIP7412Implementer, bytes calldata signedOffchainData ) external payable { - /// @custom:auditor this allows arbitrary calls - /// to be made to any contract. This may be a security risk. - /// Please review the contract carefully. + /// @dev given the EIP7412Implementer address is specified in the call, + /// there exists the possibility of arbitrary code execution IERC7412(EIP7412Implementer).fulfillOracleQuery{value: msg.value}( signedOffchainData ); diff --git a/src/utils/MulticallablePayable.sol b/src/utils/MulticallablePayable.sol new file mode 100644 index 00000000..2900d3cd --- /dev/null +++ b/src/utils/MulticallablePayable.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.20; + +/// @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 MulticallablePayable { + /// @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 *was* deliberately made non-payable to guard against double-spending, however + /// now it *is* payable to support multicalls including EIP7412.fulfillOracleQuery(). + /// (See: https://www.paradigm.xyz/2021/08/two-rights-might-make-a-wrong) + /// + /// In the context of the SMv3 Engine, double-spending is not possible. + /// Only EIP7412.fulfillOracleQuery() is payable, and although it uses msg.value + /// to pay for the oracle query fulfillment, it is not possible to double-spend + /// + /// 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 + payable + 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 m := add(resultsOffset, 0x40) + // Copy the current bytes from calldata to the memory. + calldatacopy( + m, + add(o, 0x20), // The offset of the current bytes' bytes. + calldataload(o) // The length of the current bytes. + ) + if iszero( + delegatecall( + gas(), address(), m, calldataload(o), codesize(), 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(m, returndatasize()) + returndatacopy(add(m, 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/src/utils/TrustedMulticallForwarder.sol b/src/utils/TrustedMulticallForwarder.sol deleted file mode 100644 index 30f1bf39..00000000 --- a/src/utils/TrustedMulticallForwarder.sol +++ /dev/null @@ -1,366 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -import { - ERC2771Forwarder, - Address -} from "lib/openzeppelin-contracts/contracts/metatx/ERC2771Forwarder.sol"; - -/* solhint-disable meta-transactions/no-msg-sender */ - -/// @title TrustedMulticallForwarder -/// @notice Aggregate results from multiple function calls -/// @dev Derived from Multicall3 -/// @dev Modified for support to bubble errors -/// @dev Multicall & Multicall2 backwards-compatible -/// @dev Aggregate methods are marked `payable` to save 24 gas per call -/// @dev Includes ERC-2771 trusted forwarder functionality -/// @author Michael Elliot -/// @author Joshua Levine -/// @author Nick Johnson -/// @author Andreas Bigger -/// @author Matt Solomon -/// @author Daniel Beal -/// @author Noah Litvin -/// @author Jared Borders -contract TrustedMulticallForwarder is - ERC2771Forwarder("trusted-multicall-forwarder") -{ - struct Call { - address target; - bytes callData; - } - - struct Call3 { - address target; - bool requireSuccess; - bytes callData; - } - - struct Call3Value { - address target; - bool requireSuccess; - uint256 value; - bytes callData; - } - - struct Result { - bool success; - bytes returnData; - } - - /// @notice Backwards-compatible call aggregation with Multicall - /// @param calls An array of Call structs - /// @return blockNumber The block number where the calls were executed - /// @return returnData An array of bytes containing the responses - function aggregate(Call[] calldata calls) - public - returns (uint256 blockNumber, bytes[] memory returnData) - { - blockNumber = block.number; - uint256 length = calls.length; - returnData = new bytes[](length); - Call calldata call; - for (uint256 i = 0; i < length;) { - bool success; - call = calls[i]; - (success, returnData[i]) = - call.target.call(abi.encodePacked(call.callData, msg.sender)); - if (!success) { - bytes memory revertData = returnData[i]; - uint256 len = revertData.length; - assembly { - revert(add(revertData, 0x20), len) - } - } - - unchecked { - ++i; - } - } - } - - /// @notice Backwards-compatible with Multicall2 - /// @notice Aggregate calls without requiring success - /// @param requireSuccess If true, require all calls to succeed - /// @param calls An array of Call structs - /// @return returnData An array of Result structs - function tryAggregate(bool requireSuccess, Call[] calldata calls) - public - returns (Result[] memory returnData) - { - uint256 length = calls.length; - returnData = new Result[](length); - Call calldata call; - for (uint256 i = 0; i < length;) { - Result memory result = returnData[i]; - call = calls[i]; - (result.success, result.returnData) = - call.target.call(abi.encodePacked(call.callData, msg.sender)); - if (requireSuccess && !result.success) { - bytes memory revertData = result.returnData; - uint256 len = revertData.length; - assembly { - revert(add(revertData, 0x20), len) - } - } - unchecked { - ++i; - } - } - } - - /// @notice Backwards-compatible with Multicall2 - /// @notice Aggregate calls and allow failures using tryAggregate - /// @param calls An array of Call structs - /// @return blockNumber The block number where the calls were executed - /// @return blockHash The hash of the block where the calls were executed - /// @return returnData An array of Result structs - function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) - public - payable - returns ( - uint256 blockNumber, - bytes32 blockHash, - Result[] memory returnData - ) - { - blockNumber = block.number; - blockHash = blockhash(block.number); - returnData = tryAggregate(requireSuccess, calls); - } - - /// @notice Backwards-compatible with Multicall2 - /// @notice Aggregate calls and allow failures using tryAggregate - /// @param calls An array of Call structs - /// @return blockNumber The block number where the calls were executed - /// @return blockHash The hash of the block where the calls were executed - /// @return returnData An array of Result structs - function blockAndAggregate(Call[] calldata calls) - public - payable - returns ( - uint256 blockNumber, - bytes32 blockHash, - Result[] memory returnData - ) - { - (blockNumber, blockHash, returnData) = tryBlockAndAggregate(true, calls); - } - - /// @notice Aggregate calls, ensuring each returns success if required - /// @param calls An array of Call3 structs - /// @return returnData An array of Result structs - function aggregate3(Call3[] calldata calls) - public - payable - returns (Result[] memory returnData) - { - uint256 length = calls.length; - returnData = new Result[](length); - Call3 calldata calli; - for (uint256 i = 0; i < length;) { - Result memory result = returnData[i]; - calli = calls[i]; - (result.success, result.returnData) = - calli.target.call(abi.encodePacked(calli.callData, msg.sender)); - if (calli.requireSuccess && !result.success) { - bytes memory revertData = result.returnData; - uint256 len = revertData.length; - assembly { - revert(add(revertData, 0x20), len) - } - } - unchecked { - ++i; - } - } - } - - /// @notice Aggregate calls with a msg value - /// @notice Reverts if msg.value is less than the sum of the call values - /// @param calls An array of Call3Value structs - /// @return returnData An array of Result structs - function aggregate3Value(Call3Value[] calldata calls) - public - payable - returns (Result[] memory returnData) - { - uint256 valAccumulator; - uint256 length = calls.length; - returnData = new Result[](length); - Call3Value calldata calli; - for (uint256 i = 0; i < length;) { - Result memory result = returnData[i]; - calli = calls[i]; - uint256 val = calli.value; - // Humanity will be a Type V Kardashev Civilization before this overflows - andreas - // ~ 10^25 Wei in existence << ~ 10^76 size uint fits in a uint256 - unchecked { - valAccumulator += val; - } - (result.success, result.returnData) = calli.target.call{value: val}( - abi.encodePacked(calli.callData, msg.sender) - ); - if (calli.requireSuccess && !result.success) { - bytes memory revertData = result.returnData; - uint256 len = revertData.length; - assembly { - revert(add(revertData, 0x20), len) - } - } - unchecked { - ++i; - } - } - // Finally, make sure the msg.value == SUM(call[0...i].value) - if (msg.value != valAccumulator) { - revert ERC2771ForwarderMismatchedValue(valAccumulator, msg.value); - } - } - - /// @notice Aggregate ForwardRequestData objects - /// @notice Reverts if msg.value does not equal the sum of the call values - /// @notice Reverts if the msg.sender is the zero address - /// @param requests An array of ForwardRequestData structs - /// @return returnData An array of Result structs - function executeBatch(ForwardRequestData[] calldata requests) - public - payable - returns (Result[] memory returnData) - { - uint256 length = requests.length; - returnData = new Result[](length); - - ForwardRequestData calldata req; - - uint256 requestsValue; - uint256 refundValue; - - for (uint256 i; i < length;) { - Result memory result = returnData[i]; - - req = requests[i]; - requestsValue += requests[i].value; - - ( - bool isTrustedForwarder, - bool active, - bool signerMatch, - address signer - ) = _validate(req); - - if (isTrustedForwarder && signerMatch && active) { - // Nonce should be used before the call to prevent reusing by reentrancy - uint256 currentNonce = _useNonce(signer); - - (result.success, result.returnData) = req.to.call{ - value: req.value, - gas: req.gas - }(abi.encodePacked(req.data, req.from)); - - /// @dev see ERC2771Forwarder._checkForwardedGas() for further details - if (gasleft() < req.gas / 63) { - assembly { - invalid() - } - } - - emit ExecutedForwardRequest( - signer, currentNonce, result.success - ); - } - - /// @notice If the call was not successful, we refund the value to the msg.sender - /// @dev unsuccessful calls are never reverted - if (!result.success) { - refundValue += requests[i].value; - } - - unchecked { - ++i; - } - } - - // The batch should revert if there's a mismatched msg.value provided - // to avoid request value tampering - if (requestsValue != msg.value) { - revert ERC2771ForwarderMismatchedValue(requestsValue, msg.value); - } - - // Some requests with value were invalid (possibly due to frontrunning). - // To avoid leaving ETH in the contract this value is refunded. - if (refundValue != 0) { - // We know msg.sender != address(0) && requestsValue == msg.value - // meaning we can ensure refundValue is not taken from the original contract's balance - // and msg.sender is a known account. - Address.sendValue(payable(msg.sender), refundValue); - } - } - - /// @notice Returns the block hash for the given block number - /// @param blockNumber The block number - function getBlockHash(uint256 blockNumber) - public - view - returns (bytes32 blockHash) - { - blockHash = blockhash(blockNumber); - } - - /// @notice Returns the block number - function getBlockNumber() public view returns (uint256 blockNumber) { - blockNumber = block.number; - } - - /// @notice Returns the block coinbase - function getCurrentBlockCoinbase() public view returns (address coinbase) { - coinbase = block.coinbase; - } - - /// @notice Returns the block prevrandao - function getPrevRandao() public view returns (uint256 prevrandao) { - prevrandao = block.prevrandao; - } - - /// @notice Returns the block gas limit - function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { - gaslimit = block.gaslimit; - } - - /// @notice Returns the block timestamp - function getCurrentBlockTimestamp() - public - view - returns (uint256 timestamp) - { - timestamp = block.timestamp; - } - - /// @notice Returns the (ETH) balance of a given address - function getEthBalance(address addr) - public - view - returns (uint256 balance) - { - balance = addr.balance; - } - - /// @notice Returns the block hash of the last block - function getLastBlockHash() public view returns (bytes32 blockHash) { - unchecked { - blockHash = blockhash(block.number - 1); - } - } - - /// @notice Gets the base fee of the given block - /// @notice Can revert if the BASEFEE opcode is not implemented by the given chain - function getBasefee() public view returns (uint256 basefee) { - basefee = block.basefee; - } - - /// @notice Returns the chain id - function getChainId() public view returns (uint256 chainid) { - chainid = block.chainid; - } -} diff --git a/test/AsyncOrder.t.sol b/test/AsyncOrder.t.sol index f93b7e4b..762cfc84 100644 --- a/test/AsyncOrder.t.sol +++ b/test/AsyncOrder.t.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.20; -import {Bootstrap, IPerpsMarketProxy} from "test/utils/Bootstrap.sol"; +import {Bootstrap} from "test/utils/Bootstrap.sol"; +import {IEngine} from "src/interfaces/IEngine.sol"; +import {IPerpsMarketProxy} from "src/interfaces/synthetix/IPerpsMarketProxy.sol"; import {SynthetixMock} from "test/utils/mocks/SynthetixMock.sol"; contract AsyncOrderTest is Bootstrap, SynthetixMock { @@ -101,4 +103,20 @@ contract CommitOrder is AsyncOrderTest { _referrer: REFERRER }); } + + function test_commitOrder_Unauthorized() public { + vm.prank(BAD_ACTOR); + + vm.expectRevert(abi.encodeWithSelector(IEngine.Unauthorized.selector)); + + engine.commitOrder({ + _perpsMarketId: SETH_PERPS_MARKET_ID, + _accountId: accountId, + _sizeDelta: 1 ether, + _settlementStrategyId: SETTLEMENT_STRATEGY_ID, + _acceptablePrice: type(uint256).max, + _trackingCode: TRACKING_CODE, + _referrer: REFERRER + }); + } } diff --git a/test/Authentication.t.sol b/test/Authentication.t.sol index 07c90017..c8589dd1 100644 --- a/test/Authentication.t.sol +++ b/test/Authentication.t.sol @@ -42,6 +42,16 @@ contract AccountOwner is AuthenticationTest { assertFalse(isOwner); } + + function test_isAccountOwner_zero_address_caller() public { + // may happen in the context of conditional orders + // where the caller is address(0) + vm.prank(address(0)); + + bool isOwner = engine.isAccountOwner(accountId, address(0)); + + assertFalse(isOwner); + } } contract AccountDelegate is AuthenticationTest { @@ -104,4 +114,14 @@ contract AccountDelegate is AuthenticationTest { user: NEW_ACTOR }); } + + function test_isAccountDelegate_zero_address_caller() public { + // may happen in the context of conditional orders + // where the caller is address(0) + vm.prank(address(0)); + + bool isDelegate = engine.isAccountDelegate(accountId, address(0)); + + assertFalse(isDelegate); + } } diff --git a/test/ConditionalOrder.t.sol b/test/ConditionalOrder.t.sol index 9eff781c..3b3bdce0 100644 --- a/test/ConditionalOrder.t.sol +++ b/test/ConditionalOrder.t.sol @@ -1,17 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.20; -import {Bootstrap, IPerpsMarketProxy} from "test/utils/Bootstrap.sol"; +import {Bootstrap} from "test/utils/Bootstrap.sol"; import {ConditionalOrderSignature} from "test/utils/ConditionalOrderSignature.sol"; import {IEngine} from "src/interfaces/IEngine.sol"; -import {PythMock} from "test/utils/mocks/PythMock.sol"; +import {IPerpsMarketProxy} from "src/interfaces/synthetix/IPerpsMarketProxy.sol"; import {SynthetixMock} from "test/utils/mocks/SynthetixMock.sol"; contract ConditionalOrderTest is Bootstrap, ConditionalOrderSignature, - PythMock, SynthetixMock { address signer; @@ -116,7 +115,7 @@ contract CanExecute is ConditionalOrderTest { _defineConditionalOrder(); // ensure the account has no credit - assertEq(engine.ethBalances(accountId), 0); + assertEq(engine.credit(accountId), 0); // CO_FEE is non-zero, and the account has no credit bool canExec = engine.canExecute(co, signature, CO_FEE); @@ -174,6 +173,42 @@ contract CanExecute is ConditionalOrderTest { assertFalse(canExec); } + + function test_canExecute_false_require_verify_condition_not_met() public { + bytes[] memory conditions = new bytes[](1); + conditions[0] = isTimestampAfter(block.timestamp + 100); // condition not met + + orderDetails = IEngine.OrderDetails({ + marketId: SETH_PERPS_MARKET_ID, + accountId: accountId, + sizeDelta: SIZE_DELTA, + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, + isReduceOnly: false, + trackingCode: TRACKING_CODE, + referrer: REFERRER + }); + + co = IEngine.ConditionalOrder({ + orderDetails: orderDetails, + signer: signer, + nonce: 0, + requireVerified: true, + trustedExecutor: address(this), + maxExecutorFee: type(uint256).max, + conditions: conditions + }); + + signature = getConditionalOrderSignature({ + co: co, + privateKey: signerPrivateKey, + domainSeparator: engine.DOMAIN_SEPARATOR() + }); + + bool canExec = engine.canExecute(co, signature, ZERO_CO_FEE); + + assertFalse(canExec); + } } contract VerifySigner is ConditionalOrderTest { @@ -324,16 +359,6 @@ contract VerifyConditions is ConditionalOrderTest { } function test_verify_conditions_verified() public { - int64 mock_price = 173_078_000_000; - bytes32 mock_assetId = PYTH_ETH_USD_ASSET_ID; - mock_pyth_getPrice({ - pyth: address(pyth), - id: mock_assetId, - price: mock_price, - conf: 45_999_999, - expo: -8 - }); - mock_getOpenPosition({ perpsMarketProxy: address(perpsMarketProxy), accountId: accountId, @@ -344,10 +369,13 @@ contract VerifyConditions is ConditionalOrderTest { bytes[] memory conditions = new bytes[](8); conditions[0] = isTimestampAfter(0); conditions[1] = isTimestampBefore(type(uint256).max); - conditions[2] = isPriceAbove(PYTH_ETH_USD_ASSET_ID, 0, type(uint64).max); - conditions[3] = isPriceBelow( - PYTH_ETH_USD_ASSET_ID, type(int64).max, type(uint64).max - ); + conditions[2] = + isPriceAbove({_marketId: SETH_PERPS_MARKET_ID, _price: 0, _size: 0}); + conditions[3] = isPriceBelow({ + _marketId: SETH_PERPS_MARKET_ID, + _price: type(uint256).max, + _size: 0 + }); conditions[4] = isMarketOpen(SETH_PERPS_MARKET_ID); conditions[5] = isPositionSizeAbove(accountId, SETH_PERPS_MARKET_ID, 0); conditions[6] = isPositionSizeBelow( @@ -374,21 +402,16 @@ contract VerifyConditions is ConditionalOrderTest { } function test_verify_conditions_not_verified() public { - int64 mock_price = 173_078_000_000; - bytes32 mock_assetId = PYTH_ETH_USD_ASSET_ID; - mock_pyth_getPrice({ - pyth: address(pyth), - id: mock_assetId, - price: mock_price, - conf: 45_999_999, - expo: -8 - }); - bytes[] memory conditions = new bytes[](5); conditions[0] = isTimestampAfter(0); conditions[1] = isTimestampBefore(type(uint256).max); - conditions[2] = isPriceAbove(PYTH_ETH_USD_ASSET_ID, 0, type(uint64).max); - conditions[3] = isPriceBelow(PYTH_ETH_USD_ASSET_ID, 0, type(uint64).max); // false + conditions[2] = + isPriceAbove({_marketId: SETH_PERPS_MARKET_ID, _price: 0, _size: 0}); + conditions[3] = isPriceBelow({ + _marketId: SETH_PERPS_MARKET_ID, + _price: 0, // false; price not below 0 + _size: 0 + }); conditions[4] = isMarketOpen(SETH_PERPS_MARKET_ID); IEngine.OrderDetails memory orderDetails; @@ -633,8 +656,19 @@ contract Execute is ConditionalOrderTest { } contract Fee is ConditionalOrderTest { + function creditAccount() internal { + // prank ACTOR because this address has sUSD + vm.startPrank(ACTOR); + + sUSD.approve(address(engine), type(uint256).max); + + engine.creditAccount(accountId, CO_FEE); + + vm.stopPrank(); + } + function test_fee_imposed() public { - engine.depositEth{value: 1 ether}(accountId); + creditAccount(); IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, @@ -663,17 +697,17 @@ contract Fee is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - uint256 preExecutorBalance = address(this).balance; + uint256 preExecutorBalance = sUSD.balanceOf(address(this)); engine.execute(co, signature, CO_FEE); - uint256 postExecutorBalance = address(this).balance; + uint256 postExecutorBalance = sUSD.balanceOf(address(this)); assertEq(preExecutorBalance + CO_FEE, postExecutorBalance); } function test_fee_exceeds_account_credit() public { - engine.depositEth{value: CO_FEE - 1}(accountId); + creditAccount(); IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, @@ -704,11 +738,11 @@ contract Fee is ConditionalOrderTest { vm.expectRevert(IEngine.CannotExecuteOrder.selector); - engine.execute(co, signature, CO_FEE); + engine.execute(co, signature, CO_FEE + 1); } function test_fee_exceeds_maxExecutorFee() public { - engine.depositEth{value: 1 ether}(accountId); + creditAccount(); IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, @@ -782,7 +816,11 @@ contract ReduceOnly is ConditionalOrderTest { assertTrue(fees > 0); } - function test_reduce_only_zero_size() public { + function test_reduce_only_when_position_doesnt_exist() public { + /* + ensure position exists; reduce only orders cannot increase position size + */ + IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ marketId: SETH_PERPS_MARKET_ID, accountId: accountId, @@ -810,12 +848,53 @@ contract ReduceOnly is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (, uint256 fees) = engine.execute(co, signature, ZERO_CO_FEE); + vm.expectRevert(IEngine.CannotExecuteOrder.selector); - assertEq(0, fees); + engine.execute(co, signature, ZERO_CO_FEE); + } + + function test_reduce_only_zero_size_delta() public { + /* + ensure incoming size delta is non-zero + */ + + IEngine.OrderDetails memory orderDetails = IEngine.OrderDetails({ + marketId: SETH_PERPS_MARKET_ID, + accountId: accountId, + sizeDelta: 0, // zero sizeDelta + settlementStrategyId: SETTLEMENT_STRATEGY_ID, + acceptablePrice: ACCEPTABLE_PRICE, + isReduceOnly: true, + trackingCode: TRACKING_CODE, + referrer: REFERRER + }); + + IEngine.ConditionalOrder memory 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({ + co: co, + privateKey: signerPrivateKey, + domainSeparator: engine.DOMAIN_SEPARATOR() + }); + + vm.expectRevert(IEngine.CannotExecuteOrder.selector); + + engine.execute(co, signature, ZERO_CO_FEE); } function test_reduce_only_same_sign() public { + /* + ensure incoming size delta is NOT the same sign + */ + mock_getOpenPosition( address(perpsMarketProxy), accountId, SETH_PERPS_MARKET_ID, 1 ether ); @@ -847,9 +926,9 @@ contract ReduceOnly is ConditionalOrderTest { domainSeparator: engine.DOMAIN_SEPARATOR() }); - (, uint256 fees) = engine.execute(co, signature, ZERO_CO_FEE); + vm.expectRevert(IEngine.CannotExecuteOrder.selector); - assertEq(0, fees); + engine.execute(co, signature, ZERO_CO_FEE); } function test_reduce_only_truncate_size_down() public { @@ -955,68 +1034,68 @@ contract Conditions is ConditionalOrderTest { } function test_isPriceAbove() public { - int64 mock_price = 173_078_000_000; - bytes32 mock_assetId = PYTH_ETH_USD_ASSET_ID; - uint64 mock_confidenceInterval = 45_999_999; - mock_pyth_getPrice({ - pyth: address(pyth), - id: mock_assetId, - price: 173_078_000_000, - conf: mock_confidenceInterval, - expo: -8 - }); - - bool isAbove = engine.isPriceAbove( - mock_assetId, mock_price - 1, mock_confidenceInterval - ); + (, uint256 currentFillPrice) = + perpsMarketProxy.computeOrderFees(SETH_PERPS_MARKET_ID, 0); + + bool isAbove = engine.isPriceAbove({ + _marketId: SETH_PERPS_MARKET_ID, + _price: 0, + _size: 0 + }); assertTrue(isAbove); - isAbove = engine.isPriceAbove( - mock_assetId, mock_price, mock_confidenceInterval - ); + isAbove = engine.isPriceAbove({ + _marketId: SETH_PERPS_MARKET_ID, + _price: currentFillPrice, + _size: 0 + }); assertFalse(isAbove); - isAbove = engine.isPriceAbove( - mock_assetId, mock_price + 1, mock_confidenceInterval - ); + isAbove = engine.isPriceAbove({ + _marketId: SETH_PERPS_MARKET_ID, + _price: currentFillPrice + 1, + _size: 0 + }); assertFalse(isAbove); - isAbove = engine.isPriceAbove( - mock_assetId, mock_price - 1, mock_confidenceInterval - 1 - ); - assertFalse(isAbove); + isAbove = isAbove = engine.isPriceAbove({ + _marketId: SETH_PERPS_MARKET_ID, + _price: currentFillPrice - 1, + _size: 0 + }); + assertTrue(isAbove); } function test_isPriceBelow() public { - int64 mock_price = 173_078_000_000; - bytes32 mock_assetId = PYTH_ETH_USD_ASSET_ID; - uint64 mock_confidenceInterval = 45_999_999; - mock_pyth_getPrice({ - pyth: address(pyth), - id: mock_assetId, - price: 173_078_000_000, - conf: mock_confidenceInterval, - expo: -8 - }); - - bool isBelow = engine.isPriceBelow( - mock_assetId, mock_price - 1, mock_confidenceInterval - ); - assertFalse(isBelow); + (, uint256 currentFillPrice) = + perpsMarketProxy.computeOrderFees(SETH_PERPS_MARKET_ID, 0); - isBelow = engine.isPriceBelow( - mock_assetId, mock_price, mock_confidenceInterval - ); + bool isBelow = engine.isPriceBelow({ + _marketId: SETH_PERPS_MARKET_ID, + _price: type(uint256).max, + _size: 0 + }); + assertTrue(isBelow); + + isBelow = engine.isPriceBelow({ + _marketId: SETH_PERPS_MARKET_ID, + _price: currentFillPrice, + _size: 0 + }); assertFalse(isBelow); - isBelow = engine.isPriceBelow( - mock_assetId, mock_price + 1, mock_confidenceInterval - ); + isBelow = engine.isPriceBelow({ + _marketId: SETH_PERPS_MARKET_ID, + _price: currentFillPrice + 1, + _size: 0 + }); assertTrue(isBelow); - isBelow = engine.isPriceBelow( - mock_assetId, mock_price + 1, mock_confidenceInterval - 1 - ); + isBelow = isBelow = engine.isPriceBelow({ + _marketId: SETH_PERPS_MARKET_ID, + _price: currentFillPrice - 1, + _size: 0 + }); assertFalse(isBelow); } diff --git a/test/Credit.t.sol b/test/Credit.t.sol new file mode 100644 index 00000000..7dcfaed8 --- /dev/null +++ b/test/Credit.t.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.20; + +import {IEngine} from "src/interfaces/IEngine.sol"; +import {Bootstrap} from "test/utils/Bootstrap.sol"; + +contract CreditTest is Bootstrap { + event Credited(uint128 indexed accountId, uint256 amount); + event Debited(uint128 indexed accountId, uint256 amount); + + function setUp() public { + vm.rollFork(GOERLI_BLOCK_NUMBER); + initializeOptimismGoerli(); + } +} + +contract Credit is CreditTest { + function test_credit(uint256 amount) public { + vm.startPrank(ACTOR); + + sUSD.approve(address(engine), amount); + + if (amount == 0) { + string memory parameter = "amount"; + string memory reason = "Zero amount"; + + vm.expectRevert( + abi.encodeWithSelector( + InvalidParameter.selector, parameter, reason + ) + ); + + engine.creditAccount(accountId, amount); + } else if (amount > sUSD.balanceOf(ACTOR)) { + vm.expectRevert( + abi.encodeWithSelector( + InsufficientBalance.selector, amount, sUSD.balanceOf(ACTOR) + ) + ); + + engine.creditAccount(accountId, amount); + } else { + uint256 preEngineBalance = sUSD.balanceOf(address(engine)); + uint256 preActorBalance = sUSD.balanceOf(ACTOR); + + engine.creditAccount(accountId, amount); + + uint256 postEngineBalance = sUSD.balanceOf(address(engine)); + uint256 postActorBalance = sUSD.balanceOf(ACTOR); + + assert(postEngineBalance == preEngineBalance + amount); + assert(postActorBalance == preActorBalance - amount); + } + + vm.stopPrank(); + } + + function test_credit_AccountDoesNotExist() public { + assertEq( + perpsMarketProxy.getAccountOwner(type(uint128).max), address(0) + ); + + vm.expectRevert( + abi.encodeWithSelector(IEngine.AccountDoesNotExist.selector) + ); + + engine.creditAccount(type(uint128).max, AMOUNT); + } + + function test_credit_event() public { + vm.startPrank(ACTOR); + + sUSD.approve(address(engine), AMOUNT); + + vm.expectEmit(true, true, true, true); + emit Credited(accountId, AMOUNT); + + engine.creditAccount(accountId, AMOUNT); + + vm.stopPrank(); + } +} + +contract Debit is CreditTest { + function test_debit(uint256 amount) public { + vm.startPrank(ACTOR); + + sUSD.approve(address(engine), type(uint256).max); + + engine.creditAccount(accountId, AMOUNT); + + if (amount == 0) { + string memory parameter = "amount"; + string memory reason = "Zero amount"; + + vm.expectRevert( + abi.encodeWithSelector( + InvalidParameter.selector, parameter, reason + ) + ); + + engine.debitAccount(accountId, amount); + } else if (amount > engine.credit(accountId)) { + vm.expectRevert( + abi.encodeWithSelector(IEngine.InsufficientCredit.selector) + ); + + engine.debitAccount(accountId, amount); + } else { + uint256 preEngineBalance = sUSD.balanceOf(address(engine)); + uint256 preActorBalance = sUSD.balanceOf(ACTOR); + + engine.debitAccount(accountId, amount); + + uint256 postEngineBalance = sUSD.balanceOf(address(engine)); + uint256 postActorBalance = sUSD.balanceOf(ACTOR); + + assert(postEngineBalance == preEngineBalance - amount); + assert(postActorBalance == preActorBalance + amount); + } + + vm.stopPrank(); + } + + function test_debit_Unauthorized() public { + vm.startPrank(ACTOR); + + sUSD.approve(address(engine), type(uint256).max); + + engine.creditAccount(accountId, AMOUNT); + + vm.stopPrank(); + + vm.expectRevert(abi.encodeWithSelector(IEngine.Unauthorized.selector)); + + vm.prank(BAD_ACTOR); + + engine.debitAccount(accountId, AMOUNT); + } + + function test_debit_event() public { + vm.startPrank(ACTOR); + + sUSD.approve(address(engine), type(uint256).max); + + engine.creditAccount(accountId, AMOUNT); + + vm.expectEmit(true, true, true, true); + emit Debited(accountId, AMOUNT); + + engine.debitAccount(accountId, AMOUNT); + + vm.stopPrank(); + } + + function test_debit_InsufficientBalance() public { + vm.startPrank(ACTOR); + + sUSD.approve(address(engine), type(uint256).max); + + engine.creditAccount(accountId, AMOUNT); + + vm.expectRevert( + abi.encodeWithSelector(IEngine.InsufficientCredit.selector) + ); + + engine.debitAccount(accountId, AMOUNT + 1); + + vm.stopPrank(); + } +} diff --git a/test/Deployment.t.sol b/test/Deployment.t.sol index 942adf1b..4ba22af5 100644 --- a/test/Deployment.t.sol +++ b/test/Deployment.t.sol @@ -4,8 +4,6 @@ pragma solidity 0.8.20; import {Engine, Setup} from "script/Deploy.s.sol"; import {IEngine} from "src/interfaces/IEngine.sol"; import {Test} from "lib/forge-std/src/Test.sol"; -import {TrustedMulticallForwarder} from - "src/utils/TrustedMulticallForwarder.sol"; contract DeploymentTest is Test, Setup { Setup setup; @@ -15,24 +13,20 @@ contract DeploymentTest is Test, Setup { } function test_deploy() public { - (Engine engine, TrustedMulticallForwarder forwarder) = setup - .deploySystem({ + (Engine engine) = setup.deploySystem({ perpsMarketProxy: address(0x1), spotMarketProxy: address(0x2), - sUSDProxy: address(0x3), - oracle: address(0x4) + sUSDProxy: address(0x3) }); assertTrue(address(engine) != address(0x0)); - assertTrue(address(forwarder) != address(0x0)); } function test_deploy_perps_market_proxy_zero_address() public { try setup.deploySystem({ perpsMarketProxy: address(0), spotMarketProxy: address(0x2), - sUSDProxy: address(0x3), - oracle: address(0x4) + sUSDProxy: address(0x3) }) {} catch (bytes memory reason) { assertEq(bytes4(reason), IEngine.ZeroAddress.selector); } @@ -42,8 +36,7 @@ contract DeploymentTest is Test, Setup { try setup.deploySystem({ perpsMarketProxy: address(0x1), spotMarketProxy: address(0), - sUSDProxy: address(0x3), - oracle: address(0x4) + sUSDProxy: address(0x3) }) {} catch (bytes memory reason) { assertEq(bytes4(reason), IEngine.ZeroAddress.selector); } @@ -53,33 +46,7 @@ contract DeploymentTest is Test, Setup { try setup.deploySystem({ perpsMarketProxy: address(0x1), spotMarketProxy: address(0x2), - sUSDProxy: address(0), - oracle: address(0x4) - }) {} catch (bytes memory reason) { - assertEq(bytes4(reason), IEngine.ZeroAddress.selector); - } - } - - function test_deploy_oracle_zero_address() public { - try setup.deploySystem({ - perpsMarketProxy: address(0x1), - spotMarketProxy: address(0x2), - sUSDProxy: address(0x3), - oracle: address(0) - }) {} catch (bytes memory reason) { - assertEq(bytes4(reason), IEngine.ZeroAddress.selector); - } - } - - function test_deploy_trusted_forwarder_zero_address() public { - // 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) + sUSDProxy: address(0) }) {} catch (bytes memory reason) { assertEq(bytes4(reason), IEngine.ZeroAddress.selector); } diff --git a/test/EIP7412.t.sol b/test/EIP7412.t.sol index f814d798..ad498c39 100644 --- a/test/EIP7412.t.sol +++ b/test/EIP7412.t.sol @@ -2,30 +2,111 @@ pragma solidity 0.8.20; import {EIP7412} from "src/utils/EIP7412.sol"; -import {SynthetixMock} from "test/utils/mocks/SynthetixMock.sol"; -import {Test} from "lib/forge-std/src/Test.sol"; +import { + EIP7412Mock, + EIP7412MockRefund, + EIP7412MockRevert +} from "test/utils/mocks/EIP7412Mock.sol"; +import {Bootstrap} from "test/utils/Bootstrap.sol"; -contract EIP7412Test is Test, SynthetixMock { +contract EIP7412Test is Bootstrap { + EIP7412Mock eip7412Mock; + EIP7412MockRefund eip7412MockRefund; + EIP7412MockRevert eip7412MockRevert; + + function setUp() public { + vm.rollFork(GOERLI_BLOCK_NUMBER); + initializeOptimismGoerli(); + + eip7412Mock = new EIP7412Mock(); + eip7412MockRefund = new EIP7412MockRefund(); + eip7412MockRevert = new EIP7412MockRevert(); + } +} + +contract FulfillOracleQuery is EIP7412Test { function test_fulfillOracleQuery(bytes calldata signedOffchainData) public { - address payable mock_eip7412_implementer = payable(address(0xE19)); + uint256 preBalance = address(this).balance; + uint256 preBalanceeip7412Mock = address(eip7412Mock).balance; - mock_fulfillOracleQuery(mock_eip7412_implementer, signedOffchainData); + engine.fulfillOracleQuery{value: AMOUNT}( + payable(address(eip7412Mock)), signedOffchainData + ); - EIP7412 eip7412 = new EIP7412(); + assertLt(address(this).balance, preBalance); + assertEq(address(eip7412Mock).balance, preBalanceeip7412Mock + AMOUNT); + } + function test_fulfillOracleQuery_refund(bytes calldata signedOffchainData) + public + { uint256 preBalance = address(this).balance; - (bool success,) = address(eip7412).call{value: 5 wei}( - abi.encodeWithSelector( - EIP7412.fulfillOracleQuery.selector, - mock_eip7412_implementer, - signedOffchainData - ) + // refunds are not supported + vm.expectRevert("EIP7412MockRefund"); + + engine.fulfillOracleQuery{value: AMOUNT}( + payable(address(eip7412MockRefund)), signedOffchainData ); - assertTrue(success); + assert(address(this).balance == preBalance); + } + + function test_fulfillOracleQuery_revert(bytes calldata signedOffchainData) + public + { + uint256 preBalance = address(this).balance; + + vm.expectRevert("EIP7412MockRevert"); + + engine.fulfillOracleQuery{value: AMOUNT}( + payable(address(eip7412MockRevert)), signedOffchainData + ); + + assert(address(this).balance == preBalance); + } +} + +contract MulticallFulfillOracleQuery is EIP7412Test { + function test_fulfillOracleQuery_multicall( + bytes calldata signedOffchainData + ) public { + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector( + EIP7412.fulfillOracleQuery.selector, + payable(address(eip7412Mock)), + signedOffchainData + ); + + uint256 preBalance = address(this).balance; + uint256 preBalanceeip7412Mock = address(eip7412Mock).balance; + + engine.multicall{value: AMOUNT}(data); + assertLt(address(this).balance, preBalance); + assertEq(address(eip7412Mock).balance, preBalanceeip7412Mock + AMOUNT); + } + + function test_fulfillOracleQuery_multicall_double_spend( + bytes calldata signedOffchainData + ) public { + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector( + EIP7412.fulfillOracleQuery.selector, + payable(address(eip7412Mock)), + signedOffchainData + ); + data[1] = abi.encodeWithSelector( + EIP7412.fulfillOracleQuery.selector, + payable(address(eip7412Mock)), + signedOffchainData + ); + + // Reason: EvmError + vm.expectRevert(); + + engine.multicall{value: AMOUNT}(data); } } diff --git a/test/EthManagement.t.sol b/test/EthManagement.t.sol deleted file mode 100644 index a13a89bb..00000000 --- a/test/EthManagement.t.sol +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.20; - -import {Bootstrap, TrustedMulticallForwarder} from "test/utils/Bootstrap.sol"; -import {IEngine} from "src/interfaces/IEngine.sol"; -import {SynthetixMock} from "test/utils/mocks/SynthetixMock.sol"; -import {ERC2771Forwarder} from - "lib/openzeppelin-contracts/contracts/metatx/ERC2771Forwarder.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_Account_Doesnt_Exist() public { - uint128 invalidAccountId = type(uint128).max; - assert(perpsMarketProxy.getAccountOwner(invalidAccountId) == address(0)); - - vm.expectRevert(IEngine.AccountDoesNotExist.selector); - engine.depositEth{value: AMOUNT}(invalidAccountId); - } - - function test_depositEth_via_trustedForwarder() public { - TrustedMulticallForwarder.Call3Value memory call = - TrustedMulticallForwarder.Call3Value( - address(engine), - true, // requireSuccess - 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_require_success( - ) public { - TrustedMulticallForwarder.Call3Value memory call = - TrustedMulticallForwarder.Call3Value( - address(engine), - true, // requireSuccess - AMOUNT, - abi.encodeWithSelector(engine.depositEth.selector, accountId) - ); - - TrustedMulticallForwarder.Call3Value[] memory calls = - new TrustedMulticallForwarder.Call3Value[](2); - - calls[0] = call; - calls[1] = call; - - // EvmError: OutOfFund - vm.expectRevert(); - - // msg.value is AMOUNT, but since two calls spend AMOUNT each, the accumulated - // msg.value is AMOUNT * 2. This should fail and revert. - trustedForwarderContract.aggregate3Value{value: AMOUNT}(calls); - - assertEq(engine.ethBalances(accountId), 0); - } - - function test_depositEth_via_trustedForwarder_value_mismatch() public { - TrustedMulticallForwarder.Call3Value memory call = - TrustedMulticallForwarder.Call3Value( - address(engine), - false, // requireSuccess = false - AMOUNT, - abi.encodeWithSelector(engine.depositEth.selector, accountId) - ); - - TrustedMulticallForwarder.Call3Value[] memory calls = - new TrustedMulticallForwarder.Call3Value[](2); - - calls[0] = call; - calls[1] = call; - - vm.expectRevert( - abi.encodeWithSelector( - ERC2771Forwarder.ERC2771ForwarderMismatchedValue.selector, - AMOUNT * 2, - AMOUNT - ) - ); - - // msg.value is AMOUNT, but since two calls spend AMOUNT each, the accumulated - // msg.value is AMOUNT * 2. This should fail but not revert. - 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); - - if (perpsMarketProxy.getAccountOwner(fuzzedAccountId) == address(0)) { - vm.expectRevert(IEngine.AccountDoesNotExist.selector); - engine.depositEth{value: fuzzedEthAmount}(fuzzedAccountId); - } else { - 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/MathLib.t.sol b/test/MathLib.t.sol index 92cec7b8..14e2f8cc 100644 --- a/test/MathLib.t.sol +++ b/test/MathLib.t.sol @@ -78,21 +78,6 @@ contract MathLibTest is Test { } } - function test_castU128() public { - uint256 x = 1 ether; - uint128 z = x.castU128(); - assertEq(z, 1 ether); - } - - function test_castU128_overflow() public { - uint256 x = type(uint128).max; - x++; - - vm.expectRevert(abi.encodeWithSelector(MathLib.OverflowU128.selector)); - - x.castU128(); - } - function test_isSameSign() public { int128 x = -1; int128 y = -1; diff --git a/test/Multicall3.t.sol b/test/Multicall3.t.sol deleted file mode 100644 index 5400bce7..00000000 --- a/test/Multicall3.t.sol +++ /dev/null @@ -1,360 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -import {Test} from "forge-std/Test.sol"; -import { - ERC2771Forwarder, - Address -} from "lib/openzeppelin-contracts/contracts/metatx/ERC2771Forwarder.sol"; -import {TrustedMulticallForwarder} from - "src/utils/TrustedMulticallForwarder.sol"; -import {MockCallee} from "test/utils/mocks/MockCallee.sol"; -import {EtherSink} from "test/utils/mocks/EtherSink.sol"; - -contract TrustedMulticallForwarderTest is Test { - TrustedMulticallForwarder multicall; - MockCallee callee; - EtherSink etherSink; - - /// @notice Setups up the testing suite - function setUp() public { - multicall = new TrustedMulticallForwarder(); - callee = new MockCallee(); - etherSink = new EtherSink(); - } - - /// >>>>>>>>>>>>>>>>>>>>> AGGREGATE TESTS <<<<<<<<<<<<<<<<<<<<< /// - - function testAggregation() public { - // Test successful call - TrustedMulticallForwarder.Call[] memory calls = - new TrustedMulticallForwarder.Call[](1); - calls[0] = TrustedMulticallForwarder.Call( - address(callee), - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - (uint256 blockNumber, bytes[] memory returnData) = - multicall.aggregate(calls); - assertEq(blockNumber, block.number); - assertEq( - keccak256(returnData[0]), - keccak256(abi.encodePacked(blockhash(block.number))) - ); - } - - function testUnsuccessfulAggregation() public { - // Test unexpected revert - TrustedMulticallForwarder.Call[] memory calls = - new TrustedMulticallForwarder.Call[](2); - calls[0] = TrustedMulticallForwarder.Call( - address(callee), - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - calls[1] = TrustedMulticallForwarder.Call( - address(callee), abi.encodeWithSignature("thisMethodReverts()") - ); - vm.expectRevert(bytes(hex"81775cc3")); - multicall.aggregate(calls); - } - - /// >>>>>>>>>>>>>>>>>>> TRY AGGREGATE TESTS <<<<<<<<<<<<<<<<<<< /// - - function testTryAggregate() public { - TrustedMulticallForwarder.Call[] memory calls = - new TrustedMulticallForwarder.Call[](2); - calls[0] = TrustedMulticallForwarder.Call( - address(callee), - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - calls[1] = TrustedMulticallForwarder.Call( - address(callee), abi.encodeWithSignature("thisMethodReverts()") - ); - TrustedMulticallForwarder.Result[] memory returnData = - multicall.tryAggregate(false, calls); - assertTrue(returnData[0].success); - assertEq( - keccak256(returnData[0].returnData), - keccak256(abi.encodePacked(blockhash(block.number))) - ); - assertTrue(!returnData[1].success); - } - - function testTryAggregateUnsuccessful() public { - TrustedMulticallForwarder.Call[] memory calls = - new TrustedMulticallForwarder.Call[](2); - calls[0] = TrustedMulticallForwarder.Call( - address(callee), - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - calls[1] = TrustedMulticallForwarder.Call( - address(callee), abi.encodeWithSignature("thisMethodReverts()") - ); - vm.expectRevert(bytes(hex"81775cc3")); - multicall.tryAggregate(true, calls); - } - - /// >>>>>>>>>>>>>> TRY BLOCK AND AGGREGATE TESTS <<<<<<<<<<<<<< /// - - function testTryBlockAndAggregate() public { - TrustedMulticallForwarder.Call[] memory calls = - new TrustedMulticallForwarder.Call[](2); - calls[0] = TrustedMulticallForwarder.Call( - address(callee), - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - calls[1] = TrustedMulticallForwarder.Call( - address(callee), abi.encodeWithSignature("thisMethodReverts()") - ); - ( - uint256 blockNumber, - bytes32 blockHash, - TrustedMulticallForwarder.Result[] memory returnData - ) = multicall.tryBlockAndAggregate(false, calls); - assertEq(blockNumber, block.number); - assertEq(blockHash, blockhash(block.number)); - assertTrue(returnData[0].success); - assertEq( - keccak256(returnData[0].returnData), - keccak256(abi.encodePacked(blockhash(block.number))) - ); - assertTrue(!returnData[1].success); - } - - function testTryBlockAndAggregateUnsuccessful() public { - TrustedMulticallForwarder.Call[] memory calls = - new TrustedMulticallForwarder.Call[](2); - calls[0] = TrustedMulticallForwarder.Call( - address(callee), - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - calls[1] = TrustedMulticallForwarder.Call( - address(callee), abi.encodeWithSignature("thisMethodReverts()") - ); - vm.expectRevert(bytes(hex"81775cc3")); - multicall.tryBlockAndAggregate(true, calls); - } - - function testBlockAndAggregateUnsuccessful() public { - TrustedMulticallForwarder.Call[] memory calls = - new TrustedMulticallForwarder.Call[](2); - calls[0] = TrustedMulticallForwarder.Call( - address(callee), - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - calls[1] = TrustedMulticallForwarder.Call( - address(callee), abi.encodeWithSignature("thisMethodReverts()") - ); - vm.expectRevert(bytes(hex"81775cc3")); - multicall.blockAndAggregate(calls); - } - - /// >>>>>>>>>>>>>>>>>>> AGGREGATE3 TESTS <<<<<<<<<<<<<<<<<<<<<< /// - - function testAggregate3() public { - TrustedMulticallForwarder.Call3[] memory calls = - new TrustedMulticallForwarder.Call3[](3); - calls[0] = TrustedMulticallForwarder.Call3( - address(callee), - true, - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - calls[1] = TrustedMulticallForwarder.Call3( - address(callee), - false, - abi.encodeWithSignature("thisMethodReverts()") - ); - calls[2] = TrustedMulticallForwarder.Call3( - address(multicall), - false, - abi.encodeWithSignature("getCurrentBlockTimestamp()") - ); - TrustedMulticallForwarder.Result[] memory returnData = - multicall.aggregate3(calls); - - // Call 1. - assertTrue(returnData[0].success); - assertEq( - blockhash(block.number), - abi.decode(returnData[0].returnData, (bytes32)) - ); - assertEq( - keccak256(returnData[0].returnData), - keccak256(abi.encodePacked(blockhash(block.number))) - ); - - // Call 2. - assertTrue(!returnData[1].success); - assertEq(returnData[1].returnData.length, 4); - assertEq( - bytes4(returnData[1].returnData), - bytes4(keccak256("Unsuccessful()")) - ); - - // Call 3. - assertTrue(returnData[2].success); - assertEq( - abi.decode(returnData[2].returnData, (uint256)), block.timestamp - ); - } - - function testAggregate3Unsuccessful() public { - TrustedMulticallForwarder.Call3[] memory calls = - new TrustedMulticallForwarder.Call3[](2); - calls[0] = TrustedMulticallForwarder.Call3( - address(callee), - true, - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - calls[1] = TrustedMulticallForwarder.Call3( - address(callee), - true, - abi.encodeWithSignature("thisMethodReverts()") - ); - vm.expectRevert(bytes(hex"81775cc3")); - multicall.aggregate3(calls); - } - - /// >>>>>>>>>>>>>>>>> AGGREGATE3VALUE TESTS <<<<<<<<<<<<<<<<<<< /// - - function testAggregate3Value() public { - TrustedMulticallForwarder.Call3Value[] memory calls = - new TrustedMulticallForwarder.Call3Value[](3); - calls[0] = TrustedMulticallForwarder.Call3Value( - address(callee), - true, - 0, - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - calls[1] = TrustedMulticallForwarder.Call3Value( - address(callee), - false, - 0, - abi.encodeWithSignature("thisMethodReverts()") - ); - calls[2] = TrustedMulticallForwarder.Call3Value( - address(callee), - false, - 1, - abi.encodeWithSignature( - "sendBackValue(address)", address(etherSink) - ) - ); - TrustedMulticallForwarder.Result[] memory returnData = - multicall.aggregate3Value{value: 1}(calls); - assertTrue(returnData[0].success); - assertEq( - keccak256(returnData[0].returnData), - keccak256(abi.encodePacked(blockhash(block.number))) - ); - assertTrue(!returnData[1].success); - assertTrue(returnData[2].success); - } - - function testAggregate3ValueUnsuccessful() public { - TrustedMulticallForwarder.Call3Value[] memory calls = - new TrustedMulticallForwarder.Call3Value[](3); - calls[0] = TrustedMulticallForwarder.Call3Value( - address(callee), - true, - 0, - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - calls[1] = TrustedMulticallForwarder.Call3Value( - address(callee), - true, - 0, - abi.encodeWithSignature("thisMethodReverts()") - ); - calls[2] = TrustedMulticallForwarder.Call3Value( - address(callee), - true, - 1, - abi.encodeWithSignature( - "sendBackValue(address)", address(etherSink) - ) - ); - vm.expectRevert(bytes(hex"81775cc3")); - multicall.aggregate3Value{value: 1}(calls); - - // Should fail if we don't provide enough value - TrustedMulticallForwarder.Call3Value[] memory calls2 = - new TrustedMulticallForwarder.Call3Value[](1); - calls2[0] = TrustedMulticallForwarder.Call3Value( - address(callee), - false, - 1, - abi.encodeWithSignature( - "sendBackValue(address)", address(etherSink) - ) - ); - // trying to figure out how to check the actual error here but its hard - //vm.expectRevert(ERC2771Forwarder.ERC2771ForwarderMismatchedValue.selector); - vm.expectRevert(); - multicall.aggregate3Value(calls2); - - // Works if we provide enough value - TrustedMulticallForwarder.Call3Value[] memory calls3 = - new TrustedMulticallForwarder.Call3Value[](3); - calls3[0] = TrustedMulticallForwarder.Call3Value( - address(callee), - true, - 0, - abi.encodeWithSignature("getBlockHash(uint256)", block.number) - ); - calls3[1] = TrustedMulticallForwarder.Call3Value( - address(callee), - false, - 0, - abi.encodeWithSignature("thisMethodReverts()") - ); - calls3[2] = TrustedMulticallForwarder.Call3Value( - address(callee), - true, - 1, - abi.encodeWithSignature( - "sendBackValue(address)", address(etherSink) - ) - ); - multicall.aggregate3Value{value: 1}(calls3); - } - - /// >>>>>>>>>>>>>>>>>>>>>> HELPER TESTS <<<<<<<<<<<<<<<<<<<<<<< /// - - function testGetBlockHash(uint256 blockNumber) public { - assertEq(blockhash(blockNumber), multicall.getBlockHash(blockNumber)); - } - - function testGetBlockNumber() public { - assertEq(block.number, multicall.getBlockNumber()); - } - - function testGetCurrentBlockCoinbase() public { - assertEq(block.coinbase, multicall.getCurrentBlockCoinbase()); - } - - function testGetCurrentBlockGasLimit() public { - assertEq(block.gaslimit, multicall.getCurrentBlockGasLimit()); - } - - function testGetCurrentBlockTimestamp() public { - assertEq(block.timestamp, multicall.getCurrentBlockTimestamp()); - } - - function testGetEthBalance(address addr) public { - assertEq(addr.balance, multicall.getEthBalance(addr)); - } - - function testGetLastBlockHash() public { - // Prevent arithmetic underflow on the genesis block - if (block.number == 0) return; - assertEq(blockhash(block.number - 1), multicall.getLastBlockHash()); - } - - function testGetBasefee() public { - assertEq(block.basefee, multicall.getBasefee()); - } - - function testGetChainId() public { - assertEq(block.chainid, multicall.getChainId()); - } -} diff --git a/test/MulticallablePayableTest.t.sol b/test/MulticallablePayableTest.t.sol new file mode 100644 index 00000000..768c4eeb --- /dev/null +++ b/test/MulticallablePayableTest.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.20; + +import {Test} from "lib/forge-std/src/Test.sol"; +import {MockMulticallablePayable as MP} from + "./utils/mocks/MockMulticallablePayable.sol"; + +/// @author Solady +contract MulticallablePayableTest is Test { + MP mp; + + function setUp() public { + mp = new MP(); + } + + function testMulticallableRevertWithMessage(string memory revertMessage) + public + { + bytes[] memory data = new bytes[](1); + data[0] = + abi.encodeWithSelector(MP.revertsWithString.selector, revertMessage); + vm.expectRevert(bytes(revertMessage)); + mp.multicall(data); + } + + function testMulticallableRevertWithMessage() public { + testMulticallableRevertWithMessage("Milady"); + } + + function testMulticallableRevertWithCustomError() public { + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(MP.revertsWithCustomError.selector); + vm.expectRevert(MP.CustomError.selector); + mp.multicall(data); + } + + function testMulticallableRevertWithNothing() public { + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(MP.revertsWithNothing.selector); + vm.expectRevert(); + mp.multicall(data); + } + + function testMulticallableReturnDataIsProperlyEncoded( + uint256 a0, + uint256 b0, + uint256 a1, + uint256 b1 + ) public { + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector(MP.returnsTuple.selector, a0, b0); + data[1] = abi.encodeWithSelector(MP.returnsTuple.selector, a1, b1); + bytes[] memory returnedData = mp.multicall(data); + MP.Tuple memory t0 = abi.decode(returnedData[0], (MP.Tuple)); + MP.Tuple memory t1 = abi.decode(returnedData[1], (MP.Tuple)); + assertEq(t0.a, a0); + assertEq(t0.b, b0); + assertEq(t1.a, a1); + assertEq(t1.b, b1); + } + + function testMulticallableReturnDataIsProperlyEncoded( + string memory sIn0, + string memory sIn1, + uint256 n + ) public { + n = n % 2; + bytes[] memory dataIn = new bytes[](n); + if (n > 0) { + dataIn[0] = abi.encodeWithSelector(MP.returnsString.selector, sIn0); + } + if (n > 1) { + dataIn[1] = abi.encodeWithSelector(MP.returnsString.selector, sIn1); + } + bytes[] memory dataOut = mp.multicall(dataIn); + if (n > 0) { + assertEq(abi.decode(dataOut[0], (string)), sIn0); + } + if (n > 1) { + assertEq(abi.decode(dataOut[1], (string)), sIn1); + } + } + + function testMulticallableReturnDataIsProperlyEncoded() public { + testMulticallableReturnDataIsProperlyEncoded(0, 1, 2, 3); + } + + function testMulticallableBenchmark() public { + unchecked { + bytes[] memory data = new bytes[](10); + for (uint256 i; i != data.length; ++i) { + data[i] = + abi.encodeWithSelector(MP.returnsTuple.selector, i, i + 1); + } + bytes[] memory returnedData = mp.multicall(data); + assertEq(returnedData.length, data.length); + } + } + + function testMulticallableOriginalBenchmark() public { + unchecked { + bytes[] memory data = new bytes[](10); + for (uint256 i; i != data.length; ++i) { + data[i] = + abi.encodeWithSelector(MP.returnsTuple.selector, i, i + 1); + } + bytes[] memory returnedData = mp.multicallOriginal(data); + assertEq(returnedData.length, data.length); + } + } + + function testMulticallableWithNoData() public { + bytes[] memory data = new bytes[](0); + assertEq(mp.multicall(data).length, 0); + } + + function testMulticallablePreservesMsgSender() public { + address caller = address(uint160(0xbeef)); + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(MP.returnsSender.selector); + vm.prank(caller); + address returnedAddress = abi.decode(mp.multicall(data)[0], (address)); + assertEq(caller, returnedAddress); + } +} diff --git a/test/NonceBitmap.t.sol b/test/NonceBitmap.t.sol index f6c29653..f4743d4b 100644 --- a/test/NonceBitmap.t.sol +++ b/test/NonceBitmap.t.sol @@ -88,6 +88,14 @@ contract NonceBitmapTest is Bootstrap, ConditionalOrderSignature { assertFalse(canExec); } + function test_invalidateUnorderedNonces_Unauthorized() public { + vm.prank(BAD_ACTOR); + + vm.expectRevert(abi.encodeWithSelector(IEngine.Unauthorized.selector)); + + engine.invalidateUnorderedNonces(accountId, 0, type(uint256).max); + } + function test_invalidateUnorderedNonces_event() public { vm.expectEmit(true, true, true, true); emit UnorderedNonceInvalidation(accountId, 0, type(uint256).max); diff --git a/test/Signature.test.ts b/test/Signature.test.ts index c444a5a9..f543518f 100644 --- a/test/Signature.test.ts +++ b/test/Signature.test.ts @@ -41,11 +41,9 @@ describe("Signature", function () { const Engine = await ethers.getContractFactory("Engine"); const engine = await Engine.deploy( - ONE_ADDRESS, - ONE_ADDRESS, - ONE_ADDRESS, - ONE_ADDRESS, - ONE_ADDRESS + ONE_ADDRESS, // Perps Market Proxy Address + ONE_ADDRESS, // Spot Market Proxy Address + ONE_ADDRESS // sUSD Token Proxy Address ); await engine.waitForDeployment(); diff --git a/test/TrustedMulticallForwarder.t.sol b/test/TrustedMulticallForwarder.t.sol deleted file mode 100644 index fa76a7ff..00000000 --- a/test/TrustedMulticallForwarder.t.sol +++ /dev/null @@ -1,204 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -import {Test} from "../lib/forge-std/src/Test.sol"; -import {Vm} from "../lib/forge-std/src/Vm.sol"; - -import { - TrustedMulticallForwarder, - ERC2771Forwarder, - Address -} from "src/utils/TrustedMulticallForwarder.sol"; - -contract ERC2771Example { - function isTrustedForwarder(address /*forwarder*/ ) - public - pure - returns (bool) - { - return true; - } - - function ping() public payable returns (string memory) { - return "pong"; - } -} - -contract TrustedMulticallForwarderTest is Test { - // contract(s) - TrustedMulticallForwarder internal trustedMulticallForwarder; - ERC2771Example internal erc2771Example; - - // actor(s) - address internal signer; - uint256 internal signerPrivateKey = 0x1; - address internal badSigner; - uint256 internal badSignerPrivateKey = 0x2; - - function setUp() public { - // deploy contract(s) - trustedMulticallForwarder = new TrustedMulticallForwarder(); - erc2771Example = new ERC2771Example(); - - // initialize actor specific state - signer = vm.addr(signerPrivateKey); - badSigner = vm.addr(badSignerPrivateKey); - } -} - -contract ExecuteBatch is TrustedMulticallForwarderTest { - bytes32 private constant _TYPE_HASH = keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ); - - bytes32 internal constant _FORWARD_REQUEST_TYPEHASH = keccak256( - "ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,uint48 deadline,bytes data)" - ); - - function getForwardRequestDataSignatureRaw( - ERC2771Forwarder.ForwardRequestData memory request, - uint256 nonce, - uint256 privateKey, - bytes32 domainSeparator - ) internal returns (uint8 v, bytes32 r, bytes32 s) { - bytes32 msgHash = keccak256( - abi.encodePacked( - "\x19\x01", - domainSeparator, - keccak256( - abi.encode( - _FORWARD_REQUEST_TYPEHASH, - request.from, - request.to, - request.value, - request.gas, - nonce, - request.deadline, - keccak256(request.data) - ) - ) - ) - ); - - (v, r, s) = vm.sign(privateKey, msgHash); - } - - function getForwardRequestDataSignature( - ERC2771Forwarder.ForwardRequestData memory request, - uint256 nonce, - uint256 privateKey, - bytes32 domainSeparator - ) internal returns (bytes memory sig) { - (uint8 v, bytes32 r, bytes32 s) = getForwardRequestDataSignatureRaw( - request, nonce, privateKey, domainSeparator - ); - return bytes.concat(r, s, bytes1(v)); - } - - function test_executeBatch() public { - // tx details - uint256 value = 1 ether; - uint256 gas = 1 ether; - - // prepare forward request data (with empty signature) - ERC2771Forwarder.ForwardRequestData memory request = ERC2771Forwarder - .ForwardRequestData({ - from: address(signer), - to: address(erc2771Example), - value: value, - gas: gas, - deadline: type(uint48).max, - data: abi.encodeWithSelector(ERC2771Example.ping.selector), - signature: bytes("") // initially empty - }); - - // define domain separator - bytes32 domainSeparator = keccak256( - abi.encode( - _TYPE_HASH, - keccak256(bytes("trusted-multicall-forwarder")), - keccak256(bytes("1")), - block.chainid, - address(trustedMulticallForwarder) - ) - ); - - // sign forward request data - bytes memory signature = getForwardRequestDataSignature( - request, - trustedMulticallForwarder.nonces(address(signer)), - signerPrivateKey, - domainSeparator - ); - - // update forward request data object with signature - request.signature = signature; - - // define batch of forward requests - ERC2771Forwarder.ForwardRequestData[] memory batch = - new ERC2771Forwarder.ForwardRequestData[](1); - batch[0] = request; - - // execute batch - TrustedMulticallForwarder.Result[] memory results = - trustedMulticallForwarder.executeBatch{value: value, gas: gas}({ - requests: batch - }); - - // check results - assertEq(results.length, 1); - assertEq(results[0].success, true); - assertEq(results[0].returnData, abi.encode("pong")); - } - - function test_executeBatch_invalid_signature() public { - // tx details - uint256 value = 1 ether; - uint256 gas = 1 ether; - - // prepare forward request data (with empty signature) - ERC2771Forwarder.ForwardRequestData memory request = ERC2771Forwarder - .ForwardRequestData({ - from: address(signer), - to: address(erc2771Example), - value: value, - gas: gas, - deadline: type(uint48).max, - data: abi.encodeWithSelector(ERC2771Example.ping.selector), - signature: bytes("") // initially empty - }); - - // define domain separator - bytes32 domainSeparator = keccak256( - abi.encode( - _TYPE_HASH, - keccak256(bytes("trusted-multicall-forwarder")), - keccak256(bytes("1")), - block.chainid, - address(trustedMulticallForwarder) - ) - ); - - // sign forward request data as bad signer - bytes memory invalidSignature = getForwardRequestDataSignature( - request, - trustedMulticallForwarder.nonces(address(signer)), - badSignerPrivateKey, // bad signer - domainSeparator - ); - - // update forward request data object with signature - request.signature = invalidSignature; - - // define batch of forward requests - ERC2771Forwarder.ForwardRequestData[] memory batch = - new ERC2771Forwarder.ForwardRequestData[](1); - batch[0] = request; - - // execute batch - vm.expectRevert(Address.FailedInnerCall.selector); - trustedMulticallForwarder.executeBatch{value: value, gas: gas}({ - requests: batch - }); - } -} diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index ca58275c..94e79a44 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -14,30 +14,46 @@ import { Setup } from "script/Deploy.s.sol"; import {IERC20} from "src/interfaces/tokens/IERC20.sol"; -import {IPerpsMarketProxy} from "src/interfaces/synthetix/IPerpsMarketProxy.sol"; +import {IPerpsMarketProxy} from "test/utils/interfaces/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 - "src/utils/TrustedMulticallForwarder.sol"; +/// @title Contract for bootstrapping the SMv3 system for testing purposes +/// @dev it deploys the SMv3 Engine and EngineExposed, and defines +/// the perpsMarketProxy, spotMarketProxy, sUSD, and sBTC contracts (notably) +/// @dev it deploys a SynthMinter contract for minting sUSD and sBTC +/// @dev it creates a Synthetix v3 perps market account for the "ACTOR" whose +/// address is defined in the Constants contract +/// @dev it mints "AMOUNT" of sUSD to the ACTOR for testing purposes +/// @dev it gives the Engine contract ADMIN_PERMISSION over the account owned by the ACTOR +/// which is defined by its accountId +/// +/// @custom:network it can deploy the SMv3 system to the +/// Optimism Goerli or Optimism network in a forked environment (relies on up-to-date constants) +/// +/// @custom:deployment it uses the deploy script in the script/ directory to deploy the SMv3 system +/// and effectively tests the deploy script as well +/// +/// @author JaredBorders (jaredborders@pm.me) contract Bootstrap is Test, Constants, Conditions, SynthetixV3Errors { + // lets any test contract that inherits from this contract + // use the console.log() using console2 for *; + // deployed contracts Engine public engine; EngineExposed public engineExposed; - TrustedMulticallForwarder public trustedForwarderContract; + SynthMinter public synthMinter; + + // defined contracts IPerpsMarketProxy public perpsMarketProxy; ISpotMarketProxy public spotMarketProxy; IERC20 public sUSD; IERC20 public sBTC; - IPyth public pyth; - SynthMinter public synthMinter; + // ACTOR's account id in the Synthetix v3 perps market uint128 public accountId; - receive() external payable {} - function initializeOptimismGoerli() public { BootstrapOptimismGoerli bootstrap = new BootstrapOptimismGoerli(); ( @@ -45,19 +61,14 @@ contract Bootstrap is Test, Constants, Conditions, SynthetixV3Errors { address _engineExposedAddress, address _perpesMarketProxyAddress, address _spotMarketProxyAddress, - address _sUSDAddress, - address _pythAddress, - address _trustedForwarderAddress + address _sUSDAddress ) = bootstrap.init(); engine = Engine(_engineAddress); engineExposed = EngineExposed(_engineExposedAddress); - trustedForwarderContract = - TrustedMulticallForwarder(_trustedForwarderAddress); perpsMarketProxy = IPerpsMarketProxy(_perpesMarketProxyAddress); spotMarketProxy = ISpotMarketProxy(_spotMarketProxyAddress); sUSD = IERC20(_sUSDAddress); - pyth = IPyth(_pythAddress); synthMinter = new SynthMinter(_sUSDAddress, _spotMarketProxyAddress); sBTC = synthMinter.sBTC(); @@ -80,19 +91,14 @@ contract Bootstrap is Test, Constants, Conditions, SynthetixV3Errors { address _engineExposedAddress, address _perpesMarketProxyAddress, address _spotMarketProxyAddress, - address _sUSDAddress, - address _pythAddress, - address _trustedForwarderAddress + address _sUSDAddress ) = bootstrap.init(); engine = Engine(_engineAddress); engineExposed = EngineExposed(_engineExposedAddress); - trustedForwarderContract = - TrustedMulticallForwarder(_trustedForwarderAddress); perpsMarketProxy = IPerpsMarketProxy(_perpesMarketProxyAddress); spotMarketProxy = ISpotMarketProxy(_spotMarketProxyAddress); sUSD = IERC20(_sUSDAddress); - pyth = IPyth(_pythAddress); synthMinter = new SynthMinter(_sUSDAddress, _spotMarketProxyAddress); sBTC = synthMinter.sBTC(); @@ -112,22 +118,18 @@ contract Bootstrap is Test, Constants, Conditions, SynthetixV3Errors { contract BootstrapOptimism is Setup, OptimismParameters { function init() public - returns (address, address, address, address, address, address, address) + returns (address, address, address, address, address) { - (Engine engine, TrustedMulticallForwarder trustedForwarderContract) = - Setup.deploySystem({ + (Engine engine) = Setup.deploySystem({ perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, - sUSDProxy: USD_PROXY, - oracle: PYTH + sUSDProxy: USD_PROXY }); EngineExposed engineExposed = new EngineExposed({ _perpsMarketProxy: PERPS_MARKET_PROXY, _spotMarketProxy: SPOT_MARKET_PROXY, - _sUSDProxy: USD_PROXY, - _oracle: PYTH, - _trustedForwarder: address(0x1) + _sUSDProxy: USD_PROXY }); return ( @@ -135,9 +137,7 @@ contract BootstrapOptimism is Setup, OptimismParameters { address(engineExposed), PERPS_MARKET_PROXY, SPOT_MARKET_PROXY, - USD_PROXY, - PYTH, - address(trustedForwarderContract) + USD_PROXY ); } } @@ -145,22 +145,18 @@ contract BootstrapOptimism is Setup, OptimismParameters { contract BootstrapOptimismGoerli is Setup, OptimismGoerliParameters { function init() public - returns (address, address, address, address, address, address, address) + returns (address, address, address, address, address) { - (Engine engine, TrustedMulticallForwarder trustedForwarderContract) = - Setup.deploySystem({ + (Engine engine) = Setup.deploySystem({ perpsMarketProxy: PERPS_MARKET_PROXY, spotMarketProxy: SPOT_MARKET_PROXY, - sUSDProxy: USD_PROXY, - oracle: PYTH + sUSDProxy: USD_PROXY }); EngineExposed engineExposed = new EngineExposed({ _perpsMarketProxy: PERPS_MARKET_PROXY, _spotMarketProxy: SPOT_MARKET_PROXY, - _sUSDProxy: USD_PROXY, - _oracle: PYTH, - _trustedForwarder: address(0x1) + _sUSDProxy: USD_PROXY }); return ( @@ -168,9 +164,7 @@ contract BootstrapOptimismGoerli is Setup, OptimismGoerliParameters { address(engineExposed), PERPS_MARKET_PROXY, SPOT_MARKET_PROXY, - USD_PROXY, - PYTH, - address(trustedForwarderContract) + USD_PROXY ); } } diff --git a/test/utils/ConditionalOrderSignature.sol b/test/utils/ConditionalOrderSignature.sol index a7f8f330..7dfcb581 100644 --- a/test/utils/ConditionalOrderSignature.sol +++ b/test/utils/ConditionalOrderSignature.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.20; import {IEngine} from "src/interfaces/IEngine.sol"; import {Vm} from "lib/forge-std/src/Vm.sol"; +/// @title Contract for generating signatures for conditional orders for testing purposes +/// @custom:docs see https://mirror.xyz/jaredborders.eth/G2RP5XAfLbNZv01DXgxuzv_34bQF_PuO1X2u0Nhop9g +/// @author JaredBorders (jaredborders@pm.me) contract ConditionalOrderSignature { Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); diff --git a/test/utils/Conditions.sol b/test/utils/Conditions.sol index 52e56dbb..276d3fe0 100644 --- a/test/utils/Conditions.sol +++ b/test/utils/Conditions.sol @@ -3,6 +3,9 @@ pragma solidity 0.8.20; import {Engine} from "src/Engine.sol"; +/// @title Contract for generating function signatures that define valid conditions +/// for conditional orders for testing purposes +/// @author JaredBorders (jaredborders@pm.me) contract Conditions { function isTimestampAfter(uint256 timestamp) public @@ -22,23 +25,23 @@ contract Conditions { abi.encodeWithSelector(Engine.isTimestampBefore.selector, timestamp); } - function isPriceAbove( - bytes32 _assetId, - int64 _price, - uint64 _confidenceInterval - ) public pure returns (bytes memory) { + function isPriceAbove(uint128 _marketId, uint256 _price, int128 _size) + public + pure + returns (bytes memory) + { return abi.encodeWithSelector( - Engine.isPriceAbove.selector, _assetId, _price, _confidenceInterval + Engine.isPriceAbove.selector, _marketId, _price, _size ); } - function isPriceBelow( - bytes32 _assetId, - int64 _price, - uint64 _confidenceInterval - ) public pure returns (bytes memory) { + function isPriceBelow(uint128 _marketId, uint256 _price, int128 _size) + public + pure + returns (bytes memory) + { return abi.encodeWithSelector( - Engine.isPriceBelow.selector, _assetId, _price, _confidenceInterval + Engine.isPriceBelow.selector, _marketId, _price, _size ); } diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol index 6bb534fa..abed5177 100644 --- a/test/utils/Constants.sol +++ b/test/utils/Constants.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.20; +/// @title Contract for defining constants used in testing +/// @author JaredBorders (jaredborders@pm.me) contract Constants { uint256 internal constant GOERLI_BLOCK_NUMBER = 14_862_158; @@ -52,9 +54,6 @@ contract Constants { uint128 constant SETH_PERPS_MARKET_ID = 200; - bytes32 constant PYTH_ETH_USD_ASSET_ID = - 0xca80ba6dc32e08d06f1aa886011eed1d77c77be9eb761cc10d72b7d0a2fd57a6; - uint256 internal constant AMOUNT = 10_000 ether; address internal constant MARKET_CONFIGURATION_MODULE = @@ -62,5 +61,5 @@ contract Constants { uint256 internal constant ZERO_CO_FEE = 0; - uint256 internal constant CO_FEE = 100 wei; + uint256 internal constant CO_FEE = 620_198 wei; } diff --git a/test/utils/SynthMinter.sol b/test/utils/SynthMinter.sol index f5c75532..0f5d80da 100644 --- a/test/utils/SynthMinter.sol +++ b/test/utils/SynthMinter.sol @@ -6,6 +6,8 @@ import {IERC20} from "src/interfaces/tokens/IERC20.sol"; import {ISpotMarketProxy} from "src/interfaces/synthetix/ISpotMarketProxy.sol"; import {Test} from "lib/forge-std/src/Test.sol"; +/// @title Contract for minting sUSD and sBTC for testing purposes +/// @author JaredBorders (jaredborders@pm.me) contract SynthMinter is Test, Constants { address public immutable sUSD; IERC20 public immutable sBTC; diff --git a/test/utils/errors/SynthetixV3Errors.sol b/test/utils/errors/SynthetixV3Errors.sol index ba1a1605..771ca991 100644 --- a/test/utils/errors/SynthetixV3Errors.sol +++ b/test/utils/errors/SynthetixV3Errors.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.20; +/// @title Cosolidated Errors from Synthetix v3 contracts +/// @notice This contract consolidates all errors from Synthetix v3 contracts +/// and is used for testing purposes +/// @author JaredBorders (jaredborders@pm.me) contract SynthetixV3Errors { enum SettlementStrategyType {PYTH} @@ -104,4 +108,6 @@ contract SynthetixV3Errors { error InsufficientSynthCollateral( uint128 synthMarketId, uint256 collateralAmount, uint256 withdrawAmount ); + error InsufficientAllowance(uint256 required, uint256 existing); + error InvalidParameter(string parameter, string reason); } diff --git a/test/utils/exposed/EngineExposed.sol b/test/utils/exposed/EngineExposed.sol index 80dc3c1d..ee03ab63 100644 --- a/test/utils/exposed/EngineExposed.sol +++ b/test/utils/exposed/EngineExposed.sol @@ -3,24 +3,16 @@ pragma solidity 0.8.20; import {Engine, MathLib} from "src/Engine.sol"; +/// @title Contract for exposing internal Engine functions for testing purposes +/// @author JaredBorders (jaredborders@pm.me) contract EngineExposed is Engine { using MathLib for uint256; constructor( address _perpsMarketProxy, address _spotMarketProxy, - address _sUSDProxy, - address _oracle, - address _trustedForwarder - ) - Engine( - _perpsMarketProxy, - _spotMarketProxy, - _sUSDProxy, - _oracle, - _trustedForwarder - ) - {} + address _sUSDProxy + ) Engine(_perpsMarketProxy, _spotMarketProxy, _sUSDProxy) {} function getSynthAddress(uint128 synthMarketId) public diff --git a/test/utils/interfaces/IPerpsMarketProxy.sol b/test/utils/interfaces/IPerpsMarketProxy.sol new file mode 100644 index 00000000..db6d51b0 --- /dev/null +++ b/test/utils/interfaces/IPerpsMarketProxy.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.20; + +/// @title Consolidated functions from Synthetix v3 Perps Market contracts +/// @dev Used for testing purposes but not used in the src/* contracts +/// @author JaredBorders (jaredborders@pm.me) +interface IPerpsMarketProxy { + function createAccount() external returns (uint128 accountId); + + function getAccountOwner(uint128 accountId) + external + view + returns (address owner); + + function grantPermission( + uint128 accountId, + bytes32 permission, + address user + ) external; + + function hasPermission(uint128 accountId, bytes32 permission, address user) + external + view + returns (bool hasPermission); + + function isAuthorized(uint128 accountId, bytes32 permission, address target) + external + view + returns (bool isAuthorized); + + struct Data { + uint256 settlementTime; + OrderCommitmentRequest request; + } + + struct OrderCommitmentRequest { + uint128 marketId; + uint128 accountId; + int128 sizeDelta; + uint128 settlementStrategyId; + uint256 acceptablePrice; + bytes32 trackingCode; + address referrer; + } + + function commitOrder(OrderCommitmentRequest memory commitment) + external + returns (Data memory retOrder, uint256 fees); + + function requiredMarginForOrder( + uint128 accountId, + uint128 marketId, + int128 sizeDelta + ) external view returns (uint256 requiredMargin); + + function computeOrderFees(uint128 marketId, int128 sizeDelta) + external + view + returns (uint256 orderFees, uint256 fillPrice); + + function modifyCollateral( + uint128 accountId, + uint128 synthMarketId, + int256 amountDelta + ) external; + + function getCollateralAmount(uint128 accountId, uint128 synthMarketId) + external + view + returns (uint256); + + function totalCollateralValue(uint128 accountId) + external + view + returns (uint256); + + function getOpenPosition(uint128 accountId, uint128 marketId) + external + view + returns (int256 totalPnl, int256 accruedFunding, int128 positionSize); + + function getAvailableMargin(uint128 accountId) + external + view + returns (int256 availableMargin); + + function getMaxMarketSize(uint128 marketId) + external + view + returns (uint256 maxMarketSize); +} diff --git a/test/utils/mocks/EIP7412Mock.sol b/test/utils/mocks/EIP7412Mock.sol new file mode 100644 index 00000000..fdca5650 --- /dev/null +++ b/test/utils/mocks/EIP7412Mock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.20; + +/// @title Contract(s) used to mock EIP-7412 Oracle functionality for testing purposes +/// @author JaredBorders (jaredborders@pm.me) +contract EIP7412Mock { + event Success(); + + function fulfillOracleQuery(bytes calldata) external payable { + require(msg.value > 0, "EIP7412Mock"); + emit Success(); + } +} + +contract EIP7412MockRefund { + function fulfillOracleQuery(bytes calldata) external payable { + assert(msg.value > 0); + (bool success,) = msg.sender.call{value: msg.value}(""); + require(success, "EIP7412MockRefund"); + } +} + +contract EIP7412MockRevert { + function fulfillOracleQuery(bytes calldata) external payable { + revert("EIP7412MockRevert"); + } +} diff --git a/test/utils/mocks/EtherSink.sol b/test/utils/mocks/EtherSink.sol deleted file mode 100644 index 762a55c5..00000000 --- a/test/utils/mocks/EtherSink.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -/// @title EtherSink -/// @notice Receives Ether, that's about it \( o_o )/ -/// @author andreas@nascent.xyz -contract EtherSink { - /// >>>>>>>>>>>>>>>>>>>>>> ACCEPT CALLS <<<<<<<<<<<<<<<<<<<<<<< /// - - /// @notice Allows the test to receive eth via low level calls - receive() external payable {} -} diff --git a/test/utils/mocks/MockCallee.sol b/test/utils/mocks/MockCallee.sol deleted file mode 100644 index b47a5985..00000000 --- a/test/utils/mocks/MockCallee.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -/// @title MockCallee -/// @notice Receives calls from the Multicaller -/// @author andreas@nascent.xyz -contract MockCallee { - /// @notice Failure - error Unsuccessful(); - - /// @notice Returns the block hash for the given block number - /// @param blockNumber The block number - /// @return blockHash The 32 byte block hash - function getBlockHash(uint256 blockNumber) - public - view - returns (bytes32 blockHash) - { - blockHash = blockhash(blockNumber); - } - - /// @notice Reverts o______O - function thisMethodReverts() public pure { - revert Unsuccessful(); - } - - /// @notice Accepts a value - function sendBackValue(address target) public payable { - (bool ok,) = target.call{value: msg.value}(""); - if (!ok) revert Unsuccessful(); - } -} diff --git a/test/utils/mocks/MockMulticallablePayable.sol b/test/utils/mocks/MockMulticallablePayable.sol new file mode 100644 index 00000000..79e83d60 --- /dev/null +++ b/test/utils/mocks/MockMulticallablePayable.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.20; + +import {MulticallablePayable} from "src/utils/MulticallablePayable.sol"; + +/// @dev WARNING! This mock is strictly intended for testing purposes only. +/// Do NOT copy anything here into production code unless you really know what you are doing. +/// @author Solady +contract MockMulticallablePayable is MulticallablePayable { + error CustomError(); + + struct Tuple { + uint256 a; + uint256 b; + } + + function revertsWithString(string memory e) external pure { + revert(e); + } + + function revertsWithCustomError() external pure { + revert CustomError(); + } + + function revertsWithNothing() external pure { + revert(); + } + + function returnsTuple(uint256 a, uint256 b) + external + pure + returns (Tuple memory tuple) + { + tuple = Tuple({a: a, b: b}); + } + + function returnsString(string calldata s) + external + pure + returns (string memory) + { + return s; + } + + uint256 public paid; + + function pay() external payable { + paid += msg.value; + } + + function returnsSender() external view returns (address) { + return msg.sender; + } + + function multicallOriginal(bytes[] calldata data) + public + payable + returns (bytes[] memory results) + { + unchecked { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + (bool success, bytes memory result) = + address(this).delegatecall(data[i]); + if (!success) { + // Next 5 lines from https://ethereum.stackexchange.com/a/83577 + if (result.length < 68) revert(); + /// @solidity memory-safe-assembly + assembly { + result := add(result, 0x04) + } + revert(abi.decode(result, (string))); + } + results[i] = result; + } + } + } +} diff --git a/test/utils/mocks/PythMock.sol b/test/utils/mocks/PythMock.sol deleted file mode 100644 index 1bf8b8cd..00000000 --- a/test/utils/mocks/PythMock.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.20; - -import {IPyth} from "src/interfaces/oracles/IPyth.sol"; -import {Test} from "lib/forge-std/src/Test.sol"; - -contract PythMock is Test { - function mock_pyth_getPrice( - address pyth, - bytes32 id, - int64 price, - uint64 conf, - int32 expo - ) public { - vm.mockCall( - pyth, - abi.encodeWithSelector(IPyth.getPrice.selector, id), - abi.encode(price, conf, expo, block.timestamp - 1) - ); - } -} diff --git a/test/utils/mocks/SynthetixMock.sol b/test/utils/mocks/SynthetixMock.sol index 57af2551..5852a478 100644 --- a/test/utils/mocks/SynthetixMock.sol +++ b/test/utils/mocks/SynthetixMock.sol @@ -1,27 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.20; -import {EIP7412} from "src/utils/EIP7412.sol"; -import {IPerpsMarketProxy} from "src/interfaces/synthetix/IPerpsMarketProxy.sol"; +import {IPerpsMarketProxy} from "test/utils/interfaces/IPerpsMarketProxy.sol"; import {Test} from "lib/forge-std/src/Test.sol"; +/// @title Mocking contract for mocking Synthetix v3 functionality for testing purposes +/// @author JaredBorders (jaredborders@pm.me) contract SynthetixMock is Test { - function mock_computeOrderFees( - address perpsMarketProxy, - uint128 marketId, - int128 sizeDelta, - uint256 orderFees, - uint256 fillPrice - ) public { - vm.mockCall( - perpsMarketProxy, - abi.encodeWithSelector( - IPerpsMarketProxy.computeOrderFees.selector, marketId, sizeDelta - ), - abi.encode(orderFees, fillPrice) - ); - } - function mock_getOpenPosition( address perpsMarketProxy, uint128 accountId, @@ -51,19 +36,6 @@ contract SynthetixMock is Test { ); } - function mock_fulfillOracleQuery( - address EIP7412Implementer, - bytes calldata signedOffchainData - ) public { - vm.mockCall( - EIP7412Implementer, - abi.encodeWithSelector( - EIP7412.fulfillOracleQuery.selector, signedOffchainData - ), - abi.encode() - ); - } - function mock_getAccountOwner( address perpsMarketProxy, uint128 accountId,