SettleLab
전체 코스
LESSON 02Stablecoin Systems

발행, 소각, 준비자산, 상환

핵심40분근거 4

학습 결과

  • 준비자산 기반 스테이블코인의 생명주기를 설명한다.
  • 상환 지연과 준비자산 불일치가 제품 리스크로 전이되는 과정을 파악한다.
  • mint/burn 권한과 reserve accounting을 서로 다른 검증 대상으로 분리한다.

선행 조건

  • 스테이블코인 시스템 맵

완료 기준

  • 발행과 상환의 원장 차이를 설명한다.
  • 준비자산 출처와 갱신 주기를 남겼다.
  • 상환 장애 runbook 초안을 만들었다.

발행, 소각, 준비자산, 상환

도입

mintburn은 컨트랙트 함수 이름이지만, 스테이블코인 제품에서는 그 앞뒤에 더 긴 업무 흐름이 있다. 발행은 고객의 fiat 입금, issuer의 reserve 증가, 내부 승인, onchain mint, 사용자 지갑 입금이 연결된 과정이다. 상환은 그 반대처럼 보이지만 실제로는 burn 확정, 내부 원장 차감, treasury 승인, fiat payout, 사용자 알림이 따로 움직인다.

이 강의에서 가장 중요한 구분은 "컨트랙트 권한이 있다"와 "준비자산이 충분하다"가 같은 말이 아니라는 점이다. Minter 권한은 토큰을 발행할 수 있게 해주지만 reserve를 증명하지 않는다. Burn event는 상환 절차의 한 단계지만 fiat payout이 완료됐다는 뜻은 아니다.

따라서 개발자는 발행/상환을 상태머신으로 다뤄야 한다. 어떤 상태가 onchain event로 증명되는지, 어떤 상태가 issuer 내부 원장이나 은행 rail에 의존하는지, 어디서 사용자가 기다려야 하는지, 어느 지점에서 운영자가 수동 개입해야 하는지를 나눠야 한다.

학습 목표

  • 준비자산 기반 스테이블코인의 생명주기를 설명한다.
  • 상환 지연과 준비자산 불일치가 제품 리스크로 전이되는 과정을 파악한다.
  • mint/burn 권한과 reserve accounting을 서로 다른 검증 대상으로 분리한다.

개념 설명

상태머신가로 스크롤 · 크게 보기 지원
발행, 소각, 준비자산, 상환 상태머신이 시각화는 stablecoin checkout과 정산 시스템에서 상태 전이가 어떤 증거와 실패 조건으로 움직이는지를 보여주며, '발행, 소각, 준비자산, 상환'에서 남겨야 할 설계 증거를 좁힌다.
State 1

MintRequested

준비자산 기반 스테이블코인의 생명주기를 설명한다.

fiat 입금, issuer 승인, onchain mint 요청을 별도 상태로 나눈다.
State 2

ReserveMatched

발행/상환과 결제 상태가 분리되는가

bank balance, reserve report, token supply가 같은 기준일로 대조된다.
State 3

PayoutDelayed

이벤트와 내부 원장이 대조되는가

burn 완료와 fiat payout 대기를 분리해 사용자 상태와 운영 queue에 남긴다.
State 4

ReconciliationClosed

발행/상환 원장 차이 설명하기

발행과 상환의 원장 차이를 설명한다.
크게 보기
State 1

MintRequested

준비자산 기반 스테이블코인의 생명주기를 설명한다.

fiat 입금, issuer 승인, onchain mint 요청을 별도 상태로 나눈다.
State 2

ReserveMatched

발행/상환과 결제 상태가 분리되는가

bank balance, reserve report, token supply가 같은 기준일로 대조된다.
State 3

PayoutDelayed

이벤트와 내부 원장이 대조되는가

burn 완료와 fiat payout 대기를 분리해 사용자 상태와 운영 queue에 남긴다.
State 4

ReconciliationClosed

