Flash Accounting

김현학·2024년 9월 27일

Uniswap v4 Hook

목록 보기
6/6
post-thumbnail

스토리지에 값을 추가/수정하는 SSTORE는 EVM에서 매우 비싼 연산이다. 하지만 트랜잭션 중에만 값을 유지하는 TSTORE를 사용하면 96% 이상 가스비를 절감할 수 있다.

이번 포스트에서는 EIP-1153 transient storage를 활용하여 구현된 v4의 Flash Accounting & Locking 개념에 대해 알아본다.

0. SSTORE의 가스 소비량

  • 다른 언어는 undefined, null, NIL, None 등 특수한 상태를 정의하여 초기화 이전 상태를 정의한다. 그러나 EVM의 모든 초기 상태는 0이고, 현재 시점에서 트랜잭션 종료 후에도 Contract의 상태를 유지할 수 있는 수단은 오로지 storage 밖에 없다.
  • SSTORE의 가스비는 비싸게 책정되었다. 특히, 처음 슬롯을 할당하는 시점에는 더욱 비싸다. 이유는 슬롯 할당에 대한 제약을 낮출수록 많은 이들이 스토리지를 적극 활용하는 구현을 시작할 것이고, 이는 고스란히 노드의 부담으로 돌아가 결과적으로 체인의 수명을 단축시킬 것이기 때문이다.
  • evm.codes는 각 opcode에 대한 조작 방법과 dynamic gas에 대한 계산식을 제공한다. 초기화되지 않은 cold storage를 초기화된 hot storage로 바꾸는 것은 20000 가스, 그리고 기존 hot storage를 할당되어있던 다른 값으로 수정하는 것은 2900 가스가 소요된다.
  • 각각의 Pool이 개별 컨트랙트로 정의된 상황에서는, 모든 트랜잭션 결과가 각 풀의 장부(스토리지)에 반영되어야 한다. Singleton 구조는 모든 풀 외부 컨트랙트 호출(extcall)의 비용은 절감했지만, 결국 외부 storage가 내부로 캡슐화되었다는 것 외에는 차이가 없다.

1. Transient storage

  • opcode tstore, tloadEIP-1153를 통해 매 트랜잭션마다 초기화되는 임시 storage 변수를 사용할 수 있도록 제안했다. Cancun hardfork에서 처음 모습을 드러냈으며, solc version 0.8.24부터 해당 opcode의 사용을 지원한다.
  • 이는 동일한 트랜잭션 내에서 re-entrancy가 발생하는 경우를 효율적으로 처리하기 위함이다. 만약 이 개념이 존재하지 않는다면, 울며 겨자먹기로 storage를 사용하거나, 모든 변경사항을 함수 호출 간 파라미터로 전달해야만 한다.
  • evm.codes을 통해 소요되는 가스비는 100임을 확인했다. 기존 2900 가스를 소요해야 했던 값의 수정과 비교했을 때, 약 96% 이상의 가스비 절감 효과를 얻게 되었다.

1-2. 가스비가 절약된다는 것은? 🤔

  • 기존 Solidity의 transfer 또는 Vyper의 send 함수는 고정적으로 낮은 가스비를 사용하여 Native Token을 전송했다. 덕분에 어떤 컨트랙트는 Reentrancy 공격에 취약한 구조였음에도 다행히 공격의 대상이 되지 않았다. 그러나 transient storage를 사용하면 재진입 시에도 상태를 매우 저렴한 가격에 보존할 수 있다.
  • 조사 결과, 이미 작년부터 이러한 결과를 예견한 글이 있었다. ChainSecurity 팀은 포스트를 통해 Cancun 이전의 컨트랙트와의 상호작용 괜찮지만, 새로운 컨트랙트의 상호작용에 유의해야 한다 고 설명했다. 이러한 기능 추가를 통해 개발 편의성과 연산 효율성이 증가했지만, 이와 동시에 결과적으로 기존 공격 벡터를 보다 활성화시키게 되었다.
  • TSTORE Reentrancy Example에서 2300 gas 내의 reentrancy 예제를 확인할 수 있다.

2. Transient Locking

transient storage 활용 사례 #1
Pause처럼 컨트랙트의 상태 자체를 비활성화하는 것이라면 기존 storage를 사용해야 하지만,
Lock처럼 Reentrancy를 방지하기 위한 목적이라면 동일 트랜잭션 내에서만 상태가 유지되면 된다.

  • LockingTransient Storage의 컨셉을 소개하기에 가장 적합한 활용 사례 중 하나이다. Periphery contractPoolManagerunlock() 함수를 호출하는 것으로 컨트랙트 이용 권한을 얻는다. 이후 로직 함수에 onlyWhenUnlocked modifier를 붙여 반드시 정해진 flow대로만 이용이 가능하도록 강제했다.
  • 2024/09/27, 현재 기준으로 아직 특별한 제약은 없다. msg.sender의 콜백이 모두 끝난 후, 빚진 토큰의 종류가 한 개 이상인지 확인하는 로직이 있을 뿐이다.

3. Flash Accounting

transient storage 활용 사례 #2
금을 거래한다고 실물을 배달하는 것은 거래 효율성을 떨어뜨린다.
각각의 트랜잭션에서 모든 유형의 거래를 장부에 기록하고,
최종적으로 서로가 원하던 결과를 얻었음만 확인하면 된다.

동일한 원리가 Flash Accounting에도 적용된다.
실제 토큰을 전송하지 않고도, 장부에 기록된 내용을 활용하여 거래가 가능하다.
구현체에 대한 자세한 분석은 🚧 작성 예정

  • DeFi 프로토콜이 지속 가능한 형태로 유지되기 위해서는 일방적인 손실이 발생해서는 안 된다. Uniswap v2에서는 x*y>=k 조건을 활용했지만, v4에서는 거래 간 발생하는 delta, 즉, 유저와 PM 모두 서로에게 빚진 토큰이 없는지를 기준으로 삼는다.
  • 이러한 방식의 유효성 판별은 상태 변화를 추적할 필요가 없고, 트랜잭션 시작 시 0으로 시작해서, 끝날 때에도 값이 0일 것을 기대한다. 따라서 transient storage를 활용할 경우, 기존에 비해 가스비를 크게 절감할 수 있다.
    • 특히, multi-hop swap을 진행하면 그 효율은 극대화된다. 한 트랜잭션 내에 n-1개 풀에 대한 swap 연산을 진행했을 때, v3는 모든 n개의 토큰 컨트랙트에 대한 상태 업데이트가 모두 이루어졌지만, v4는 ERC-1155: Multi Token Standard를 간소화 한 ERC-6909: Minimal Multi-Token Interface를 사용하여, 소유주의 다양한 토큰을 단 하나의 컨트랙트에서 모두 관리한다.

0개의 댓글