diff --git a/README.md b/README.md index 144eebd..648f76d 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,30 @@ contract Example { } ``` +### protocols/MevShare.sol + +Helper library to send bundle requests with the Mev-Share protocol. + +#### Example usage + +```solidity +import "suave-std/protocols/MevShare.sol"; + +contract Example { + function example() { + Transactions.Legacy memory legacyTxn0 = Transactions.Legacy({}); + bytes memory rlp = Transactions.encodeRLP(legacyTxn0); + + MevShare.Bundle memory bundle; + bundle.bodies = new bytes[](1); + bundle.bodies[0] = rlp; + // ... + + MevShare.sendBundle(bundle); + } +} +``` + ## Forge integration In order to use `forge`, you need to have a running `Suave` node and the `suave` binary in your path. diff --git a/src/protocols/MevShare.sol b/src/protocols/MevShare.sol new file mode 100644 index 0000000..2990a20 --- /dev/null +++ b/src/protocols/MevShare.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.13; + +import "../suavelib/Suave.sol"; +import "solady/src/utils/LibString.sol"; +import "solady/src/utils/JSONParserLib.sol"; + +// https://github.com/flashbots/mev-share/blob/main/specs/bundles/v0.1.md#json-rpc-request-scheme +library MevShare { + struct Bundle { + uint64 inclusionBlock; + bytes[] bodies; + bool[] canRevert; + uint8[] refundPercents; + } + + function encodeBundle(Bundle memory bundle) internal pure returns (Suave.HttpRequest memory) { + require(bundle.bodies.length == bundle.canRevert.length, "MevShare: bodies and canRevert length mismatch"); + bytes memory body = abi.encodePacked('{"jsonrpc":"2.0","method":"mev_sendBundle","params":[{"version":"v0.1",'); + + // -> inclusion + body = + abi.encodePacked(body, '"inclusion":{"block":"', LibString.toMinimalHexString(bundle.inclusionBlock), '"},'); + + // -> body + body = abi.encodePacked(body, '"body":['); + + for (uint256 i = 0; i < bundle.bodies.length; i++) { + body = abi.encodePacked( + body, + '{"tx":"', + LibString.toHexString(bundle.bodies[i]), + '","canRevert":', + bundle.canRevert[i] ? "true" : "false", + "}" + ); + + if (i < bundle.bodies.length - 1) { + body = abi.encodePacked(body, ","); + } + } + + body = abi.encodePacked(body, "],"); + + // -> validity + body = abi.encodePacked(body, '"validity":{"refund":['); + + for (uint256 i = 0; i < bundle.refundPercents.length; i++) { + body = abi.encodePacked( + body, + '{"bodyIdx":', + LibString.toString(i), + ',"percent":', + LibString.toString(bundle.refundPercents[i]), + "}" + ); + + if (i < bundle.refundPercents.length - 1) { + body = abi.encodePacked(body, ","); + } + } + + body = abi.encodePacked(body, "]}"); + + Suave.HttpRequest memory request; + request.headers = new string[](1); + request.headers[0] = "Content-Type:application/json"; + request.body = body; + request.withFlashbotsSignature = true; + + return request; + } + + function sendBundle(string memory url, Bundle memory bundle) internal view { + Suave.HttpRequest memory request = encodeBundle(bundle); + request.url = url; + Suave.doHTTPRequest(request); + } +} diff --git a/test/protocols/MevShare.t.sol b/test/protocols/MevShare.t.sol new file mode 100644 index 0000000..2a6cdc5 --- /dev/null +++ b/test/protocols/MevShare.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "src/protocols/MevShare.sol"; +import "src/suavelib/Suave.sol"; + +contract MevShareTest is Test { + function testEncodeMevShare() public { + MevShare.Bundle memory bundle; + bundle.inclusionBlock = 1; + + bundle.bodies = new bytes[](1); + bundle.bodies[0] = hex"1234"; + + bundle.canRevert = new bool[](1); + bundle.canRevert[0] = true; + + bundle.refundPercents = new uint8[](1); + bundle.refundPercents[0] = 10; + + Suave.HttpRequest memory request = MevShare.encodeBundle(bundle); + assertEq( + string(request.body), + '{"jsonrpc":"2.0","method":"mev_sendBundle","params":[{"version":"v0.1","inclusion":{"block":"0x1"},"body":[{"tx":"0x1234","canRevert":true}],"validity":{"refund":[{"bodyIdx":0,"percent":10}]}' + ); + assertTrue(request.withFlashbotsSignature); + } +}