Skip to content

Commit ebe3850

Browse files
authored
Single refund tx (#592)
## 📝 Summary Only 1 refundTxHashes is allowed. ## ✅ I have completed the following steps: * [X] Run `make lint` * [X] Run `make test` * [ ] Added tests (if applicable)
1 parent 3d0742c commit ebe3850

File tree

3 files changed

+110
-21
lines changed

3 files changed

+110
-21
lines changed

crates/rbuilder/src/building/testing/bundle_tests/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ fn test_bundle_ok_refunds() -> eyre::Result<()> {
344344
test_setup.set_bundle_refund(BundleRefund {
345345
percent,
346346
recipient,
347-
tx_hashes: vec![profit_tx_hash],
347+
tx_hash: profit_tx_hash,
348348
});
349349
let result = test_setup.commit_order_ok();
350350
let recipient_balance_after = test_setup.balance(recipient_named_address)?;

crates/rbuilder/src/primitives/mod.rs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ pub struct BundleRefund {
109109
pub percent: u8,
110110
/// Address where to refund to.
111111
pub recipient: Address,
112-
/// A list of transaction hashes to refund.
113-
/// This means that part (percent%) of the profit from the execution these txs goes to refund.recipient
114-
pub tx_hashes: Vec<TxHash>,
112+
/// Transaction hash to refund.
113+
/// This means that part (percent%) of the profit from the execution this txs goes to refund.recipient
114+
pub tx_hash: TxHash,
115115
}
116116

117117
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -206,10 +206,7 @@ impl Bundle {
206206
/// Returns `true` if the provided transaction hash is refundable.
207207
/// This means that part the profit from this execution goes to the self.refund.recipient
208208
pub fn is_tx_refundable(&self, hash: &B256) -> bool {
209-
self.refund
210-
.as_ref()
211-
.map(|r| r.tx_hashes.contains(hash))
212-
.unwrap_or_default()
209+
self.refund.as_ref().is_some_and(|r| r.tx_hash == *hash)
213210
}
214211

215212
fn uuid_v1(&mut self) -> Uuid {
@@ -250,7 +247,11 @@ impl Bundle {
250247
+ 32
251248
+ 32 * (self.reverting_tx_hashes.len()
252249
+ self.dropping_tx_hashes.len()
253-
+ self.refund.as_ref().map(|r| r.tx_hashes.len()).unwrap_or(0))
250+
+ if self.refund.is_some() {
251+
1usize
252+
} else {
253+
0usize
254+
})
254255
+ size_of::<char>()
255256
+ size_of::<Address>(),
256257
);
@@ -271,11 +272,9 @@ impl Bundle {
271272
if let Some(refund) = &mut self.refund {
272273
buff.push(refund.percent);
273274
buff.extend_from_slice(refund.recipient.as_slice());
274-
refund.tx_hashes.sort();
275-
buff.append(&mut (refund.tx_hashes.len() as u64).encode_var_vec());
276-
for tx_hash in &refund.tx_hashes {
277-
buff.extend_from_slice(tx_hash.as_slice());
278-
}
275+
// We used to allow multiple hashes and encode the len, we keep the 1 to be backwards compatible.
276+
buff.append(&mut (1u64).encode_var_vec());
277+
buff.extend_from_slice(refund.tx_hash.as_slice());
279278
}
280279
Self::uuid_from_buffer(buff)
281280
}

crates/rbuilder/src/primitives/serialize.rs

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ pub enum RawBundleConvertError {
190190
UnsupportedVersion(String),
191191
#[error("Field {0} not supported by version {1:?}")]
192192
FieldNotSupportedByVersion(String, BundleVersion),
193+
#[error("More than one refund tx hash not supported")]
194+
MoreThanOneRefundTxHash,
193195
}
194196

195197
/// Since we use the same API (eth_sendBundle) to get new bundles and also to cancel them we need this struct.
@@ -333,13 +335,22 @@ impl RawBundle {
333335
if let Some(percent) = refund_percent {
334336
// Refund can be configured only if bundle is not empty.
335337
// If bundle contains only one transaction, first == last.
338+
// If refund_tx_hashes is empty we use the last tx.
336339
if let Some((first_tx, last_tx)) = txs.first().zip(txs.last()) {
340+
let tx_hash = if let Some(refund_tx_hashes) = refund_tx_hashes {
341+
if refund_tx_hashes.len() > 1 {
342+
return Err(RawBundleConvertError::MoreThanOneRefundTxHash);
343+
}
344+
refund_tx_hashes.first().copied()
345+
} else {
346+
None
347+
}
348+
.unwrap_or(last_tx.hash());
349+
337350
refund = Some(BundleRefund {
338351
percent,
339352
recipient: refund_recipient.unwrap_or_else(|| first_tx.signer()),
340-
tx_hashes: refund_tx_hashes
341-
.filter(|tx_hashes| !tx_hashes.is_empty())
342-
.unwrap_or_else(|| Vec::from([last_tx.hash()])),
353+
tx_hash,
343354
});
344355
}
345356
}
@@ -411,7 +422,7 @@ impl RawBundle {
411422
replacement_nonce,
412423
refund_percent: value.refund.as_ref().map(|br| br.percent),
413424
refund_recipient: value.refund.as_ref().map(|br| br.recipient),
414-
refund_tx_hashes: value.refund.map(|br| br.tx_hashes),
425+
refund_tx_hashes: value.refund.map(|br| vec![br.tx_hash]),
415426
first_seen_at: None,
416427
version: Some(Self::encode_version(value.version)),
417428
}
@@ -1128,21 +1139,100 @@ mod tests {
11281139
.clone()
11291140
.decode_new_bundle(TxEncoding::WithBlobData)
11301141
.expect("failed to convert bundle request to bundle");
1131-
1142+
println!("{}", bundle.txs[0].hash());
11321143
assert_eq!(bundle.block, None);
11331144
assert_eq!(
11341145
bundle.refund,
11351146
Some(BundleRefund {
11361147
percent: 1,
11371148
recipient: Address::from_str("0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5").unwrap(),
1138-
tx_hashes: Vec::from([b256!(
1149+
tx_hash: b256!(
11391150
"0x75662ab9cb6d1be7334723db5587435616352c7e581a52867959ac24006ac1fe"
1140-
)]),
1151+
),
11411152
})
11421153
);
11431154
assert_eq!(bundle.uuid, uuid!("e2bdb8cd-9473-5a1b-b425-57fa7ecfe2c1"));
11441155
}
11451156

1157+
/// If refundTxHashes is missing it should use the last tx and the id should be the same.
1158+
#[test]
1159+
fn test_correct_bundle_decoding_refund_hash_missing() {
1160+
// raw json string
1161+
let base_bundle_json = r#"
1162+
{
1163+
"version": "v2",
1164+
"txs": [
1165+
"0x02f86b83aa36a780800982520894f24a01ae29dec4629dfb4170647c4ed4efc392cd861ca62a4c95b880c080a07d37bb5a4da153a6fbe24cf1f346ef35748003d1d0fc59cf6c17fb22d49e42cea02c231ac233220b494b1ad501c440c8b1a34535cdb8ca633992d6f35b14428672"
1166+
],
1167+
"blockNumber": 0,
1168+
"minTimestamp": 123,
1169+
"maxTimestamp": 1234,
1170+
"revertingTxHashes": ["0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],
1171+
"droppingTxHashes": ["0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
1172+
"refundPercent": 1,
1173+
"refundRecipient": "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5"
1174+
"#;
1175+
let bundles = [
1176+
base_bundle_json.to_owned() + "}",
1177+
base_bundle_json.to_owned()
1178+
+ r#","refundTxHashes": ["0x84310f7f7860f0cd65407fe340d471ca008d0c58976746a560312d4aebba3f4a"]}"#,
1179+
];
1180+
1181+
for bundle_json in bundles {
1182+
let bundle_request: RawBundle =
1183+
serde_json::from_str(&bundle_json).expect("failed to decode bundle");
1184+
1185+
let bundle = bundle_request
1186+
.clone()
1187+
.decode_new_bundle(TxEncoding::WithBlobData)
1188+
.expect("failed to convert bundle request to bundle");
1189+
assert_eq!(bundle.block, None);
1190+
assert_eq!(
1191+
bundle.refund,
1192+
Some(BundleRefund {
1193+
percent: 1,
1194+
recipient: Address::from_str("0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5")
1195+
.unwrap(),
1196+
tx_hash: b256!(
1197+
"0x84310f7f7860f0cd65407fe340d471ca008d0c58976746a560312d4aebba3f4a"
1198+
),
1199+
})
1200+
);
1201+
assert_eq!(bundle.uuid, uuid!("ea9954e1-b7be-5af0-9c39-6b11c9d24c05"));
1202+
}
1203+
}
1204+
1205+
/// More than 1 refundTxHashes should fail.
1206+
#[test]
1207+
fn test_fail_bundle_decoding_2_refund_hashes() {
1208+
// raw json string
1209+
let bundle_json = r#"
1210+
{
1211+
"version": "v2",
1212+
"txs": [
1213+
"0x02f86b83aa36a780800982520894f24a01ae29dec4629dfb4170647c4ed4efc392cd861ca62a4c95b880c080a07d37bb5a4da153a6fbe24cf1f346ef35748003d1d0fc59cf6c17fb22d49e42cea02c231ac233220b494b1ad501c440c8b1a34535cdb8ca633992d6f35b14428672"
1214+
],
1215+
"blockNumber": 0,
1216+
"minTimestamp": 123,
1217+
"maxTimestamp": 1234,
1218+
"revertingTxHashes": ["0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],
1219+
"droppingTxHashes": ["0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
1220+
"refundPercent": 1,
1221+
"refundRecipient": "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5",
1222+
"refundTxHashes": ["0x84310f7f7860f0cd65407fe340d471ca008d0c58976746a560312d4aebba3f4a","0x84310f7f7860f0cd65407fe340d471ca008d0c58976746a560312d4aebba3f4a"]
1223+
}
1224+
"#;
1225+
let bundle_request: RawBundle =
1226+
serde_json::from_str(bundle_json).expect("failed to decode bundle");
1227+
1228+
assert!(matches!(
1229+
bundle_request
1230+
.clone()
1231+
.decode_new_bundle(TxEncoding::WithBlobData),
1232+
Err(RawBundleConvertError::MoreThanOneRefundTxHash)
1233+
));
1234+
}
1235+
11461236
/// Should default to last version.
11471237
#[test]
11481238
fn test_correct_bundle_decoding_no_version() {

0 commit comments

Comments
 (0)