2

I'm writing a bot that monitors a DeFi protocol for liquidation opportunities. The bot behaves like in the pseudo-code below:

let pending_txs: [];

for every_new_block in Ethereum {
  // Check the state of pending txs.
  for pending_tx in pending_txs {
    let receipt = get_receipt(pending_tx);

    if receipt != null {
      // If we got a receipt from Ethereum, clear the tx from the local state.
      delete_tx_from_array(pending_tx);
    } else {
      // Otherwise, replace the tx and update it in the array.
      let new_gas_price = bump_gas_price(pending_tx.gas_price);
      let tx = send_tx(new_gas_price);
      update_tx_in_array(tx);
    }
  }

  ...
   
  // Search for new liquidation opportunities.
  if liquidation_opportunity and if !pending_tx_for_liquidation_opportunity {
    let tx = send_tx(initial_gas_price);
    add_tx_in_array(tx);
  }
}

The above works well most of the times, but there it an exception:

  1. Submit tx 0x123
  2. Wait 15 seconds, check if we got a receipt from Ethereum.
  3. If we didn't, re-calculate the gas price and send a replacement tx 0xabc.
  4. By the time the replacement tx 0xabc is broadcast to the Ethereum network, the 0x123 gets mined.
  5. The bot will attempt to replace the 0xabc tx forever, because the receipt is always null for 0xabc.

When I update the tx in the array, I also override the previous tx hash (this makes the implementation simple). In other words, the script ceases to be aware of past tx hashes once it broadcasts a replacement tx, which causes an infinite regress when a racing condition occurs.

The question is whether there is any way to avoid this situation? I've only run my bot on Rinkeby, against an Infura node. Maybe it's something that happens frequently on testnets, but rarely on mainnet?

I know that one solution is to modify the code to track multiple tx hashes per liquidation opportunity, but that would greatly increase the complexity of the implementation (I'm using Rust).

1

You can use the nonce and transaction count to validate if the nonce was mined.

if (tx.nonce < getTransactionCount(sender)) {
  drop transaction
}

I don't think this situation is particular to testnets. I'd say it is worse in mainnet where short chain reorgs are common.

3
  • 1
    Riiight, so I would add this in the else block. I forgot about the relation between the account nonce and the dropping of txs. Cheers! – Paul Razvan Berg Jan 21 at 20:59
  • 1
    Update: shouldn't it be getTransactionCount(sender) > tx.nonce? What I want is to stop replacing the tx if the tx count of the sender has increased. If the tx count is equal to the tx nonce, then I can safely replace the tx. – Paul Razvan Berg Jan 21 at 21:14
  • 1
    @PaulRazvanBerg You are correct. getTransactionCount(sender) is the next nonce to be mined so tx.nonce < getTransactionCount(sender) we are sure it was mined. – Ismael Jan 21 at 21:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.