Skip to content
This repository was archived by the owner on May 12, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 158 additions & 26 deletions src/protocols/Bundle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,64 +10,183 @@ 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;

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++) {
Expand All @@ -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;
Expand All @@ -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;
Expand Down
33 changes: 27 additions & 6 deletions test/protocols/Bundle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}'
);
}

Expand Down