Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Stable에서의 USDT0 동작

Ethereum에서 컨트랙트를 이식하는 경우, 배포하기 전에 이 페이지를 읽으세요. Stable에서 USDT0는 네이티브 가스 토큰이자 동일한 잔액 위에 있는 ERC-20 토큰입니다. 그 결과 Ethereum에서 가정되던 네 가지 동작이 깨집니다: 컨트랙트로의 호출 없이도 컨트랙트의 네이티브 잔액이 변할 수 있고, EXTCODEHASH가 0과 빈 해시 사이를 오갈 수 있으며, 영주소 전송이 되돌려지고(revert), 단일 논리적 전송이 소수 잔액 조정으로 인해 여러 개의 Transfer 이벤트를 발생시킬 수 있습니다.

이 페이지는 각 사례를 살펴보고 안전한 컨트랙트 패턴을 제시합니다. 한 섹션만 읽는다면 마이그레이션 체크리스트를 읽으세요. 이것이 Ethereum 컨트랙트를 여기로 이식하기 위한 요약본입니다.

이중 역할 개요

Stable에서 USDT0는 네이티브 가스 토큰이자 ERC-20 토큰입니다. 이 이중 역할 모델은 잔액 동작, 컨트랙트 설계, 이벤트 처리에 영향을 미칩니다. 아래 섹션들은 이중 역할이 예상 동작을 변화시키는 모든 사례를 다룹니다.

USDT0가 이런 방식으로 동작하는 배경에 대해서는 가스로서의 USDT를 참조하세요. 실제 전송을 통해 동작을 체험하려면 첫 USDT0 전송하기를 참조하세요.

잔액 조정

USDT0는 네이티브 자산으로는 18 소수 자릿수를, ERC-20 토큰으로는 6 소수 자릿수를 사용합니다. 네이티브 전송과 ERC-20 전송은 동일한 기초 잔액에서 작동하지만, 12자리 정밀도 차이로 인해 전송이 정수 미만 정밀도를 포함할 경우 시스템은 소수 금액을 조정해야 합니다.

before
  0.000001 USDT0 (ERC-20) + 0.000000000000000000 USDT0 (internal)
  // address(account).balance = 0.000001000000000000
  // USDT0.balanceOf(account) = 0.000001
 
if transfer 0.0000001 USDT0 to another account
 
after
  0.000000 USDT0 (ERC-20) + 0.000000900000000000 USDT0 (internal)
  // address(account).balance = 0.000000900000000000
  // USDT0.balanceOf(account) = 0.000000

이로 인해 address(account).balanceUSDT0.balanceOf(account)가 최대 0.000001 USDT0까지 차이날 수 있습니다.

이벤트 처리

각 조정 전송은 추가적인 Transfer 이벤트를 발생시킵니다. 단일 논리적 USDT0 전송은 송신자와 수신자의 소수 잔액이 어떻게 영향을 받는지에 따라 최대 두 개의 추가 Transfer 이벤트를 생성할 수 있습니다:

  • 송신자 조정: 송신자의 소수 잔액이 부족하면 0.000001 USDT0가 송신자에서 리저브 주소로 이동됩니다. 이때 추가 Transfer 이벤트가 발생합니다.
  • 수신자 조정: 수신자의 소수 잔액이 넘치면 0.000001 USDT0가 리저브 주소에서 수신자로 이동됩니다. 이때 추가 Transfer 이벤트가 발생합니다.
  • 양쪽 조정: 동일한 전송에서 두 조건이 모두 발생하면 리저브를 우회합니다. 송신자는 메인 전송의 일부로 0.000001 USDT0를 수신자에게 직접 전송합니다. 추가 이벤트는 발생하지 않습니다.

이러한 보조 이벤트는 리저브 주소 0x5113954bbC0eD721F1C68671EBa3d91e9e9bF7b5와 관련됩니다. Transfer 이벤트를 재생하여 USDT0 잔액을 추적하는 인덱서 및 오프체인 서비스는 이 주소로 들어오고 나가는 전송을 필터링하거나 고려해야 합니다.

컨트랙트 설계 요구사항

네이티브 잔액 가변성

Ethereum에서 컨트랙트의 네이티브 잔액은 일반적으로 컨트랙트 실행의 결과로만 변경됩니다. Stable에서는 컨트랙트의 네이티브 USDT0 잔액이 transferFrompermit을 포함한 ERC-20 허용량 기반 작업으로 인해 변경될 수도 있습니다. 이러한 작업은 어떤 컨트랙트 코드도 호출하지 않고 컨트랙트의 네이티브 잔액을 감소시킬 수 있습니다.

그 결과 다음 가정은 Stable에서 유효하지 않습니다:

컨트랙트의 네이티브 잔액은 컨트랙트가 호출될 때만 감소할 수 있다.

네이티브 잔액을 미러링하지 마세요

Ethereum에서는 내부 변수로 예치금을 추적하는 것이 일반적입니다. Stable에서는 ERC-20 transferFrom이 외부에서 네이티브 잔액을 고갈시킬 수 있기 때문에 이것이 안전하지 않습니다.

// UNSAFE on Stable
uint256 public deposited;
 
function deposit() external payable {
    deposited += msg.value;
}

