0

enter link description here smart contract code ↓

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "./IBEP20.sol";

interface WBNB is IERC20 { function deposit() external payable; function withdraw(uint) external; }

interface IPancakeRouter02 { function swapExactTokensForTokensSupportingFeeOnTransferTokens( uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external;

function getAmountsOut(uint256 amountIn, address[] calldata path)
    external
    view
    returns (uint256[] memory amounts);

}

interface ISandWicher { struct SimulationResult { uint256 expectedBuy; uint256 balanceBeforeBuy; uint256 balanceAfterBuy; uint256 balanceBeforeSell; uint256 balanceAfterSell; uint256 expectedSell; } }

contract SandwichAttack is ISandWicher { address private owner; modifier onlyOwner() { require(msg.sender == owner, "Only the owner can call this function"); _; }

constructor() {
    owner = msg.sender;
}

/**
 * @dev Buys tokens
 */
function buyToken(bytes calldata _data)
    external
    onlyOwner
{
    _buy(_data);
}

/**
 * Sells tokens
 * Balance of tokens we are selling to be gt > 0
 */
function sellToken(bytes calldata _data)
    external onlyOwner
{
    _sell(_data);
}

function simulate(bytes calldata _buydata, bytes calldata _selldata)
    external
    onlyOwner
    returns (SimulationResult memory result)
{
    address[] memory path;
    address router;
    uint256 amountIn;
    // Buy
    (router, amountIn, , path) = abi.decode(
        _buydata,
        (address, uint256, uint256, address[])
    );

    IERC20 toToken = IERC20(path[path.length - 1]);

    uint256 balanceBeforeBuy = toToken.balanceOf(address(this));

    uint256 expectedBuy = getAmountsOut(router, amountIn, path);

    _buy(_buydata);

    uint256 balanceAfterBuy = toToken.balanceOf(address(this));

    // Sell

    (router, path, ) = abi.decode(_selldata, (address, address[], uint256));
    IERC20 fromToken = IERC20(path[path.length - 1]);

    uint256 balanceBeforeSell = fromToken.balanceOf(address(this));
    amountIn = IERC20(path[0]).balanceOf(address(this));
    uint256 expectedSell = getAmountsOut(router, amountIn, path);
    _sell(_selldata);

    uint256 balanceAfterSell = fromToken.balanceOf(address(this));

    return
        SimulationResult({
            expectedBuy: expectedBuy,
            balanceBeforeBuy: balanceBeforeBuy,
            balanceAfterBuy: balanceAfterBuy,
            balanceBeforeSell: balanceBeforeSell,
            balanceAfterSell: balanceAfterSell,
            expectedSell: expectedSell
        });
}

function swapAnalysis(
    address router,
    uint256 amountIn,
    address[] memory path
) public payable returns (bool) {
    (bool success, ) = router.call(
        abi.encodeWithSignature(
            "swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256,uint256,address[],address,uint256)",
            amountIn,
            0,
            path,
            address(this),
            block.timestamp
        )
    );
    if (success == false) {
        return false;
    } else {
        return true;
    }
}

function _buy(bytes calldata _data) internal {
    (
        address router,
        uint256 amountIn,
        uint256 amountOutMin,
        address[] memory path
    ) = abi.decode(_data, (address, uint256, uint256, address[]));

    IERC20 fromToken = IERC20(path[0]);

    _approve(fromToken, router, amountIn);
    IPancakeRouter02(router).swapExactTokensForTokensSupportingFeeOnTransferTokens(
        amountIn,
        amountOutMin,
        path,
        address(this),
        block.timestamp
    );
}

function _sell(bytes calldata _data) internal {
    (address router, address[] memory path, uint256 amountOutMin) = abi.decode(
        _data,
        (address, address[], uint256)
    );

    IERC20 fromToken = IERC20(path[0]);
    uint256 amountIn = fromToken.balanceOf(address(this));

    require(amountIn > 0, "!BAL");

    _approve(fromToken, router, amountIn);
    IPancakeRouter02(router).swapExactTokensForTokensSupportingFeeOnTransferTokens(
        amountIn,
        amountOutMin,
        path,
        address(this),
        block.timestamp
    );
}

function _approve(
    IERC20 token,
    address router,
    uint256 amountIn
) internal {
    if (token.allowance(address(this), router) < amountIn) {
        // approving the tokens to be spent by router
        SafeERC20.safeApprove(token, router, amountIn);
    }
}

function getAmountsOut(
    address router,
    uint256 amountIn,
    address[] memory path
) internal view returns (uint256) {
    uint256[] memory amounts = IPancakeRouter02(router).getAmountsOut(
        amountIn,
        path
    );
    return amounts[amounts.length - 1];strong text
}

function withdrawBNBToOwner() external onlyOwner {
    uint256 balance = address(this).balance;
    payable(msg.sender).transfer(balance);
}

function withdrawWBNB(address tokenAddress, uint256 amount) public onlyOwner {
    IBEP20 token = IBEP20(tokenAddress);
    token.transfer(msg.sender, amount);
}

function withdrawBEP20(address tokenAddress, uint256 amount) public onlyOwner {
    IBEP20 token = IBEP20(tokenAddress);
    uint256 contractBalance = token.balanceOf(address(this));
    require(contractBalance >= amount, "Insufficient contract balance.");
    token.transfer(msg.sender, amount);
}

function call(address payable _to, uint256 _value, bytes memory _data) external onlyOwner {
    (bool success, ) = _to.call{value: _value}(_data);
    require(success, "External call failed");
}

receive() external payable {}

}

Al-Qa'qa'
  • 296
  • 4
  • I am not sure what you are asking or where you are encountering the error. Please read/follow https://stackoverflow.com/help/how-to-ask to create a reproducible example and make the question more clear so we can help. – ruby_newbie Oct 03 '23 at 15:55

1 Answers1

0

A badly formatted question, but still sufficient enough to do an investigation. Because I was curious and it might be helpful to others.

So first let's break down OP's situation and their question.

Background

OP is running a bot on the Binance Smart Chain. The bot is a sandwich attack bot — at least that's what the contract name says.

The bot failed to do a transaction that OP provides to us: 0x1f6adbca045fa2e836e51b44213d70958128304ae3dff82131d116fe420c7ce6.

At a first glance, the revert reason is very familiar to anyone with the DEX experience: Fail with error 'PancakeRouter: INSUFFICIENT_OUTPUT_AMOUNT. The obvious answer is — there was not enough tokens to get in the trade (or a slippage issue).

But that'd be a lazy answer (also acceptable, I guess). So let's see if we can find out what the actual reason is.

Investigation

Now that we have the transaction data and the actual contract as source, we can dive into it.

First let's have a look at the transaction data on BscScan under Input Data:

Function: buyToken(bytes signature) ***

MethodID: 0x8d3b76c5

Looks like BscScan already recognizes the first 4 bytes of the transaction 0x8d3b76c5 (you can check this if you switch to the original data view in Input Data) as a known methodID which is buyToken(bytes). This seems to be correct since we can check this in our contract: function buyToken(bytes calldata _data). There can be false positives here, but since we have access to the contract source, we are able to verify that this is indeed buyToken(bytes). The 4byte.directory database for Ethereum signatures also identifies 0x8d3b76c5 as buyToken(bytes).

Since it's buyToken(bytes), we know the function expects a bytes type parameter. Based on this, we identify [0] and [1] as an offset for the dynamic data (bytes is dynamic per the ABI spec) and the data length. Contract ABI specification is our source here.

This makes [2] to [8] the actual calldata passed to the buyToken(bytes) function.

Let's decipher those.

Looking at the contract again, here's the full buyToken external function:

