0

I created a contract factory (parent contract) for creating a child contract. In the parent contract, there is a function to transfer the balance of the parent contract to the child contract, by entering the child contract address. I found that the transfer will be successful when the receive function is plain (no other action in the receive function). When I add other actions like changing the state variable, then the transaction will fail and show the error:

transact to ContractFactory.pay 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.

Here is the contract code:

pragma solidity >=0.6.0 <0.8.0;

contract ContractFactory {
    address[] contractsAddress;
    mapping(address => Contract) private contracts;
    
    function createContract(uint price) public {
        Contract newContract = new Contract(msg.sender, price);
        contractsAddress.push(address(newContract));
        contracts[address(newContract)] = newContract;
    }
    
    receive() external payable{
    }
    
    function pay(address payable contractAddress) public payable{
        contractAddress.transfer(address(this).balance);
    }
    
    function getAllContractsAddress() view public returns(address[] memory) {
        return contractsAddress;
    }
}

contract Contract {
    enum Status {Unavailable, Available}
    Status public contractStatus;
    
    address payable public contractOwner;
    address payable public buyer;
    
    uint public price;
    
    event Occupy(address _occupant);
    
    constructor(address payable _contractOwner, uint _price) {
        contractOwner = _contractOwner;
        price = _price;
        contractStatus = Status.Available;
    }
    
    modifier onlyWhenAvailable {
        require(contractStatus == Status.Available);
        _;
    }
    
    modifier notOwner {
        require(msg.sender != contractOwner);
        _;
    }
    
    modifier payEnoughAmount {
        uint priceInEther = price * 1 ether;
        require(msg.value >= priceInEther);
        _;
    }
    
    receive() external payable onlyWhenAvailable notOwner payEnoughAmount {
        contractStatus = Status.Unavailable;
        buyer = msg.sender;
    }
}
1

You need to change:

contractAddress.transfer(address(this).balance);

To:

contractAddress.call{value: address(this).balance}("");

Why? Because when using methods transfer() and send() the called contract can use only 2300 gas which apparently is not enough to update state. And this is why your function succeed when the receive method of contract Contract was empty and not updating the state of the called contract.

2
  • Great answer. Just to add a little background for others who find this answer. The "gas stipend" (2,300) rule was added following the DAO incident. It provides retroactive re-entrance protection for naive contracts (including legacy deployments) that do not observe the "checks, effects, interactions" pattern. – Rob Hitchens Dec 29 '20 at 16:06
  • 1
    Thank you for the addition, it is totally valid. – Miroslav Nedelchev Dec 29 '20 at 16:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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