이 글의 첫 번째 부분에서는 스왑 서비스 rabbit.io가 클라이언트에게 소량의 SOL을 보내려 했지만 트랜잭션이 계속 거부된 사례를 다뤘습니다. 모든 알려진 Solana 블록체인 규칙상에는 문제가 없어 보였음에도 불구하고 말이죠.
첫 번째 부분에서는 또한 기술적으로는 올바른 트랜잭션이 XRP Ledger, Stellar, 라이트닝 네트워크(비트코인의 레이어 2) 같은 네트워크에서 여전히 거부될 수 있는 상황들을 다뤘습니다. 또한 발신자 쪽에서는 아무런 문제가 없어 보이는데도 USDC 스마트 계약의 블랙리스트 구현 세부사항으로 인해 USDC 트랜잭션이 예기치 않게 실패할 수 있는 사례도 설명했습니다.
파트 I을 놓치셨다면 여기에서 읽을 수 있습니다.
오늘은 다른 네트워크와 토큰과 관련된 몇 가지 유사한 사례를 살펴보려 합니다. 각각의 경우 처음에는 모든 것이 정상으로 보이지만 트랜잭션은 여전히 실패합니다.
이 사례는 DeFi 프로토콜의 개발자와 운영자에게 특히 해당됩니다. 그럼에도 불구하고 일반 사용자도 그 영향을 느낄 수 있습니다.
다음과 같은 상황을 상상해 보세요. 어떤 프로토콜에 USDC, DAI, USDT를 예치하려 합니다. USDC와 DAI로는 예치가 잘 되는데 USDT 전송만 되돌아갑니다. 주소는 맞고, 네트워크도 맞고, 가스도 충분합니다. 이유가 뭘까요?
그 이유는 이더리움의 USDT가 ERC-20 표준을 완전히 준수하지 않기 때문입니다. 표준은 transfer와 transferFrom 함수가 불리언 값을 반환할 것을 요구합니다: 성공 시 true, 실패 시 false. 그러나 USDT는 아무런 값을 반환하지 않습니다.
만약 스마트 계약이 ERC-20 표준에 엄격히 따라 작성되어 transfer 호출에 대한 응답으로 불리언 값을 기대하는데 USDT가 아무 값도 반환하지 않으면, Solidity 0.4.22 이후 버전으로 동작하는 EVM 가상머신은 이를 오류로 해석하고 전송을 되돌립니다. 기술적으로 트랜잭션은 성공했을 수 있지만 강제로 롤백됩니다. 그리고 발신자는 네트워크나 스마트 계약 어느 쪽에서도 무엇이 정확히 잘못되었는지에 대한 명확한 설명을 받지 못합니다.
이 예시는 rabbit.io의 스왑과는 무관합니다. 우리 시스템은 가장 단순하고 신뢰할 수 있는 방식으로 구성되어 있습니다: 토큰을 수동으로 보낼 주소를 받고, 그 대가로 우리가 필요한 토큰을 제공합니다. DeFi 스마트 계약은 관여하지 않습니다.
그럼에도 불구하고 rabbit.io에서 스왑을 할 뿐만 아니라 DeFi와 활발히 상호작용하는 독자들—특히 개발자—을 위해 간단한 해결책을 제안할 수 있습니다. 비표준 토큰을 올바르게 처리하는 OpenZeppelin의 SafeERC20 라이브러리를 사용하세요.
전문적으로 작성된 대부분의 DeFi 프로토콜은 이미 이를 사용합니다. 그러나 오래되었거나 아마추어가 만든 스마트 계약은 여전히 USDT와 상호작용할 때 깨집니다. 이 문제는 너무 널리 퍼져 있어 알려진 토큰 특이사항 데이터베이스인 weird-erc20에 포함되어 있으며, 유사한 동작을 하는 다른 토큰들도 찾을 수 있습니다.
이더리움의 USDT는 스마트 계약과의 트랜잭션이 정기적으로 실패하게 하는 또 다른 특이점을 가지고 있습니다.
ERC-20 표준에서 스마트 계약이 당신의 토큰을 사용할 수 있도록 허용하려면 approve(spender, amount) 함수를 호출합니다. 예를 들어 허용량을 100에서 200 USDC로 늘리고 싶다면 단순히 approve(spender, 200)을 호출하면 됩니다.
USDT는 다르게 동작합니다. 그 코드에는 수신 주소가 이미 0이 아닌 허용량을 가지고 있다면 직접 새로운 0이 아닌 값으로 설정하는 것을 허용하지 않는다고 명시되어 있습니다. 거의 모든 다른 토큰에서는 완전히 유효한 트랜잭션이 USDT와 함께 사용될 때 거부됩니다.
이 동작은 허용량 관련 이중 지출 공격을 방지하기 위해 도입되었습니다. 개발자들은 공격자가 기존 허용량과 새 허용량을 동시에 즉시 소비할 수 있을 것을 우려했습니다.
따라서 USDT 허용량을 변경하는 필수 패턴은 다음과 같습니다:
approve(spender, 0)을 호출하여 허용량을 리셋approve(spender, new_value)를 호출하여 새 값 설정탈중앙화 애플리케이션 사용자에게는 혼란스러울 수 있습니다. 사용자는 이전에 애플리케이션에 허용량을 부여했고, 이제 다른 허용량을 설정하려 서명했지만 아무 것도 바뀌지 않고 명확한 설명도 없는 상황이 발생합니다.
이 예시 역시 rabbit.io의 스왑과는 관련이 없습니다. 우리 플랫폼에서는 지갑 연결을 요구하지 않으며 지갑의 토큰 사용 권한을 요청하지 않기 때문에 이러한 거부된 트랜잭션이 발생하지 않습니다.
그러나 어떤 dApp에서 USDT 관련 approve 트랜잭션이 실패한다면 해결 방법은 대체로 간단합니다. 현재 허용량 수준을 확인하세요. 0이 아니라면 먼저 0으로 리셋한 뒤 새 값을 설정하세요.
비트코인 네트워크에는 더스트(dust)라는 개념이 있습니다. 이는 이후에 지출하기에는 가치가 너무 작은 트랜잭션 출력(output)을 가리킵니다. 다시 말해 지갑에 동전이 기술적으로 존재하지만, 이를 보내는 데 필요한 수수료가 동전 자체보다 클 수 있습니다.
여기서 종종 혼동되는 근본적인 구분을 이해하는 것이 중요합니다: 컨센서스 규칙(consensus rules)과 노드 정책(node policy)의 차이입니다.
출력이 더스트 한도보다 작은 트랜잭션은 기술적으로 비트코인 컨센서스 규칙에 따라 유효할 수 있습니다. 그러나 대부분의 노드는 기본적으로 그러한 트랜잭션의 중계를 거부하며 대부분의 채굴자도 블록에 포함시키기를 거부합니다.
이유는 간단합니다: 극히 작은 코인들을 저장하는 것은 데이터베이스에 부담을 줍니다만 실질적인 경제적 이익을 제공하지 않습니다.
더스트 한도는 주소 유형과 최소 중계 수수료에 따라 달라집니다. 역사적으로 더스트 한도는 546 사토시로 간주됩니다. 최소 수수료율이 1 sat/vByte일 때 더 작은 출력은 전송되는 금액보다 수수료가 더 많이 들기 때문입니다. 그러나 이는 "1"로 시작하는 레거시 주소에만 적용됩니다.
더 현대적인 주소 유형은 블록체인에서 차지하는 공간이 더 적은 트랜잭션을 생성하므로 더스트 한도가 낮을 수 있습니다. 예를 들어 일반적인 bc1q... 주소의 경우 수수료율 1 sat/vByte에서 더스트 한도는 294 사토시이며, 현재 적용되는 최소 중계 수수료(0.1 sat/vByte)에서는 더스트 한도가 열 배 작아집니다.

