SettleLab
전체 코스
LESSON 09Stablecoin Systems

오라클, 청산, Proof of Reserve

핵심40분근거 5

학습 결과

  • 오라클과 준비자산 증명의 목적 차이를 설명한다.
  • 청산과 보고 지연이 시스템 리스크로 전이되는 흐름을 추적한다.
  • stale data, zero value, reserve shortfall을 제품 상태와 circuit breaker로 바꾼다.

선행 조건

  • Crypto-backed 스테이블코인

완료 기준

  • 오라클과 PoR을 구분했다.
  • stale data 대응 조건을 정의했다.
  • 청산 실패 후처리를 문서화했다.

오라클, 청산, Proof of Reserve

도입

스테이블코인 시스템은 스스로 세계의 가격과 준비자산을 알지 못한다. 담보 가격, reserve 규모, tokenized fund의 NAV, wrapped asset의 backing 상태는 외부 데이터가 들어와야 판단할 수 있다. 이때 oracle과 Proof of Reserve는 단순한 보조 기능이 아니라 발행, 청산, 신규 결제 허용, dashboard 경고를 움직이는 신뢰 경계가 된다.

이 강의에서는 oracle을 "가격을 읽는 함수"로 끝내지 않는다. 어떤 값이 어디서 오고, 얼마나 최신이며, stale 또는 0이 되었을 때 어떤 product state로 전환되는지까지 설계한다. PoR도 "감사 완료"가 아니라 reserve data를 제품 정책에 연결하는 장치로 읽는다.

학습 목표

  • 오라클과 준비자산 증명의 목적 차이를 설명한다.
  • 청산과 보고 지연이 시스템 리스크로 전이되는 흐름을 추적한다.
  • stale data, zero value, reserve shortfall을 제품 상태와 circuit breaker로 바꾼다.

개념 설명

원장 흐름가로 스크롤 · 크게 보기 지원
오라클, 청산, Proof of Reserve 원장·책임 흐름이 시각화는 stablecoin checkout과 정산 시스템에서 사용자 잔액, 내부 원장, 외부 정산이 어긋날 수 있는 지점이 어디인지를 보여주며, '오라클, 청산, Proof of Reserve'에서 남겨야 할 설계 증거를 좁힌다.
FROM
사용자 지갑
TO
토큰 컨트랙트
요청/권한

오라클과 준비자산 증명의 목적 차이를 설명한다.

FROM
토큰 컨트랙트
TO
정산 원장
상태 전이

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

FROM
정산 원장
TO
운영자/학습자
검증 로그

오라클과 PoR을 구분했다.

크게 보기
FROM
사용자 지갑
TO
토큰 컨트랙트
요청/권한

오라클과 준비자산 증명의 목적 차이를 설명한다.

FROM
토큰 컨트랙트
TO
정산 원장
상태 전이

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

FROM
정산 원장
TO
운영자/학습자
검증 로그

오라클과 PoR을 구분했다.

1. Oracle, liquidation, PoR은 서로 다른 질문에 답한다

세 용어는 한 강의에 같이 나오지만 같은 기능이 아니다. 먼저 질문을 분리한다.

표 자료가로 스크롤 · 크게 보기 지원
개념답하는 질문잘못 쓰면 생기는 문제
Price oracle이 담보나 자산의 현재 기준 가격은 얼마인가?과소/과대 청산, 잘못된 LTV, 잘못된 checkout quote
Liquidation담보 가치가 debt를 충분히 덮지 못할 때 어떻게 회수할 것인가?keeper 손실, auction backlog, bad debt, 불공정 청산
Proof of Reserve발행된 token liability를 뒷받침할 reserve가 있는가?reserve 부족 상태에서 신규 mint 또는 결제 허용
크게 보기
개념답하는 질문잘못 쓰면 생기는 문제
Price oracle이 담보나 자산의 현재 기준 가격은 얼마인가?과소/과대 청산, 잘못된 LTV, 잘못된 checkout quote
Liquidation담보 가치가 debt를 충분히 덮지 못할 때 어떻게 회수할 것인가?keeper 손실, auction backlog, bad debt, 불공정 청산
Proof of Reserve발행된 token liability를 뒷받침할 reserve가 있는가?reserve 부족 상태에서 신규 mint 또는 결제 허용

