ethereumblockchainsoliditysmartcontractsremix-ide

Call function in withdraw() returns false


The call function in the withdraw() returns false and the require statement below it logs "Transfer failed". I've asked ChatGPT and it says that "check your gas limit".(my gas limit for this contract is 3000000). I have no idea how to solve this error.

Smart Contract

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

contract Thrive {

    error InvalidDeadline(string message);

    enum campaignStatus {
        OPEN, 
        CLOSED
    }

    struct Campaign {
        address payable owner;
        string title;
        string description;
        uint256 opening;
        uint256 deadline;
        uint256 amountCollected;
        string image;
        campaignStatus status;
        address[] donators;
        uint256[] donations;
    }

    event Action (uint256 id, string actionType, address executor, uint256 timestamp);

    mapping(uint256 => Campaign) public campaigns;

    uint256 public numberOfCampaigns = 0;

    function createCampaign(address _owner, string memory _title, string memory _description, uint256 _deadline, string memory _image) public returns (uint256) {
        Campaign storage campaign = campaigns[numberOfCampaigns];
    
        // require(campaign.deadline > block.timestamp, "The deadline should be a date in the future.");
        if(_deadline < block.timestamp) {
            campaign.status = campaignStatus.CLOSED;
            revert InvalidDeadline("The deadline should be a date in the future");
        }
        campaign.owner = payable(_owner);
        campaign.title = _title;
        campaign.description = _description;
        campaign.opening = block.timestamp;
        campaign.deadline = _deadline;
        campaign.amountCollected = 0;
        campaign.image = _image;
        campaign.status = campaignStatus.OPEN;

        numberOfCampaigns++;

        emit Action (numberOfCampaigns - 1, "Campaign created", _owner, block.timestamp);

        return numberOfCampaigns - 1;
    }

    function donateToCampaign(uint256 _id) public payable {
        Campaign storage campaign = campaigns[_id];
        require(campaign.status == campaignStatus.OPEN, "The campaign is closed");

        uint256 amount = msg.value;

        campaign.donators.push(msg.sender);
        campaign.donations.push(amount);

        (bool sent,) = payable(campaign.owner).call{value: amount}("");
        require(sent, "transaction failed");

        if(sent) {
            campaign.amountCollected = campaign.amountCollected + amount;
        }

        emit Action (_id, "Fund donated to campaign", msg.sender, block.timestamp);
    }

    function withdraw(uint256 _id) public {
        Campaign storage campaign = campaigns[_id];

        require(campaign.status == campaignStatus.OPEN, "The campaign is closed");
        require(msg.sender == campaign.owner, "Withdrawer should be the owner of the campaign");
        require(campaign.amountCollected > 0, "Not enough fund generated");

        uint256 amount = campaign.amountCollected;
        require(amount > 0, "Amount not collected yet");

        (bool success, ) = payable(campaign.owner).call{value: amount}("");
        require(success, "Transfer failed");
 
        campaign.amountCollected = 0;
        campaign.status = campaignStatus.CLOSED;
        
        emit Action (_id, "Fund withdrawed", msg.sender, block.timestamp);
    }

    function getDonators(uint256 _id) view public returns (address[] memory, uint256[] memory) {
        return (campaigns[_id].donators, campaigns[_id].donations);
    }

    function getCampaigns() public view returns (Campaign[] memory) {
        Campaign[] memory allCampaigns = new Campaign[](numberOfCampaigns);

        for(uint i = 0; i < numberOfCampaigns; i++) {
            Campaign storage item = campaigns[i];

            allCampaigns[i] = item;
        }

        return allCampaigns;
    }

    function deleteCampaign(uint256 _id) public returns (uint256) {
        Campaign storage campaign = campaigns[_id];

        require(campaign.status == campaignStatus.CLOSED, "The campaign is open");

        campaign.owner = payable(address(0));
        campaign.title = "";
        campaign.description = "";
        campaign.opening = 0;
        campaign.deadline = 0;
        campaign.amountCollected = 0;
        campaign.image = "";

        numberOfCampaigns--;

        emit Action (_id, "Campaign deleted", msg.sender, block.timestamp);

        return numberOfCampaigns;
    }

    function getOwner() public view returns (address) {
        return address(this);
    }

    function getCampaignOwner(uint256 _id) public view returns (address) {
        return campaigns[_id].owner;
    }

    function getAmountCollected(uint256 _id) public view returns (uint256) {
        return campaigns[_id].amountCollected;
    }

}

Error:

status  0x0 Transaction mined but execution failed
transaction hash    0xe3e3e02272ee5393d3d7d48e94972a83fee50b37d0d3f58283b3ece5507cf838
block hash  0x2ac76a4b78da8572c51720eb198912d60d00b27b537d7145424ab679f51f8965
block number    198
from    0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
to  Thrive.withdraw(uint256) 0x45373635641f5C51bf1029FdF2A225674D61AD45
gas 3000000 gas
transaction cost    35923 gas 
execution cost  14731 gas 
input   0x2e1...00000
decoded input   {
    "uint256 _id": "0"
}
decoded output  {}
logs    []
transact to Thrive.withdraw errored: Error occurred: revert.

revert
    The transaction has been reverted to the initial state.
Reason provided by the contract: "Transfer failed".
You may want to cautiously increase the gas limit if the transaction went out of gas.

I tried to withdraw Eth from a particular campaign and expected it to work properly as all the require statements passed above the call function. But the transaction failed and reverted to previous state.


Solution

  • The code that you provided is transferring the tokens to the campaign owner in the donateToCampaign function, and after that you are trying to re-withdraw a amount of tokens which is not present in the contract and has been already transferred to the owner.

    The code below delete the transferring operation on the donateToCampaign function and keeps the tokens inside of the contract and withdraw function should work fine:

     function donateToCampaign(uint256 _id) public payable {
            Campaign storage campaign = campaigns[_id];
            require(campaign.status == campaignStatus.OPEN, "The campaign is closed");
    
            uint256 amount = msg.value;
    
            campaign.donators.push(msg.sender);
            campaign.donations.push(amount);
    
            campaign.amountCollected = campaign.amountCollected + amount;
    
            emit Action (_id, "Fund donated to campaign", msg.sender, block.timestamp);
        }
    

    Plus the withdraw function is also polluted to re-entrancy attack, you must set the state of the contract before the transfer operation, here is the fixed withdraw function:

        function withdraw(uint256 _id) public {
            Campaign storage campaign = campaigns[_id];
    
            require(campaign.status == campaignStatus.OPEN, "The campaign is closed");
            require(msg.sender == campaign.owner, "Withdrawer should be the owner of the campaign");
            require(campaign.amountCollected > 0, "Not enough fund generated");
    
            uint256 amount = campaign.amountCollected;
            require(amount > 0, "Amount not collected yet");
    
            campaign.amountCollected = 0;
            campaign.status = campaignStatus.CLOSED;
    
            (bool success, ) = payable(campaign.owner).call{value: amount}("");
            require(success, "Transfer failed");
     
            emit Action (_id, "Fund withdrawed", msg.sender, block.timestamp);
        }