해피데스데이! 토큰은 그 곳에 있지 않았다.

거래소의 허망한 실수 —토큰의 오입금 편.

Hexlant는 Blockchain 개발팀으로 다양한 기업들의 Smart Contract 코드를 Audit 하는 업무를 진행합니다.

저희가 여러 의뢰를 받아 다양한 문제를 분석하다 보니 최근 몇몇 거래소에서 입금되지 않은 토큰을 입금되는 것으로 처리하는 거래소들이 발견되어 해당 문제를 분석하게 되었습니다.

처음 저희는 BEC, SMT 토큰 무한생성 사건처럼 컨트랙트 코드의 문제로 접근해 보았지만, 컨트랙트 코드들에는 아무런 문제점이 발견되지 않았습니다.

그러면 어떻게 입금되지 않은 토큰이 입금된 것으로 처리된 것일까요?

이 문제를 알기 위해선 실제로 토큰전송이 어떻게 이루어지는지 이해할 필요가 있습니다.


Token Transfer, 토큰은 어떻게 전송되는가.

golem token Contract Code

위 컨트랙트 코드는 ERC-20의 대표적인 Golem(GNT) 토큰의 코드입니다.
그 중 일반적인 토큰 전송을 처리하는 transfer 함수의 코드를 발췌하였습니다.

/// @notice Transfer `_value` GNT tokens from sender's account
/// `msg.sender` to provided account address `_to`.
/// @notice This function is disabled during the fundin
/// @dev Required state: Operational
/// @param _to The address of the tokens recipient
/// @param _value The amount of token to be transferred
/// @return Whether the transfer was successful or not
function transfer(address _to, uint256 _value) returns (bool) {
// Abort if not in Operational state.
if (funding) throw;

var senderBalance = balances[msg.sender];
if (senderBalance >= _value && _value > 0) {
senderBalance -= _value;
balances[msg.sender] = senderBalance;
balances[_to] += _value;
Transfer(msg.sender, _to, _value);
return true;
}
return false;
}

transfer 함수를 보면 return 결과로 토큰 전송의 성공/실패 여부를 반환하게 되어있습니다.

토큰 전송이 실패하는 경우는 어떤 경우일까요?

if (senderBalance >= _value && _value > 0)

위 조건을 보면, 전송하려는 금액 _value가 보내려는 사람이 가진 토큰 잔액보다 적어야 합니다.

정상적인 조건이라면,

senderBalance -= _value;
balances[msg.sender] = senderBalance;
balances[_to] += _value;
Transfer(msg.sender, _to, _value);
return true;

실제로 전송에 대해 처리를 하고, Transfer Event를 발생시키게 됩니다. 마지막으로는 함수의 실행결과를 true로 처리하여 토큰전송이 성공하였음을 알리게 됩니다.

하지만 100 GNT 를 가진 사람이 1,000 GNT 를 보내려고 하면 저 조건을 만족하지 못하게 되니 Event도 발생시키지 않고, 아래의 결과로 함수를 종료시킵니다.

return false;

이처럼 GNT token은 함수의 결과 값을 통하여 토큰전송의 성공 여부를 파악하게 됩니다.

그럼 실제로 내가 가진 GNT보다 많은 GNT를 전송하면 어떻게 보일까요?

전송하려는 금액이 현재의 잔액보다 많은 Transaction Receipt

위 링크는 4,380 GNT 를 가진 주소에서 7,804 GNT를 전송하면서 발생한 Transaction 결과입니다.

토큰 전송의 실패

보면 토큰전송에 에러가 발생했다고 나옵니다. 이는 return 결과가 false이고, Transfer Event가 발생하지 않았기 때문입니다.

눈치채신 분들도 있겠지만, Transaction Receipt 상태는 Success로 나옵니다. 분명 토큰전송은 실패했는데 말이죠.

TxReceipt Status 결과

비잔티움 하드포크

