| r2 | ||
|---|---|---|
| r1 (새 문서) | 1 | [include(틀:Solidity 디자인 패턴)] |
| 2 | ||
| 3 | [[분류:블록체인]] | |
| 4 | [[분류:스마트 컨트랙트]] | |
| 5 | [[분류:Solidity]] | |
| 6 | [[분류:디자인 패턴]] | |
| 7 | ||
| 8 | {{{+2 '''State Machine 패턴'''}}} | |
| 9 | ||
| 10 | [목차] | |
| 11 | ||
| 12 | == 개요 == | |
| 13 | State Machine 패턴은 컨트랙트의 생애 주기를 유한한 상태(state)들로 나누고, 각 상태에서 허용하는 행위와 전이(transition) 규칙을 명시적으로 정의하는 패턴이다.[*1 Wohrer, Maximilian; Zdun, Uwe (2018). [[https://ieeexplore.ieee.org/document/8327565|Smart Contracts: Security Patterns in the Ethereum Ecosystem and Solidity]]. ''2018 International Workshop on Blockchain Oriented Software Engineering (IWBOSE)''. IEEE. pp. 2–8.] | |
| 14 | ||
| 15 | 컴퓨터 과학의 [[유한 상태 기계]](FSM) 이론에 기반한다.[*2 Hopcroft, John E.; Ullman, Jeffrey D. (1979). ''Introduction to Automata Theory, Languages, and Computation''. Addison-Wesley. ISBN 0-201-02988-X.] | |
| 16 | ||
| 17 | 컨트랙트는 반드시 하나의 상태에만 존재하며 사전에 정의된 규칙에 따라서만 상태가 변경된다. | |
| 18 | ||
| 19 | == 동기 == | |
| 20 | 스마트 컨트랙트는 배포 후 코드 수정이 불가능하다. 그런데 경매, 에스크로, 거버넌스 등 대부분 온체인 비즈니스 로직은 '''여러 단계를 순서대로 거치는 생애주기'''를 가진다. | |
| 21 | ||
| 22 | 경매에서 정산이 끝난 뒤 입찰을 넣거나 에스크로에서 입금 전에 물건 수령을 확인하는 것은 허용되면 안 된다. | |
| 23 | ||
| 24 | 이러한 "잘못된 시점에 잘못된 함수가 호출되는" 버그는 전통 소프트웨어에서는 패치로 해결할 수 있지만 불변(immutable)한 스마트 컨트랙트에서는 자금 손실로 직결된다.[*3 Atzei, Nicola; Bartoletti, Massimo; Cimoli, Tiziana (2017). [[https://link.springer.com/chapter/10.1007/978-3-662-54455-6_8|A Survey of Attacks on Ethereum Smart Contracts (SoK)]]. ''Principles of Security and Trust''. Springer. pp. 164–186.] | |
| 25 | ||
| 26 | State Machine 패턴은 이 문제를 해결하기 위해 설계되었다. | |
| 27 | ||
| 28 | 이 패턴이 해결하는 핵심 문제는 다음과 같다. | |
| 29 | ||
| 30 | * '''단계 간 순서 강제''' - 정의되지 않은 경로로의 상태 전이를 구조적으로 차단한다. | |
| 31 | * '''함수 호출 시점 제어''' - 각 함수가 실행 가능한 상태를 컴파일 타임에 선언하여, 잘못된 시점의 호출을 런타임에서 revert한다. | |
| 32 | * '''시간 조건 자동 적용''' - 블록체인에 cron이 없는 환경에서 시간 경과에 따른 단계 전환을 트랜잭션 호출 시점에 자동 반영한다. | |
| 33 | * '''감사 용이성''' - 상태와 전이를 다이어그램으로 시각화할 수 있어 코드 리뷰와 보안 감사가 용이해진다.[*1] | |
| 34 | ||
| 35 | == 구성 요소 == | |
| 36 | ||
| 37 | === 상태 정의 - enum === | |
| r2 | 38 | {{{ |
| r1 (새 문서) | 39 | enum Stage { |
| 40 | Created, | |
| 41 | Bidding, | |
| 42 | Revealing, | |
| 43 | Settled | |
| 44 | } | |
| 45 | Stage public currentStage = Stage.Created; | |
| 46 | }}} | |
| 47 | ||
| 48 | 내부적으로 {{{uint8}}}로 저장된다. 정의되지 않은 값으로의 캐스팅은 revert된다. (Solidity 0.8+)[*4 Solidity Documentation. [[https://docs.soliditylang.org/en/latest/types.html#enums|Enums]]. docs.soliditylang.org.] | |
| 49 | ||
| 50 | === 상태 검증 - modifier === | |
| r2 | 51 | {{{ |
| r1 (새 문서) | 52 | modifier atStage(Stage expected) { |
| 53 | require(currentStage == expected, "Invalid stage"); | |
| 54 | _; | |
| 55 | } | |
| 56 | }}} | |
| 57 | ||
| 58 | 함수에 {{{atStage(Stage.Bidding)}}}을 붙이면 Bidding 상태에서만 실행된다. | |
| 59 | ||
| 60 | === 전이 함수 === | |
| 61 | '''선형 전이''' - 순서대로만 진행: | |
| r2 | 62 | {{{ |
| r1 (새 문서) | 63 | function nextStage() internal { |
| 64 | currentStage = Stage(uint(currentStage) + 1); | |
| 65 | } | |
| 66 | }}} | |
| 67 | ||
| 68 | '''명시적 전이''' - 허용된 경로만: | |
| r2 | 69 | {{{ |
| r1 (새 문서) | 70 | function transitionTo(Stage next) internal { |
| 71 | require(allowedTransitions[currentStage][next]); | |
| 72 | currentStage = next; | |
| 73 | } | |
| 74 | }}} | |
| 75 | ||
| 76 | === 시간 기반 전이 - timedTransition === | |
| 77 | 블록체인에는 cron이 없으므로 다음 트랜잭션 호출 시 시간 경과를 검사한다.[*5 Solidity Documentation. [[https://docs.soliditylang.org/en/latest/common-patterns.html#state-machine|Common Patterns: State Machine]]. docs.soliditylang.org.] | |
| 78 | ||
| r2 | 79 | {{{ |
| r1 (새 문서) | 80 | modifier timedTransition() { |
| 81 | if (currentStage == Stage.Bidding | |
| 82 | && block.timestamp >= createdAt + BIDDING_DURATION) | |
| 83 | currentStage = Stage.Revealing; | |
| 84 | if (currentStage == Stage.Revealing | |
| 85 | && block.timestamp >= createdAt + BIDDING_DURATION + REVEAL_DURATION) | |
| 86 | currentStage = Stage.Settled; | |
| 87 | _; | |
| 88 | } | |
| 89 | }}} | |
| 90 | ||
| 91 | {{{#!wiki style="border-left: 3px solid #edab00; background-color: #fef6e7; padding: 10px 15px; margin: 10px 0;" | |
| 92 | '''주의''': {{{if}}}를 연속 사용한다({{{else if}}} 아님). 오랜 기간 트랜잭션이 없었을 때 한 번의 호출로 연쇄 전이가 가능해야 하기 때문이다. | |
| 93 | }}} | |
| 94 | ||
| 95 | === 이벤트 === | |
| r2 | 96 | {{{ |
| r1 (새 문서) | 97 | event StageChanged(Stage indexed from, Stage indexed to, address indexed triggeredBy); |
| 98 | }}} | |
| 99 | ||
| 100 | 모든 상태 전이에서 이벤트를 발행하여 오프체인 추적을 가능하게 한다. | |
| 101 | ||
| 102 | == 구현 패턴 == | |
| 103 | ||
| 104 | === 선형 전이 === | |
| 105 | 상태가 순차적으로만 진행되는 경우. {{{nextStage()}}}로 {{{uint(currentStage) + 1}}} 계산. | |
| 106 | ||
| 107 | 경매, ICO, 투표, 타임락에 적합하다. | |
| 108 | ||
| 109 | === 비선형 전이 - 전이 테이블 === | |
| 110 | 분기·복귀가 있는 워크플로우에서 사용한다. 허용된 전이를 mapping으로 정의한다.[*1] | |
| 111 | ||
| r2 | 112 | {{{ |
| r1 (새 문서) | 113 | mapping(Stage => mapping(Stage => bool)) private allowedTransitions; |
| 114 | ||
| 115 | constructor() { | |
| 116 | allowedTransitions[Stage.AwaitingPayment][Stage.Funded] = true; | |
| 117 | allowedTransitions[Stage.Funded][Stage.Delivered] = true; | |
| 118 | allowedTransitions[Stage.Funded][Stage.Disputed] = true; | |
| 119 | allowedTransitions[Stage.Disputed][Stage.Delivered] = true; // resolve | |
| 120 | allowedTransitions[Stage.Disputed][Stage.Cancelled] = true; // cancel | |
| 121 | } | |
| 122 | }}} | |
| 123 | ||
| 124 | === 비트마스크 최적화 === | |
| 125 | 이중 mapping은 검증당 SLOAD 2회가 필요하다. 비트마스크를 사용하면 SLOAD 1회 + 비트 연산으로 줄일 수 있다.[*6 Ethereum Yellow Paper. [[https://ethereum.github.io/yellowpaper/paper.pdf|Ethereum: A Secure Decentralised Generalised Transaction Ledger]]. Appendix G. Fee Schedule.] | |
| 126 | ||
| r2 | 127 | {{{ |
| r1 (새 문서) | 128 | mapping(Stage => uint8) private transitionMap; |
| 129 | ||
| 130 | // AwaitingPayment -> Funded(bit 1) 허용 | |
| 131 | transitionMap[Stage.AwaitingPayment] = 1 << uint8(Stage.Funded); | |
| 132 | ||
| 133 | function transitionTo(Stage next) internal { | |
| 134 | require(transitionMap[currentStage] & (1 << uint8(next)) != 0); | |
| 135 | currentStage = next; | |
| 136 | } | |
| 137 | }}} | |
| 138 | ||
| 139 | ||<tablealign=center><tablewidth=80%><tablebordercolor=#a2a9b1><tablebgcolor=#f8f9fa> '''방식''' || '''가스 비용 (검증 1회)''' || '''적합한 경우''' || | |
| 140 | || 이중 mapping || ~4,200 gas (SLOAD × 2) || 가독성 우선 || | |
| 141 | || 비트마스크 || ~2,100 gas (SLOAD × 1) || 호출 빈번, 가스 최적화 필요 || | |
| 142 | || 선형 nextStage() || ~200 gas (연산만) || 순차 전이만 있을 때 || | |
| 143 | ||
| 144 | == 보안 고려사항 == | |
| 145 | ||
| 146 | === Reentrancy (재진입 공격) === | |
| 147 | 상태 전이 함수가 외부 컨트랙트에 ETH를 송금하거나 외부 함수를 호출할 때, 호출받은 컨트랙트가 원래 함수를 다시 호출(재진입)할 수 있다. 이때 상태가 아직 변경되지 않았다면, {{{atStage}}} modifier를 통과하여 동일한 함수가 반복 실행된다.[*3] | |
| 148 | ||
| 149 | 대응은 Checks-Effects-Interactions(CEI) 패턴이다. 상태 변경(Effect)을 외부 호출(Interaction)보다 먼저 수행하면, 재진입 시 이미 변경된 상태에 의해 {{{atStage}}}에서 revert된다.[*7 ConsenSys Diligence. [[https://consensysdiligence.github.io/smart-contract-best-practices/attacks/reentrancy/|Reentrancy]]. ''Smart Contract Best Practices''.] | |
| 150 | ||
| 151 | === block.timestamp 조작 === | |
| 152 | 시간 기반 전이({{{timedTransition}}})는 {{{block.timestamp}}}에 의존한다. 블록 제안자는 이 값을 실제 시간 대비 약 15초 범위 내에서 조작할 수 있다.[*8 ConsenSys Diligence. [[https://consensysdiligence.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/timestamp-dependence/|Timestamp Dependence]]. ''Smart Contract Best Practices''.] 따라서 각 단계의 최소 길이를 수 분 이상으로 설정해야 한다. | |
| 153 | ||
| 154 | === Front-running === | |
| 155 | 상태 전이를 발생시키는 트랜잭션이 mempool에 공개되면, 공격자가 이를 관찰하고 더 높은 가스비로 자신의 트랜잭션을 먼저 포함시킬 수 있다.[*9 Daian, Philip et al. (2020). [[https://www.computer.org/csdl/proceedings-article/sp/2020/349700b910/1j2LgsZYg2A|Flash Boys 2.0]]. ''2020 IEEE Symposium on Security and Privacy''. pp. 910–927.] 대응으로는 commit-reveal 스킴이나 전이 전 최소 지연 시간을 두는 방법이 있다. | |
| 156 | ||
| 157 | == 활용 사례 == | |
| 158 | ||<tablealign=center><tablewidth=80%><tablebordercolor=#a2a9b1><tablebgcolor=#f8f9fa> '''분야''' || '''예시''' || '''상태 흐름''' || | |
| 159 | || DeFi 거버넌스 || Aave, Compound Governor || Pending → Active → Succeeded/Defeated → Queued → Executed || | |
| 160 | || NFT 민팅 || ERC-721 드롭 || Inactive → Whitelist → Public → SoldOut → Revealed || | |
| 161 | || 에스크로 || 결제 보호 || AwaitingPayment → Funded → Delivered → Complete || | |
| 162 | || 공급망 || 제품 추적 || Manufactured → Shipped → InTransit → Delivered → Verified || | |
| 163 | || 스테이킹 || PoS 프로토콜 || Unlocked → Staked → Cooldown → Withdrawable || | |
| 164 | ||
| 165 | == 관련 패턴 == | |
| 166 | * [[Guard Check 패턴]] - {{{require}}}/{{{modifier}}}를 사용한 사전 조건 검증. State Machine의 {{{atStage}}}가 이에 해당한다. | |
| 167 | * [[Access Restriction 패턴]] - 역할 기반 접근 제어. State Machine과 결합하여 "누가 + 언제" 조건을 완성한다. | |
| 168 | * [[Checks-Effects-Interactions 패턴]] - 상태 변경 후 외부 호출. State Machine의 전이 보안에 필수적이다. | |
| 169 | * [[Pull over Push 패턴]] - {{{withdraw()}}} 패턴. 상태 전이와 자금 인출을 분리하여 안전성을 높인다. | |
| 170 | ||
| r2 | 171 | |
| r1 (새 문서) | 172 | == 관련 문서 == |
| 173 | * [[블록체인]] | |
| 174 | * [[스마트 컨트랙트]] | |
| 175 | * [[Solidity]] | |
| 176 | * [[이더리움]] | |
| 177 | * [[유한 상태 기계]] | |
| 178 | * [[디자인 패턴]] |