Oracle은 데이터를 가져오는 통로이고, liquidation은 부족담보를 처리하는 상태 전이이며, PoR은 reserve data를 onchain 또는 product policy로 끌어오는 검증 레이어다. 결제 제품에서는 세 가지를 섞지 않아야 한다. 가격 feed가 정상이라고 해서 reserve가 충분하다는 뜻은 아니고, PoR feed가 정상이라고 해서 redemption liquidity가 즉시 충분하다는 뜻도 아니다.

2. Oracle이 쓰이는 위치를 먼저 그린다

표 자료가로 스크롤 · 크게 보기 지원
위치필요한 값실패 영향
Vault collateral담보 가격과 업데이트 시각안전한 vault가 청산되거나, 위험한 vault가 방치된다.
Liquidationauction 시작 가격, debt, penalty, keeper incentiveauction이 너무 싸게 시작되거나 아무도 입찰하지 않아 bad debt가 커진다.
Reserve proofbacking asset 규모와 source 범위과발행 또는 reserve shortfall을 제품이 늦게 감지한다.
Cross-chain assetwrapped asset과 native backing의 관계unbacked wrapped token을 담보로 인정한다.
RWA NAVfund/share price, valuation time, redemption window잘못된 NAV로 mint/redeem이 실행된다.
크게 보기
위치필요한 값실패 영향
Vault collateral담보 가격과 업데이트 시각안전한 vault가 청산되거나, 위험한 vault가 방치된다.
Liquidationauction 시작 가격, debt, penalty, keeper incentiveauction이 너무 싸게 시작되거나 아무도 입찰하지 않아 bad debt가 커진다.
Reserve proofbacking asset 규모와 source 범위과발행 또는 reserve shortfall을 제품이 늦게 감지한다.
Cross-chain assetwrapped asset과 native backing의 관계unbacked wrapped token을 담보로 인정한다.
RWA NAVfund/share price, valuation time, redemption window잘못된 NAV로 mint/redeem이 실행된다.

이 표를 작성한 뒤에야 contract integration을 시작한다. Feed 주소만 붙이는 작업은 마지막 단계다. 먼저 feed가 가격인지 reserve인지 NAV인지, 어떤 frequency로 갱신되는지, stale이면 어떤 함수가 멈춰야 하는지 정해야 한다.

3. Oracle consumer는 값보다 timestamp를 먼저 본다

Chainlink Data Feeds API의 latestRoundData()answer뿐 아니라 startedAt, updatedAt, roundId 같은 정보를 반환한다. 반대로 timestamp 없이 최신 answer만 주는 오래된 함수들은 freshness 판단에 부적합하다. 그래서 consumer contract나 backend policy는 다음 검사를 가져야 한다.

표 자료가로 스크롤 · 크게 보기 지원
검사왜 필요한가실패 시 상태
answer > 00 또는 음수 가격/준비금은 정상 quote로 쓰면 안 된다.quote 중지, mint 중지
block.timestamp - updatedAt <= maxStaleness오래된 가격은 현재 시장 가격이 아니다.stale oracle 상태, fallback route 확인
deviation threshold직전 값 또는 secondary source와 크게 다르면 조작/시장 급변 가능성이 있다.manual review 또는 tighter limit
feed decimalsdecimal mismatch는 100배, 1만 배 오류를 만든다.배포 전 config test 실패
operator/aggregator changefeed 구현이나 권한 변경은 risk review 대상이다.admin approval 전 high-risk 상태
크게 보기
검사왜 필요한가실패 시 상태
answer > 00 또는 음수 가격/준비금은 정상 quote로 쓰면 안 된다.quote 중지, mint 중지
block.timestamp - updatedAt <= maxStaleness오래된 가격은 현재 시장 가격이 아니다.stale oracle 상태, fallback route 확인
deviation threshold직전 값 또는 secondary source와 크게 다르면 조작/시장 급변 가능성이 있다.manual review 또는 tighter limit
feed decimalsdecimal mismatch는 100배, 1만 배 오류를 만든다.배포 전 config test 실패
operator/aggregator changefeed 구현이나 권한 변경은 risk review 대상이다.admin approval 전 high-risk 상태

