4

I've got a contract deployed at 0xe559835D7979dAe994331DEfaEc2FBA057E56C27 on the mainnet.

Every time I go to call the payable function, which is solve, a bot beats me to it. My transaction gets rejected and theirs goes through.

pragma solidity >=0.4.21 <0.6.0;

// deployed at 
// 0xe559835d7979dae994331defaec2fba057e56c27

contract EqHunt {

  struct Equation {
    string repr;
    int answer;
    bool exists;
  }

  mapping(string => Equation) equations;
  mapping(string => address[]) public solvers;
  address payable private owner;

  uint256 public lastSolveTime;

  uint256 private delay;

  constructor() public payable {
    owner = msg.sender;
    lastSolveTime = now;
    delay = 10 minutes;
  }

  function locked() public view returns(bool) {
    if(now - lastSolveTime <= delay) {
      return true;
    } else {
      return false;
    }
  }

  function create(string memory _id, string memory _repr, int _answer) public {

    require(msg.sender == owner);

    require(!equations[_id].exists);

    Equation memory e = Equation(_repr, _answer, true);

    equations[_id] = e;

  }

  function getEquation(string memory _id) public view returns(string memory) {
    return equations[_id].repr;
  }

  function check(string memory _id, int _answer) internal view returns(bool) {

    require(equations[_id].exists);

    if(equations[_id].answer == _answer) {
      return true;
    } else {
      return false;
    }
  }

  function rand() internal view returns(uint256) {
    return uint256(uint256(keccak256(abi.encode(block.timestamp)))%10) + 1;
  }

  function payout() internal view returns(uint256) {
    return 40000000000000000/rand();
  }

  function reward(address payable _payee) internal {

    uint256 r = payout();

    require(address(this).balance>=r);

    require(now - lastSolveTime > delay);

    _payee.transfer(r);

    lastSolveTime = now;

  }

  function solve(string memory _id, int256 _answer) public payable {
    require(!hasSolved(_id));
    bool correct = check(_id, _answer);
    require(correct);
    if(correct) {
      addSolver(_id);
      reward(msg.sender);
    }
  }

  function addSolver(string memory _id) internal {
    solvers[_id].push(msg.sender);
  }

  function hasSolved(string memory _id) internal view returns(bool) {
    address[] memory _solvers = solvers[_id];
    for(uint256 i=0; i<_solvers.length; i++) {
      if(_solvers[i] == msg.sender) {
        return true;
      }
    }
    return false;
  }

  function getNumSolvers(string memory _id) public view returns(uint256) {
    address[] memory _solvers = solvers[_id];
    return _solvers.length;
  }

  function getSolvers(string memory _id) public view returns(address[] memory) {
    return solvers[_id];
  }

  function withdraw() public payable {
    require(msg.sender == owner);
    owner.transfer(address(this).balance);
  }

  function() payable external {}

  // // TEST CODE
  //
  // function testRand() public view returns(uint256) {
  //   return rand();
  // }
  //
  // function testPayout() public view returns(uint256) {
  //   return payout();
  // }
  //
  // function testReward(address payable _payee) public {
  //   lastSolveTime -= 10 minutes;
  //   reward(_payee);
  // }
  //
  // function getBalance() public view returns (uint256) {
  //   return address(this).balance;
  // }
  //
  // function testHasSolved(string memory _id) public view returns(bool) {
  //   return hasSolved(_id);
  // }
  //
  // function testCheck(string memory _id, int _answer) public view returns(bool) {
  //   return check(_id, _answer);
  // }

}

I want real people interacting with the contract, not bots - I thought using a 4 character ID would prevent that.

1 Answer 1

8

It sounds like you are being front run. This means that a bot is watching you send a transaction, reading the input (specifically, _answer), and submitting a transaction with the correct answer but a higher gasPrice. A miner will accept their transaction before yours, causing theirs to succeed and yours to fail.

You can see that this is, in fact, what is happening by observing your transaction and the attacker's transaction. You will see that the attacker used 5 Gwei as a gasPrice while you used only 3. A miner will accept their transaction first because the miner will be rewarded with a higher tx fee.

In order to avoid this, you will have to rearchitect your contract. You can use a commit-reveal scheme, or something similar to fix this.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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