diff --git a/src/protocols/Bundle.sol b/src/protocols/Bundle.sol index fdd5203..a201281 100644 --- a/src/protocols/Bundle.sol +++ b/src/protocols/Bundle.sol @@ -10,56 +10,173 @@ import "solady/src/utils/JSONParserLib.sol"; library Bundle { /// @notice BundleObj is a struct that represents a bundle to be sent to the Flashbots relay. /// @param blockNumber the block number at which the bundle should be executed. + /// @param txns the transactions to be included in the bundle. /// @param minTimestamp the minimum timestamp at which the bundle should be executed. /// @param maxTimestamp the maximum timestamp at which the bundle should be executed. - /// @param txns the transactions to be included in the bundle. + /// @param revertingHashes the hashes of the transactions that the bundle should allow to revert. + /// @param replacementUuid the UUID of the bundle submission that should be replaced by this bundle. This argument must have been passed to the bundle being replaced. + /// @param refundPercent (mev-share) percentage of gas fees that should be refunded to the tx originator. struct BundleObj { uint64 blockNumber; + bytes[] txns; + // sendBundle exclusive: uint64 minTimestamp; uint64 maxTimestamp; - bytes[] txns; + bytes32[] revertingHashes; + string replacementUuid; + // SBundle (sim) exclusive + uint256 refundPercent; } using JSONParserLib for string; using JSONParserLib for JSONParserLib.Item; + using LibString for *; /// @notice send a bundle to the Flashbots relay. - /// @param url the URL of the Flashbots relay. /// @param bundle the bundle to send. + /// @param url the URL of the Flashbots relay. /// @return response raw bytes response from the Flashbots relay. - function sendBundle(string memory url, BundleObj memory bundle) internal returns (bytes memory) { - Suave.HttpRequest memory request = encodeBundle(bundle); + function sendBundle( + BundleObj memory bundle, + string memory url + ) internal returns (bytes memory) { + Suave.HttpRequest memory request = encodeSendBundle(bundle); request.url = url; return Suave.doHTTPRequest(request); } - function encodeBundle(BundleObj memory args) internal pure returns (Suave.HttpRequest memory) { + /// @notice simulate a bundle using the Flashbots bundle API. + /// @param bundle the bundle to simulate. + /// @return egp the simulated effective gas price of the bundle. + function simulateBundle( + BundleObj memory bundle + ) internal returns (uint64 egp) { + bytes memory simParams = encodeSimBundle(bundle); + egp = Suave.simulateBundle(simParams); + } + + /// @notice Encodes an [RpcSBundle](https://github.com/flashbots/suave-geth/blob/main/core/types/sbundle.go#L21-L27) for `Suave.simulateBundle`. + /// @param args the bundle to encode. + /// @return params the encoded simulateBundle payload. + function encodeSimBundle( + BundleObj memory args + ) internal pure returns (bytes memory params) { require(args.txns.length > 0, "Bundle: no txns"); - bytes memory params = - abi.encodePacked('{"blockNumber": "', LibString.toHexString(args.blockNumber), '", "txs": ['); + params = abi.encodePacked( + '{"blockNumber": "', + args.blockNumber.toMinimalHexString(), + '", ' + ); + if (args.refundPercent > 0) { + params = abi.encodePacked( + params, + '"percent": ', + args.refundPercent.toString(), + ", " + ); + } + if (args.revertingHashes.length > 0) { + params = abi.encodePacked(params, '"revertingHashes": ['); + for (uint256 i = 0; i < args.revertingHashes.length; i++) { + params = abi.encodePacked( + params, + '"', + uint256(args.revertingHashes[i]).toHexString(), + '"' + ); + if (i < args.revertingHashes.length - 1) { + params = abi.encodePacked(params, ", "); + } + } + params = abi.encodePacked(params, "], "); + } + params = abi.encodePacked(params, '"txs": ['); for (uint256 i = 0; i < args.txns.length; i++) { - params = abi.encodePacked(params, '"', LibString.toHexString(args.txns[i]), '"'); + params = abi.encodePacked( + params, + '"', + args.txns[i].toHexString(), + '"' + ); if (i < args.txns.length - 1) { - params = abi.encodePacked(params, ","); - } else { - params = abi.encodePacked(params, "]"); + params = abi.encodePacked(params, ", "); } } + params = abi.encodePacked(params, "]}"); + } + + /// @notice Encodes a bundle into an RPC request to `eth_sendBundle`. + /// @param args the bundle to encode. + /// @return request the encoded HTTP request. + function encodeSendBundle( + BundleObj memory args + ) internal pure returns (Suave.HttpRequest memory) { + require(args.txns.length > 0, "Bundle: no txns"); + + // body + bytes memory body = abi.encodePacked( + '{"jsonrpc": "2.0", "method": "eth_sendBundle", "id": 1, "params": [{' + ); + + // params + body = abi.encodePacked( + body, + '"blockNumber": "', + args.blockNumber.toMinimalHexString(), + '", ' + ); if (args.minTimestamp > 0) { - params = abi.encodePacked(params, ', "minTimestamp": ', LibString.toString(args.minTimestamp)); + body = abi.encodePacked( + body, + '"minTimestamp": ', + args.minTimestamp.toString(), + ", " + ); } if (args.maxTimestamp > 0) { - params = abi.encodePacked(params, ', "maxTimestamp": ', LibString.toString(args.maxTimestamp)); + body = abi.encodePacked( + body, + '"maxTimestamp": ', + args.maxTimestamp.toString(), + ", " + ); } - params = abi.encodePacked(params, "}"); - - bytes memory body = - abi.encodePacked('{"jsonrpc":"2.0","method":"eth_sendBundle","params":[', params, '],"id":1}'); + if (args.revertingHashes.length > 0) { + body = abi.encodePacked(body, '"revertingHashes": ['); + for (uint256 i = 0; i < args.revertingHashes.length; i++) { + body = abi.encodePacked( + body, + '"', + abi.encodePacked(args.revertingHashes[i]), + '"' + ); + if (i < args.revertingHashes.length - 1) { + body = abi.encodePacked(body, ", "); + } + } + body = abi.encodePacked(body, "], "); + } + if (abi.encodePacked(args.replacementUuid).length > 0) { + body = abi.encodePacked( + body, + '"replacementUuid": "', + args.replacementUuid, + '", ' + ); + } + body = abi.encodePacked(body, '"txs": ['); + for (uint256 i = 0; i < args.txns.length; i++) { + body = abi.encodePacked(body, '"', args.txns[i].toHexString(), '"'); + if (i < args.txns.length - 1) { + body = abi.encodePacked(body, ", "); + } + } + body = abi.encodePacked(body, "]}]}"); Suave.HttpRequest memory request; request.method = "POST"; - request.body = body; + request.body = abi.encodePacked(body); request.headers = new string[](1); request.headers[0] = "Content-Type: application/json"; request.withFlashbotsSignature = true; @@ -67,7 +184,9 @@ library Bundle { return request; } - function _stripQuotesAndPrefix(string memory s) internal pure returns (string memory) { + function _stripQuotesAndPrefix( + string memory s + ) internal pure returns (string memory) { bytes memory strBytes = bytes(s); bytes memory result = new bytes(strBytes.length - 4); for (uint256 i = 3; i < strBytes.length - 1; i++) { @@ -79,7 +198,9 @@ library Bundle { /// @notice decode a bundle from a JSON string. /// @param bundleJson the JSON string of the bundle. /// @return bundle the decoded bundle. - function decodeBundle(string memory bundleJson) public pure returns (Bundle.BundleObj memory) { + function decodeBundle( + string memory bundleJson + ) public pure returns (Bundle.BundleObj memory) { JSONParserLib.Item memory root = bundleJson.parse(); JSONParserLib.Item memory txnsNode = root.at('"txs"'); Bundle.BundleObj memory bundle; @@ -89,20 +210,31 @@ library Bundle { for (uint256 i = 0; i < txnsLength; i++) { JSONParserLib.Item memory txnNode = txnsNode.at(i); - bytes memory txn = HexStrings.fromHexString(_stripQuotesAndPrefix(txnNode.value())); + bytes memory txn = HexStrings.fromHexString( + _stripQuotesAndPrefix(txnNode.value()) + ); txns[i] = txn; } bundle.txns = txns; - require(root.at('"blockNumber"').isString(), "Bundle: blockNumber is not a string"); - bundle.blockNumber = uint64(root.at('"blockNumber"').value().decodeString().parseUintFromHex()); + require( + root.at('"blockNumber"').isString(), + "Bundle: blockNumber is not a string" + ); + bundle.blockNumber = uint64( + root.at('"blockNumber"').value().decodeString().parseUintFromHex() + ); if (root.at('"minTimestamp"').isNumber()) { - bundle.minTimestamp = uint64(root.at('"minTimestamp"').value().parseUint()); + bundle.minTimestamp = uint64( + root.at('"minTimestamp"').value().parseUint() + ); } if (root.at('"maxTimestamp"').isNumber()) { - bundle.maxTimestamp = uint64(root.at('"maxTimestamp"').value().parseUint()); + bundle.maxTimestamp = uint64( + root.at('"maxTimestamp"').value().parseUint() + ); } bundle.txns = txns; diff --git a/test/protocols/Bundle.t.sol b/test/protocols/Bundle.t.sol index 8a85428..6008406 100644 --- a/test/protocols/Bundle.t.sol +++ b/test/protocols/Bundle.t.sol @@ -15,29 +15,50 @@ contract EthSendBundle is Test { bundle.txns = new bytes[](1); bundle.txns[0] = hex"1234"; - Suave.HttpRequest memory request = Bundle.encodeBundle(bundle); + Suave.HttpRequest memory request = Bundle.encodeSendBundle(bundle); assertEq( string(request.body), - '{"jsonrpc":"2.0","method":"eth_sendBundle","params":[{"blockNumber": "0x01", "txs": ["0x1234"]}],"id":1}' + '{"jsonrpc": "2.0", "method": "eth_sendBundle", "id": 1, "params": [{"blockNumber": "0x01", "txs": ["0x1234"]}]}' ); assertTrue(request.withFlashbotsSignature); // encode with 'minTimestamp' bundle.minTimestamp = 2; - Suave.HttpRequest memory request2 = Bundle.encodeBundle(bundle); + Suave.HttpRequest memory request2 = Bundle.encodeSendBundle(bundle); assertEq( string(request2.body), - '{"jsonrpc":"2.0","method":"eth_sendBundle","params":[{"blockNumber": "0x01", "txs": ["0x1234"], "minTimestamp": 2}],"id":1}' + '{"jsonrpc": "2.0", "method": "eth_sendBundle", "id": 1, "params": [{"blockNumber": "0x01", "minTimestamp": 2, "txs": ["0x1234"]}]}' ); // encode with 'maxTimestamp' bundle.maxTimestamp = 3; - Suave.HttpRequest memory request3 = Bundle.encodeBundle(bundle); + Suave.HttpRequest memory request3 = Bundle.encodeSendBundle(bundle); assertEq( string(request3.body), - '{"jsonrpc":"2.0","method":"eth_sendBundle","params":[{"blockNumber": "0x01", "txs": ["0x1234"], "minTimestamp": 2, "maxTimestamp": 3}],"id":1}' + '{"jsonrpc": "2.0", "method": "eth_sendBundle", "id": 1, "params": [{"blockNumber": "0x01", "minTimestamp": 2, "maxTimestamp": 3, "txs": ["0x1234"]}]}' + ); + } + + function testEncodeSimBundle() public { + Bundle.BundleObj memory bundle; + bundle.blockNumber = 1; + bundle.txns = new bytes[](1); + bundle.txns[0] = hex"1234"; + bundle.refundPercent = 50; + + bytes memory params = Bundle.encodeSimBundle(bundle); + assertEq(string(params), '{"blockNumber": "0x01", "percent": 50, "txs": ["0x1234"]}'); + + // encode with 'revertingHashes' + bundle.revertingHashes = new bytes32[](1); + bundle.revertingHashes[0] = keccak256("hashem"); + + bytes memory params2 = Bundle.encodeSimBundle(bundle); + assertEq( + string(params2), + '{"blockNumber": "0x01", "percent": 50, "revertingHashes": ["0x0a80df9c7574c9524999e774c05a27acf214618b45f4948b88ad1083e13a871a"], "txs": ["0x1234"]}' ); }