
솔리디티 컴파일러의 고위험 취약점: 상태 변수 할당 실수로 인한 삭제
본 문서는 Solidity(0.8.13<=solidity<0.8.17) 컴파일러가 Yul 최적화 메커니즘의 결함으로 인해 상태 변수 할당 작업이 잘못 제거되는 중/고위험 취약점의 원리와 이에 대한 예방 조치를 소스 코드 수준에서 상세히 설명합니다.
스마트 계약 개발자가 계약 개발 시 보안 의식을 높이고, SOL-2022-7 취약점이 계약 코드 보안성에 미치는 영향을 효과적으로 회피하거나 완화할 수 있도록 돕습니다.
취약점 상세 정보
Yul 최적화 기능은 Solidity로 스마트 계약 코드를 컴파일할 때 선택 가능한 기능으로, 불필요한 명령어를 줄여 배포 및 실행 과정에서의 가스 비용을 절감할 수 있습니다. 구체적인 Yul 최적화 메커니즘은 공식 문서를 참고하십시오.
컴파일 과정 중 UnusedStoreEliminator 최적화 단계에서 컴파일러는 "불필요한" Storage 쓰기 작업을 제거하지만, "불필요함"을 판단하는 로직에 결함이 있어 특정 사용자 정의 함수(내부에 호출 블록의 실행 흐름에 영향을 주지 않는 분기문을 포함하는 함수)를 Yul 함수 블록 내에서 호출하고, 해당 함수 호출 전후로 동일한 상태 변수에 대한 쓰기 작업이 존재할 경우, Yul 최적화 과정에서 그 함수 호출 이전의 모든 Storage 쓰기 작업이 영구적으로 삭제되는 문제가 발생합니다.
다음 코드를 살펴보겠습니다:
contract Eocene {
uint public x;
function attack() public {
x = 1;
x = 2;
}
}
UnusedStoreEliminator 최적화 과정에서 x = 1은 함수 attack() 전체 실행 흐름상 불필요한 작업입니다. 따라서 최적화된 Yul 코드에서는 x = 1을 제거하여 가스 소비를 줄입니다.
이제 중간에 사용자 정의 함수 호출을 삽입한 경우를 생각해 봅시다:
contract Eocene {
uint public x;
function attack(uint i) public {
x = 1;
y(i);
x = 2;
}
function y(uint i) internal{
if (i > 0){
return;
}
assembly { return( 0, 0) }
}
}
명백히 y() 함수가 호출되었으므로, y() 함수가 attack() 함수의 실행 흐름에 영향을 미치는지 판단해야 합니다. 만약 y() 함수가 전체 실행 흐름을 종료시킬 수 있다면(롤백이 아님에 주의하세요. Yul 코드 내 return() 함수는 이를 가능하게 함), x = 1은 삭제되어서는 안 됩니다. 위의 계약에서 y() 함수 내 assembly {return(0, 0)}이 전체 메시지 호출을 종료시킬 수 있으므로, x = 1은 삭제되어서는 안 됩니다.
그러나 Solidity 컴파일러의 코드 로직 문제로 인해 x = 1이 컴파일 시 잘못 삭제되어 코드 로직이 영구적으로 변경됩니다.
실제 컴파일 테스트 결과는 다음과 같습니다:
놀랍게도 최적화되어서는 안 될 x = 1의 Yul 코드가 사라졌습니다! 자세한 내용은 아래를 계속 읽어보세요.

Solidity 컴파일러 코드의 UnusedStoreEliminator는 SSA 변수 추적과 제어 흐름 추적을 통해 Storage 쓰기 작업이 불필요한지 판단합니다. 사용자 정의 함수 진입 시 UnusedStoreEliminator는 다음을 수행합니다:
-
memory 또는 storage 쓰기 작업 발생 시: 해당 작업을 m_store 변수에 저장하고 초기 상태를 Undecided로 설정
-
함수 호출 발생 시: 해당 함수의 memory 또는 storage 읽기/쓰기 작업 위치를 조회하고, 이를 m_store 내 모든 Undecided 상태 작업들과 비교
-
m_store 내 작업을 덮어쓰는 쓰기 작업이라면, 해당 작업 상태를 Unused로 변경
-
m_store 내 작업을 읽는 작업이라면, 해당 작업 상태를 Used로 변경
-
해당 함수에 추가 메시지 호출을 지속할 수 있는 분기가 없다면, m_store 내 모든 메모리 쓰기 작업을 Unused로 변경
-
상기 조건 하에서, 함수가 실행 흐름을 종료시킬 수 있다면 m_store 내 상태가 Undecided인 storage 쓰기 작업을 Used로 변경; 그렇지 않으면 Unused로 표시
-
함수 종료 시: 상태가 Unused로 표시된 모든 쓰기 작업을 삭제
memory 또는 storage 쓰기 작업의 초기화 코드는 다음과 같습니다:
memory 및 storage 쓰기 작업이 발생하면 이를 m_store에 저장하는 것을 확인할 수 있습니다.