학습자는 여기서 중요한 습관을 가져야 한다. "Chainlink feed를 썼다"가 완료 기준이 아니다. 어떤 feed를 어떤 한계값과 fallback으로 소비하는지가 완료 기준이다.

4. Proof of Reserve는 감사가 아니라 policy input이다

Chainlink는 Proof of Reserve를 stablecoin, tokenized asset, wrapped asset의 reserve monitoring과 token mint logic에 연결할 수 있는 방식으로 설명한다. 이때 개발자가 해야 할 일은 PoR feed를 절대 진실로 취급하는 것이 아니라, 데이터 원천과 제품 정책을 분리해 기록하는 것이다.

표 자료가로 스크롤 · 크게 보기 지원
체크 질문제품 정책으로 바꿀 내용
reserve value가 bank API, custodian report, auditor report, onchain wallet 중 어디서 오는가?reserveSourceKind, reportingDelay, sourceOwner
업데이트 주기는 얼마인가?maxReserveAge, dashboard freshness badge
stale value일 때 mint/redeem이 멈추는가?mint pause, checkout warning, treasury transfer hold
reserve가 token liability보다 낮으면 어떤 circuit breaker가 작동하는가?신규 mint 중지, 신규 결제 중지, withdrawal review
oracle signer/operator가 바뀌면 누가 승인하는가?governance/admin approval, incident ticket
크게 보기
체크 질문제품 정책으로 바꿀 내용
reserve value가 bank API, custodian report, auditor report, onchain wallet 중 어디서 오는가?reserveSourceKind, reportingDelay, sourceOwner
업데이트 주기는 얼마인가?maxReserveAge, dashboard freshness badge
stale value일 때 mint/redeem이 멈추는가?mint pause, checkout warning, treasury transfer hold
reserve가 token liability보다 낮으면 어떤 circuit breaker가 작동하는가?신규 mint 중지, 신규 결제 중지, withdrawal review
oracle signer/operator가 바뀌면 누가 승인하는가?governance/admin approval, incident ticket

PoR은 "은행 계좌에 돈이 있다"는 법률 의견을 대체하지 않는다. 또한 attestation의 시간 차이, reserve 범위, liability 계산 방식, chain별 wrapped supply 차이를 해결해 주지 않는다. 그러므로 PoR은 제품의 risk input이지 최종 보증 문구가 아니다.

5. PoR feed를 product policy로 감싼다

원본 문서의 흐름은 아래와 같은 상태머신으로 확장할 수 있다.

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

이 흐름은 컨트랙트와 backend 양쪽에 나눠 들어갈 수 있다. 컨트랙트가 mint를 막고, backend가 checkout token을 숨기며, dashboard가 마지막 feed update와 reserve ratio를 보여주는 식이다.

6. 청산은 oracle 오류를 증폭시킬 수 있다

Crypto-backed stablecoin에서 oracle은 liquidation의 시작점이다. Sky Protocol 문서는 부족담보 vault의 collateral과 debt가 protocol로 이동하고, auction이 시작되어 debt를 회수하려는 구조를 설명한다. Dog는 undercollateralized vault를 confiscate하고 auction module로 넘기며, Clipper는 auction을 시작한다. 이 설명은 개발자가 청산을 "자동 판매"로 단순화하면 안 된다는 뜻이다.

