Denial of Service 는 서비스의 작동을 망가뜨리는 것을 말한다.
간단하게 아래의 코드로 예시를 들어보면,
contract EtherGame {
address public owner;
uint public balance;
function getOwner() external payable {
require(msg.value > balance, "Not enough balances.");
payable(owner).transfer(msg.value);
balance += msg.value;
owner = msg.sender;
}
}
EtherGame 컨트랙트는 owner 에게 현재 balance 이상의 이더를 지불하고 본인이 새로운 owner 가 될 수 있다.
이 컨트랙트를 망가뜨릴 수 있는 방법이 있다.
contract Attack {
function attack(EtherGame _EtherGame) public payable {
_EtherGame.getOwner{value : msg.value}();
}
}
Attack 컨트랙트에서 fallback 또는 receive 함수를 선언해두지 않는 것이다.
컨트랙트가 이더를 받기 위해서는 위 두 함수가 필요하고, 그렇지 않다면 모든 트랜잭션은 revert 될 것이다.
위의 코드를 직접 확인해보았다.
1이더를 사용해 한 사용자가 새로운 owner 가 되었다.
그리고 2이더를 지불하고 Attack 컨트랙트가 새로운 오너가 되었다.
그리고 새로운 사용자가 4이더를 지불하고 owner 가 되기 위해 gerOwner 함수를 실행했으나, 트랜잭션이 revert 된다.
위의 공격은 Pull over Push 패턴 이라고 불리는 방법으로 처리한다.
입금과 출금으로 두개의 함수로 나누어 처리하는 것이다.
현재는 함수를 실행하면, 사용자의 주소에서 owner 의 주소로 곧바로 이더가 전송되는 구조이다.
이런 경우에는 컨트랙트에서 fallback 또는 receive 를 선언하지 않는 것을 미리 예측하거나 막을 수 있는 방법이 없다.
아래의 예시 코드처럼 수정하면 문제 없이 잘 작동할 것이다.
contract EtherGame_Defense {
address public owner;
uint public balance;
mapping(address => uint) ownerBalance;
receive() external payable { }
function deposit() external payable {
require(msg.value > balance, "Not enough balances.");
payable(address(this)).transfer(msg.value);
balance += msg.value;
ownerBalance[owner] += msg.value;
owner = msg.sender;
}
function withdraw() external {
require(msg.sender != owner,"Not allowed for current owner.");
uint amount = ownerBalance[msg.sender];
ownerBalance[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
컨트랙트가 owner 거래에 대한 계약의 중개자로써 활용되는 것이다.
deposit 으로 컨트랙트에 이더를 입금. withdraw 는 이전에 owner 였던 특정 주소만이 이더를 가져갈 수 있다.
공격에는 안전해지겠지만 사용자들은 입금, 출금 두번으로 나누어 실행해야하기에 유저 경험적인 부분이나 가스비 소비에서 손해를 보게 된다.