I have two contracts, upcomingRace.sol and RaceFactory.sol. RaceFactory is a contract factory for upcoming races. When I deploy upcomingRace on its own, I can call the announceWinner() function without issue and all amounts get transferred to the correct accounts. However, when I call RaceFactory.endRace(), I get the error below. This is unexpected because announceWinner() isn't a payable function and can be called when deployed individually.

transact to RaceFactory.endRace errored: VM error: revert. revert The transaction has been reverted
to the initial state. Note: The called function should be payable if you send value and the value you 
send should be less than your current balance. Debug the transaction to get more information.


pragma solidity ^0.8.0;
import "./Ownable.sol";

contract upcomingRace is Ownable{

    mapping(string => bool) public horses;
    mapping(address => uint) internal winningBettors;
    struct Bet {
        address bettor;
        uint betSize;
        string horse;
    Bet[] betPool;
    bool isEnabled = true;
    string race_id;
    uint startTime;
    uint cut = 5;
    event initialized(string, address);
    event Received(address, uint);
    address factory;

    constructor(string memory _race_id, string[12] memory _horses, uint _startTime) payable {
        initialized(_race_id, address(this));
        race_id = _race_id;
        startTime = _startTime;
        string memory _horse;
        for (uint i = 0; i < _horses.length; i++){
            _horse = _horses[i];
            horses[_horse] = true;
        factory = msg.sender;

    receive() external payable {
        emit Received(msg.sender, msg.value);

    function placeBet(uint  _betSize, string calldata _horse) public payable {
        // require that the current time is not past the start of the race
        // require(uint32(block.timestamp) <= startTime);
        uint betSize = _betSize*10**18;
        require(msg.value == betSize);
        address payable race = payable(address(this));
        Bet memory bet = Bet(msg.sender, betSize, _horse);

    function disableBetting() private onlyOwner() {
        isEnabled = false;
    function raceOver() private onlyOwner() {
        address payable _owner = payable(owner());
        uint _cut = address(this).balance * cut /100;
    function balanceOf() external view returns (uint){
        return address(this).balance;
    function findWinningAmounts(string calldata _winner) internal view returns (uint){
        uint _winningPool = 0;
        for (uint i = 0; i < betPool.length; i++){
            if (keccak256(abi.encodePacked(_winner)) == keccak256(abi.encodePacked(betPool[i].horse))){
                _winningPool += betPool[i].betSize;
        return _winningPool;
    function payWinners(string calldata _winner) public payable onlyOwner() {
        uint _winningPool = findWinningAmounts(_winner);
        uint _winnings;
        if (_winningPool > 0){
            for (uint i = 0; i < betPool.length; i++){
                if (keccak256(abi.encodePacked(_winner)) == keccak256(abi.encodePacked(betPool[i].horse))){
                    address payable _bettor = payable(betPool[i].bettor);
                    _winnings = (betPool[i].betSize/_winningPool)*address(this).balance;
            address payable _owner = payable(owner());
    function announceWinner(string calldata _winner) external onlyOwner() {



pragma solidity ^0.8.0;

import "./Ownable.sol";
import "./upcomingRace.sol";

contract RaceFactory is Ownable {
    mapping(string => upcomingRace) public races;
    event raceCreated(string, address);
    event raceEnded(string, string);
    function createUpcomingRace(string calldata _raceID, string[12] calldata _horses, uint32 _startTime ) public onlyOwner returns (address){
        upcomingRace _race = new upcomingRace(_raceID, _horses, _startTime);
        address _raceAddress = address(_race);
        races[_raceID] = _race;
        emit raceCreated(_raceID, _raceAddress);
        return _raceAddress;
    function endRace(string calldata _raceID, string calldata _winner) public onlyOwner {
        upcomingRace _race;
        _race = races[_raceID];

Additional note: if you have any feedback on contract structure/ code style/ question layout/ a better way to do something you see here it would be greatly appreciated.

  • 1
    Did you try debugging the contract? Some problems in the code: 1/ Making transfers inside the loop will change address(this).balance so distribution will not be fair, 2/ if one transfer always fails it will revert the whole transaction causing a denial of service since you couldn't be able to close the race, 3/ announceWinner can be raced since the winner is in the clear, 4/ creating a contract for each race is wasteful consider using something like EIP 1167 Minimal Proxy. I'd suggest separating race closing from payments.
    – Ismael
    Jun 3, 2021 at 6:00
  • 1
    @Ismael thanks for taking the time to reply. After stepping away from it for a bit, the issue was that the contract factory was not able to receive funds from the child contract. With regard to proxy contracts, is it still recommended if I'm going to be deploying to a L2 such as polygon where gas fees are significantly lower?
    – buckage
    Jun 3, 2021 at 13:06
  • Using a minimal proxy isn't necessary if you would be using L2. You might consider it again in the future, for example it will make the factory contract cheaper to deploy.
    – Ismael
    Jun 4, 2021 at 0:52


Your Answer

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

Browse other questions tagged or ask your own question.