From eb98aaf571f26d66897b5cd51d0a93efc5d07cc0 Mon Sep 17 00:00:00 2001 From: ilya Date: Mon, 2 Dec 2024 13:18:39 +0000 Subject: [PATCH] Support JIT orders in the trade verifier (#3085) # Description Closes task n2 from https://github.com/cowprotocol/services/issues/3082 by implementing support of the quotes with JIT orders in the trade verifier. For a gradual migration, it has to support both quote versions. # Changes - [ ] Added a new version of the trade with JIT orders. - [ ] Utilized the clearing prices to calculate the out amount. - [ ] Altered the `Solver.sol` helper contract to fetch all token balances as was proposed in [one of the comments](https://github.com/cowprotocol/services/pull/3085#discussion_r1830692576), which reduces the overall code complexity. - [ ] Bumped into an issue while converting floats into `BigRational`. ~~Implemented a workaround with converting float's string representation into `BigRational`.~~ Used `BigDecimal` in the config instead. ## How to test Unit tests. e2e would be possible only once the driver support is implemented(see https://github.com/cowprotocol/services/pull/3103). --- Cargo.lock | 1 + crates/autopilot/src/run.rs | 1 + crates/contracts/artifacts/Solver.json | 2 +- crates/contracts/solidity/Solver.sol | 41 +- crates/e2e/Cargo.toml | 1 + crates/e2e/tests/e2e/quote_verification.rs | 13 +- crates/orderbook/src/run.rs | 1 + crates/shared/src/price_estimation/factory.rs | 31 +- crates/shared/src/price_estimation/mod.rs | 3 +- .../src/price_estimation/trade_verifier.rs | 455 ++++++++++++++---- crates/shared/src/trade_finding/external.rs | 94 +++- crates/shared/src/trade_finding/mod.rs | 138 +++++- 12 files changed, 617 insertions(+), 164 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4d50d7d64..17ca562be0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1856,6 +1856,7 @@ dependencies = [ "app-data", "autopilot", "axum", + "bigdecimal", "chrono", "clap", "contracts", diff --git a/crates/autopilot/src/run.rs b/crates/autopilot/src/run.rs index 4a5f0a6503..59abed8253 100644 --- a/crates/autopilot/src/run.rs +++ b/crates/autopilot/src/run.rs @@ -332,6 +332,7 @@ pub async fn run(args: Arguments) { code_fetcher: code_fetcher.clone(), }, ) + .await .expect("failed to initialize price estimator factory"); let native_price_estimator = price_estimator_factory diff --git a/crates/contracts/artifacts/Solver.json b/crates/contracts/artifacts/Solver.json index 8248b515c2..f4de20dccc 100644 --- a/crates/contracts/artifacts/Solver.json +++ b/crates/contracts/artifacts/Solver.json @@ -1 +1 @@ -{"abi":[{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"bool","name":"countGas","type":"bool"}],"name":"storeBalance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract ISettlement","name":"settlementContract","type":"address"},{"internalType":"address payable","name":"trader","type":"address"},{"internalType":"address","name":"sellToken","type":"address"},{"internalType":"uint256","name":"sellAmount","type":"uint256"},{"internalType":"address","name":"buyToken","type":"address"},{"internalType":"address","name":"nativeToken","type":"address"},{"internalType":"address payable","name":"receiver","type":"address"},{"internalType":"bytes","name":"settlementCall","type":"bytes"},{"internalType":"bool","name":"mockPreconditions","type":"bool"}],"name":"swap","outputs":[{"internalType":"uint256","name":"gasUsed","type":"uint256"},{"internalType":"uint256[]","name":"queriedBalances","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"}],"bytecode":"0x608060405234801561001057600080fd5b50610992806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80633bbb2e1d1461003b5780639efde0c214610050575b600080fd5b61004e61004936600461074f565b61007a565b005b61006361005e366004610796565b6101af565b604051610071929190610894565b60405180910390f35b60005a9050600173eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee73ffffffffffffffffffffffffffffffffffffffff861614610147576040517f70a0823100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff85811660048301528616906370a0823190602401602060405180830381865afa15801561011e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061014291906108e2565b610160565b8373ffffffffffffffffffffffffffffffffffffffff16315b8154600181018355600092835260209092209091015581156101a9575a610187908261092a565b6101939061116c610943565b6000808282546101a39190610943565b90915550505b50505050565b60006060333014610246576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603860248201527f6f6e6c792073696d756c6174696f6e206c6f67696320697320616c6c6f77656460448201527f20746f2063616c6c202773776170272066756e6374696f6e0000000000000000606482015260840160405180910390fd5b82156102e5576040517f57d5a1d300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8d811660048301528b81166024830152604482018b905288811660648301528c16906357d5a1d390608401600060405180830381600087803b1580156102cc57600080fd5b505af11580156102e0573d6000803e3d6000fd5b505050505b60405173ffffffffffffffffffffffffffffffffffffffff8716906000908181818181875af1925050503d806000811461033b576040519150601f19603f3d011682016040523d82523d6000602084013e610340565b606091505b50506040517f3bbb2e1d00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8c811660048301528e16602482015260006044820152309150633bbb2e1d90606401600060405180830381600087803b1580156103b957600080fd5b505af11580156103cd573d6000803e3d6000fd5b50506040517f3bbb2e1d00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8b811660048301528f16602482015260006044820152309250633bbb2e1d9150606401600060405180830381600087803b15801561044757600080fd5b505af115801561045b573d6000803e3d6000fd5b5050505060005a90506104d186868080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f820116905080830192505050505050508e73ffffffffffffffffffffffffffffffffffffffff1661067a90919063ffffffff16565b506000545a6104e0908361092a565b6104ea919061092a565b6040517f3bbb2e1d00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8d811660048301528f166024820152600060448201529093503090633bbb2e1d90606401600060405180830381600087803b15801561056357600080fd5b505af1158015610577573d6000803e3d6000fd5b505050503073ffffffffffffffffffffffffffffffffffffffff16633bbb2e1d8a8f60006040518463ffffffff1660e01b81526004016105e59392919073ffffffffffffffffffffffffffffffffffffffff9384168152919092166020820152901515604082015260600190565b600060405180830381600087803b1580156105ff57600080fd5b505af1158015610613573d6000803e3d6000fd5b50505050600180548060200260200160405190810160405280929190818152602001828054801561066357602002820191906000526020600020905b81548152602001906001019080831161064f575b50505050509150509a509a98505050505050505050565b60606106888360008461068f565b9392505050565b606060008473ffffffffffffffffffffffffffffffffffffffff1684846040516106b99190610956565b60006040518083038185875af1925050503d80600081146106f6576040519150601f19603f3d011682016040523d82523d6000602084013e6106fb565b606091505b50925090508061070d57815160208301fd5b509392505050565b73ffffffffffffffffffffffffffffffffffffffff8116811461073757600080fd5b50565b8035801515811461074a57600080fd5b919050565b60008060006060848603121561076457600080fd5b833561076f81610715565b9250602084013561077f81610715565b915061078d6040850161073a565b90509250925092565b6000806000806000806000806000806101208b8d0312156107b657600080fd5b8a356107c181610715565b995060208b01356107d181610715565b985060408b01356107e181610715565b975060608b0135965060808b01356107f881610715565b955060a08b013561080881610715565b945060c08b013561081881610715565b935060e08b013567ffffffffffffffff8082111561083557600080fd5b818d0191508d601f83011261084957600080fd5b81358181111561085857600080fd5b8e602082850101111561086a57600080fd5b6020830195508094505050506108836101008c0161073a565b90509295989b9194979a5092959850565b6000604082018483526020604081850152818551808452606086019150828701935060005b818110156108d5578451835293830193918301916001016108b9565b5090979650505050505050565b6000602082840312156108f457600080fd5b5051919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8181038181111561093d5761093d6108fb565b92915050565b8082018082111561093d5761093d6108fb565b6000825160005b81811015610977576020818601810151858301520161095d565b50600092019182525091905056fea164736f6c6343000811000a","deployedBytecode":"0x608060405234801561001057600080fd5b50600436106100365760003560e01c80633bbb2e1d1461003b5780639efde0c214610050575b600080fd5b61004e61004936600461074f565b61007a565b005b61006361005e366004610796565b6101af565b604051610071929190610894565b60405180910390f35b60005a9050600173eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee73ffffffffffffffffffffffffffffffffffffffff861614610147576040517f70a0823100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff85811660048301528616906370a0823190602401602060405180830381865afa15801561011e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061014291906108e2565b610160565b8373ffffffffffffffffffffffffffffffffffffffff16315b8154600181018355600092835260209092209091015581156101a9575a610187908261092a565b6101939061116c610943565b6000808282546101a39190610943565b90915550505b50505050565b60006060333014610246576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603860248201527f6f6e6c792073696d756c6174696f6e206c6f67696320697320616c6c6f77656460448201527f20746f2063616c6c202773776170272066756e6374696f6e0000000000000000606482015260840160405180910390fd5b82156102e5576040517f57d5a1d300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8d811660048301528b81166024830152604482018b905288811660648301528c16906357d5a1d390608401600060405180830381600087803b1580156102cc57600080fd5b505af11580156102e0573d6000803e3d6000fd5b505050505b60405173ffffffffffffffffffffffffffffffffffffffff8716906000908181818181875af1925050503d806000811461033b576040519150601f19603f3d011682016040523d82523d6000602084013e610340565b606091505b50506040517f3bbb2e1d00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8c811660048301528e16602482015260006044820152309150633bbb2e1d90606401600060405180830381600087803b1580156103b957600080fd5b505af11580156103cd573d6000803e3d6000fd5b50506040517f3bbb2e1d00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8b811660048301528f16602482015260006044820152309250633bbb2e1d9150606401600060405180830381600087803b15801561044757600080fd5b505af115801561045b573d6000803e3d6000fd5b5050505060005a90506104d186868080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f820116905080830192505050505050508e73ffffffffffffffffffffffffffffffffffffffff1661067a90919063ffffffff16565b506000545a6104e0908361092a565b6104ea919061092a565b6040517f3bbb2e1d00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8d811660048301528f166024820152600060448201529093503090633bbb2e1d90606401600060405180830381600087803b15801561056357600080fd5b505af1158015610577573d6000803e3d6000fd5b505050503073ffffffffffffffffffffffffffffffffffffffff16633bbb2e1d8a8f60006040518463ffffffff1660e01b81526004016105e59392919073ffffffffffffffffffffffffffffffffffffffff9384168152919092166020820152901515604082015260600190565b600060405180830381600087803b1580156105ff57600080fd5b505af1158015610613573d6000803e3d6000fd5b50505050600180548060200260200160405190810160405280929190818152602001828054801561066357602002820191906000526020600020905b81548152602001906001019080831161064f575b50505050509150509a509a98505050505050505050565b60606106888360008461068f565b9392505050565b606060008473ffffffffffffffffffffffffffffffffffffffff1684846040516106b99190610956565b60006040518083038185875af1925050503d80600081146106f6576040519150601f19603f3d011682016040523d82523d6000602084013e6106fb565b606091505b50925090508061070d57815160208301fd5b509392505050565b73ffffffffffffffffffffffffffffffffffffffff8116811461073757600080fd5b50565b8035801515811461074a57600080fd5b919050565b60008060006060848603121561076457600080fd5b833561076f81610715565b9250602084013561077f81610715565b915061078d6040850161073a565b90509250925092565b6000806000806000806000806000806101208b8d0312156107b657600080fd5b8a356107c181610715565b995060208b01356107d181610715565b985060408b01356107e181610715565b975060608b0135965060808b01356107f881610715565b955060a08b013561080881610715565b945060c08b013561081881610715565b935060e08b013567ffffffffffffffff8082111561083557600080fd5b818d0191508d601f83011261084957600080fd5b81358181111561085857600080fd5b8e602082850101111561086a57600080fd5b6020830195508094505050506108836101008c0161073a565b90509295989b9194979a5092959850565b6000604082018483526020604081850152818551808452606086019150828701935060005b818110156108d5578451835293830193918301916001016108b9565b5090979650505050505050565b6000602082840312156108f457600080fd5b5051919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8181038181111561093d5761093d6108fb565b92915050565b8082018082111561093d5761093d6108fb565b6000825160005b81811015610977576020818601810151858301520161095d565b50600092019182525091905056fea164736f6c6343000811000a","devdoc":{"methods":{}},"userdoc":{"methods":{}}} +{"abi":[{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"bool","name":"countGas","type":"bool"}],"name":"storeBalance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract ISettlement","name":"settlementContract","type":"address"},{"internalType":"address payable","name":"trader","type":"address"},{"internalType":"address","name":"sellToken","type":"address"},{"internalType":"uint256","name":"sellAmount","type":"uint256"},{"internalType":"address","name":"nativeToken","type":"address"},{"internalType":"address[]","name":"tokens","type":"address[]"},{"internalType":"address payable","name":"receiver","type":"address"},{"internalType":"bytes","name":"settlementCall","type":"bytes"},{"internalType":"bool","name":"mockPreconditions","type":"bool"}],"name":"swap","outputs":[{"internalType":"uint256","name":"gasUsed","type":"uint256"},{"internalType":"uint256[]","name":"queriedBalances","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"}],"bytecode":"0x608060405234801561001057600080fd5b50610941806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80633bbb2e1d1461003b578063cc6b67a914610050575b600080fd5b61004e610049366004610600565b61007a565b005b61006361005e3660046106d5565b6101af565b6040516100719291906107bf565b60405180910390f35b60005a9050600173eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee73ffffffffffffffffffffffffffffffffffffffff861614610147576040517f70a0823100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff85811660048301528616906370a0823190602401602060405180830381865afa15801561011e573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610142919061080d565b610160565b8373ffffffffffffffffffffffffffffffffffffffff16315b8154600181018355600092835260209092209091015581156101a9575a6101879082610855565b6101939061116c61086e565b6000808282546101a3919061086e565b90915550505b50505050565b60006060333014610246576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603860248201527f6f6e6c792073696d756c6174696f6e206c6f67696320697320616c6c6f77656460448201527f20746f2063616c6c202773776170272066756e6374696f6e0000000000000000606482015260840160405180910390fd5b82156102e5576040517f57d5a1d300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8e811660048301528c81166024830152604482018c90528a811660648301528d16906357d5a1d390608401600060405180830381600087803b1580156102cc57600080fd5b505af11580156102e0573d6000803e3d6000fd5b505050505b60405173ffffffffffffffffffffffffffffffffffffffff8716906000908181818181875af1925050503d806000811461033b576040519150601f19603f3d011682016040523d82523d6000602084013e610340565b606091505b50505061034e88888f6103c9565b6103598d86866104a1565b915061036688888f6103c9565b60018054806020026020016040519081016040528092919081815260200182805480156103b257602002820191906000526020600020905b81548152602001906001019080831161039e575b505050505090509b509b9950505050505050505050565b60005b828110156101a95730633bbb2e1d8585848181106103ec576103ec610881565b905060200201602081019061040191906108b0565b6040517fffffffff0000000000000000000000000000000000000000000000000000000060e084901b16815273ffffffffffffffffffffffffffffffffffffffff9182166004820152908516602482015260006044820152606401600060405180830381600087803b15801561047657600080fd5b505af115801561048a573d6000803e3d6000fd5b505050508080610499906108cd565b9150506103cc565b6000805a90506104fe84848080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505073ffffffffffffffffffffffffffffffffffffffff891692915050610520565b506000545a61050d9083610855565b6105179190610855565b95945050505050565b606061052e83600084610535565b9392505050565b606060008473ffffffffffffffffffffffffffffffffffffffff16848460405161055f9190610905565b60006040518083038185875af1925050503d806000811461059c576040519150601f19603f3d011682016040523d82523d6000602084013e6105a1565b606091505b5092509050806105b357815160208301fd5b509392505050565b73ffffffffffffffffffffffffffffffffffffffff811681146105dd57600080fd5b50565b80356105eb816105bb565b919050565b803580151581146105eb57600080fd5b60008060006060848603121561061557600080fd5b8335610620816105bb565b92506020840135610630816105bb565b915061063e604085016105f0565b90509250925092565b60008083601f84011261065957600080fd5b50813567ffffffffffffffff81111561067157600080fd5b6020830191508360208260051b850101111561068c57600080fd5b9250929050565b60008083601f8401126106a557600080fd5b50813567ffffffffffffffff8111156106bd57600080fd5b60208301915083602082850101111561068c57600080fd5b60008060008060008060008060008060006101208c8e0312156106f757600080fd5b6107018c356105bb565b8b359a5061071260208d01356105bb565b60208c0135995061072560408d016105e0565b985060608c0135975061073a60808d016105e0565b965067ffffffffffffffff8060a08e0135111561075657600080fd5b6107668e60a08f01358f01610647565b909750955061077760c08e016105e0565b94508060e08e0135111561078a57600080fd5b5061079b8d60e08e01358e01610693565b90935091506107ad6101008d016105f0565b90509295989b509295989b9093969950565b6000604082018483526020604081850152818551808452606086019150828701935060005b81811015610800578451835293830193918301916001016107e4565b5090979650505050505050565b60006020828403121561081f57600080fd5b5051919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8181038181111561086857610868610826565b92915050565b8082018082111561086857610868610826565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6000602082840312156108c257600080fd5b813561052e816105bb565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036108fe576108fe610826565b5060010190565b6000825160005b81811015610926576020818601810151858301520161090c565b50600092019182525091905056fea164736f6c6343000811000a","deployedBytecode":"0x608060405234801561001057600080fd5b50600436106100365760003560e01c80633bbb2e1d1461003b578063cc6b67a914610050575b600080fd5b61004e610049366004610600565b61007a565b005b61006361005e3660046106d5565b6101af565b6040516100719291906107bf565b60405180910390f35b60005a9050600173eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee73ffffffffffffffffffffffffffffffffffffffff861614610147576040517f70a0823100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff85811660048301528616906370a0823190602401602060405180830381865afa15801561011e573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610142919061080d565b610160565b8373ffffffffffffffffffffffffffffffffffffffff16315b8154600181018355600092835260209092209091015581156101a9575a6101879082610855565b6101939061116c61086e565b6000808282546101a3919061086e565b90915550505b50505050565b60006060333014610246576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603860248201527f6f6e6c792073696d756c6174696f6e206c6f67696320697320616c6c6f77656460448201527f20746f2063616c6c202773776170272066756e6374696f6e0000000000000000606482015260840160405180910390fd5b82156102e5576040517f57d5a1d300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8e811660048301528c81166024830152604482018c90528a811660648301528d16906357d5a1d390608401600060405180830381600087803b1580156102cc57600080fd5b505af11580156102e0573d6000803e3d6000fd5b505050505b60405173ffffffffffffffffffffffffffffffffffffffff8716906000908181818181875af1925050503d806000811461033b576040519150601f19603f3d011682016040523d82523d6000602084013e610340565b606091505b50505061034e88888f6103c9565b6103598d86866104a1565b915061036688888f6103c9565b60018054806020026020016040519081016040528092919081815260200182805480156103b257602002820191906000526020600020905b81548152602001906001019080831161039e575b505050505090509b509b9950505050505050505050565b60005b828110156101a95730633bbb2e1d8585848181106103ec576103ec610881565b905060200201602081019061040191906108b0565b6040517fffffffff0000000000000000000000000000000000000000000000000000000060e084901b16815273ffffffffffffffffffffffffffffffffffffffff9182166004820152908516602482015260006044820152606401600060405180830381600087803b15801561047657600080fd5b505af115801561048a573d6000803e3d6000fd5b505050508080610499906108cd565b9150506103cc565b6000805a90506104fe84848080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505073ffffffffffffffffffffffffffffffffffffffff891692915050610520565b506000545a61050d9083610855565b6105179190610855565b95945050505050565b606061052e83600084610535565b9392505050565b606060008473ffffffffffffffffffffffffffffffffffffffff16848460405161055f9190610905565b60006040518083038185875af1925050503d806000811461059c576040519150601f19603f3d011682016040523d82523d6000602084013e6105a1565b606091505b5092509050806105b357815160208301fd5b509392505050565b73ffffffffffffffffffffffffffffffffffffffff811681146105dd57600080fd5b50565b80356105eb816105bb565b919050565b803580151581146105eb57600080fd5b60008060006060848603121561061557600080fd5b8335610620816105bb565b92506020840135610630816105bb565b915061063e604085016105f0565b90509250925092565b60008083601f84011261065957600080fd5b50813567ffffffffffffffff81111561067157600080fd5b6020830191508360208260051b850101111561068c57600080fd5b9250929050565b60008083601f8401126106a557600080fd5b50813567ffffffffffffffff8111156106bd57600080fd5b60208301915083602082850101111561068c57600080fd5b60008060008060008060008060008060006101208c8e0312156106f757600080fd5b6107018c356105bb565b8b359a5061071260208d01356105bb565b60208c0135995061072560408d016105e0565b985060608c0135975061073a60808d016105e0565b965067ffffffffffffffff8060a08e0135111561075657600080fd5b6107668e60a08f01358f01610647565b909750955061077760c08e016105e0565b94508060e08e0135111561078a57600080fd5b5061079b8d60e08e01358e01610693565b90935091506107ad6101008d016105f0565b90509295989b509295989b9093969950565b6000604082018483526020604081850152818551808452606086019150828701935060005b81811015610800578451835293830193918301916001016107e4565b5090979650505050505050565b60006020828403121561081f57600080fd5b5051919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8181038181111561086857610868610826565b92915050565b8082018082111561086857610868610826565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6000602082840312156108c257600080fd5b813561052e816105bb565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036108fe576108fe610826565b5060010190565b6000825160005b81811015610926576020818601810151858301520161090c565b50600092019182525091905056fea164736f6c6343000811000a","devdoc":{"methods":{}},"userdoc":{"methods":{}}} diff --git a/crates/contracts/solidity/Solver.sol b/crates/contracts/solidity/Solver.sol index 8e9236b20a..9833c1e5c1 100644 --- a/crates/contracts/solidity/Solver.sol +++ b/crates/contracts/solidity/Solver.sol @@ -31,8 +31,8 @@ contract Solver { /// @param trader - address of the order owner doing the trade /// @param sellToken - address of the token being sold /// @param sellAmount - amount being sold - /// @param buyToken - address of the token being bought /// @param nativeToken - ERC20 version of the chain's token + /// @param tokens - list of tokens used in the trade /// @param receiver - address receiving the bought tokens /// @param settlementCall - the calldata of the `settle()` call /// @param mockPreconditions - controls whether things like ETH wrapping @@ -47,8 +47,8 @@ contract Solver { address payable trader, address sellToken, uint256 sellAmount, - address buyToken, address nativeToken, + address[] calldata tokens, address payable receiver, bytes calldata settlementCall, bool mockPreconditions @@ -76,13 +76,14 @@ contract Solver { // contract. receiver.call{value: 0}(""); - this.storeBalance(sellToken, address(settlementContract), false); - this.storeBalance(buyToken, address(settlementContract), false); - uint256 gasStart = gasleft(); - address(settlementContract).doCall(settlementCall); - gasUsed = gasStart - gasleft() - _simulationOverhead; - this.storeBalance(sellToken, address(settlementContract), false); - this.storeBalance(buyToken, address(settlementContract), false); + // Store pre-settlement balances + _storeSettlementBalances(tokens, settlementContract); + + gasUsed = _executeSettlement(address(settlementContract), settlementCall); + + // Store post-settlement balances + _storeSettlementBalances(tokens, settlementContract); + queriedBalances = _queriedBalances; } @@ -104,4 +105,26 @@ contract Solver { _simulationOverhead += gasStart - gasleft() + 4460; } } + + /// @dev Helper function that reads and stores the balances of the `settlementContract` for each token in `tokens`. + /// @param tokens - list of tokens used in the trade + /// @param settlementContract - the settlement contract whose balances are being read + function _storeSettlementBalances(address[] calldata tokens, ISettlement settlementContract) internal { + for (uint256 i = 0; i < tokens.length; i++) { + this.storeBalance(tokens[i], address(settlementContract), false); + } + } + + /// @dev Executes the settlement and measures the gas used. + /// @param settlementContract The address of the settlement contract. + /// @param settlementCall The calldata for the settlement function. + /// @return gasUsed The amount of gas used during the settlement execution. + function _executeSettlement( + address settlementContract, + bytes calldata settlementCall + ) private returns (uint256 gasUsed) { + uint256 gasStart = gasleft(); + address(settlementContract).doCall(settlementCall); + gasUsed = gasStart - gasleft() - _simulationOverhead; + } } diff --git a/crates/e2e/Cargo.toml b/crates/e2e/Cargo.toml index 96a90421ca..659750454d 100644 --- a/crates/e2e/Cargo.toml +++ b/crates/e2e/Cargo.toml @@ -10,6 +10,7 @@ app-data = { path = "../app-data" } anyhow = { workspace = true } autopilot = { path = "../autopilot" } axum = { workspace = true } +bigdecimal = { workspace = true } chrono = { workspace = true } clap = { workspace = true } contracts = { path = "../contracts" } diff --git a/crates/e2e/tests/e2e/quote_verification.rs b/crates/e2e/tests/e2e/quote_verification.rs index cc9cf2c132..5e442d2e35 100644 --- a/crates/e2e/tests/e2e/quote_verification.rs +++ b/crates/e2e/tests/e2e/quote_verification.rs @@ -1,4 +1,5 @@ use { + bigdecimal::{BigDecimal, Zero}, e2e::setup::*, ethcontract::H160, ethrpc::Web3, @@ -13,7 +14,7 @@ use { Estimate, Verification, }, - trade_finding::{Interaction, Trade}, + trade_finding::{Interaction, LegacyTrade, TradeKind}, }, std::{str::FromStr, sync::Arc}, }; @@ -61,8 +62,10 @@ async fn test_bypass_verification_for_rfq_quotes(web3: Web3) { block_stream, onchain.contracts().gp_settlement.address(), onchain.contracts().weth.address(), - 0.0, - ); + BigDecimal::zero(), + ) + .await + .unwrap(); let verify_trade = |tx_origin| { let verifier = verifier.clone(); @@ -86,7 +89,7 @@ async fn test_bypass_verification_for_rfq_quotes(web3: Web3) { sell_token_source: SellTokenSource::Erc20, buy_token_destination: BuyTokenDestination::Erc20, }, - Trade { + TradeKind::Legacy(LegacyTrade { out_amount: 16380122291179526144u128.into(), gas_estimate: Some(225000), interactions: vec![Interaction { @@ -98,7 +101,7 @@ async fn test_bypass_verification_for_rfq_quotes(web3: Web3) { solver: H160::from_str("0xe3067c7c27c1038de4e8ad95a83b927d23dfbd99") .unwrap(), tx_origin, - }, + }), ) .await } diff --git a/crates/orderbook/src/run.rs b/crates/orderbook/src/run.rs index c3a022ef31..16c27567b0 100644 --- a/crates/orderbook/src/run.rs +++ b/crates/orderbook/src/run.rs @@ -273,6 +273,7 @@ pub async fn run(args: Arguments) { code_fetcher: code_fetcher.clone(), }, ) + .await .expect("failed to initialize price estimator factory"); let native_price_estimator = price_estimator_factory diff --git a/crates/shared/src/price_estimation/factory.rs b/crates/shared/src/price_estimation/factory.rs index 6019356cf9..03b2083264 100644 --- a/crates/shared/src/price_estimation/factory.rs +++ b/crates/shared/src/price_estimation/factory.rs @@ -73,14 +73,14 @@ pub struct Components { } impl<'a> PriceEstimatorFactory<'a> { - pub fn new( + pub async fn new( args: &'a Arguments, shared_args: &'a arguments::Arguments, network: Network, components: Components, ) -> Result { Ok(Self { - trade_verifier: Self::trade_verifier(args, shared_args, &network, &components), + trade_verifier: Self::trade_verifier(args, shared_args, &network, &components).await?, args, network, components, @@ -88,13 +88,15 @@ impl<'a> PriceEstimatorFactory<'a> { }) } - fn trade_verifier( + async fn trade_verifier( args: &'a Arguments, shared_args: &arguments::Arguments, network: &Network, components: &Components, - ) -> Option> { - let web3 = network.simulation_web3.clone()?; + ) -> Result>> { + let Some(web3) = network.simulation_web3.clone() else { + return Ok(None); + }; let web3 = ethrpc::instrumented::instrument_with_label(&web3, "simulator".into()); let tenderly = shared_args @@ -111,14 +113,17 @@ impl<'a> PriceEstimatorFactory<'a> { None => Arc::new(web3.clone()), }; - Some(Arc::new(TradeVerifier::new( - web3, - simulator, - components.code_fetcher.clone(), - network.block_stream.clone(), - network.settlement, - network.native_token, - args.quote_inaccuracy_limit, + Ok(Some(Arc::new( + TradeVerifier::new( + web3, + simulator, + components.code_fetcher.clone(), + network.block_stream.clone(), + network.settlement, + network.native_token, + args.quote_inaccuracy_limit.clone(), + ) + .await?, ))) } diff --git a/crates/shared/src/price_estimation/mod.rs b/crates/shared/src/price_estimation/mod.rs index 6a8fa88674..8355acef1c 100644 --- a/crates/shared/src/price_estimation/mod.rs +++ b/crates/shared/src/price_estimation/mod.rs @@ -5,6 +5,7 @@ use { trade_finding::Interaction, }, anyhow::Result, + bigdecimal::BigDecimal, ethcontract::{H160, U256}, futures::future::BoxFuture, itertools::Itertools, @@ -192,7 +193,7 @@ pub struct Arguments { /// E.g. a value of `0.01` means at most 1 percent of the sell or buy tokens /// can be paid out of the settlement contract buffers. #[clap(long, env, default_value = "1.")] - pub quote_inaccuracy_limit: f64, + pub quote_inaccuracy_limit: BigDecimal, /// How strict quote verification should be. #[clap( diff --git a/crates/shared/src/price_estimation/trade_verifier.rs b/crates/shared/src/price_estimation/trade_verifier.rs index 258e928ef3..02e20a24d9 100644 --- a/crates/shared/src/price_estimation/trade_verifier.rs +++ b/crates/shared/src/price_estimation/trade_verifier.rs @@ -3,11 +3,16 @@ use { crate::{ code_fetching::CodeFetching, code_simulation::CodeSimulating, - encoded_settlement::{encode_trade, EncodedSettlement}, + encoded_settlement::{encode_trade, EncodedSettlement, EncodedTrade}, interaction::EncodedInteraction, - trade_finding::{Interaction, Trade}, + trade_finding::{ + external::{dto, dto::JitOrder}, + Interaction, + TradeKind, + }, }, anyhow::{Context, Result}, + bigdecimal::BigDecimal, contracts::{ deployed_bytecode, dummy_contract, @@ -21,21 +26,26 @@ use { model::{ order::{OrderData, OrderKind, BUY_ETH_ADDRESS}, signature::{Signature, SigningScheme}, + DomainSeparator, }, num::BigRational, - number::{conversions::u256_to_big_rational, nonzero::U256 as NonZeroU256}, + number::{ + conversions::{big_decimal_to_big_rational, u256_to_big_rational}, + nonzero::U256 as NonZeroU256, + }, std::{collections::HashMap, sync::Arc}, web3::{ethabi::Token, types::CallRequest}, }; #[async_trait::async_trait] pub trait TradeVerifying: Send + Sync + 'static { - /// Verifies if the proposed [`Trade`] actually fulfills the [`PriceQuery`]. + /// Verifies if the proposed [`TradeKind`] actually fulfills the + /// [`PriceQuery`]. async fn verify( &self, query: &PriceQuery, verification: &Verification, - trade: Trade, + trade: TradeKind, ) -> Result; } @@ -50,38 +60,43 @@ pub struct TradeVerifier { settlement: GPv2Settlement, native_token: H160, quote_inaccuracy_limit: BigRational, + domain_separator: DomainSeparator, } impl TradeVerifier { const DEFAULT_GAS: u64 = 8_000_000; const TRADER_IMPL: H160 = addr!("0000000000000000000000000000000000010000"); - pub fn new( + pub async fn new( web3: Web3, simulator: Arc, code_fetcher: Arc, block_stream: CurrentBlockWatcher, settlement: H160, native_token: H160, - quote_inaccuracy_limit: f64, - ) -> Self { - Self { + quote_inaccuracy_limit: BigDecimal, + ) -> Result { + let settlement_contract = GPv2Settlement::at(&web3, settlement); + let domain_separator = + DomainSeparator(settlement_contract.domain_separator().call().await?.0); + Ok(Self { simulator, code_fetcher, block_stream, - settlement: GPv2Settlement::at(&web3, settlement), + settlement: settlement_contract, native_token, - quote_inaccuracy_limit: BigRational::from_float(quote_inaccuracy_limit) - .expect("can represent all finite values"), + quote_inaccuracy_limit: big_decimal_to_big_rational("e_inaccuracy_limit), web3, - } + domain_separator, + }) } async fn verify_inner( &self, query: &PriceQuery, verification: &Verification, - trade: &Trade, + trade: &TradeKind, + out_amount: &U256, ) -> Result { if verification.from.is_zero() { // Don't waste time on common simulations which will always fail. @@ -92,17 +107,33 @@ impl TradeVerifier { // Use `tx_origin` if response indicates that a special address is needed for // the simulation to pass. Otherwise just use the solver address. - let solver = trade.tx_origin.unwrap_or(trade.solver); + let solver = trade.tx_origin().unwrap_or(trade.solver()); let solver = dummy_contract!(Solver, solver); - let settlement = encode_settlement(query, verification, trade, self.native_token); - let settlement = add_balance_queries( - settlement, + let (tokens, clearing_prices) = match trade { + TradeKind::Legacy(_) => { + let tokens = vec![query.sell_token, query.buy_token]; + let prices = match query.kind { + OrderKind::Sell => vec![*out_amount, query.in_amount.get()], + OrderKind::Buy => vec![query.in_amount.get(), *out_amount], + }; + (tokens, prices) + } + TradeKind::Regular(trade) => trade.clearing_prices.iter().map(|e| e.to_owned()).unzip(), + }; + + let settlement = encode_settlement( query, verification, - self.settlement.address(), - &solver, - ); + trade, + &tokens, + &clearing_prices, + out_amount, + self.native_token, + &self.domain_separator, + )?; + + let settlement = add_balance_queries(settlement, query, verification, &solver); let settlement = self .settlement @@ -117,7 +148,7 @@ impl TradeVerifier { let sell_amount = match query.kind { OrderKind::Sell => query.in_amount.get(), - OrderKind::Buy => trade.out_amount, + OrderKind::Buy => *out_amount, }; let simulation = solver @@ -127,8 +158,8 @@ impl TradeVerifier { verification.from, query.sell_token, sell_amount, - query.buy_token, self.native_token, + tokens.clone(), verification.receiver, Bytes(settlement.data.unwrap().0), // only if the user did not provide pre-interactions is it safe @@ -173,11 +204,11 @@ impl TradeVerifier { // for a different `tx.origin` we need to pretend these // quotes actually simulated successfully to not lose these competitive quotes // when we enable quote verification in prod. - if trade.tx_origin == Some(H160::zero()) { + if trade.tx_origin() == Some(H160::zero()) { let estimate = Estimate { - out_amount: trade.out_amount, - gas: trade.gas_estimate.context("no gas estimate")?, - solver: trade.solver, + out_amount: *out_amount, + gas: trade.gas_estimate().context("no gas estimate")?, + solver: trade.solver(), verified: true, }; tracing::warn!( @@ -189,7 +220,7 @@ impl TradeVerifier { } }; - let mut summary = SettleOutput::decode(&output?, query.kind) + let mut summary = SettleOutput::decode(&output?, query.kind, &tokens) .context("could not decode simulation output") .map_err(Error::SimulationFailed)?; @@ -205,32 +236,42 @@ impl TradeVerifier { // It looks like the contract lost a lot of sell tokens but only because it was // the trader and had to pay for the trade. Adjust tokens lost downward. if verification.from == self.settlement.address() { - summary.sell_tokens_lost -= u256_to_big_rational(&sell_amount); + summary + .tokens_lost + .entry(query.sell_token) + .and_modify(|balance| *balance -= u256_to_big_rational(&sell_amount)); } // It looks like the contract gained a lot of buy tokens (negative loss) but // only because it was the receiver and got the payout. Adjust the tokens lost // upward. if verification.receiver == self.settlement.address() { - summary.buy_tokens_lost += u256_to_big_rational(&buy_amount); + summary + .tokens_lost + .entry(query.buy_token) + .and_modify(|balance| *balance += u256_to_big_rational(&buy_amount)); } } tracing::debug!( - lost_buy_amount = %summary.buy_tokens_lost, - lost_sell_amount = %summary.sell_tokens_lost, - gas_diff = ?trade.gas_estimate.unwrap_or_default().abs_diff(summary.gas_used.as_u64()), + tokens_lost = ?summary.tokens_lost, + gas_diff = ?trade.gas_estimate().unwrap_or_default().abs_diff(summary.gas_used.as_u64()), time = ?start.elapsed(), - promised_out_amount = ?trade.out_amount, + promised_out_amount = ?out_amount, verified_out_amount = ?summary.out_amount, - promised_gas = trade.gas_estimate, + promised_gas = trade.gas_estimate(), verified_gas = ?summary.gas_used, - out_diff = ?trade.out_amount.abs_diff(summary.out_amount), + out_diff = ?out_amount.abs_diff(summary.out_amount), ?query, ?verification, "verified quote", ); - ensure_quote_accuracy(&self.quote_inaccuracy_limit, query, trade.solver, &summary) + ensure_quote_accuracy( + &self.quote_inaccuracy_limit, + query, + trade.solver(), + &summary, + ) } /// Configures all the state overrides that are needed to mock the given @@ -238,7 +279,7 @@ impl TradeVerifier { async fn prepare_state_overrides( &self, verification: &Verification, - trade: &Trade, + trade: &TradeKind, ) -> Result> { // Set up mocked trader. let mut overrides = hashmap! { @@ -273,10 +314,13 @@ impl TradeVerifier { // If the trade requires a special tx.origin we also need to fake the // authenticator and tx origin balance. - if trade.tx_origin.is_some_and(|origin| origin != trade.solver) { + if trade + .tx_origin() + .is_some_and(|origin| origin != trade.solver()) + { let (authenticator, balance) = futures::join!( self.settlement.authenticator().call(), - self.web3.eth().balance(trade.solver, None) + self.web3.eth().balance(trade.solver(), None) ); let authenticator = authenticator.context("could not fetch authenticator")?; overrides.insert( @@ -289,7 +333,7 @@ impl TradeVerifier { let balance = balance.context("could not fetch balance")?; solver_override.balance = Some(balance); } - overrides.insert(trade.tx_origin.unwrap_or(trade.solver), solver_override); + overrides.insert(trade.tx_origin().unwrap_or(trade.solver()), solver_override); Ok(overrides) } @@ -301,16 +345,27 @@ impl TradeVerifying for TradeVerifier { &self, query: &PriceQuery, verification: &Verification, - trade: Trade, + trade: TradeKind, ) -> Result { - match self.verify_inner(query, verification, &trade).await { + let out_amount = trade + .out_amount( + &query.buy_token, + &query.sell_token, + &query.in_amount.get(), + &query.kind, + ) + .context("failed to compute trade out amount")?; + match self + .verify_inner(query, verification, &trade, &out_amount) + .await + { Ok(verified) => Ok(verified), - Err(Error::SimulationFailed(err)) => match trade.gas_estimate { + Err(Error::SimulationFailed(err)) => match trade.gas_estimate() { Some(gas) => { let estimate = Estimate { - out_amount: trade.out_amount, + out_amount, gas, - solver: trade.solver, + solver: trade.solver(), verified: false, }; tracing::warn!( @@ -341,20 +396,25 @@ fn encode_interactions(interactions: &[Interaction]) -> Vec interactions.iter().map(|i| i.encode()).collect() } +#[allow(clippy::too_many_arguments)] fn encode_settlement( query: &PriceQuery, verification: &Verification, - trade: &Trade, + trade: &TradeKind, + tokens: &[H160], + clearing_prices: &[U256], + out_amount: &U256, native_token: H160, -) -> EncodedSettlement { - let mut trade_interactions = encode_interactions(&trade.interactions); + domain_separator: &DomainSeparator, +) -> Result { + let mut trade_interactions = encode_interactions(&trade.interactions()); if query.buy_token == BUY_ETH_ADDRESS { // Because the `driver` manages `WETH` unwraps under the hood the `TradeFinder` // does not have to emit unwraps to pay out `ETH` in a trade. // However, for the simulation to be successful this has to happen so we do it // ourselves here. let buy_amount = match query.kind { - OrderKind::Sell => trade.out_amount, + OrderKind::Sell => *out_amount, OrderKind::Buy => query.in_amount.get(), }; let weth = dummy_contract!(WETH9, native_token); @@ -363,12 +423,40 @@ fn encode_settlement( tracing::trace!("adding unwrap interaction for paying out ETH"); } - let tokens = vec![query.sell_token, query.buy_token]; - let clearing_prices = match query.kind { - OrderKind::Sell => vec![trade.out_amount, query.in_amount.get()], - OrderKind::Buy => vec![query.in_amount.get(), trade.out_amount], - }; + let fake_trade = encode_fake_trade(query, verification, out_amount, tokens)?; + let mut trades = vec![fake_trade]; + if let TradeKind::Regular(trade) = trade { + trades.extend(encode_jit_orders( + &trade.jit_orders, + tokens, + domain_separator, + )?); + } + + let pre_interactions = [ + verification.pre_interactions.clone(), + trade.pre_interactions(), + ] + .concat(); + Ok(EncodedSettlement { + tokens: tokens.to_vec(), + clearing_prices: clearing_prices.to_vec(), + trades, + interactions: [ + encode_interactions(&pre_interactions), + trade_interactions, + encode_interactions(&verification.post_interactions), + ], + }) +} + +fn encode_fake_trade( + query: &PriceQuery, + verification: &Verification, + out_amount: &U256, + tokens: &[H160], +) -> Result { // Configure the most disadvantageous trade possible (while taking possible // overflows into account). Should the trader not receive the amount promised by // the [`Trade`] the simulation will still work and we can compute the actual @@ -376,7 +464,7 @@ fn encode_settlement( let (sell_amount, buy_amount) = match query.kind { OrderKind::Sell => (query.in_amount.get(), 0.into()), OrderKind::Buy => ( - trade.out_amount.max(U256::from(u128::MAX)), + (*out_amount).max(U256::from(u128::MAX)), query.in_amount.get(), ), }; @@ -400,21 +488,96 @@ fn encode_settlement( &fake_order, &fake_signature, verification.from, - 0, - 1, + // the tokens set length is small so the linear search is acceptable + tokens + .iter() + .position(|token| token == &query.sell_token) + .context("missing sell token index")?, + tokens + .iter() + .position(|token| token == &query.buy_token) + .context("missing buy token index")?, &query.in_amount.get(), ); - EncodedSettlement { - tokens, - clearing_prices, - trades: vec![encoded_trade], - interactions: [ - encode_interactions(&verification.pre_interactions), - trade_interactions, - encode_interactions(&verification.post_interactions), - ], - } + Ok(encoded_trade) +} + +fn encode_jit_orders( + jit_orders: &[dto::JitOrder], + tokens: &[H160], + domain_separator: &DomainSeparator, +) -> Result, Error> { + jit_orders + .iter() + .map(|jit_order| { + let order_data = OrderData { + sell_token: jit_order.sell_token, + buy_token: jit_order.buy_token, + receiver: Some(jit_order.receiver), + sell_amount: jit_order.sell_amount, + buy_amount: jit_order.buy_amount, + valid_to: jit_order.valid_to, + app_data: jit_order.app_data, + fee_amount: 0.into(), + kind: match &jit_order.side { + dto::Side::Buy => OrderKind::Buy, + dto::Side::Sell => OrderKind::Sell, + }, + partially_fillable: jit_order.partially_fillable, + sell_token_balance: jit_order.sell_token_source, + buy_token_balance: jit_order.buy_token_destination, + }; + let (owner, signature) = + recover_jit_order_owner(jit_order, &order_data, domain_separator)?; + + Ok(encode_trade( + &order_data, + &signature, + owner, + // the tokens set length is small so the linear search is acceptable + tokens + .iter() + .position(|token| token == &jit_order.sell_token) + .context("missing jit order sell token index")?, + tokens + .iter() + .position(|token| token == &jit_order.buy_token) + .context("missing jit order buy token index")?, + &jit_order.executed_amount, + )) + }) + .collect::, Error>>() +} + +/// Recovers the owner and signature from a `JitOrder`. +fn recover_jit_order_owner( + jit_order: &JitOrder, + order_data: &OrderData, + domain_separator: &DomainSeparator, +) -> Result<(H160, Signature), Error> { + let (owner, signature) = match jit_order.signing_scheme { + SigningScheme::Eip1271 => { + let (owner, signature) = jit_order.signature.split_at(20); + let owner = H160::from_slice(owner); + let signature = Signature::from_bytes(jit_order.signing_scheme, signature)?; + (owner, signature) + } + SigningScheme::PreSign => { + let owner = H160::from_slice(&jit_order.signature); + let signature = Signature::from_bytes(jit_order.signing_scheme, Vec::new().as_slice())?; + (owner, signature) + } + _ => { + let signature = Signature::from_bytes(jit_order.signing_scheme, &jit_order.signature)?; + let owner = signature + .recover(domain_separator, &order_data.hash_struct())? + .context("could not recover the owner")? + .signer; + (owner, signature) + } + }; + Ok((owner, signature)) } /// Adds the interactions that are only needed to query important balances @@ -424,7 +587,6 @@ fn add_balance_queries( mut settlement: EncodedSettlement, query: &PriceQuery, verification: &Verification, - settlement_contract: H160, solver: &Solver, ) -> EncodedSettlement { let (token, owner) = match query.kind { @@ -438,14 +600,14 @@ fn add_balance_queries( (query.buy_token, receiver) } - // track how much `sell_token` the settlement contract actually spent - OrderKind::Buy => (query.sell_token, settlement_contract), + // track how much `sell_token` the `from` address actually spent + OrderKind::Buy => (query.sell_token, verification.from), }; let query_balance = solver.methods().store_balance(token, owner, true); let query_balance = Bytes(query_balance.tx.data.unwrap().0); let interaction = (solver.address(), 0.into(), query_balance); - // query balance right after we receive all `sell_token` - settlement.interactions[1].insert(0, interaction.clone()); + // query balance query at the end of pre-interactions + settlement.interactions[0].push(interaction.clone()); // query balance right after we payed out all `buy_token` settlement.interactions[2].insert(0, interaction); settlement @@ -459,16 +621,12 @@ struct SettleOutput { /// `out_amount` perceived by the trader (sell token for buy orders or buy /// token for sell order) out_amount: U256, - /// Difference in buy tokens of the settlement contract before and after the - /// trade. - buy_tokens_lost: BigRational, - /// Difference in sell tokens of the settlement contract before and after - /// the trade. - sell_tokens_lost: BigRational, + /// Tokens difference of the settlement contract before and after the trade. + tokens_lost: HashMap, } impl SettleOutput { - fn decode(output: &[u8], kind: OrderKind) -> Result { + fn decode(output: &[u8], kind: OrderKind, tokens_vec: &[H160]) -> Result { let function = Solver::raw_contract() .interface .abi @@ -477,14 +635,29 @@ impl SettleOutput { let tokens = function.decode_output(output).context("decode")?; let (gas_used, balances): (U256, Vec) = Tokenize::from_token(Token::Tuple(tokens))?; - let settlement_sell_balance_before = u256_to_big_rational(&balances[0]); - let settlement_buy_balance_before = u256_to_big_rational(&balances[1]); - - let trader_balance_before = balances[2]; - let trader_balance_after = balances[3]; + // The balances are stored in the following order: + // [...tokens_before, user_balance_before, user_balance_after, ...tokens_after] + let mut i = 0; + let mut tokens_lost = HashMap::new(); + // Get settlement contract balances before the trade + for token in tokens_vec.iter() { + let balance_before = u256_to_big_rational(&balances[i]); + tokens_lost.insert(*token, balance_before); + i += 1; + } - let settlement_sell_balance_after = u256_to_big_rational(&balances[4]); - let settlement_buy_balance_after = u256_to_big_rational(&balances[5]); + let trader_balance_before = balances[i]; + let trader_balance_after = balances[i + 1]; + i += 2; + + // Get settlement contract balances after the trade + for token in tokens_vec.iter() { + let balance_after = u256_to_big_rational(&balances[i]); + tokens_lost + .entry(*token) + .and_modify(|balance_before| *balance_before -= balance_after); + i += 1; + } let out_amount = match kind { // for sell orders we track the buy_token amount which increases during the settlement @@ -497,8 +670,7 @@ impl SettleOutput { Ok(SettleOutput { gas_used, out_amount, - buy_tokens_lost: settlement_buy_balance_before - settlement_buy_balance_after, - sell_tokens_lost: settlement_sell_balance_before - settlement_sell_balance_after, + tokens_lost, }) } } @@ -510,16 +682,29 @@ fn ensure_quote_accuracy( query: &PriceQuery, solver: H160, summary: &SettleOutput, -) -> Result { +) -> std::result::Result { // amounts verified by the simulation let (sell_amount, buy_amount) = match query.kind { OrderKind::Buy => (summary.out_amount, query.in_amount.get()), OrderKind::Sell => (query.in_amount.get(), summary.out_amount), }; - - if summary.sell_tokens_lost >= inaccuracy_limit * u256_to_big_rational(&sell_amount) - || summary.buy_tokens_lost >= inaccuracy_limit * u256_to_big_rational(&buy_amount) - { + let (sell_amount, buy_amount) = ( + u256_to_big_rational(&sell_amount), + u256_to_big_rational(&buy_amount), + ); + let sell_token_lost_limit = inaccuracy_limit * &sell_amount; + let buy_token_lost_limit = inaccuracy_limit * &buy_amount; + + let sell_token_lost = summary + .tokens_lost + .get(&query.sell_token) + .context("summary sell token is missing")?; + let buy_token_lost = summary + .tokens_lost + .get(&query.buy_token) + .context("summary buy token is missing")?; + + if *sell_token_lost >= sell_token_lost_limit || *buy_token_lost >= buy_token_lost_limit { return Err(Error::TooInaccurate); } @@ -553,27 +738,71 @@ enum Error { #[cfg(test)] mod tests { - use super::*; + use {super::*, std::str::FromStr}; #[test] fn discards_inaccurate_quotes() { // let's use 0.5 as the base case to avoid rounding issues introduced by float // conversion - let low_threshold = BigRational::from_float(0.5).unwrap(); - let high_threshold = BigRational::from_float(0.51).unwrap(); + let low_threshold = big_decimal_to_big_rational(&BigDecimal::from_str("0.5").unwrap()); + let high_threshold = big_decimal_to_big_rational(&BigDecimal::from_str("0.51").unwrap()); + + let sell_token = H160([1u8; 20]); + let buy_token = H160([2u8; 20]); let query = PriceQuery { in_amount: 1_000.try_into().unwrap(), kind: OrderKind::Sell, - sell_token: H160::zero(), - buy_token: H160::zero(), + sell_token, + buy_token, + }; + + // buy token is lost + let tokens_lost = hashmap! { + sell_token => BigRational::from_integer(500.into()), + }; + let summary = SettleOutput { + gas_used: 0.into(), + out_amount: 2_000.into(), + tokens_lost, + }; + let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &summary); + assert!(matches!(estimate, Err(Error::SimulationFailed(_)))); + + // sell token is lost + let tokens_lost = hashmap! { + buy_token => BigRational::from_integer(0.into()), + }; + let summary = SettleOutput { + gas_used: 0.into(), + out_amount: 2_000.into(), + tokens_lost, + }; + let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &summary); + assert!(matches!(estimate, Err(Error::SimulationFailed(_)))); + + // everything is in-place + let tokens_lost = hashmap! { + sell_token => BigRational::from_integer(400.into()), + buy_token => BigRational::from_integer(0.into()), + }; + let summary = SettleOutput { + gas_used: 0.into(), + out_amount: 2_000.into(), + tokens_lost, + }; + let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &summary); + assert!(estimate.is_ok()); + + let tokens_lost = hashmap! { + sell_token => BigRational::from_integer(500.into()), + buy_token => BigRational::from_integer(0.into()), }; let sell_more = SettleOutput { gas_used: 0.into(), out_amount: 2_000.into(), - buy_tokens_lost: BigRational::from_integer(0.into()), - sell_tokens_lost: BigRational::from_integer(500.into()), + tokens_lost, }; let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &sell_more); @@ -583,11 +812,15 @@ mod tests { let estimate = ensure_quote_accuracy(&high_threshold, &query, H160::zero(), &sell_more); assert!(estimate.is_ok()); + let tokens_lost = hashmap! { + sell_token => BigRational::from_integer(0.into()), + buy_token => BigRational::from_integer(1_000.into()), + }; + let pay_out_more = SettleOutput { gas_used: 0.into(), out_amount: 2_000.into(), - buy_tokens_lost: BigRational::from_integer(1_000.into()), - sell_tokens_lost: BigRational::from_integer(0.into()), + tokens_lost, }; let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &pay_out_more); @@ -597,21 +830,29 @@ mod tests { let estimate = ensure_quote_accuracy(&high_threshold, &query, H160::zero(), &pay_out_more); assert!(estimate.is_ok()); + let tokens_lost = hashmap! { + sell_token => BigRational::from_integer((-500).into()), + buy_token => BigRational::from_integer(0.into()), + }; + let sell_less = SettleOutput { gas_used: 0.into(), out_amount: 2_000.into(), - buy_tokens_lost: BigRational::from_integer(0.into()), - sell_tokens_lost: BigRational::from_integer((-500).into()), + tokens_lost, }; // Ending up with surplus in the buffers is always fine let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &sell_less); assert!(estimate.is_ok()); + let tokens_lost = hashmap! { + sell_token => BigRational::from_integer(0.into()), + buy_token => BigRational::from_integer((-1_000).into()), + }; + let pay_out_less = SettleOutput { gas_used: 0.into(), out_amount: 2_000.into(), - buy_tokens_lost: BigRational::from_integer((-1_000).into()), - sell_tokens_lost: BigRational::from_integer(0.into()), + tokens_lost, }; // Ending up with surplus in the buffers is always fine let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &pay_out_less); diff --git a/crates/shared/src/trade_finding/external.rs b/crates/shared/src/trade_finding/external.rs index 60784e2e4b..8aef3be293 100644 --- a/crates/shared/src/trade_finding/external.rs +++ b/crates/shared/src/trade_finding/external.rs @@ -4,7 +4,15 @@ use { crate::{ price_estimation::{PriceEstimationError, Query}, request_sharing::{BoxRequestSharing, RequestSharing}, - trade_finding::{Interaction, Quote, Trade, TradeError, TradeFinding}, + trade_finding::{ + Interaction, + LegacyTrade, + Quote, + Trade, + TradeError, + TradeFinding, + TradeKind, + }, }, anyhow::{anyhow, Context}, ethrpc::block_stream::CurrentBlockWatcher, @@ -20,7 +28,7 @@ pub struct ExternalTradeFinder { /// Utility to make sure no 2 identical requests are in-flight at the same /// time. Instead of issuing a duplicated request this awaits the /// response of the in-flight request. - sharing: BoxRequestSharing>, + sharing: BoxRequestSharing>, /// Client to issue http requests with. client: Client, @@ -50,7 +58,7 @@ impl ExternalTradeFinder { /// Queries the `/quote` endpoint of the configured driver and deserializes /// the result into a Quote or Trade. - async fn shared_query(&self, query: &Query) -> Result { + async fn shared_query(&self, query: &Query) -> Result { let fut = move |query: &Query| { let order = dto::Order { sell_token: query.sell_token, @@ -93,19 +101,15 @@ impl ExternalTradeFinder { .text() .await .map_err(|err| PriceEstimationError::EstimatorInternal(anyhow!(err)))?; - let quote = serde_json::from_str::(&text).map_err(|err| { - if let Ok(err) = serde_json::from_str::(&text) { - PriceEstimationError::from(err) - } else { - PriceEstimationError::EstimatorInternal(anyhow!(err)) - } - })?; - match quote { - dto::QuoteKind::Legacy(quote) => Ok(Trade::from(quote)), - dto::QuoteKind::Regular(_) => Err(PriceEstimationError::EstimatorInternal( - anyhow!("Quote with JIT orders is not currently supported"), - )), - } + serde_json::from_str::(&text) + .map(TradeKind::from) + .map_err(|err| { + if let Ok(err) = serde_json::from_str::(&text) { + PriceEstimationError::from(err) + } else { + PriceEstimationError::EstimatorInternal(anyhow!(err)) + } + }) } .boxed() }; @@ -117,7 +121,16 @@ impl ExternalTradeFinder { } } -impl From for Trade { +impl From for TradeKind { + fn from(quote: dto::QuoteKind) -> Self { + match quote { + dto::QuoteKind::Legacy(quote) => TradeKind::Legacy(quote.into()), + dto::QuoteKind::Regular(quote) => TradeKind::Regular(quote.into()), + } + } +} + +impl From for LegacyTrade { fn from(quote: dto::LegacyQuote) -> Self { Self { out_amount: quote.amount, @@ -137,6 +150,36 @@ impl From for Trade { } } +impl From for Trade { + fn from(quote: dto::Quote) -> Self { + Self { + clearing_prices: quote.clearing_prices, + gas_estimate: quote.gas, + pre_interactions: quote + .pre_interactions + .into_iter() + .map(|interaction| Interaction { + target: interaction.target, + value: interaction.value, + data: interaction.call_data, + }) + .collect(), + interactions: quote + .interactions + .into_iter() + .map(|interaction| Interaction { + target: interaction.target, + value: interaction.value, + data: interaction.call_data, + }) + .collect(), + solver: quote.solver, + tx_origin: quote.tx_origin, + jit_orders: quote.jit_orders, + } + } +} + impl From for PriceEstimationError { fn from(value: dto::Error) -> Self { match value.kind.as_str() { @@ -163,22 +206,29 @@ impl TradeFinding for ExternalTradeFinder { // reuse the same logic here. let trade = self.get_trade(query).await?; let gas_estimate = trade - .gas_estimate + .gas_estimate() .context("no gas estimate") .map_err(TradeError::Other)?; Ok(Quote { - out_amount: trade.out_amount, + out_amount: trade + .out_amount( + &query.buy_token, + &query.sell_token, + &query.in_amount.get(), + &query.kind, + ) + .map_err(TradeError::Other)?, gas_estimate, - solver: trade.solver, + solver: trade.solver(), }) } - async fn get_trade(&self, query: &Query) -> Result { + async fn get_trade(&self, query: &Query) -> Result { self.shared_query(query).await } } -mod dto { +pub(crate) mod dto { use { app_data::AppDataHash, bytes_hex::BytesHex, diff --git a/crates/shared/src/trade_finding/mod.rs b/crates/shared/src/trade_finding/mod.rs index 221964b0e7..f8e589c467 100644 --- a/crates/shared/src/trade_finding/mod.rs +++ b/crates/shared/src/trade_finding/mod.rs @@ -4,12 +4,19 @@ pub mod external; use { - crate::price_estimation::{PriceEstimationError, Query}, - anyhow::Result, + crate::{ + conversions::U256Ext, + price_estimation::{PriceEstimationError, Query}, + trade_finding::external::dto, + }, + anyhow::{Context, Result}, derive_more::Debug, ethcontract::{contract::MethodBuilder, tokens::Tokenize, web3::Transport, Bytes, H160, U256}, - model::interaction::InteractionData, + model::{interaction::InteractionData, order::OrderKind}, + num::CheckedDiv, + number::conversions::big_rational_to_u256, serde::Serialize, + std::{collections::HashMap, ops::Mul}, thiserror::Error, }; @@ -21,7 +28,7 @@ use { #[async_trait::async_trait] pub trait TradeFinding: Send + Sync + 'static { async fn get_quote(&self, query: &Query) -> Result; - async fn get_trade(&self, query: &Query) -> Result; + async fn get_trade(&self, query: &Query) -> Result; } /// A quote. @@ -32,9 +39,73 @@ pub struct Quote { pub solver: H160, } -/// A trade. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TradeKind { + Legacy(LegacyTrade), + Regular(Trade), +} + +impl TradeKind { + pub fn gas_estimate(&self) -> Option { + match self { + TradeKind::Legacy(trade) => trade.gas_estimate, + TradeKind::Regular(trade) => trade.gas_estimate, + } + } + + pub fn solver(&self) -> H160 { + match self { + TradeKind::Legacy(trade) => trade.solver, + TradeKind::Regular(trade) => trade.solver, + } + } + + pub fn tx_origin(&self) -> Option { + match self { + TradeKind::Legacy(trade) => trade.tx_origin, + TradeKind::Regular(trade) => trade.tx_origin, + } + } + + pub fn out_amount( + &self, + buy_token: &H160, + sell_token: &H160, + in_amount: &U256, + order_kind: &OrderKind, + ) -> Result { + match self { + TradeKind::Legacy(trade) => Ok(trade.out_amount), + TradeKind::Regular(trade) => { + trade.out_amount(buy_token, sell_token, in_amount, order_kind) + } + } + } + + pub fn interactions(&self) -> Vec { + match self { + TradeKind::Legacy(trade) => trade.interactions.clone(), + TradeKind::Regular(trade) => trade.interactions.clone(), + } + } + + pub fn pre_interactions(&self) -> Vec { + match self { + TradeKind::Legacy(_) => Vec::new(), + TradeKind::Regular(trade) => trade.pre_interactions.clone(), + } + } +} + +impl Default for TradeKind { + fn default() -> Self { + TradeKind::Legacy(LegacyTrade::default()) + } +} + +/// A legacy trade. #[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct Trade { +pub struct LegacyTrade { /// For sell orders: how many buy_tokens this trade will produce. /// For buy orders: how many sell_tokens this trade will cost. pub out_amount: U256, @@ -49,6 +120,61 @@ pub struct Trade { pub tx_origin: Option, } +/// A trade with JIT orders. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Trade { + pub clearing_prices: HashMap, + /// How many units of gas this trade will roughly cost. + pub gas_estimate: Option, + /// The onchain calls to run before sending user funds to the settlement + /// contract. + pub pre_interactions: Vec, + /// Interactions needed to produce the expected trade amount. + pub interactions: Vec, + /// Which solver provided this trade. + pub solver: H160, + /// If this is set the quote verification need to use this as the + /// `tx.origin` to make the quote pass the simulation. + pub tx_origin: Option, + pub jit_orders: Vec, +} + +impl Trade { + pub fn out_amount( + &self, + buy_token: &H160, + sell_token: &H160, + in_amount: &U256, + order_kind: &OrderKind, + ) -> Result { + let sell_price = self + .clearing_prices + .get(sell_token) + .context("clearing sell price missing")? + .to_big_rational(); + let buy_price = self + .clearing_prices + .get(buy_token) + .context("clearing buy price missing")? + .to_big_rational(); + let order_amount = in_amount.to_big_rational(); + + let out_amount = match order_kind { + OrderKind::Sell => order_amount + .mul(&sell_price) + .checked_div(&buy_price) + .context("div by zero: buy price")? + .ceil(), /* `ceil` is used to compute buy amount only: https://github.com/cowprotocol/contracts/blob/main/src/contracts/GPv2Settlement.sol#L389-L411 */ + OrderKind::Buy => order_amount + .mul(&buy_price) + .checked_div(&sell_price) + .context("div by zero: sell price")?, + }; + + big_rational_to_u256(&out_amount).context("out amount is not a valid U256") + } +} + /// Data for a raw GPv2 interaction. #[derive(Clone, PartialEq, Eq, Hash, Default, Serialize, Debug)] pub struct Interaction {