Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add eth_sendBundle protocol #28

Merged
merged 5 commits into from Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 63 additions & 9 deletions src/Transactions.sol
Expand Up @@ -9,20 +9,30 @@ library Transactions {
using RLPReader for RLPReader.Iterator;
using RLPReader for bytes;

struct Legacy {
struct EIP155 {
address to;
uint64 gas;
uint64 gasPrice;
uint64 value;
uint64 nonce;
uint256 gas;
uint256 gasPrice;
uint256 value;
uint256 nonce;
bytes data;
uint64 chainId;
uint256 chainId;
bytes r;
bytes s;
bytes v;
}

function encodeRLP(Legacy memory txStruct) internal pure returns (bytes memory) {
struct EIP155Request {
address to;
uint256 gas;
uint256 gasPrice;
uint256 value;
uint256 nonce;
bytes data;
uint256 chainId;
}

function encodeRLP(EIP155 memory txStruct) internal pure returns (bytes memory) {
bytes[] memory items = new bytes[](9);

items[0] = RLPWriter.writeUint(txStruct.nonce);
Expand All @@ -43,8 +53,29 @@ library Transactions {
return RLPWriter.writeList(items);
}

function decodeRLP(bytes memory rlp) internal pure returns (Legacy memory) {
Legacy memory txStruct;
function encodeRLP(EIP155Request memory txStruct) internal pure returns (bytes memory) {
bytes[] memory items = new bytes[](9);

items[0] = RLPWriter.writeUint(txStruct.nonce);
items[1] = RLPWriter.writeUint(txStruct.gasPrice);
items[2] = RLPWriter.writeUint(txStruct.gas);

if (txStruct.to == address(0)) {
items[3] = RLPWriter.writeBytes(bytes(""));
} else {
items[3] = RLPWriter.writeAddress(txStruct.to);
}
items[4] = RLPWriter.writeUint(txStruct.value);
items[5] = RLPWriter.writeBytes(txStruct.data);
items[6] = RLPWriter.writeUint(txStruct.chainId);
items[7] = RLPWriter.writeBytes("");
items[8] = RLPWriter.writeBytes("");

return RLPWriter.writeList(items);
}

function decodeRLP_EIP155(bytes memory rlp) internal pure returns (EIP155 memory) {
EIP155 memory txStruct;

RLPReader.RLPItem[] memory ls = rlp.toRlpItem().toList();
require(ls.length == 9, "invalid transaction");
Expand All @@ -67,4 +98,27 @@ library Transactions {

return txStruct;
}

function decodeRLP_EIP155Request(bytes memory rlp) internal pure returns (EIP155Request memory) {
EIP155Request memory txStruct;

RLPReader.RLPItem[] memory ls = rlp.toRlpItem().toList();
require(ls.length == 9, "invalid transaction");

txStruct.nonce = ls[0].toUint();
txStruct.gasPrice = ls[1].toUint();
txStruct.gas = ls[2].toUint();

if (ls[3].toRlpBytes().length == 1) {
txStruct.to = address(0);
} else {
txStruct.to = ls[3].toAddress();
}

txStruct.value = ls[4].toUint();
txStruct.data = ls[5].toBytes();
txStruct.chainId = uint64(ls[6].toUint());

return txStruct;
}
}
79 changes: 79 additions & 0 deletions src/protocols/Bundle.sol
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;

import "../suavelib/Suave.sol";
import "solady/src/utils/LibString.sol";

// https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint#eth_sendbundle
library Bundle {
struct BundleObj {
uint64 blockNumber;
uint64 minTimestamp;
uint64 maxTimestamp;
bytes[] txns;
}

function sendBundle(
string memory url,
BundleObj memory bundle
) internal view returns (bytes memory) {
Suave.HttpRequest memory request = encodeBundle(bundle);
request.url = url;
return Suave.doHTTPRequest(request);
}

function encodeBundle(
BundleObj memory args
) internal pure returns (Suave.HttpRequest memory) {
require(args.txns.length > 0, "Bundle: no txns");

bytes memory params = abi.encodePacked(
'{"blockNumber": "',
LibString.toHexString(args.blockNumber),
'", "txs": ['
);
for (uint256 i = 0; i < args.txns.length; i++) {
params = abi.encodePacked(
params,
'"',
LibString.toHexString(args.txns[i]),
'"'
);
if (i < args.txns.length - 1) {
params = abi.encodePacked(params, ",");
} else {
params = abi.encodePacked(params, "]");
}
}
if (args.minTimestamp > 0) {
params = abi.encodePacked(
params,
', "minTimestamp": ',
LibString.toString(args.minTimestamp)
);
}
if (args.maxTimestamp > 0) {
params = abi.encodePacked(
params,
', "maxTimestamp": ',
LibString.toString(args.maxTimestamp)
);
}
params = abi.encodePacked(params, "}");

bytes memory body = abi.encodePacked(
'{"jsonrpc":"2.0","method":"eth_sendBundle","params":[',
params,
'],"id":1}'
);

Suave.HttpRequest memory request;
request.method = "POST";
request.body = body;
request.headers = new string[](1);
request.headers[0] = "Content-Type: application/json";
request.withFlashbotsSignature = true;

return request;
}
}
14 changes: 7 additions & 7 deletions test/Transactions.t.sol
Expand Up @@ -7,8 +7,8 @@ import "src/Transactions.sol";
contract TestTransactions is Test {
using Transactions for *;

function testLegacyTransactionRLPEncoding() public {
Transactions.Legacy memory txnWithToAddress = Transactions.Legacy({
function testEIP155TransactionRLPEncoding() public {
Transactions.EIP155 memory txnWithToAddress = Transactions.EIP155({
to: address(0x095E7BAea6a6c7c4c2DfeB977eFac326aF552d87),
gas: 50000,
gasPrice: 10,
Expand All @@ -24,9 +24,9 @@ contract TestTransactions is Test {
bytes memory expected = abi.encodePacked(
hex"f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1"
);
_testLegacyTransaction(txnWithToAddress, expected);
_testEIP155Transaction(txnWithToAddress, expected);

Transactions.Legacy memory txnWithoutToAddress = Transactions.Legacy({
Transactions.EIP155 memory txnWithoutToAddress = Transactions.EIP155({
to: address(0),
gas: 50000,
gasPrice: 10,
Expand All @@ -42,14 +42,14 @@ contract TestTransactions is Test {
expected = abi.encodePacked(
hex"f84b010a82c350800a021ba0754a33a9c37cfcf61cd61939fd93f5fe194b7d1ee6ef07490e8c880f3bd0d87da0715bd50fa2c24e2ce0ea595025a44a39ac238558882f9f07dd885ddc51839419"
);
_testLegacyTransaction(txnWithoutToAddress, expected);
_testEIP155Transaction(txnWithoutToAddress, expected);
}

function _testLegacyTransaction(Transactions.Legacy memory legacyTxn, bytes memory expectedRlp) public {
function _testEIP155Transaction(Transactions.EIP155 memory legacyTxn, bytes memory expectedRlp) public {
bytes memory rlp = Transactions.encodeRLP(legacyTxn);
assertEq0(rlp, expectedRlp);

Transactions.Legacy memory legacyTxn1 = Transactions.decodeRLP(rlp);
Transactions.EIP155 memory legacyTxn1 = Transactions.decodeRLP_EIP155(rlp);

// re-encode to validate that the decoding was correct
bytes memory rlp1 = Transactions.encodeRLP(legacyTxn1);
Expand Down
40 changes: 40 additions & 0 deletions test/protocols/Bundle.t.sol
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "src/protocols/Bundle.sol";
import "src/suavelib/Suave.sol";

contract EthSendBundle is Test {
function testEthSendBundleEncode() public {
Bundle.BundleObj memory bundle;
bundle.blockNumber = 1;
bundle.txns = new bytes[](1);
bundle.txns[0] = hex"1234";

Suave.HttpRequest memory request = Bundle.encodeBundle(bundle);
assertEq(
string(request.body),
'{"jsonrpc":"2.0","method":"eth_sendBundle","params":[{"blockNumber": "0x01", "txs": ["0x1234"]}],"id":1}'
);
assertTrue(request.withFlashbotsSignature);

// encode with 'minTimestamp'
bundle.minTimestamp = 2;

Suave.HttpRequest memory request2 = Bundle.encodeBundle(bundle);
assertEq(
string(request2.body),
'{"jsonrpc":"2.0","method":"eth_sendBundle","params":[{"blockNumber": "0x01", "txs": ["0x1234"], "minTimestamp": 2}],"id":1}'
);

// encode with 'maxTimestamp'
bundle.maxTimestamp = 3;

Suave.HttpRequest memory request3 = Bundle.encodeBundle(bundle);
assertEq(
string(request3.body),
'{"jsonrpc":"2.0","method":"eth_sendBundle","params":[{"blockNumber": "0x01", "txs": ["0x1234"], "minTimestamp": 2, "maxTimestamp": 3}],"id":1}'
);
}
}