Skip to content

Latest commit

 

History

History
119 lines (91 loc) · 7.93 KB

SendMoneySelfDestruct.md

File metadata and controls

119 lines (91 loc) · 7.93 KB

Unusual ways to send money to smart contracts

Every smart contract that performs functions on the basis of its own balance, maintaining security with checks done by those functions, is forgetting a big problem: anyone can send money to that smart contract. Once a smart contract has been deployed on the network, sending it money directly with a simple transaction is not possible: Warning! Error encountered during contract execution [execution reverted].

How can to send money to a smart contract?

If the contract has not yet been deployed to the network, but you know the address that will develop it, you can pre-compute the address of the smart contract and send money to it, because there is no code already, the money will be transferred correctly to the contract. Then when the contract is deployed it will already have funds.

The address of an Ethereum smart contract is deterministically computed from its creator address and the number of transaction it made (nonce). The address of a contract is defined as the rightmost 160 bits of the Keccak hash of the RLP (Recursive Length Prefix) encoding of the structure containing only the account sender and its nonce.

This can simply be performed in Solidity with:

function precomputeContractAddress(address _addr, bytes1 nonce) public {
    contractAddress = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), _addr, bytes1(nonce))))));
}

This method could also lead to a very simple way to hide money: by sending ether to an empty address, the funds will be unrecoverable until we create a contract with a certain nonce and we become again the owners of the funds.

Another way to send funds to a contract is the selfdestruct command, that destroy a contract on the blockchain and transfers the ethers in it. It was originally created in case of emergency situation, like a hacking:

"The DAO attack continued for several days and the organization even noticed that their contract had been attacked at that time. However, they could not stop the attack or transfer the Ethers because of the immutability feature of smart contracts. If the contract contains a selfdestruct function, the DAO organization can transfer all the Ethers easily, and reduce the financial loss".

This command is also very useful when you are finished with a contract: because the destruction of the contract frees up space on the blockchain, it costs far less gas than just sending the balance with address.send(this.balance), the selfdestruct optcode in fact uses negative gas. Selfdestruct command permits:

  • sending ether to a contract that disallows incoming ether transfer (no payable functions);
  • sending ether to a contract making it no notice any income transfer (this could be a serious problem for a contract that for each income ethers do a certain operation). After selfdestructing a contract every transaction or funds to it will result in many case in funds lost forever. This could be a problem for apps interacting with a contract that has a selfdestruct function: it's needed before making any interaction with a contract to see if it responds currently and hasn't been selfdestructed. Another problem of the selfdestruct function is that while it send all the ether from a contract, tokens and other assets aren't sent. The command is also frowned upon by some people since it breaks contract's immutability principle.

Example: Retirement fund

This smart contract is from a capturetheether challenge, whose goal is to set the isComplete bool value to True.

pragma solidity ^0.4.21;

contract RetirementFundChallenge {
    uint256 startBalance;
    address owner = msg.sender;
    address beneficiary;
    uint256 expiration = now + 10 years;

    function RetirementFundChallenge(address player) public payable {
        require(msg.value == 1 ether);

        beneficiary = player;
        startBalance = msg.value;
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function withdraw() public {
        require(msg.sender == owner);

        if (now < expiration) {
            // early withdrawal incurs a 10% penalty
            msg.sender.transfer(address(this).balance * 9 / 10);
        } else {
            msg.sender.transfer(address(this).balance);
        }
    }

    function collectPenalty() public {
        require(msg.sender == beneficiary);

        uint256 withdrawn = startBalance - address(this).balance;

        // an early withdrawal occurred
        require(withdrawn > 0);

        // penalty is what's left
        msg.sender.transfer(address(this).balance);
    }
}

Analyzing the smart contract we can see that there are 4 functions:

  • RetirementFundChallenge, the contract code constructor which is called when the smart contract is created. When this function is called, 1 ether must be sent, which will be the balance of the contract that must be transferred. The function also assign data to beneficiary and startBalance variables;
  • isComplete, which checks if the challenge is solved by checking that the balance of the contract (initially equal to 1 ether) is equal to zero;
  • withdraw, which permits the owner of the contract (a smart contract address controlled by Capture The Ether) to withdraw 90% of the smart contract balance if it has not been 10 years since the contract was created, the entire sum otherwise.
  • collectPenalty, that transfers the remaining balance if the contract balance is different from the value in startBalance (1 ether) and if the function call comes from the beneficiary address, the address that started the challenge in the Capture the Ether portal.

In the contract the only function we can call, apart from isComplete, is collectPenalty that does a subtraction between to value and in case the result is greater than zero it transfers all the money. Because the solidity version is previous than the 0.8.0, in which arithmetic overflow or underflow produces errors, and there are no checks on the value assigned to the withdraw uint256 variable the smart contract balance could be empty just by performing an arithmetic underflow, that occurs if the current smart contract address is greater than 1 ether.

To make this happen is simply possible to create another smart contract with a selfdestruct that transfers the funds to the RetirementFundChallenge smart contract:

pragma solidity ^0.8.0;

contract destroyMe{

    constructor(address payable _target) payable{
        selfdestruct(_target); 
    }
}

This will make the RetirementFundChallenge balance greater than 1 ether, so if we call collectPenalty the value in withdraw balance will be subject to an arithmetic underflow resulting in a value greater than zero and RetirementFundChallenge balance being transfer to the beneficiary. If we now call the isComplete it will return True.

Mitigation

The problem in the smart contract was that we were able to send ethers to the contract without any control. To solve the problem it's possible to use fallback functions. A fallback function is a function executed when a function identifier does not match any of the available functions. Those functions are unnamed, can't accept arguments, can't return any value and are unique (one fallback function for every smart contract). Those function can be considered a valve of sorts. So whenever a smart contract receives funds without any other data associated with the transaction with a fallback payable function it's possible to keep the contract safe. Implementation of a fallback function that solves the problem of RetirementFundChallenge contract:

pragma solidity ^0.4.21;

contract SecureRetirementFundChallenge {
    // ...
    function() payable {
        // ...
    }

}

Another vulnerability mitigation is to use an uint256 balance variable instead of address(this).balance.