함수 호출 처리 로직 코드는 다음과 같습니다:
여기서 operationFromFunctionCall()과 applyOperation()이 위의 2.1, 2.2 처리 로직을 구현하며, 아래쪽의 canContinue 및 canTerminate 기반 if 문이 2.3 로직을 구현합니다.
주의해야 할 것은 바로 아래의 if 조건 판별 로직의 결함이 이 취약점을 만들어냈다는 점입니다!!

operationFromFunctionCall()은 해당 함수의 모든 memory 또는 storage 읽기/쓰기 작업을 가져옵니다. 여기서 Yul에는 sstore(), return()과 같은 많은 내장 함수가 있으며, 내장 함수와 사용자 정의 함수는 서로 다른 처리 로직을 갖는다는 점에 유의해야 합니다.

applyOperation() 함수는 operationFromFunctionCall()로부터 얻은 모든 읽기/쓰기 작업들을 대조하여 m_store에 저장된 작업들이 현재 함수 호출 중에 읽히거나 쓰이는지를 판단하고, m_store 내 해당 작업의 상태를 수정합니다.

위의 UnusedStoreEliminator 최적화 로직이 Eocene 계약의 attack() 함수를 어떻게 처리하는지 살펴보겠습니다:
x = 1 쓰기 작업을 m_store 변수에 저장하고 상태를 Undecided로 설정
1. y() 함수 호출 시, y() 함수의 모든 읽기/쓰기 작업을 가져옴
2. m_store 변수를 순회하며, y() 호출로 인한 읽기/쓰기 작업이 x = 1과 무관하므로 x = 1 상태는 여전히 Undecided
- y() 함수의 제어 흐름 로직을 분석하면, y() 함수는 정상적으로 반환될 수 있는 분기를 가지므로 canContinue가 True이며, if 문에 진입하지 않음. 따라서 x = 1 상태는 여전히 Undecided 상태입니다!!!
3. x = 2 쓰기 작업 발생 시:
-
m_store를 순회하며 Undecided 상태의 x = 1 발견. x = 2가 x = 1을 덮어쓰므로 x = 1 상태를 Unused로 설정
-
x = 2 작업을 m_store에 저장, 초기 상태는 undecided
4. 함수 종료 시:
-
m_store 내 모든 undecided 상태 작업의 상태를 Used로 변경
-
m_store 내 모든 Unused 상태 작업을 삭제
명백히 함수 호출 시, 호출된 함수가 메시지 실행을 종료시킬 수 있다면 호출 이전의 모든 Undecided 상태 쓰기 작업을 Used로 변경해야 하지만, 이 로직은 여전히 Undecided로 남겨두어 호출 이전의 쓰기 작업이 잘못 삭제되게 만듭니다.
또한 각 사용자 정의 함수의 제어 흐름 식별자는 전달되기 때문에, 다중 함수 재귀 호출 상황에서도 가장 하위 함수가 위 조건을 충족하더라도 x = 1이 삭제될 수 있음에 유의해야 합니다.
Solidity 공식 블로그에서는 거의 동일한 로직임에도 불구하고 이 취약점의 영향을 받지 않는 계약 코드 예시를 들고 있습니다. 그러나 이 코드가 취약점의 영향을 받지 않는 이유는 UnusedStoreEliminator의 처리 로직에 다른 가능성 때문이 아니라, UnusedStoreEliminator 이전의 Yul 최적화 단계에서 FullInliner 최적화 과정이 작동하기 때문입니다. 즉, 매우 작거나 한 번만 호출되는 함수는 호출 함수 내로 인라인되어 사용자 정의 함수라는 취약점 트리거 조건 자체가 제거되기 때문입니다.
contract Normal {
uint public x;
function f(bool a) public {
x = 1;
g(a);
x = 2;
}
function g(bool a) internal {
if (!a)
assembly { return( 0, 0) }
}
}
컴파일 결과는 다음과 같습니다:
함수 g(bool a)가 함수 f() 내로 인라인되어 사용자 정의 함수라는 취약점 조건이 제거되었고, 따라서 취약점이 발생하지 않습니다.

해결 방안
가장 근본적인 해결책은 영향을 받는 범위의 Solidity 컴파일러를 사용하지 않는 것입니다. 만약 취약 버전의 컴파일러를 반드시 사용해야 한다면, 컴파일 시 UnusedStoreEliminator 최적화 단계를 제거하는 방법을 고려할 수 있습니다.
계약 코드 차원에서 취약점을 완화하고자 한다면, 여러 최적화 단계와 실제 함수 호출 흐름의 복잡성을 고려할 때, 전문 보안 담당자에게 코드 감사를 의뢰하여 이 취약점으로 인한 보안 문제를 발견하도록 하는 것이 좋습니다.
TechFlow 공식 커뮤니티에 오신 것을 환영합니다
Telegram 구독 그룹:https://t.me/TechFlowDaily
트위터 공식 계정:https://x.com/TechFlowPost
트위터 영어 계정:https://x.com/BlockFlow_News