    function buyToken(bytes calldata _data)
        external
        onlyOwner
    {
        _buy(_data);
    }

Let's look at what's in _buy:

function _buy(bytes calldata _data) internal {
        (
            address router,
            uint256 amountIn,
            uint256 amountOutMin,
            address[] memory path
        ) = abi.decode(_data, (address, uint256, uint256, address[]));

The error message (revert reason) at the start of this little investigation was pretty clear that the bot was trying to buy tokens through a Pancake router, but now we can get the actual Pancake router contract address and the rest:

  • [2]: 00000000000000000000000010ed43c718714eb63d5aa57b78b54704e256024e or 0x10ED43C718714eb63d5aA57B78B54704E256024E
  • [3]: 0000000000000000000000000000000000000000000000000138d3b8500e6958amountIn (in hex) or in 18 decimal token numbers: 0.088052981304289624 of an 18 decimal token. Or the amount of the token the bot is willing to exchange in the trade.
  • [4]: 0000000000000000000000000000000000000000690344636df5b34ddfc5b4faamountOutMin (in hex) or in 18 decimal token numbers: 32499875763.77201080322265625 of an 18 decimal token. Or the minimum amount of the token the bot is willing to get as a result of the trade.

Then we get to address[] memory path which starts another nested dynamic type, which means [5] and [6] are an offset and length again, so we just skip those.

Now we get to:

  • [7]: 000000000000000000000000bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c or 0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c which is WBNB. BscScan conveniently shows that it's an 18 decimal token.
  • [8]: 000000000000000000000000e9560d6ae1b7ab0ee8994faf40aa86562bf66653 or 0xe9560d6ae1b7ab0ee8994faf40aa86562bf66653 which is some token called DIABLO. BscScan again shows that it's an 18 decimal token. So our previous calculations of the token amounts that were attempted to trade were correct.

Let's do a summary of what we have so far.

The bot contract tried to swap 0.088 of WBNB for about 32 billion of DIABLO available at PancakeSwap and failed with INSUFFICIENT_OUTPUT_AMOUNT.

Some immediate hypotheses:

  • The token has a transfer fee (aka tax, reflection) which interfered with the bot operation.
  • Not enough liqiudity, i.e. not enough DIABLO tokens in the contract to provide the bot with the requested 32 billion DIABLO in exchange for 0.088 WBNB.
  • Something else.

The transfer fee and/or the liquidity issue possibilities are good enough to check first, so that's what I'm going to do.

Transfer fee

We previously figured out from the postion [8] in the transaction data that the token we are trying to get through the router is DIABLO deployed at 0xe9560d6ae1b7ab0ee8994faf40aa86562bf66653.

Transfer fee (tax, reflection) tokens became popular around 2021 with probably the most known example being SafeMoon. A transfer fee means every time a token is transferred, a portion of the transferred value is redirected e.g. to an address. This is set in the token's smart contract. For example, if you are transferring (an exchange on a DEX is also a transfer) 100 tokens of a token with 10% transfer fee, the actual amount the addressee receives is 90 tokens while 10 tokens go to a different address encoded in a contract. If you want a basic implementation walkthrough, there's a decent one on OpenZeppelin forum.

Tokens with transfer fees can print INSUFFICIENT_OUTPUT_AMOUNT on DEX trades if the set slippage % is less than the transfer fee % in the token contract. This mostly affects the sell transaction but may sometimes occur on the buy as well.

Let's do a quick a scan of the DIABLO token contract code.

CMD-F/CTRL-F fee and you get a match. Is this it?

Is DIABLO a transfer fee token and the bot failed to account for it when attempting the trade?

Let's have a look:

    constructor(
        string memory name_,
        string memory symbol_,
        uint8 decimals_,
        uint256 totalSupply_,
        address serviceFeeReceiver_,
        uint256 serviceFee_
    )

On closer inspection, the fee is only a part of the constructor function. A constructor function is a special one and only runs once on contract deployment to initialize the state. The deployed bytecode does not contain the constructor code. So no fees in the actual token contract.

(In fact, I remember now that there's a web app service that used to be popular that'd let you deploy an ERC-20 contract called StandardToken for a fee that'd get transferred as part of the deployment. DIABLO seems to be a token deployed using that service. Today, there's a much better service that lets you do this for free and it's the awesome OpenZeppelin Wizard. There really was no reason for the DIABLO token deployer to pay that fee).

Okay, it's not a transfer fee token that's causing the issue.

Let's move on to the next hypothesis.

Liquidity

Checking the transaction again but now for the block time stamps, we can see that OP's bot transaction failed 95 days after the DIABLO token deployment.

Could it be that the liquidity has been pulled before OP attempted the bot transaction and that caused the INSUFFICIENT_OUTPUT_AMOUNT revert? It's possible. How do we check that?

We need a few things:

  • The address of the PancakePair contract, which is the contract actually holding the liquidity pair WBNB-DIABLO. (Not the router contract that the bot calls in its transaction).
  • A block number at which to query the state of the contract holding the liquidity pair WBNB-DIABLO (the default contract name is PancakePair).
  • A node endpoint through which to do the query.
  • Construct the actual query.

PancakePair contract address

A typical DEX setup is familiar to most Ethereum StackExchange users:

  • Router contract — the contract that routes the trades. It doesn't hold any tokens or have the core DEX logic. This is the contract that the bot calls.
  • Factory — the contract that deploys the liquidity pair contracts. The contracts that hold the actual token pairs.
  • PancakePair — the contract that holds the actual token pairs, i.e. the liquidity. This is the contract that we want to call at the state of the block when the bot transaction failed.

Right now, we only have the Router contract address: 0x10ED43C718714eb63d5aA57B78B54704E256024E that we decoded from the position [2] of the failed transaction. But we know the setup: Router, Factory, PancakePair. So let's get to it.

BscScan by the Etherscan team is pretty awesome in a lot of ways including being an easy front end to the blockchain data that we can easily query.

  1. Open the Router contract in BscScan on the Read Contract tab.
  2. Hit factory and there you have it — the factory address: 0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73
  3. Now on the Factory contract open Read Contract.
  4. In getPair provide the token addresses of the pair: WBNB: 0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c and DIABLO: 0xe9560d6ae1b7ab0ee8994faf40aa86562bf66653 and hit Query.

The query will return the address of the PancakePair contract holding WBNB and DIABLO: 0x19855fc5ED3A36dD20acEbed5C5fE018e9e59F4C.

If you open the Read Contract tab of PancakePair and on getReserves hit Query, you will get the available liquidity on the contract as of the latest block. Good, but how do we find out the available liquidity at the time of the bot transaction?

Let's find out.

Block number

This is already getting very long, so for brevity — the state of the network changes with every block. For us, this means that we need to query the state of the contract at the same block that the bot transaction failed in or one block prior.

Let's have a look at the failed transaction again: 0x1f6adbca045fa2e836e51b44213d70958128304ae3dff82131d116fe420c7ce6

Fields of interest:

  • Block: 32210585
  • Other Attributes: Position in Block: 2

This means that the bot transaction was the third one (0, 1, 2) included in the block out of 100 (typical of a bot to be included in the first positions due to the higher fee). Unlikely that there was an emergency high fee liquidity pull in the block before that, so let's check the state of the contract one block prior at 32210584.

Node endpoint

How do we actually check the state of the network in the past? Latest block state is easy, we can get it directly off the BscScan nodes, but what about the past?

The short answer is there are full nodes and there are archive nodes. A full node has the full chain data but keeps an archive of states typically up to 128 blocks in the past and then prunes the state. An archive node is much bigger in size and keeps an archive of all states, which means you can query the chain at any state since the genesis block.

This is when you need to get an archive node endpoint. Typically, you'd get one from a Web3 infrastructure provider.

Construct the query

We are going to use eth_call to do a call to the getReserves function of the PancakePair contract 0x19855fc5ED3A36dD20acEbed5C5fE018e9e59F4C at block 32210584 to get the liquidity data at the state of one block prior to the failed bot transaction.

To properly construct the query, we need the function signature for getReserves() — the first 4 bytes of the Keccak-256 hash of the getReserves() string. Easiest way is to just use an online tool like this one.

The signature for getReserves() is 0x0902f1ac.

Btw, let's also convert the block number 32210584 from decimal to hex: 0x1EB7E98.

Now that we have all the data, let's do the query.

For simplicity, let's use curl:

curl -X POST \
     -H "Content-Type: application/json" \
     --data '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0x19855fc5ED3A36dD20acEbed5C5fE018e9e59F4C","data":"0x0902f1ac"}, "0x1EB7E98"]}' \
     ARCHIVE_NODE_ENDPOINT

The result is:

{"jsonrpc":"2.0","id":1,"result":"0x00000000000000000000000000000000000000000000000050b5f11976965b96000000000000000000000000000000000000001bcb51057a2e4647d1c02b48bb000000000000000000000000000000000000000000000000000000006518cf17"}

Let's decipher the result by checking the getReserves() function in the contract.

We have three outputs:

  • reserve0: 00000000000000000000000000000000000000000000000050b5f11976965b96
  • reserve1: 000000000000000000000000000000000000001bcb51057a2e4647d1c02b48bb
  • blockTimestampLast: 000000000000000000000000000000000000000000000000000000006518cf17

reserve0 is WBNB. By converting hex to decimal to 18 decimals we get roughly 5.8 WBNB. reserve1 is DIABLO. By converting hex to decimal to 18 decimals we get roughly 2 trillion DIABLO.

We have 5.8 WBNB & 2 trillion DIABLO at block 32210584.

Recall now the start of our investigation when we figured out that the bot tried to swap 0.088 of WBNB for about 32 billion of DIABLO at block 32210585.

This means that liquidity was not the issue. A failed hypothesis again.

Where do we go from here?

Something else

Okay, here's another idea. Let's have a look at all the DIABLO token transfers in block 32210585 (the block in which the bot transaction failed). Maybe we'll get a clue from that.

One way to do this is to get all transfer events emitted for the DIABLO token in block 32210585.

Let's double check if event emission on token transfer is actually in the contract.

Okay, yes, it's there.

Let's use eth_getLogs to get the transfer events for the token. We need to get the event signature for the emitted transfer event.

The event emission is emit Transfer(sender, recipient, amount), so the event signature is Transfer(address,address,uint256). To be able to filter by the transfer event, we'll need to pass it as a topic to our call. A topic for the event signature is again the Keccak-256 hash of it. We can use any simple Keccak-256 online hash tool and just paste Transfer(address,address,uint256) and get the topic as output: ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

Now let's construct our query (and make it readable with JQ):

curl -X POST \
     -H "Content-Type: application/json" \
     --data '{"jsonrpc":"2.0","id":1,"method":"eth_getLogs","params":[{"fromBlock":"0x1EB7E99","toBlock":"0x1EB7E99","address":"0xE9560d6AE1B7Ab0eE8994FAf40AA86562bF66653","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]}]}' \
     NODE_ENDPOINT | \
     jq '.result[] | {transactionHash: .transactionHash, logIndex: .logIndex}'

Here's the result:

{
  "transactionHash": "0xdec21fe9d6cc5bc214ec2edad3bde43de0aeee3336d1d4fe27a6ee74370d9acc",
  "logIndex": "0x1"
}
{
  "transactionHash": "0x87ca77df0bf81fece2967d42fdbb52be6b5378f474bc4b62b64a3d9933ba3f17",
  "logIndex": "0xa"
}
{
  "transactionHash": "0x87cd3e406b787f9ead48d58788aa2afb77761f83e9a6d6a4531c852724d4f085",
  "logIndex": "0xd"
}

logIndex is the position in which event logs are emitted. This already looks interesting as we here see a transfer of the DIABLO token that was logged at position 1 (0x1 in hex), which means it happened very early in the block. Recall that the position of the failed bot transaction is 2. So looks like there was an earlier transaction in the block that had a DIABLO token transfer with the hash: 0xdec21fe9d6cc5bc214ec2edad3bde43de0aeee3336d1d4fe27a6ee74370d9acc

Let's have a look at this transaction:

  • The contract it interacted with: 0x00000000009FB6869c8213A8e2D8DFA6260b59a4 — this immediately looks like a bot contract for two reasons:

    • The address seems to be a vanity one, meaning that the owner had to grind for so many 0s in the prefix.
    • The contract tab says ReInit, which means the owner likely optimized the code by calling SELFDESTRUCT on the contract and then deploying new bytecode on the same address. Commonly called a metamorphic contract.
  • Position in Block: 0 — it's the very first transaction included in the block.

  • BEP-20 Tokens Transferred — it's a Pancake swap transaction of 0.08 WBNB for about 29 billion DIABLO.

Bingo.

Let's have a quick look at the OP's bot failed transaction and this newly discovered bot transaction side by side:

OP's bot:

  • Block 32210585
  • Position In Block: 2
  • Attempted swap: 0.088 WBNB for 32 billion DIABLO
  • Transaction Fee: 0.004 WBNB
  • Result: failed

Newly discovered bot:

  • Block 32210585
  • Position In Block: 0
  • Executed swap: 0.08 WBNB for 29 billion DIABLO
  • Transaction Fee: 0.006 WBNB
  • Result: success

Looks like OP's bot got front run by another bot.

Confirmation

Let's do a quick check to close this case.

We'll simulate the failed bot transaction against the state of Block 32210584 and Block 32210585.

If OP's bot got front run, the simulated transaction should show as successfull at Block 32210584, since this was the state that the bot targeted, and as reverted at Block 32210585.

We'll do an eth_call and just use the original raw transaction data. We'll do the call from OP's address 0x6Ac41145a20C7fCA777f53fE525cbF84642855Bc to the bot address 0x887263E940EC04B296627dfEc2af6463CbC9499E.

Reminder that you need an archive node endpoint to get the state at a block in the past.

Block 32210584 (0x1EB7E98 in hex)

Call:

curl -X POST \
     -H "Content-Type: application/json" \
     --data '{
        "jsonrpc":"2.0",
        "id":1,
        "method":"eth_call",
        "params":[{
            "from":"0x6Ac41145a20C7fCA777f53fE525cbF84642855Bc",
            "to":"0x887263E940EC04B296627dfEc2af6463CbC9499E",
            "data":"0x8d3b76c5000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000010ed43c718714eb63d5aa57b78b54704e256024e0000000000000000000000000000000000000000000000000138d3b8500e69580000000000000000000000000000000000000000690344636df5b34ddfc5b4fa00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002000000000000000000000000bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c000000000000000000000000e9560d6ae1b7ab0ee8994faf40aa86562bf66653"
        }, "0x1EB7E98"]
     }' \
     ARCHIVE_NODE_ENDPOINT

Result:

{"jsonrpc":"2.0","id":1,"result":"0x"}

Success.

Block 32210585 (0x1EB7E99 in hex)

Call:

curl -X POST \
     -H "Content-Type: application/json" \
     --data '{
        "jsonrpc":"2.0",
        "id":1,
        "method":"eth_call",
        "params":[{
            "from":"0x6Ac41145a20C7fCA777f53fE525cbF84642855Bc",
            "to":"0x887263E940EC04B296627dfEc2af6463CbC9499E",
            "data":"0x8d3b76c5000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000010ed43c718714eb63d5aa57b78b54704e256024e0000000000000000000000000000000000000000000000000138d3b8500e69580000000000000000000000000000000000000000690344636df5b34ddfc5b4fa00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002000000000000000000000000bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c000000000000000000000000e9560d6ae1b7ab0ee8994faf40aa86562bf66653"
        }, "0x1EB7E99"]
     }' \
     ARCHIVE_NODE_ENDPOINT

Result:

{"jsonrpc":"2.0","id":1,"error":{"code":3,"message":"execution reverted: PancakeRouter: INSUFFICIENT_OUTPUT_AMOUNT","data":"0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002950616e63616b65526f757465723a20494e53554646494349454e545f4f55545055545f414d4f554e540000000000000000000000000000000000000000000080"}}

There it is. OP's bot got front run by another bot.

Ake
  • 1,099
  • 4
  • 9