표 자료가로 스크롤 · 크게 보기 지원
청산 단계필요한 데이터/행위실패 시나리오
Vault 평가collateral price, debt, liquidation ratiostale price로 안전한 vault를 청산하거나 위험 vault를 놓친다.
Liquidation triggerauthorized caller, circuit breaker, gas costkeeper incentive가 부족해 liquidation이 지연된다.
Auction startinitial price, lot, tab, penalty시작 가격이 잘못돼 collateral이 헐값에 팔리거나 안 팔린다.
Auction executionkeeper participation, market liquidityauction backlog가 쌓이고 bad debt가 늘어난다.
후처리남은 debt, returned collateral, user notice사용자/운영 원장이 실제 상태와 달라진다.
크게 보기
청산 단계필요한 데이터/행위실패 시나리오
Vault 평가collateral price, debt, liquidation ratiostale price로 안전한 vault를 청산하거나 위험 vault를 놓친다.
Liquidation triggerauthorized caller, circuit breaker, gas costkeeper incentive가 부족해 liquidation이 지연된다.
Auction startinitial price, lot, tab, penalty시작 가격이 잘못돼 collateral이 헐값에 팔리거나 안 팔린다.
Auction executionkeeper participation, market liquidityauction backlog가 쌓이고 bad debt가 늘어난다.
후처리남은 debt, returned collateral, user notice사용자/운영 원장이 실제 상태와 달라진다.

결제 시스템이 DAI/USDS 같은 crypto-backed stablecoin을 받는다면 단순히 peg price만 보지 말고 oracle freshness, liquidation backlog, keeper activity, governance parameter change를 dashboard signal로 둬야 한다.

7. 테스트는 happy path보다 실패값을 먼저 넣는다

표 자료가로 스크롤 · 크게 보기 지원
테스트 입력기대 결과
oracle answer가 0mint, collateral deposit, quote 생성이 중단된다.
updatedAt이 maxStaleness를 초과stale 상태로 전환되고 fallback 또는 manual review가 실행된다.
collateral price가 30% 급락liquidation 후보가 계산되고 dashboard에 위험 vault가 표시된다.
reserve < circulating supply신규 mint와 checkout acceptance가 중단된다.
feed decimals가 잘못 설정배포 전 config validation이 실패한다.
oracle operator/aggregator가 변경admin approval 없이는 high-risk 상태로 남는다.
크게 보기
테스트 입력기대 결과
oracle answer가 0mint, collateral deposit, quote 생성이 중단된다.
updatedAt이 maxStaleness를 초과stale 상태로 전환되고 fallback 또는 manual review가 실행된다.
collateral price가 30% 급락liquidation 후보가 계산되고 dashboard에 위험 vault가 표시된다.
reserve < circulating supply신규 mint와 checkout acceptance가 중단된다.
feed decimals가 잘못 설정배포 전 config validation이 실패한다.
oracle operator/aggregator가 변경admin approval 없이는 high-risk 상태로 남는다.

코드로 확인하기

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

컨트랙트Chainlink Aggregator 안전 호출 패턴

latestRoundData()를 직접 받지 말고 updatedAt, answeredInRound, answer > 0 을 모두 확인해 stale/zero/예전 라운드를 차단한다.