발행/상환 원장 차이 설명하기

발행과 상환의 원장 차이를 설명한다.

1. 발행과 상환은 서로 다른 원장을 지난다

발행과 상환은 단순히 mint와 burn을 반대로 놓은 흐름이 아니다. 발행은 fiat 또는 treasury asset이 들어온 뒤 token liability가 늘어나는 과정이고, 상환은 token liability를 줄인 뒤 fiat payout이 나가는 과정이다. 둘 다 onchain state, issuer ledger, bank/treasury rail, customer support 상태를 함께 본다.

흐름도가로 스크롤 · 크게 보기 지원
강의 흐름도상태, 책임, 검증 지점을 순서대로 읽기 위한 다이어그램이다.
크게 보기

위 흐름에서 onchain mintonchain burn만 확정성이 높다. Fiat 입금 확인, reserve ledger 증가, payout 실행은 offchain 시스템에 의존한다. 그러므로 checkout 제품이나 treasury dashboard는 onchain event만 보지 말고 issuer 상태와 내부 원장 상태를 함께 저장해야 한다.

2. 준비자산은 onchain invariant가 아니다

Circle 공식 문서는 USDC가 highly liquid cash와 cash-equivalent assets로 backing되고 1:1 redeem 가능하다고 설명하며, Transparency 페이지에서 reserve 관련 자료와 monthly attestation reports를 공개한다. 이 내용은 2026-05-14 기준으로 확인했다. 여기서 중요한 점은 reserve disclosure가 컨트랙트 내부 상태가 아니라 issuer 운영 및 외부 보고 레이어라는 것이다.

표 자료가로 스크롤 · 크게 보기 지원
항목왜 중요한가
현금즉시 상환 liquidity
단기 국채 또는 정부 MMF안정성과 수익을 제공하지만 settlement timing이 있다
repo/MMFliquidity와 counterparty risk가 있다
은행 예금bank concentration risk
attestationreserve가 circulation 이상인지 확인하는 외부 근거
circulation / supplyonchain totalSupply와 issuer reporting을 비교할 기준
크게 보기
항목왜 중요한가
현금즉시 상환 liquidity
단기 국채 또는 정부 MMF안정성과 수익을 제공하지만 settlement timing이 있다
repo/MMFliquidity와 counterparty risk가 있다
은행 예금bank concentration risk
attestationreserve가 circulation 이상인지 확인하는 외부 근거
circulation / supplyonchain totalSupply와 issuer reporting을 비교할 기준

개발자가 할 일은 reserve를 직접 증명하는 척하는 것이 아니다. 출처, 보고 주기, 적용 범위, 마지막 확인일을 제품 정책과 dashboard에 남기는 것이다. Reserve 정보가 늦게 갱신되거나 issuer가 redemption 상태를 바꾸면 checkout, withdrawal, treasury movement에 어떤 제한을 걸지 정해야 한다.

3. 발행자 권한은 필요한 동시에 위험하다

표 자료가로 스크롤 · 크게 보기 지원
권한필요성위험
Minter신규 발행reserve 없는 mint
Burner상환/회수부당 소각
Freezer제재/사고 대응사용자 자금 동결
Pauser긴급 중지전체 결제 마비
Upgrader버그 수정구현체 악성 변경
Adminrole 관리권한 탈취 또는 분실
크게 보기
권한필요성위험
Minter신규 발행reserve 없는 mint
Burner상환/회수부당 소각
Freezer제재/사고 대응사용자 자금 동결
Pauser긴급 중지전체 결제 마비
Upgrader버그 수정구현체 악성 변경
Adminrole 관리권한 탈취 또는 분실

