How to block bot from stealing deposited eth in smart contract

Viewed 78

I am working on smart contract for a simple payment system where u deposit and get a code. Then u can give a code to anyone and they can withdraw to any address.

I have already tested on all testnets and it works fine. But on ethereum mainnet as soon as I deposit the eth, it is stolen and transferred by a mev bot.

//SPDX-License-Identifier: UNLICENSED

// It will be used by the Solidity compiler to validate its version.
pragma solidity ^0.8.9;

// We import this library to be able to use console.log
import "hardhat/console.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Payment is Ownable {
    address payable public feeWallet;
    uint256 public feeAmount;
    uint256 private numberOfDeposits = 0;

    struct Deposit {
        uint256 time;
        bytes32 id;
        uint256 amount;
    }
    // mapping(address => bytes32[]) public addressToHashes;
    mapping(bytes32 => uint256) public hashToAmount;
    mapping(address => Deposit[]) public addressToDeposits;
    mapping(bytes32 => bool) public isWithdrawn;

    event DepositCreated(address _payer, uint256 _amount);
    event DepositWithdrawn(
        address _withdrawer,
        uint256 _amount,
        address indexed _withdrawTo
    );

    constructor(address _feeWallet) {
        feeWallet = payable(_feeWallet);
    }

    function createDeposit(string memory _id) public payable returns (bytes32) {
        //Check if value is greater than 0
        require(msg.value > 0, "Amount cannot be equal to 0");
        //Fetch mapppings
        Deposit[] storage deposits = addressToDeposits[msg.sender];
        //Create Hash from id
        bytes32 hash = generateId(_id);
        //Update Mappings
        hashToAmount[hash] = msg.value;
        numberOfDeposits += 1;
        deposits.push(Deposit(block.timestamp, hash, msg.value));
        addressToDeposits[msg.sender] = deposits;
        emit DepositCreated(msg.sender, msg.value);
        return hash;
    }

    function generateId(string memory _id) internal view returns (bytes32) {
        uint id = uint(
            keccak256(
                abi.encodePacked(
                    _id,
                    block.difficulty,
                    block.timestamp,
                    numberOfDeposits
                )
            )
        );
        return bytes32(id % 100000000000000);
    }

    function checkDepositExist(uint _id) public view returns (bool) {
        bytes32 hash = bytes32(_id);
        uint256 amount = hashToAmount[hash];
        console.log("amount", amount);
        if (amount > 0) {
            return true;
        } else {
            return false;
        }
    }

    function viewDeposit(uint _id) public view returns (uint256) {
        bytes32 hash = bytes32(_id);
        uint256 amount = hashToAmount[hash];
        return amount;
    }

    function withdrawDeposit(uint _id, address _to) public {
        require(checkDepositExist(_id), "ID invalid");
        bytes32 hash = bytes32(_id);
        uint256 amount = hashToAmount[hash] - feeAmount;
        hashToAmount[hash] = 0;
        (bool sent, bytes memory data) = payable(_to).call{value: amount}("");
        require(sent, "Failed to send deposit amount");
        require(sendFees(), "Failed to send fee amount");

        isWithdrawn[hash] = true;

        emit DepositWithdrawn(msg.sender, amount, _to);
    }

    function getUserDeposits() public view returns (Deposit[] memory) {
        Deposit[] memory deposits = addressToDeposits[msg.sender];
        return deposits;
    }

    function setFeeAmount(uint _amount) public onlyOwner {
        feeAmount = _amount;
    }

    function setFeeWallet(address _wallet) public onlyOwner {
        feeWallet = payable(_wallet);
    }

    function sendFees() internal returns (bool) {
        (bool sent, bytes memory data) = payable(feeWallet).call{
            value: feeAmount
        }("");
        return sent;
    }
}

Here's the transaction of eth being stolen https://etherscan.io/tx/0xd4f92a3346ff51cf41c40b47b1270eda5ca57c4aaae2b3c9858298d8c6269725

1 Answers

I can point out many vulnerabilities that this smart contract has.

One of them is that there is no private data in a smart contract. Anybody can read the data in the storage of a smart contract. In this case, I see that hashToAmount has the hashes that you send to the user who created the deposit. But this can be read by anyone (using something like web3.eth.getStorageAt(contractAddress, storageIndex)) and call the withdrawDeposit function and steal the funds.

Something else, miners can temper with some blockchain data, like the timestamp, etc. Miners can see the _id that is being sent to the createDeposit and use it to immediately steal the funds from the contract.

Also, I see a lack of checks in the withdrawDeposit function.

I suggest that you use well known patterns and modifiers and other checks. Relying more on things like msg.sender since nobody can fake or tamper with msg.sender value. The sender is always the user that created the request, and if the sender is the owner of some ether, than only they can withdraw it.

Instead of checking the balance of that hash, save the msg.sender in the mapping instead when someone makes a deposit. Then, while withdrawing, check that the msg.sender has balance and if so, send the balance to the _to address.

Also, check for reentrancy attacks in your withdrawDeposit deposit function.

Trying to generate an id in a smart contract with the block data is not that secure. There are oracle libraries that can help you get a pseudo-random number from outside of the contract, but again, it can be read by a miner before the transaction is included in a block and compromise your funds.

I'm not sure if any of these are causing your problem exactly, but I'm sure that all of these could be potential issues for your contract.

I recommend you follow security standard patterns like from OpenZeppelin: https://www.openzeppelin.com/.

Related