전송 전에 항상 실제 잔액을 확인하세요

모든 네이티브 가치 전송은 내부 회계 변수가 아니라 전송 직전에 address(this).balance를 사용하여 지급능력(solvency)을 검증해야 합니다:

// SAFE
function withdraw() external {
    uint256 amount = credit[msg.sender];
    credit[msg.sender] = 0;
    require(address(this).balance >= amount, "insufficient balance");
    payable(msg.sender).call{value: amount}("");
}

상태 진행은 잔액에 독립적이어야 합니다

진행 상황, 마일스톤 또는 완료 조건에 의존하는 프로토콜 로직은 카운터나 에포크와 같은 잔액이 아닌 상태 변수를 사용하여 이를 명시적으로 추적해야 합니다. 네이티브 잔액은 지급 시점의 지급능력 검증에만 사용해야 합니다.

영주소 전송 불가

Stable에서는 address(0)로의 네이티브 및 ERC-20 전송이 모두 되돌려집니다(revert).

// REVERT on Stable
payable(address(0)).call{value: amount}("")
USDT0.transfer(address(0), amount);

네이티브 USDT0를 보내는 컨트랙트 로직은 전송 호출 전에 수신자를 검증하고 address(0)을 명시적으로 거부해야 합니다:

// SAFE
require(recipient != address(0), "zero address recipient");
payable(recipient).call{value: amount}("");

컨트랙트가 영주소 전송을 소각 메커니즘으로 사용하는 경우 재설계해야 합니다. 되돌릴 수 없는 손실 의미가 필요하다면 명시적인 싱크(sink) 컨트랙트를 사용하세요.

EXTCODEHASH 동작

Ethereum에서 EXTCODEHASH 옵코드는 다음을 반환합니다:

  • 제로 해시 (0x0000...): 주소가 한 번도 사용된 적이 없는 경우(nonce=0, balance=0, 코드 없음).
  • 빈 해시 (0xc5d2…a470, 빈 코드의 Keccak-256 해시): 주소는 존재하지만 코드가 없는 경우.

Ethereum에서는 주소가 한 번 제로 해시에서 빈 해시로 전환되면 다시 제로 해시로 돌아갈 수 없습니다. Stable에서는 USDT0가 permit() 기반 승인을 지원하기 때문에, 주소가 트랜잭션을 보내지 않고도 승인을 생성할 수 있습니다. 이를 transferFrom()과 결합하면 nonce 증가 없이 네이티브 잔액 변경이 가능해지며, 잠재적으로 EXTCODEHASH가 제로 해시와 빈 해시 사이를 오갈 수 있게 됩니다.

// UNSAFE on Stable
function isUnusedAddress(address addr) public view returns (bool) {
    bytes32 codeHash;
    assembly {
        codeHash := extcodehash(addr)
    }
    return codeHash == bytes32(0);
}

대신 명시적 추적을 사용하세요:

// SAFE
contract SafeAddressTracker {
    mapping(address => bool) public hasBeenUsed;
 
    function markAsUsed(address addr) internal {
        hasBeenUsed[addr] = true;
    }
 
    function isUnused(address addr) public view returns (bool) {
        return !hasBeenUsed[addr];
    }
}

테스트 요구사항

Stable 배포를 위한 테스트 스위트에는 다음이 포함되어야 합니다:

  • 허용량 기반 고갈 시나리오 (approve + transferFrom)
  • 실제 네이티브 잔액을 사용한 지급능력 강제
  • EXTCODEHASH에 의존하지 않는 주소 사용 로직
  • 영주소 전송에 대한 명시적 실패 케이스

마이그레이션 체크리스트

Ethereum에서 Stable로 컨트랙트를 이식할 때:

  • 내부 네이티브 잔액 미러를 제거하세요
  • 모든 지급능력 검사를 address(this).balance로 교체하세요
  • address(0)로의 모든 네이티브 또는 ERC-20 전송을 제거하세요
  • 모든 USDT0 승인을 감사하세요
  • permit 및 허용량 기반 흐름을 다루는 테스트를 추가하세요
  • 오프체인 인덱서가 소수 잔액 조정으로 인한 보조 Transfer 이벤트를 처리하는지 검증하세요

핵심 요약

Stable에서 올바른 컨트랙트 설계를 위해서는 다음이 필요합니다:

  • USDT0를 이중 역할 자산으로 취급하기
  • 실제 잔액에 대해 지급능력 강제하기
  • 허용량 기반 고갈 경로 피하기
  • Ethereum 특유의 잔액 및 주소 가정에 대한 의존 제거하기

오프체인 서비스 및 인덱서는 다음을 수행해야 합니다:

  • 소수 잔액 조정으로 인한 보조 Transfer 이벤트 고려하기
  • 이벤트 기반 잔액 재구성 대신 직접 잔액 조회 사용하기

다음 권장 사항

  • 가스로서의 USDT — USDT0가 네이티브 자산과 ERC-20 토큰 양쪽으로 작동하는 이유를 이해하세요.
  • 첫 USDT0 전송하기 — 테스트넷에서 네이티브 및 ERC-20 경로를 통해 USDT0 전송을 제출하세요.
  • Ethereum 비교 — Ethereum에서 이식할 때의 모든 동작 차이를 검토하세요.