CODE SURFACEsolidity
interface AggregatorV3Interface {    function decimals() external view returns (uint8);    function latestRoundData() external view returns (        uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound    );}contract OracleConsumer {    AggregatorV3Interface public immutable feed;    uint256 public maxStaleness = 1 hours;    error StaleOracle(uint256 updatedAt, uint256 nowTime);    error InvalidAnswer(int256 answer);    error StaleRound(uint80 roundId, uint80 answeredInRound);    constructor(address feedAddr) {        feed = AggregatorV3Interface(feedAddr);    }    function safePrice() public view returns (uint256 price, uint8 priceDecimals) {        (uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = feed.latestRoundData();        if (answer <= 0) revert InvalidAnswer(answer);        if (block.timestamp - updatedAt > maxStaleness) revert StaleOracle(updatedAt, block.timestamp);        if (answeredInRound < roundId) revert StaleRound(roundId, answeredInRound);        return (uint256(answer), feed.decimals());    }}

컨트랙트Proof of Reserve 게이트 — mint 차단

reserve < circulating 일 때 mint를 차단한다. PoR feed가 stale 이면 보수적으로 mint도 막는다.

CODE SURFACEsolidity
interface IPoRFeed {    function latestAnswer() external view returns (int256);   // reserve in USD    function latestTimestamp() external view returns (uint256);}contract ReserveGatedMinter {    IPoRFeed public por;    IERC20Mintable public token;    uint256 public porMaxStaleness = 24 hours;    error ReserveStale(uint256 updatedAt);    error ReserveBelowCirculating(int256 reserve, uint256 circulating);    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {        if (block.timestamp - por.latestTimestamp() > porMaxStaleness) {            revert ReserveStale(por.latestTimestamp());        }        int256 reserve = por.latestAnswer();        uint256 newCirculating = token.totalSupply() + amount;        if (reserve <= 0 || uint256(reserve) < newCirculating) {            revert ReserveBelowCirculating(reserve, newCirculating);        }        token.mint(to, amount);    }}

백엔드Oracle freshness + circuit breaker — off-chain consumer

contract 측 검사 외에 백엔드도 같은 검사를 한 번 더 한다. circuit breaker는 운영자가 즉시 mint/route 를 막을 수 있는 권한.

CODE SURFACEtypescript
import { createPublicClient, http, parseAbi } from "viem";const aggregatorAbi = parseAbi([  "function latestRoundData() view returns (uint80, int256, uint256, uint256, uint80)",  "function decimals() view returns (uint8)"]);export async function isFeedHealthy(client: any, feed: `0x${string}`, maxStalenessSec: number) {  const [, answer, , updatedAt, answeredInRound] = await client.readContract({    address: feed, abi: aggregatorAbi, functionName: "latestRoundData"  });  const now = BigInt(Math.floor(Date.now() / 1000));  if (answer <= 0n) return { healthy: false, reason: "answer<=0" };  if (now - updatedAt > BigInt(maxStalenessSec)) {    return { healthy: false, reason: `stale by ${Number(now - updatedAt)}s` };  }  if (answeredInRound === 0n) return { healthy: false, reason: "round id zero" };  return { healthy: true };}export class CircuitBreaker {  private tripped = false;  trip(reason: string) {    this.tripped = true;    console.error(`circuit breaker tripped: ${reason}`);  }  reset(operator: string) {    this.tripped = false;    console.log(`circuit breaker reset by ${operator}`);  }  assertOpen(action: string) {    if (this.tripped) throw new Error(`${action} blocked: circuit breaker is tripped`);  }}

강의 포인트

표 자료가로 스크롤 · 크게 보기 지원
관점강의 중 확인할 질문학습 후 남길 증거
데이터 원천가격, reserve, NAV가 각각 어디서 오는가?feed/source register
freshnessupdatedAt과 heartbeat 기준을 어떻게 잡았는가?stale data policy
제품 상태reserve shortfall 또는 oracle failure가 사용자 화면에서 무엇으로 보이는가?checkout pause, warning, admin review 전이
청산 후처리liquidation trigger 후 auction, bad debt, user notice를 추적하는가?liquidation runbook
크게 보기
관점강의 중 확인할 질문학습 후 남길 증거
데이터 원천가격, reserve, NAV가 각각 어디서 오는가?feed/source register
freshnessupdatedAt과 heartbeat 기준을 어떻게 잡았는가?stale data policy
제품 상태reserve shortfall 또는 oracle failure가 사용자 화면에서 무엇으로 보이는가?checkout pause, warning, admin review 전이
청산 후처리liquidation trigger 후 auction, bad debt, user notice를 추적하는가?liquidation runbook

실무 예시

컨트랙트[BACKEND] 상황: checkout 서비스가 세 종류의 토큰을 지원한다. USDC는 issuer reserve disclosure를 참고하고, DAI/USDS는 oracle과 liquidation health를 보며, wrapped BTC는 PoR feed로 backing을 확인한다. 이때 하나의 priceUsd 필드로 모든 것을 처리하면 위험하다.

표 자료가로 스크롤 · 크게 보기 지원
토큰 유형필요한 feed제품 상태
fiat-backed stablecoinreserve disclosure, issuer status, chain별 공식 주소reserve report stale이면 신규 merchant settlement 제한
crypto-backed stablecoinprice oracle, liquidation backlog, governance parameteroracle stale이면 신규 결제 중지, refund route 점검
wrapped/tokenized assetPoR feed, native backing, bridge/custodian statusreserve shortfall이면 collateral 사용 중지
크게 보기
토큰 유형필요한 feed제품 상태
fiat-backed stablecoinreserve disclosure, issuer status, chain별 공식 주소reserve report stale이면 신규 merchant settlement 제한
crypto-backed stablecoinprice oracle, liquidation backlog, governance parameteroracle stale이면 신규 결제 중지, refund route 점검
wrapped/tokenized assetPoR feed, native backing, bridge/custodian statusreserve shortfall이면 collateral 사용 중지

이 예시에서 좋은 설계는 asset.priceFeed, asset.reserveFeed, asset.navFeed, asset.maxStaleness, asset.pausePolicy를 분리한다. 그래야 가격은 정상인데 reserve가 stale인 경우, reserve는 정상인데 liquidity가 부족한 경우를 서로 다른 상태로 처리할 수 있다.

흔한 오해와 실패 시나리오

표 자료가로 스크롤 · 크게 보기 지원
오해실제로 확인할 것
Oracle price가 있으면 모든 자산이 안전하다고 본다.price, reserve, NAV, liquidity는 서로 다른 데이터다.
PoR을 완전한 감사 또는 법적 보증처럼 표시한다.PoR은 reserve data input이며 source 범위와 reporting delay를 함께 보여줘야 한다.
answer만 읽고 timestamp를 보지 않는다.stale data로 mint, redeem, liquidation이 실행될 수 있다.
청산은 자동이므로 운영 runbook이 필요 없다고 본다.keeper shortage, auction backlog, bad debt, 사용자 안내가 모두 후처리 대상이다.
크게 보기
오해실제로 확인할 것
Oracle price가 있으면 모든 자산이 안전하다고 본다.price, reserve, NAV, liquidity는 서로 다른 데이터다.
PoR을 완전한 감사 또는 법적 보증처럼 표시한다.PoR은 reserve data input이며 source 범위와 reporting delay를 함께 보여줘야 한다.
answer만 읽고 timestamp를 보지 않는다.stale data로 mint, redeem, liquidation이 실행될 수 있다.
청산은 자동이므로 운영 runbook이 필요 없다고 본다.keeper shortage, auction backlog, bad debt, 사용자 안내가 모두 후처리 대상이다.

실습 과제