권한 표의 목적은 "권한이 많으니 나쁘다"가 아니다. 발행자형 스테이블코인은 운영상 mint, burn, freeze, pause가 필요할 수 있다. 문제는 각 권한의 책임과 감시가 분리되지 않을 때 생긴다. Minter와 Burner가 같아도 되는지, Freezer가 payment product의 pending 상태에 어떤 영향을 주는지, Upgrader가 사용자 잔고 규칙을 바꿀 수 있는지, Admin을 잃었을 때 복구 경로가 있는지 확인해야 한다.

4. 컨트랙트 설계와 운영 설계를 함께 둔다

  • mintburn 권한은 분리한다.
  • role 변경은 event로 남기고 dashboard에서 감시한다.
  • 마지막 admin을 제거할 수 없게 한다.
  • pause와 freeze를 구분한다. pause는 전체 시스템, freeze는 주소 단위다.
  • reserve accounting은 onchain totalSupply와 같지 않다. reserve는 offchain asset이므로 별도 ledger와 reporting이 필요하다.
  • redemption queue, payout 지연, manual review 상태를 내부 원장에 둔다.
  • burn이 성공했지만 fiat payout이 실패한 경우를 별도 상태로 표현한다.

5. 상환 리스크는 사용자 UX로 전이된다

상환은 사용자가 token을 fiat로 바꾸는 과정이다. 여기서 깨질 수 있는 지점은 많고, 각각 사용자에게 보여줘야 하는 상태가 다르다.

표 자료가로 스크롤 · 크게 보기 지원
실패 지점사용자에게 보이는 상태운영자가 확인할 것
issuer redemption 지연상환 요청 접수, 지급 대기issuer status, treasury liquidity, cut-off time
bank rail 장애fiat 지급 지연bank/API status, payout batch, retry 가능성
sanction/KYT reviewmanual reviewuser risk flag, compliance decision, support note
chain congestionburn pendingtx hash, block confirmation, replacement policy
burn 성공 후 ledger 불일치reconciliation holdburn event, internal ledger row, idempotency key
reserve disclosure 이상withdrawal 제한 또는 route disablelatest attestation, circulation, risk policy
크게 보기
실패 지점사용자에게 보이는 상태운영자가 확인할 것
issuer redemption 지연상환 요청 접수, 지급 대기issuer status, treasury liquidity, cut-off time
bank rail 장애fiat 지급 지연bank/API status, payout batch, retry 가능성
sanction/KYT reviewmanual reviewuser risk flag, compliance decision, support note
chain congestionburn pendingtx hash, block confirmation, replacement policy
burn 성공 후 ledger 불일치reconciliation holdburn event, internal ledger row, idempotency key
reserve disclosure 이상withdrawal 제한 또는 route disablelatest attestation, circulation, risk policy

6. 테스트와 runbook으로 바꾼다

테스트는 컨트랙트 단위와 운영 상태 단위로 나눠야 한다.

표 자료가로 스크롤 · 크게 보기 지원
검증 범위테스트 또는 확인 항목
Access controlminter가 아닌 주소의 mint 실패, burner가 아닌 주소의 burn 실패
Supply accountingmint/burn 후 totalSupply와 balance 변화 일치
Pause/freezepaused 상태에서 mint/burn/transfer 정책이 의도대로 동작
Admin safetyrole 이전 후에도 최소 admin 1명 유지
Ledger reconciliationburn event와 internal redemption row가 한 번만 매칭됨
Runbookburn 성공/payout 지연, payout 성공/ledger 실패, reserve alert 발생 시 처리 순서
크게 보기
검증 범위테스트 또는 확인 항목
Access controlminter가 아닌 주소의 mint 실패, burner가 아닌 주소의 burn 실패
Supply accountingmint/burn 후 totalSupply와 balance 변화 일치
Pause/freezepaused 상태에서 mint/burn/transfer 정책이 의도대로 동작
Admin safetyrole 이전 후에도 최소 admin 1명 유지
Ledger reconciliationburn event와 internal redemption row가 한 번만 매칭됨
Runbookburn 성공/payout 지연, payout 성공/ledger 실패, reserve alert 발생 시 처리 순서