이 의문점을 해소하려면, 작년 10월에 있었던 비잔티움 하드포크를 이해해야 합니다.

The Ethereum network will be undergoing a planned hard fork at block number 4.37mil (4,370,000), which will likely occur between 12:00 UTC and 13:00 UTC on Monday, October 16, 2017. The Ropsten test network underwent a hard fork on September 19th (UTC) at block number 1.7mil (1,700,000). A countdown timer can be seen at https://fork.codetract.io/.

https://blog.ethereum.org/2017/10/12/byzantium-hf-announcement/

이더리움 메인넷은 4,370,000 Block을 기점으로 하드포크를 진행하였습니다.

변경 점을 살펴보면, 비잔티움 하드포크 이후로 Transaction Receipt에 Status Field가 생긴 것을 알 수 있습니다.

What changes are included in the Byzantium hard fork

실제로 Etherescan을 확인해 보면,

하드 포크 이전의 4,369,999 Block에 있는 Transaction에는 TxStatus 필드가 없습니다.

https://etherscan.io/tx/0xea3f47450fb9b22dc39b3eccf2e903eafd6acb2dc74c7b0140be0c3b9c06c72e

하드포크 이전의 Transaction Receipt

하지만 하드포크 시점인 4,370,000 Block의 Transaction에는 TxStatus 필드 생긴 것을 확인할 수 있습니다.

https://etherscan.io/tx/0x1fcb1196d8a3bff0bcf13309d2d2bb1a23ae1ac13f5674c801be0ff9254d5ab5

하드포크 이후의 Transaction Receipt

비잔티움 하드포크 이후 발행된 토큰들은 transfer 결과를 True/False로 return 하는 것이 아니라 Revert 기능을 이용하여 Transaction 자체를 Fail로 처리하는 방법을 사용할 수 있게 되었습니다.


거래소는 왜 입금되지 않은 토큰을 입금처리한 것인가.

저희가 파악한 문제의 거래소들은 대부분 신생 거래소들이었습니다.

비잔티움 하드포크 이전에 운영하던 거래소들은 TxStatus가 없던 시절부터 입/출금 서비스를 운영해 왔기에, 입금 여부를 transfer 함수의 True/False 여부와 Event 등을 통하여 입금처리를 해왔을 것입니다.

하지만 비잔티움 하드포크 이후 개발된 문제의 거래소들은 TxStatus의 여부만 가지고 토큰의 입금 여부를 확인한 것으로 보입니다.

그러다 보니 100개의 토큰을 가진 해커가 1,000개의 토큰을 거래소 지갑으로 전송을 요청하면, TxStatus 여부만 확인하고 입금으로 처리하게 된 것입니다.


결론

이번 사태는 암호화폐 거래소의 시스템 문제로 판단됩니다.

TxStatus만 보고 입금되었다고 판단한 거래소에서 실제 보유한 토큰물량보다 많은 토큰이 거래되면서 토큰가격에 혼란을 초래하였습니다.

현재까지 저희가 파악한 문제의 거래소들은 해외 거래소들이고, 비슷한 문제를 가진 거래소들이 좀 더 있을 것으로 생각됩니다.

ICO 이후 상장을 준비하는 팀들은 거래소의 오입금 사태로 가격이 폭락하는 피해가 있을 수 있으니, 상장 전에 미리 거래소 시스템을 점검하는 것을 강력히 권유드립니다.

아직까지는 큰 이슈화 되지 않았지만, 각 거래소는 자신들의 시스템을 점검하여 오입금 사태가 발생하지 않도록 대비하여야 합니다.

특히 본인들이 직접 개발한 것이 아니라, 통합 솔루션을 구매하여 운영하는 거래소일수록 이런 문제를 발견하기 어렵기 때문에, 거래소 운영팀에서는 해당 문제가 발생하는지 검증에 좀 더 신경을 많이 써야 합니다.