스테이블코인 보안리뷰 체크리스트
도입
보안 리뷰 체크리스트는 코드 리뷰 전에 복사해서 쓰는 목록이지만, 그대로 체크만 하면 출시 판단에 약하다. 실무에서는 각 항목에 evidence, owner, status, blocker 여부를 붙여야 한다. 리뷰가 끝났을 때 남아야 하는 것은 "봤다"가 아니라 "어떤 근거로 출시 가능하다고 판단했는가"이다.
이 강의는 앞선 Solidity 기초, AccessControl, invariant, Slither/Echidna 결과를 하나의 review package로 묶는다. 특히 permit checkout처럼 서명과 token transfer가 섞이는 시스템은 권한, 상태, 서명, 외부 호출, 테스트, 운영을 모두 통과해야 한다.
학습 목표
- 코드 리뷰 체크리스트를 출시 차단 기준으로 바꾼다.
- 토큰/결제/운영 영역별 리뷰 증거를 남긴다.
- 잔여 위험을 owner, 만료일, 승인 조건이 있는 문서로 관리한다.
개념 설명
스테이블코인 보안리뷰 체크리스트를 제품 설계에 넣어도 되는가?
권한 경계가 테스트되는가
불변조건이 실패 상태를 잡는가
운영 변경이 모니터링되는가
1. Review package 형식
| 필드 | 의미 |
|---|---|
| Area | 권한, 상태, 서명, 외부 호출, 테스트, 운영 |
| Check | 확인해야 할 항목 |
| Evidence | 코드 링크, 테스트 결과, 도구 리포트, runbook, screenshot |
| Owner | 누가 해결하거나 승인할 것인가 |
| Status | Green, Yellow, Red |
| Blocker | Red일 때 출시 차단인지 |
| Notes | 남은 위험과 후속 작업 |
체크리스트 항목은 release gate와 연결된다. High severity 또는 P0/P1 항목은 "추후 개선"으로 넘기지 않는다.
2. 권한 리뷰
| Check | Evidence | Blocker |
|---|---|---|
| 모든 privileged function에 modifier 또는 explicit check가 있다. | function list와 modifier map | Yes |
| role admin이 누구인지 문서화되어 있다. | role matrix | Yes |
| 마지막 admin 제거가 불가능하다. | unit/invariant test | Yes |
| role grant/revoke event가 있다. | event test, dashboard alert | Yes |
| production admin은 EOA가 아니라 multisig/timelock이다. | Safe/timelock config | Yes |
권한 리뷰는 코드만 보지 않는다. Production owner가 누구인지, signer rotation은 가능한지, key compromise 때 어떤 alert가 뜨는지까지 포함한다.
3. 상태 리뷰
| Check | Evidence | Blocker |
|---|---|---|
| totalSupply와 balances 변화가 일관된다. | invariant test, mint/burn tests | Yes |
| payment state transition이 단방향이다. | state diagram, transition tests | Yes |
| refund/claim이 idempotent하다. | duplicate refund/claim tests | Yes |
| deadline/validBefore 만료 처리가 있다. | boundary tests | Yes |
| pause/freeze 상태가 모든 관련 함수에 적용된다. | bypass tests | Yes |
상태 리뷰의 핵심은 "성공 경로"가 아니라 "한 번 처리된 일이 다시 처리되지 않는가"이다. 결제에서는 idempotency가 보안이다.
4. 서명 리뷰
| Check | Evidence | Blocker |
|---|---|---|
| EIP-712 domain에 chain ID와 verifying contract가 있다. | domain test, typed data fixture | Yes |
| nonce가 재사용 불가능하다. | replay test | Yes |
| signature signer가 zero address가 아니다. | invalid signature test | Yes |
| signature malleability를 고려한다. | low-s or library usage evidence | Yes |
| typed data에 사용자가 이해할 field가 들어간다. | UI screenshot, copy review | Yellow |
서명 리뷰는 보안과 UX가 만나는 영역이다. 사용자가 어떤 spender, recipient, amount, token, chain에 서명하는지 볼 수 없으면 phishing과 운영 사고 가능성이 커진다.
5. 외부 호출 리뷰
| Check | Evidence | Blocker |
|---|---|---|
| token transfer return value를 안전하게 처리한다. | SafeERC20 또는 equivalent test | Yes |
| external call 전후 state update 순서가 의도적이다. | CEI review note | Yes |
| reentrancy 가능성을 검토했다. | Slither finding triage, reentrancy test | Yes |
| callback이 있는 token 표준을 받지 않는다면 allowlist로 제한한다. | token allowlist, route policy | Yes |
| oracle/price/route output을 신뢰할 때 freshness와 slippage를 본다. | dashboard threshold, route simulation | Yellow 또는 Yes |
외부 호출 리뷰는 "호출이 성공하는가"보다 "실패했을 때 어떤 상태가 남는가"를 묻는다. Refund 실패가 payment success를 덮어쓰면 안 된다.
6. 테스트 리뷰
| Check | Evidence | Blocker |
|---|---|---|
| 권한 실패 테스트 | unit tests | Yes |
| freeze/pause 우회 테스트 | bypass tests | Yes |
| nonce replay 테스트 | signature replay suite | Yes |
| fuzz 테스트 | fuzz report | Yellow |
| invariant 테스트 | invariant campaign result | Yes |
| event 검증 | emitted event tests | Yellow |
| fork/integration 테스트 필요성 검토 | dependency list | Yellow |
테스트는 통과 여부뿐 아니라 커버하는 위험을 보여줘야 한다. "invariant 테스트 있음" 대신 어떤 invariant를 막는지 적는다.
7. 운영 리뷰
| Check | Evidence | Blocker |
|---|---|---|
| key rotation 절차가 있다. | ops runbook | Yellow |
| pause/freeze 실행 권한과 승인 절차가 있다. | emergency runbook | Yes |
| monitoring alert가 있다. | dashboard screenshot, alert test | Yes |
| incident response runbook이 있다. | incident doc | Yes |
| upgrade plan과 rollback plan이 있다. | upgrade proposal template | Yes |
운영 리뷰는 감사 리포트 이후에도 남는다. 배포 후 role change, pause, freeze, upgrade event를 보지 못하면 리뷰 당시 안전했던 설계도 운영 중 무너질 수 있다.
8. 출시 차단 기준
| Red condition | 출시 판정 |
|---|---|
| 권한 없는 mint/burn/freeze/pause 가능 | No-Go |
| replay 가능한 서명 또는 nonce 재사용 | No-Go |
| refund/settlement 중복 처리 가능 | No-Go |
| paused/frozen 우회 경로 존재 | No-Go |
| failing invariant 또는 untriaged high Slither finding | No-Go |
| production admin이 단일 EOA이고 보완 통제가 없음 | No-Go |
| monitoring/runbook 없이 upgrade 가능 | No-Go |
Yellow 항목은 제한 출시 조건과 owner가 있어야 한다. 예를 들어 typed data copy가 부족하면 beta 한도와 개선 기한을 둔다. 하지만 돈의 상태를 직접 깨뜨리는 항목은 Yellow가 아니라 Red다.
9. 잔여 위험 승인 양식
| Field | 작성 내용 |
|---|---|
| Risk statement | 무엇이 남아 있는가 |
| Impact | user funds, merchant settlement, compliance, availability 영향 |
| Evidence | 왜 지금 release blocker가 아닌가 |
| Compensating control | 한도, monitoring, manual review, timelock |
| Owner | 위험을 추적할 사람 |
| Expiry | 재검토 날짜 |
| Approver | product/security/engineering 승인자 |
Accepted risk는 영구 면제가 아니다. 만료일이 지나면 다시 Red 또는 Yellow로 돌아온다.
코드로 확인하기
앞의 보안 기준을 코드와 테스트로 확인한다. 함수가 어떤 전제를 세우고, 테스트가 어떤 실패 조건을 고정하는지 함께 읽는다.
운영리뷰 패킷 YAML 템플릿
각 강의를 통해 모은 증거를 한 곳에 모아 release gate 가 읽을 수 있는 형태로 남긴다. PR 머지 조건으로 이 파일의 모든 status가
green또는accepted여야 한다.
# security-review/v1.2.0.yamlrelease: v1.2.0reviewers: - role: security-lead name: alice - role: protocol-lead name: bobsections: permissions: status: green evidence: - "test: RoleMatrix.t.sol → PASS" - "audit: external-audit-2026-04.pdf §3.2" signatures: status: green evidence: - "test: PermitDomain.t.sol → PASS" - "test: AuthorizationReplay.t.sol → PASS" external_calls: status: yellow evidence: - "transferFrom 실패 상태 처리 partial — issue #482" compensating_controls: - "merchant beta whitelist 적용" - "결제 한도 1k USDC" owner: payments-team expires: 2026-07-01 invariants: status: green evidence: - "echidna: 20000 runs, 0 failing" - "foundry invariant: nightly 200 depth 통과"release_gate: blocker: - "any section status == red" - "expired residual risk without re-approval"운영CI에서 release gate 검사 — GitHub Actions
security-review yaml을 PR 머지 조건으로 강제한다. yellow는 owner와 expiry가 있어야 통과, red는 차단.
name: release-gateon: pull_request: paths: [security-review/**, src/**]jobs: gate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Validate review packet run: | npx tsx scripts/check-review-packet.ts security-review/v1.2.0.yaml// scripts/check-review-packet.tsimport { readFileSync } from "node:fs";import { parse } from "yaml";const packet = parse(readFileSync(process.argv[2], "utf8"));const errors: string[] = [];for (const [name, section] of Object.entries(packet.sections) as [string, any][]) { if (section.status === "red") { errors.push(`${name}: status=red — release blocked`); } if (section.status === "yellow") { if (!section.owner) errors.push(`${name}: yellow without owner`); if (!section.expires) errors.push(`${name}: yellow without expiry`); else if (new Date(section.expires) < new Date()) { errors.push(`${name}: yellow expired ${section.expires}`); } }}if (errors.length > 0) { console.error(errors.join("\n")); process.exit(1);}강의 포인트
| 관점 | 강의 중 확인할 질문 | 학습 후 남길 증거 |
|---|---|---|
| Review scope | 권한, 상태, 서명, 외부 호출, 테스트, 운영을 모두 봤는가? | review package |
| Evidence | 각 항목의 근거가 링크와 테스트로 남았는가? | evidence column |
| Release gate | 어떤 항목이 No-Go인지 명확한가? | blocker table |
| Residual risk | 남은 위험에 owner와 expiry가 있는가? | 승인 양식 |
실무 예시
운영[CONTRACT] 상황: permit checkout contract를 리뷰한다. 사용자는 EIP-712 permit에 서명하고, relayer가 permit + transferFrom을 실행해 merchant에게 결제한다.
| Area | Check | Evidence | Status |
|---|---|---|---|
| 서명 | domain에 chain ID와 verifying contract 포함 | typed data fixture test | Green |
| 서명 | nonce replay 방지 | replay test | Green |
| 권한 | relayer가 amount/recipient를 바꿀 수 없음 | calldata binding test | Green |
| 외부 호출 | permit 성공 후 transferFrom 실패 상태 처리 | state transition test | Yellow |
| 상태 | payment가 두 번 Paid 되지 않음 | idempotency test | Green |
| 운영 | relayer outage runbook | draft only | Yellow |
| 테스트 | invariant campaign 통과 | report link | Green |
이 표에서 Yellow는 출시 전 제한 조건을 요구한다. 예를 들어 relayer outage runbook이 draft라면 beta merchant만 허용하고 결제 한도를 낮춘다. transferFrom 실패 상태 처리가 없으면 사용자의 permit만 소모되고 결제가 실패할 수 있으므로 구현 전에는 Green으로 바꿀 수 없다.
흔한 오해와 실패 시나리오
| 오해 | 실제로 확인할 것 |
|---|---|
| 체크리스트 항목에 체크하면 리뷰가 끝났다고 본다. | evidence, owner, status가 있어야 한다. |
| false positive는 설명 없이 닫아도 된다고 본다. | accepted risk 양식과 expiry가 필요하다. |
| 테스트 수가 많으면 충분하다고 본다. | 어떤 위험을 막는 테스트인지 연결해야 한다. |
| 운영 문서는 보안 리뷰 범위 밖이라고 본다. | pause, freeze, upgrade, monitoring은 보안 경계다. |
실습 과제
- 운영Security review package 만들기: 권한, 상태, 서명, 외부 호출, 테스트, 운영 영역별로 check, evidence, owner, status, blocker 여부를 채운다.
- 컨트랙트Permit checkout 리뷰표 작성하기: permit checkout contract를 가정하고 domain separator, nonce, allowance, transferFrom, refund, monitoring 항목의 보안 리뷰 표를 작성한다.
- 운영잔여 위험 승인 양식 작성하기: false positive 또는 accepted risk마다 risk statement, impact, compensating control, owner, expiry, approver를 포함한 승인 양식을 만든다.
완료 기준
- 리뷰 범위를 권한, 상태, 서명, 외부 호출, 테스트, 운영 영역으로 나눴다.
- 출시 차단 항목을 정의했다.
- 잔여 위험 승인 양식을 만들었다.
- permit checkout 예시 리뷰표를 작성했다.
근거 자료
- 보안리뷰 체크리스트: 02-보안-테스트/06-보안리뷰-체크리스트.md
- Solidity Security Considerations: https://docs.soliditylang.org/en/latest/security-considerations.html
- OpenZeppelin Contracts: Access Control: https://docs.openzeppelin.com/contracts/5.x/access-control
- OpenZeppelin Upgrades: https://docs.openzeppelin.com/upgrades/
- Foundry Book: Invariant Testing: https://book.getfoundry.sh/forge/invariant-testing
- Safe Smart Account Guards: https://docs.safe.global/advanced/smart-account-guards