  1. 컨트랙트Oracle consumer 검증표 만들기: price/reserve/NAV feed별로 source, heartbeat, deviation threshold, updatedAt, zero/negative answer, fallback, pauser 권한을 표로 정리한다.
  2. 컨트랙트[BACKEND] PoR 기반 mint/acceptance policy 작성하기: reserve feed 정상, stale, reserve < circulating supply, operator change 상황에서 mint, redeem, checkout acceptance가 어떻게 바뀌는지 상태 전이를 작성한다.
  3. 운영청산 실패 후처리 runbook 쓰기: oracle stale, collateral price gap, keeper shortage, auction backlog, bad debt 발생 시 사용자 안내, 운영 조치, dashboard 지표를 연결한다.

완료 기준

  1. 오라클과 PoR을 구분했다.
  2. stale data 대응 조건을 정의했다.
  3. 청산 실패 후처리를 문서화했다.
  4. oracle consumer가 검사해야 할 timestamp, answer, fallback, 권한 변경 항목을 정리했다.

근거 자료

Final checkpoint

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

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

  • 오라클과 PoR을 구분했다.
  • stale data 대응 조건을 정의했다.
  • 청산 실패 후처리를 문서화했다.

학습 자료 근거

오라클 청산 Proof of Reserve
이 LMS 레슨의 개념, 예시, 과제 구성을 잡는 데 사용한 근거 문서.
내부 참고 문서
Chainlink Proof of Reserve
https://chain.link/proof-of-reserve
Chainlink Data Feeds API Reference
https://docs.chain.link/data-feeds/api-reference
Chainlink: What Are Proof of Reserves?
https://chain.link/education-hub/proof-of-reserves
Sky Protocol: Collateral Liquidation
https://developers.sky.money/protocol/vaults/collateral-liquidation/