Move VM 메모리 안전성 vs EVM 재진입성 : Aptos와 Sui의 리소스 모델이 스마트 컨트랙트 취약점의 전체 클래스를 제거하는 이유
2016년 DAO 해킹 사건은 단 한 번의 오후 만에 이더리움에서 6,000만 달러를 소진시켰습니다. 9년이 지난 2024년에도 재진입 공격(reentrancy attacks)은 22건의 개별 사건을 통해 DeFi 프로토콜에 3,570만 달러의 피해를 입혔습니다. 상태가 업데이트되기 전에 공격자가 계약을 다시 호출하는 동일한 유형의 취약점은 수년간의 개발자 교육, 감사 도구 및 검증된 패턴에도 불구하고 여전히 EVM 생태계를 괴롭히고 있습니다.
Move 언어를 기반으로 구축된 Aptos와 Sui는 근본적으로 다른 접근 방식을 취합니다. 이들은 설계 단계에서부터 전체 카테고리의 취약점을 불가능하게 만듭니다.
근본 원인: EVM 재진입이 발생하는 방식
Move가 왜 다른지 이해하려면, 우선 왜 이더리움에서 재진입이 가능한지 정확히 이해하는 것이 도움이 됩니다.
Solidity 계약은 실행 도중에 외부 계약을 호출할 수 있습니다. 계약 A가 계약 B를 호출하면 실행 권한이 완전히 B로 넘어갑니다. 그러면 B는 A가 내부 상태 업데이트를 마치기 전에 A를 다시 호출하여 "재진입(re-entering)"할 수 있습니다. 만약 A의 출금 로직이 다음과 같다면:
// 취약한 패턴
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount);
(bool success, ) = msg.sender.call{value: amount}(""); // 외부 호출이 먼저 발생
balances[msg.sender] -= amount; // 상태 업데이트가 나중에 발생
}
공격자의 계약은 콜백에서 ETH를 받은 즉시 다시 withdraw를 호출하여 balances[msg.sender]가 차감되기 전에 자금을 빼낼 수 있습니다. 이것이 바로 The DAO에서 일어난 일입니다. 공격자의 계약은 루프 내에서 출금 함수를 360만 번 재귀적으로 다시 호출했습니다.
해결책은 간단해 보입니다. 외부 호출을 하기 전에 상태를 업데이트하는 것입니다 ("Checks-Effects-Interactions" 패턴). 하지만 개발자들은 이를 잊어버리고, 감사자(Auditors)들은 이를 놓칩니다. 이 패턴은 언어 자체가 아닌 인간의 근면함에 의해서만 강제되는 관습일 뿐입니다.
Move의 접근 방식: 리소스는 복제되거나 파괴될 수 없습니다
Move는 타입 시스템 수준에서 이러한 종류의 오류를 방지하도록 처음부터 설계되었습니다. 핵심 개념은 선형 타입 시스템(linear type system) 과 리소스 타입(resource types) 입니다.
Move에서 리소스는 다음과 같은 특성을 가진 특별한 종류의 값입니다:
- 복사될 수 없음 — 저장 위치 간에 이동만 가능합니다.
- 폐기될 수 없음 — 명시적으로 소비되거나 어딘가에 저장되어야 합니다.
- 특정 시점에 단 한 명의 소유자만 가짐 — 매핑이 아닌 VM에 의해 추적됩니다.
이는 추상적으로 들릴 수 있지만, 그 영향은 구체적입니다. 토큰 전송이 어떻게 작동하는지 생각해 보세요:
- Solidity에서 토큰 잔액은 매핑 내의
uint256입니다. 업데이트 순서가 틀리면 이론적으로 숫자를 조작할 수 있습니다. - Move에서 토큰은 계정의 저장소에 존재하는 실제 리소스 객체입니다. 리소스를 한 위치에서 다른 위치로 물리적으로 이동시킵니다. 리소스가 두 곳에 존재하거나 어디에도 존재하지 않는 중간 상태는 없습니다.
Move VM은 소스 수준이 아니라 바이트코드 수준에서 이러한 불변성(invariants)을 강제합니다. 개발자가 버그가 있는 코드를 작성하더라도, VM은 리소스를 복제하거나 자동으로 폐기하려는 모든 트랜잭션을 거부합니다.
Move에서 재진입이 구조적으로 불가능한 이유
재진입에는 두 가지 조건이 필요합니다. 실행 중에 외부 코드를 호출할 수 있는 능력과, 해당 콜백 중에 조작할 수 있는 가변 공유 상태(mutable shared state)입니다. Move는 이 두 가지를 모두 차단합니다.
Move는 Solidity와 같은 방식으로 동적 디스패치(dynamic dispatch)를 허용하지 않습니다. 알 수 없는 코드로 제어권을 넘기는 임의의 외부 호출이 없습니다. 함수는 정적으로 호출되어야 하며, 호출 대상은 컴파일 시점에 알려집니다. 즉, 공격자는 콜백 중에 모듈에 재진입하는 계약을 배포할 수 없습니다. 왜냐하면 모듈이 실행 권한을 알려지지 않은 외부 계약에 넘겨주지 않기 때문입니다.
또한, Sui와 Aptos의 Move 객체 모델은 객체가 함수 내부와 외부로 명시적으로 전달되는 소유권 시스템을 사용합니다. 함수로 "이동(moved into)"된 객체는 함수가 이를 반환할 때까지 다른 어디에서도 접근할 수 없습니다. 단일 트랜잭션에서 동일한 리소스에 대한 동시 액세스는 단순히 불가능합니다.
2025년에 발표된 연구에 따르면 "Move에서는 동적 콜백 이 불가능하므로 재진입이 불가능하다. 이는 재진입이 여전히 주요 위협으로 남아 있는 Solidity와의 근본적인 차이점이다"라고 확인되었습니다.
뮤텍스 락(Mutex Locks) 없는 이중 지불 방지
EVM 기반 시스템에서 이중 지불 방지는 세심한 프로그래밍에 의존합니다. 개발자는 상태 업데이트를 부지런히 추적하여 하나의 트랜잭션에서 토큰이 두 번 사용되지 않도록 수동으로 보장해야 합니다.
Move의 선형 타입 시스템은 이중 지불을 구조적으로 불가능하게 만듭니다. 리소스는 복사할 수 없기 때문에 코인을 사용하는 것은 문자 그대로 계정의 저장소에서 코인을 제거하는 것입니다. 첫 번째 사용 후에는 리소스가 더 이상 제어 하에 존재하지 않으므로 트랜잭션에서 동일한 리소스를 두 번 사용할 방법이 없습니다. VM이 이를 강제하며, 이는 관습이 아닌 제약 사항입니다.
이는 Sui의 Capability 객체로도 확장됩니다. Capability 리소스는 한 번 소비되면 다시 사용할 수 없습니다. 이를 Capability가 일반적으로 불리언(boolean) 또는 주소 매핑으로 인코딩된 역할이며 여러 번 확인할 수 있는 EVM 액세스 제어 패턴과 비교해 보십시오.
Sui에서의 한 실제 사례는 미묘한 차이를 강조합니다. 한 DEX에서 출금 로직이 Capability 자체가 아닌 Capability에 대한 가변 참조(mutable reference) 에 단일 사용 제약 조건을 강제하지 못하는 결함 이 발견되었습니다. 이는 Move의 리소스 모델이 전체 클래스의 버그를 제거하지만, 개발자가 소유된 리소스가 아닌 참조로 작업할 때 여전히 로직 오류를 발생시킬 수 있음을 보여줍니다. 위협 범위는 극적으로 좁아졌지만 제로는 아닙니다.
정수 오버플로: Move가 기본적으로 해결하는 또 다른 문제
초기 Solidity (0.8.0 이전)에서는 정수 산술 연산 시 오버플로가 발생하면 아무런 경고 없이 값이 순환(wrap around)되었습니다. 이로 인해 공격자가 오버플로 조건을 유발하여 토큰 잔액을 조작할 수 있었으며, 이는 여러 대규모 DeFi 해킹 사건의 원인이 되었습니다.
Solidity 0.8.0 버전에서 자동 오버플로 검사가 도입되었지만, 이는 이미 수년간 피해가 발생한 이후였습니다. Move는 언어 설계 초기부터 이러한 보호 기능을 포함했습니다. 정수 오버플로를 유발하는 모든 트랜잭션은 자동으로 중단(abort)됩니다. 예외 처리는 없으며, 기본적으로 unchecked와 같은 기능도 존재하지 않습니다. 또한 예전 방식으로 작동하는 레거시 컨트랙트 걱정도 필요 없습니다.