다음 상황을 상상해 보세요.
당신은 100,000 사토시(0.001 BTC)를 가지고 있습니다. 누군가에게 99,000 사토시를 보내고 싶습니다. 트랜잭션을 구성했고 네트워크 수수료는 980 사토시였습니다. 남는 20 사토시는 거스름돈으로 당신에게 돌아와야 합니다.
그러나 그런 일은 일어나지 않습니다. 당신은 거스름돈을 받지 못하고, 주 지급도 수령인에게 도달하지 않습니다. 중계 노드 네트워크는 출력 중 하나가 더스트 한도 이하이기 때문에 트랜잭션을 조용히 거부할 것입니다.
또 관련된 문제로는 더스트를 나중에 소비하는 문제가 있습니다. 수수료가 낮을 때 294 사토시보다 작은 여러 출력을 받았고 이후 수수료가 상승하면, 그 코인들을 지출하려는 어떤 트랜잭션도 매우 비싸져서 전송되는 금액이 필요한 수수료조차 충당하지 못할 수 있습니다.
따라서 다른 암호자산을 비트코인으로 교환할 때 생성되는 출력이 더스트 한도를 초과하는지 확인하세요. Rabbit.io는 현재 더스트 한도보다 여러 배 큰 금액으로 스왑을 허용합니다. 그러나 네트워크 수수료가 증가하면 이러한 소액은 이후 전송이 어려워질 수 있습니다.
이더리움과 모든 EVM 호환 블록체인(예: BSC, HyperEVM 등)에서 특정 주소에서 전송되는 각 트랜잭션에는 순차 번호인 nonce가 있습니다.
네트워크는 트랜잭션을 엄격히 순서대로 처리합니다: 먼저 nonce = 0인 트랜잭션, 그다음 nonce = 1, 그다음 2, ... 이렇게 숫자를 건너뛸 수 없습니다.
만약 어떤 트랜잭션이 가스 비용이 너무 낮아 멤풀(mempool)에 걸려 있다면, 동일 주소에서 더 높은 nonce를 가진 모든 후속 트랜잭션도 정상 수수료가 있더라도 대기 상태로 남습니다.
아주 현실적인 시나리오가 있습니다. 몇 주 전에 가스비를 너무 낮게 설정해 트랜잭션을 보냈습니다. 그것이 걸려버렸습니다. 당신은 잊어버렸습니다. 이제 새 트랜잭션을 보내려 합니다. 올바른 주소를 지정하고 적절한 수수료를 설정했지만 트랜잭션이 계속 처리되지 않습니다.
문제는 현재 트랜잭션이 아니라 오래된 보류 중인 트랜잭션입니다.
해결책은 동일한 오래된 nonce로 더 높은 가스 수수료를 써서 트랜잭션을 다시 보내는 것입니다. 이렇게 하면 원래 트랜잭션이 가속되거나(속도 향상) 취소됩니다. 취소는 동일한 nonce로 자신에게 전송을 보내고 더 높은 가스 수수료를 설정할 때 발생합니다.
다만 모든 지갑이 사용자가 nonce 값을 제어하도록 허용하는 것은 아님을 유의하세요. 예를 들어 Trust Wallet과 Exodus 같은 인기 지갑은 이 기능을 제공하지 않습니다. 반면 많은 이들이 구식이고 불편하다고 생각하는 MetaMask는 실제로 이 기능을 제공합니다.
TRON 네트워크는 독특한 자원 모델을 가지고 있습니다. 단일 트랜잭션 수수료 대신 트랜잭션은 두 가지 유형의 자원을 소비합니다:
에너지는 TRX를 동결(freeze)하여 얻을 수 있습니다. 동결하지 않으면 네트워크가 자동으로 TRX를 소각하여 비용을 충당합니다.
그리고 여기서 예기치 않은 가격 책정 문제가 나타납니다:
만약 발신자에게 수수료로 사용할 수 있는 15 TRX만 있고 전송 대상이 신규 주소라면 트랜잭션은 OUT OF ENERGY 오류로 실패할 수 있습니다. 이 시도에서 소모된 TRX는 환불되지 않습니다.
다시 말해 상황은 이 글의 다른 예들과 유사합니다: 주소는 맞고 네트워크도 맞고 발신자는 USDT를 가지고 있으며 TRX도 있었지만 트랜잭션은 실패합니다.
결론은 간단합니다: USDT TRC-20와 안정적으로 작업하려면 가능한 트랜잭션 비용을 충당할 수 있도록 잔고에 최소 27 TRX를 유지하는 것이 바람직합니다.
SOL 전송 실패 사례는 나에게 귀중한 교훈을 주었습니다. 블록체인을 오해했다는 의미가 아니라, 각 블록체인은 자체 생태계이며 문서화가 항상 명확하지 않은 숨겨진 규칙들이 있고 때로는 특이한 상황에서만 드러난다는 것입니다.
혼란스러운 실패 하나하나는 좌절의 이유가 아니라 새로운 것을 배울 기회입니다. 그리고 내가 배울수록 암호화폐 작업을 계속하는 것이 더 흥미로워집니다.
같은 생각이라면, 특이한 사례를 문서화하고 당신의 발견을 공유하세요. 실무 경험이 공식 문서보다 빠르게 퍼질 때 암호화폐 산업은 더 신뢰할 수 있게 됩니다.