코드로 확인하기

위 설계 결정을 코드에서 확인한다. 상태, 서명, 원장 이동, 실패 처리가 어디에 놓이는지 따라 읽으면 앞의 모델이 실제 구현 경계로 내려온다.

컨트랙트mint / burn 함수와 supply invariant

mint와 burn은 단순한 supply 변경이 아니라 issuer의 운영 결정이 컨트랙트 호출로 내려온 결과다. role + pause + event를 한꺼번에 묶는다.

CODE SURFACEsolidity
contract IssuableStablecoin is AccessControl, Pausable {    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");    event Mint(address indexed to, uint256 amount, bytes32 issuerRefId);    event Burn(address indexed from, uint256 amount, bytes32 redemptionRefId);    /// @param issuerRefId issuer 내부 ledger 의 입금/승인 식별자 — onchain ↔ offchain 매칭 키    function mint(address to, uint256 amount, bytes32 issuerRefId)        external        onlyRole(MINTER_ROLE)        whenNotPaused    {        if (to == address(0)) revert();        if (amount == 0) revert();        _mint(to, amount);        emit Mint(to, amount, issuerRefId);    }    /// @param redemptionRefId support ticket 또는 redemption batch ID    function burn(address from, uint256 amount, bytes32 redemptionRefId)        external        onlyRole(BURNER_ROLE)    {        // burn 은 pause 중에도 허용 — redemption 흐름이 사용자 보호 차원에서 열려 있어야 한다        _burn(from, amount);        emit Burn(from, amount, redemptionRefId);    }}

인덱서burn event ↔ redemption ledger 매칭 (TypeScript)

burn event 한 줄로 redemption 처리가 끝났다고 보면 안 된다. fiat payout 까지 완료된 후에만 사용자에게 "완료" 로 안내한다.

CODE SURFACEtypescript
type RedemptionStatus =  | "requested"  | "compliance_review"  | "burn_submitted"  | "burn_confirmed"  | "payout_pending"  | "payout_succeeded"  | "payout_failed";async function reconcileBurnEvent(event: { from: `0x${string}`; amount: bigint; redemptionRefId: `0x${string}` }) {  const row = await prisma.redemption.findUnique({ where: { refId: event.redemptionRefId } });  if (!row) {    await alerts.fire({ severity: "critical", reason: "burn without redemption row", event });    return;  }  if (row.amount !== event.amount) {    await alerts.fire({ severity: "critical", reason: "burn amount mismatch", row, event });    return;  }  await prisma.redemption.update({    where: { id: row.id },    data: { status: "burn_confirmed", burnTxHash: event.transactionHash }  });  // 이후 fiat payout job 이 별도로 payout_pending → payout_succeeded 로 옮긴다}

백엔드Reserve attestation 외부 호출 + cache + freshness 가드

Circle Transparency 같은 외부 attestation 은 onchain invariant 가 아니다. cache + freshness 체크로 다뤄야 stale 표시를 사용자에게 정확히 줄 수 있다.

CODE SURFACEtypescript
type ReserveAttestation = {  issuer: string;  reserveUsd: number;  circulatingUsd: number;  attestedAt: string; // ISO  reportUrl: string;};const FRESHNESS_HOURS = 30 * 24; // 30 daysexport async function getReserveAttestation(issuer: "circle" | "paypal" | "tether"): Promise<{  data: ReserveAttestation;  freshness: "fresh" | "stale" | "missing";}> {  const cached = await redis.get(`reserve:${issuer}`);  let data: ReserveAttestation;  if (cached) {    data = JSON.parse(cached);  } else {    data = await fetchAttestation(issuer); // 발행자별 endpoint    await redis.set(`reserve:${issuer}`, JSON.stringify(data), "EX", 3600);  }  const ageHours = (Date.now() - Date.parse(data.attestedAt)) / 3_600_000;  const freshness = ageHours > FRESHNESS_HOURS ? "stale" : "fresh";  return { data, freshness };}

강의 포인트

표 자료가로 스크롤 · 크게 보기 지원
관점강의 중 확인할 질문학습 후 남길 증거
발행 흐름fiat 입금과 onchain mint 사이에 어떤 승인 단계가 있는가?issuer ledger, reserve ledger, mint event의 차이
상환 흐름burn 성공과 fiat payout 완료가 왜 다른가?burn, payout, support 상태를 분리한 상태머신
준비자산reserve 정보는 어디서 오고 얼마나 최신인가?출처 URL, reporting 주기, 마지막 확인일
권한mint/burn/freeze/pause/admin은 누가 갖고 어떻게 감시하는가?역할 매트릭스와 admin event 모니터링 항목
장애 대응상환이 지연되면 무엇을 먼저 제한하는가?withdrawal 상태 메시지와 운영 runbook 초안
크게 보기
관점강의 중 확인할 질문학습 후 남길 증거
발행 흐름fiat 입금과 onchain mint 사이에 어떤 승인 단계가 있는가?issuer ledger, reserve ledger, mint event의 차이
상환 흐름burn 성공과 fiat payout 완료가 왜 다른가?burn, payout, support 상태를 분리한 상태머신
준비자산reserve 정보는 어디서 오고 얼마나 최신인가?출처 URL, reporting 주기, 마지막 확인일
권한mint/burn/freeze/pause/admin은 누가 갖고 어떻게 감시하는가?역할 매트릭스와 admin event 모니터링 항목
장애 대응상환이 지연되면 무엇을 먼저 제한하는가?withdrawal 상태 메시지와 운영 runbook 초안

실무 예시

백엔드[INDEXER] [OPS] 사용자가 10,000 USDC를 fiat로 상환하려고 한다. 사용자는 지갑에서 burn 트랜잭션을 보냈고, explorer에서는 성공으로 보인다. 그런데 fiat payout이 은행 rail 문제로 지연된다. 이때 제품이 "상환 완료"라고 표시하면 안 된다.

표 자료가로 스크롤 · 크게 보기 지원
상태의미사용자 표시운영 처리
RedeemRequested사용자가 상환을 요청했다상환 요청 접수KYC/KYT와 계좌 확인
BurnSubmittedburn tx가 제출됐다토큰 소각 처리 중tx hash 추적
BurnConfirmedonchain burn이 확정됐다토큰 소각 완료, fiat 지급 대기ledger liability 감소 확인
PayoutQueuedfiat 지급 batch에 들어갔다은행 지급 대기payout provider 상태 확인
PayoutFailedfiat 지급이 실패했다지급 지연, support 확인 중retry 또는 manual payout
RedeemSettledfiat가 지급됐다상환 완료reconciliation 완료
크게 보기
상태의미사용자 표시운영 처리
RedeemRequested사용자가 상환을 요청했다상환 요청 접수KYC/KYT와 계좌 확인
BurnSubmittedburn tx가 제출됐다토큰 소각 처리 중tx hash 추적
BurnConfirmedonchain burn이 확정됐다토큰 소각 완료, fiat 지급 대기ledger liability 감소 확인
PayoutQueuedfiat 지급 batch에 들어갔다은행 지급 대기payout provider 상태 확인
PayoutFailedfiat 지급이 실패했다지급 지연, support 확인 중retry 또는 manual payout
RedeemSettledfiat가 지급됐다상환 완료reconciliation 완료

이 상태표는 결제 상태머신 레슨과 바로 연결된다. 사용자 화면, 내부 원장, onchain event, support 상태가 모두 같은 단어를 쓰면 장애 때 원인을 찾기 어렵다. 각 상태가 어떤 시스템의 증거로 바뀌는지 분리해야 한다.

흔한 오해와 실패 시나리오

표 자료가로 스크롤 · 크게 보기 지원
오해실패 시나리오바로잡는 방법
mint 권한이 있으면 reserve도 보장된다고 생각한다reserve 없는 mint 또는 reporting 지연을 놓친다권한 검증과 reserve disclosure 검증을 분리한다
burn이 성공하면 상환 완료라고 표시한다fiat payout 실패 상태를 사용자가 알 수 없다burn confirmed와 payout settled를 다른 상태로 둔다
pause와 freeze를 같은 기능으로 본다전체 결제 중지와 특정 주소 제한 정책이 뒤섞인다system-wide pause와 address-level freeze를 분리한다
monthly attestation을 실시간 reserve feed처럼 쓴다현재 redemption liquidity 판단을 과거 보고서에만 의존한다reporting 주기, 접근일, 운영 정책을 함께 표시한다
admin role만 보호하면 충분하다고 본다minter, burner, freezer, upgrader 경로가 각각 다른 장애를 만든다역할별 권한과 이벤트 모니터링을 따로 둔다
크게 보기
오해실패 시나리오바로잡는 방법
mint 권한이 있으면 reserve도 보장된다고 생각한다reserve 없는 mint 또는 reporting 지연을 놓친다권한 검증과 reserve disclosure 검증을 분리한다
burn이 성공하면 상환 완료라고 표시한다fiat payout 실패 상태를 사용자가 알 수 없다burn confirmed와 payout settled를 다른 상태로 둔다
pause와 freeze를 같은 기능으로 본다전체 결제 중지와 특정 주소 제한 정책이 뒤섞인다system-wide pause와 address-level freeze를 분리한다
monthly attestation을 실시간 reserve feed처럼 쓴다현재 redemption liquidity 판단을 과거 보고서에만 의존한다reporting 주기, 접근일, 운영 정책을 함께 표시한다
admin role만 보호하면 충분하다고 본다minter, burner, freezer, upgrader 경로가 각각 다른 장애를 만든다역할별 권한과 이벤트 모니터링을 따로 둔다

실습 과제

  1. 백엔드[INDEXER] 발행/상환 원장 차이 설명하기: fiat 입금, reserve 증가, mint 승인, onchain mint, burn, reserve 감소, fiat payout이 각각 어느 원장과 이벤트에 남는지 표로 정리한다.
  2. 운영상환 장애 runbook 작성하기: burn은 성공했지만 fiat payout이 지연되는 상황을 가정하고 사용자 표시 상태, 운영자 확인 항목, dashboard alert, 수동 복구 기준을 작성한다.
  3. 컨트랙트권한과 reserve 검증 분리하기: Minter, Burner, Freezer, Pauser, Upgrader, Admin 권한을 표로 정리하고, 각 권한이 reserve accounting을 직접 증명하지 못하는 이유를 설명한다.

완료 기준

  1. 발행과 상환의 원장 차이를 설명한다.
  2. 준비자산 출처와 갱신 주기를 남겼다.
  3. 상환 장애 runbook 초안을 만들었다.
  4. mint, burn, reserve, fiat payout 상태를 분리한 상태머신을 작성했다.

근거 자료

Final checkpoint

읽기를 마쳤다면 여기서 기록한다

아래 버튼은 읽기 진도를 저장한다. 체크리스트, 과제, 랩 산출물은 위 Workbook에서 따로 관리한다.

  • 발행과 상환의 원장 차이를 설명한다.
  • 준비자산 출처와 갱신 주기를 남겼다.
  • 상환 장애 runbook 초안을 만들었다.

학습 자료 근거

발행 소각 준비자산 상환
이 LMS 레슨의 개념, 예시, 과제 구성을 잡는 데 사용한 근거 문서.
내부 참고 문서
Circle Transparency
https://www.circle.com/transparency
Circle: What is USDC?
https://developers.circle.com/stablecoins/what-is-usdc
OpenZeppelin Contracts: Access Control
https://docs.openzeppelin.com/contracts/5.x/access-control