오라클 Push/Pull은 누가 실패를 감당하는가의 문제다
오라클에서 중요한 것은 가격을 어떻게 가져오느냐보다, 틀린 가격과 오래된 가격을 어디서 멈추게 하느냐입니다. Push와 Pull의 차이는 결국 실패 책임의 배치입니다.
처음엔 오라클을 외부 가격을 체인에 옮기는 장치로 보기 쉽다. 블록체인은 외부 API를 직접 호출할 수 없으니 누군가 ETH 가격이나 BTC 가격을 가져다줘야 한다는 설명이다.
틀린 말은 아니다. 하지만 DeFi에서 오라클 사고는 보통 "가격을 못 가져와서"만 생기지 않는다. 오래된 가격을 너무 오래 믿거나, 잘못된 feed를 읽거나, 특정 action용 가격을 다른 문맥에 재사용하거나, 이상 가격을 청산 로직에 그대로 연결할 때 생긴다.
그래서 가격 오라클에서 더 중요한 질문은 이것이다.
이 가격이 틀렸거나 오래됐을 때 어디서 멈추는가?
Push와 Pull도 이 질문으로 다시 봐야 한다. 둘은 단순히 "오라클이 먼저 올리느냐, 사용자가 들고 오느냐"의 차이가 아니다. 누가 업데이트 책임을 지는지, 누가 비용을 내는지, 누가 freshness를 검증하는지, 장애가 났을 때 어떤 기능을 멈추는지의 차이다.
한눈에 보는 논지
| 질문 | Push | Pull | 하이브리드 |
|---|---|---|---|
| 무엇을 맡나 | 기준 가격을 온체인에 유지한다. | 실행 시점 가격을 transaction에 싣는다. | 기준 가격과 실행 가격을 분리한다. |
| 누가 움직이나 | oracle updater가 feed를 갱신한다. | caller, keeper, dApp이 payload를 가져온다. | updater와 caller가 각자 다른 책임을 진다. |
| 주된 실패 | stale price를 계속 읽는다. | 오래되었거나 다른 문맥의 payload를 재사용한다. | 두 입력값이 어긋났을 때 정책이 없으면 멈추지 못한다. |
| 필수 guard | updatedAt, max staleness, pause | expiry, nonce, purpose/context binding | deviation check, circuit breaker, fallback policy |
| 잘 맞는 곳 | 렌딩, 담보 평가, RWA/NAV | 스왑, perp, 빠른 정산 | 레버리지, 청산, 큰 금액이 걸린 실서비스 |
가격은 상태 전이를 바꾼다
스마트 컨트랙트가 오라클 값을 읽기만 한다면 큰 문제가 아닐 수 있다. 문제는 그 값을 근거로 돈을 움직일 때 시작된다.
렌딩 프로토콜에서 ETH 가격이 실제보다 높게 들어오면 사용자는 과도하게 빌릴 수 있다. 반대로 실제보다 낮게 들어오면 정상 포지션이 청산될 수 있다. Perp에서는 mark price, margin, liquidation, funding이 모두 가격 입력과 연결된다.
이 순간 오라클은 API가 아니라 리스크 관리 시스템이 된다.
Oracle = data delivery + verification + freshness + fallback + risk guard
verification은 누가 만든 가격인지 묻는다. freshness는 아직 쓸 수 있는 가격인지 묻는다. fallback은 기본 경로가 깨졌을 때 무엇을 할지 묻는다. risk guard는 가격이 이상할 때 어떤 상태 전이를 막을지 정한다.
Push는 기준 가격판이다
Push 모델은 오라클이 가격을 미리 온체인에 써두는 방식이다. dApp은 가격이 필요할 때 feed contract를 읽는다.
Chainlink Data Feeds가 대표적인 예다. Chainlink 문서는 Data Feeds가 여러 데이터 소스를 집계하고, onchain aggregator에 값을 publish하며, consumer contract가 proxy/aggregator를 통해 값을 읽는 구조를 설명한다. 업데이트는 streaming처럼 매 순간 일어나는 것이 아니라, 보통 deviation threshold를 넘거나 heartbeat 시간이 지나면 새 round가 시작된다.
(, int256 answer, , uint256 updatedAt, ) = feed.latestRoundData();
require(answer > 0, "invalid price");
require(block.timestamp - updatedAt <= MAX_STALENESS, "stale price");
Push에서 가장 많이 놓치는 부분은 latestRoundData()가 "항상 최신 시장 가격"을 뜻하지 않는다는 점이다. 온체인에 값이 있다는 말과 지금 바로 써도 된다는 말은 다르다. Chainlink 문서도 latestRoundData()의 updatedAt을 확인하고, 앱이 정한 허용 시간 안에 업데이트되지 않으면 pause 또는 alternate mode로 전환하라고 안내한다.
Push 모델에서 봐야 할 변수는 heartbeat, deviation threshold, max staleness, fallback이다. 질문은 하나다. 이 자산은 얼마나 오래된 가격까지 허용할 수 있고, 이상할 때는 어디까지 멈출 것인가.
Push는 렌딩, 담보 평가, 기준 가격, RWA NAV처럼 여러 곳에서 반복적으로 읽는 값에 잘 맞는다. 컨트랙트 인터페이스도 단순하다. 대신 자산 수가 많아질수록 모든 feed를 계속 온체인에 업데이트하는 비용이 커지고, 업데이트가 멈췄을 때 stale price를 쓰는 위험이 남는다.
Pull은 실행 시점 가격이다
Pull 모델은 가격을 미리 모든 체인에 계속 써두지 않는다. 가격이 필요한 순간 caller가 최신 price payload를 가져와 transaction에 포함하거나, 같은 transaction 안에서 price update를 먼저 실행한다.
Pull이라고 해서 모두 같은 구조는 아니다.
| Pull 계열 | 가격이 들어오는 방식 | 예시 | 주의할 점 |
|---|---|---|---|
| Onchain update first | caller가 update payload를 제출하고 같은 transaction에서 읽는다. | Pyth updatePriceFeeds() | update fee, stale threshold, 사용자가 과거 payload를 선택하는 문제 |
| Signed report verification | offchain report를 가져와 onchain verifier가 검증한다. | Chainlink Data Streams | report schema, verifier, timestamp, feed ID 검증 |
| Calldata injection | user transaction calldata에 oracle payload가 붙는다. | RedStone Pull | authorized signer, timestamp, payload extraction, signer threshold |
| Centralized signed quote | 중앙화 서버가 특정 action용 가격에 서명한다. | 자체 MVP/사내 시스템 | replay, execution-context binding, signer key 보안 |
Pyth는 현재 push feed도 제공하지만, Pyth를 설명할 때 자주 언급되는 강한 특징은 pull integration pattern이다. 이 경로에서는 누구나 Hermes 같은 offchain service에서 최신 priceUpdate를 가져와 onchain Pyth contract에 제출할 수 있다. 일반적인 흐름은 transaction 안에서 updatePriceFeeds()로 가격을 갱신한 뒤 getPriceNoOlderThan() 같은 freshness check가 있는 메서드로 가격을 읽는 방식이다.
Chainlink Data Streams도 pull-based design을 쓴다. dApp은 필요할 때 report를 가져오고 onchain에서 검증한다. RedStone Pull은 user transaction에 데이터 payload를 붙이고, 컨트랙트가 authorized signer, timestamp, aggregation rule을 검증하는 쪽에 가깝다.
Pull의 장점은 실행 시점 가격에 가깝게 갈 수 있다는 점이다. 특히 perp, 스왑, 레버리지, 빠른 정산처럼 몇 초 차이가 손익으로 이어지는 곳에서는 이 장점이 크다. Pyth 문서도 pull oracle이 더 높은 update frequency와 낮은 latency를 제공할 수 있고, push feed는 각 chain/feed마다 지속적인 gas expenditure가 필요해 확장성에 제약이 있다고 설명한다.
하지만 Pull이 자동으로 안전한 것은 아니다. Pyth 문서는 사용자가 어떤 price update를 transaction에 사용할지 선택할 수 있다는 점을 "adversarial selection"으로 설명한다. onchain price는 시간상 앞으로만 움직이고 너무 오래된 값은 쓸 수 없지만, 사용자가 허용 범위 안의 과거 update를 고르는 것은 여전히 latency와 비슷한 효과를 낼 수 있다. 그래서 Pull에서도 staleness threshold는 필수다.
Signed quote는 가격만 서명하면 부족하다
자체 중앙화 Pull 모델을 만들 때 가장 쉬운 착각은 "서명만 맞으면 된다"는 것이다. signer가 맞아도 그 서명이 어느 체인, 어느 컨트랙트, 어느 feed, 어느 action, 어느 만료 시간에 묶였는지 확인하지 않으면 재사용 공격이 가능하다.
중앙화 signed quote라면 가격만 서명하면 부족하다. 최소한 feedId, price, decimals, timestamp, validUntil, purpose, nonce, 그리고 실행 문맥이 함께 묶여야 한다.
여기서 실행 문맥은 항상 msg.sender일 필요는 없다. keeper, relayer, solver, permissionless liquidation을 허용하는 구조라면 account, positionId, beneficiary, executor, allowlisted executor class처럼 실제 권한 경계에 맞는 필드를 써야 한다. msg.sender만 기계적으로 묶으면 합법적인 제3자 실행을 막거나, 나중에 우회 필드를 덧붙이는 식으로 더 위험한 구조가 될 수 있다.
중앙화 signed quote에서 EIP-712 typed data를 쓰는 이유도 여기 있다. 단순 문자열 서명은 사람이 읽기도 어렵고, 구조화된 필드가 어떤 domain에 묶이는지도 약하다. EIP-712는 typed structured data와 domain separator를 제공하고, domain에는 chainId, verifyingContract, name, version 같은 필드를 넣을 수 있다.
다만 EIP-712 자체가 replay protection을 완성해주지는 않는다. EIP-712 명세도 replay protection을 포함하지 않는다고 명시한다. 그래서 validUntil, nonce, 실행 문맥, purpose, chainId, verifyingContract 같은 필드를 설계자가 직접 의미 있게 묶어야 한다.
프로토콜형 Pull oracle은 이와 다르게 각자의 report/update 검증 방식을 갖는다. Pyth update, Chainlink Data Streams report, RedStone payload는 각각 해당 프로토콜의 verifier와 schema를 따라야 한다. 그래도 끝까지 남는 질문은 비슷하다. 누가 만든 가격인지, 어떤 feed인지, 아직 유효한지, 그리고 이 payload가 다른 실행 문맥으로 재사용될 수 없는지다.
실패 방식으로 보면 차이가 선명하다
Push와 Pull의 차이를 실제 장애 상황으로 바꿔보면 더 잘 보인다. Push는 이미 온체인에 올라온 값을 계속 읽기 때문에 "그 값이 아직 살아 있는가"가 중요하다. Pull은 transaction 시점에 값을 가져오므로 "그 payload가 이 action에 맞는가"가 중요하다.
| 실패 상황 | Push에서 생기는 문제 | Pull에서 생기는 문제 | 필요한 guard |
|---|---|---|---|
| 업데이트 중단 | feed에 예전 가격이 남아 있는데 컨트랙트가 계속 읽는다. | 새 payload/report를 가져오지 못해 실행이 막힌다. | max staleness, pause, read-only mode |
| 가격 급변 | heartbeat 전까지 늦은 가격을 쓸 수 있다. | 허용 범위 안에서 과거 payload를 골라 쓸 수 있다. | deviation check, short validity window |
| feed 혼동 | 잘못된 feed address, quote currency, decimals를 읽는다. | 다른 feed/action용 payload를 재사용한다. | feedId, decimals, quote currency 고정 |
| signer/verifier 문제 | updater key가 잘못된 가격을 publish할 수 있다. | signer 검증 실패나 key compromise가 곧 가짜 가격으로 이어진다. | signer set, verifier, key rotation, emergency revoke |
| 실행 문맥 혼동 | 같은 feed를 너무 넓은 용도에 쓴다. | borrow용 quote를 liquidation이나 swap에 재사용한다. | purpose binding, account/position/executor binding, verifying contract binding |
둘 중 하나가 항상 우월하지는 않다. Push는 느리지만 안전하고 Pull은 빠르지만 위험하다는 식의 단순 구도도 맞지 않는다. Push도 stale check가 없으면 위험하고, Pull도 verifier와 freshness check가 잘 되어 있으면 강한 구조가 될 수 있다.
결국 선택 기준은 이 질문이다.
이 가격은 기준선인가, 실행 가격인가?
담보 평가처럼 여러 transaction에서 계속 참조되는 기준선이라면 Push가 자연스럽다. 스왑이나 perp 체결처럼 바로 그 transaction의 실행 결과를 결정하는 가격이라면 Pull이 자연스럽다.
실서비스에서는 기준 가격과 실행 가격을 나눈다
금전 리스크가 크고 실행 가격의 민감도가 높은 서비스라면 Push와 Pull을 역할별로 나누는 구조를 검토할 만하다.
Push = 기준 가격판
Pull = 실행 시점 가격
RiskGuard = 둘 사이의 괴리와 freshness를 검사하는 장치
예를 들어 스왑이나 perp 실행에서는 Pull 가격을 쓰되, Push baseline과 너무 벌어지면 revert할 수 있다.
require(!isStale(pushUpdatedAt), "stale baseline");
require(!isExpired(pullValidUntil), "expired execution price");
require(
absDiffBps(pullPrice, pushPrice) <= maxDiffBps,
"price deviation too high"
);
이때 Push 가격은 정답이 아니다. Pull 가격도 정답이 아니다. 둘은 서로 다른 실패 방식을 가진 입력값이다. RiskGuard는 두 입력값의 괴리, 업데이트 시각, 자산별 변동성, 거래 규모, 레버리지, 시장 상태를 보고 실행을 허용할지 결정한다.
여기서 특히 중요한 것은 raw oracle price를 어떤 상태 전이에 곧바로 연결할지 명시적으로 정하는 것이다. 일부 시스템은 oracle 또는 index-derived price를 settlement나 primary risk input으로 쓸 수 있다. 문제는 직접 사용 자체가 아니라, 지연에 민감한 레버리지 상품에서 freshness, deviation, circuit breaker, liquidation guard 없이 그 값을 청산이나 정산에 그대로 연결하는 것이다.
fallback oracle도 백업 피드 하나를 더 붙이는 문제가 아니다. 장애가 나면 신규 포지션을 막을지, 청산만 완화할지, read-only로 돌릴지, TWAP이나 EMA 같은 임시 모드로 갈지를 상태 기계처럼 정해야 한다.
Perp에서는 이 구분이 더 중요하다.
oracle price
↓
mark price / index price calculation
↓
risk engine
↓
margin / liquidation / funding / PnL
mark price는 오라클 가격의 다른 이름이 아니다. 외부 시장 가격, 내부 mid price, liquidity, funding, fallback, smoothing, protection rule을 어떻게 섞을지에 대한 리스크 설계다. 고배율 시장에서는 몇 초의 latency도 공격면이 될 수 있으므로, liquidation guard와 circuit breaker 없이 raw price만 믿으면 위험하다.
중앙화 구현은 data plane과 control plane을 나눠야 한다
중앙화 오라클은 무조건 안 된다고 말할 필요는 없다. 테스트넷, PoC, MVP, 내부 서비스, 금전 리스크가 작은 앱, 게임 아이템, 포인트 시스템, 원천 데이터 자체가 중앙화된 RWA/NAV에서는 충분히 쓸 수 있다.
다만 중앙화 구현에서는 가격 서버보다 운영 권한의 집중이 더 큰 문제가 되기도 한다. signer, feed mapping, pause/resume, fallback 전환, settlement policy가 한 손에 있으면 가격 리스크는 곧 운영 리스크가 된다.
여기서 data plane과 control plane을 섞으면 안 된다. 3초마다 가격을 올리거나 저지연 quote를 발급하는 live signer는 사람이 누르는 multisig로 운영하기 어렵다. 이 경로에는 HSM, KMS, TEE, threshold signing, key rotation, signer health check 같은 data-plane 보호가 필요하다. 반대로 signer set 변경, feed 설정, pause/fallback 권한, 긴급 폐기 절차는 multisig, timelock, guardian 같은 control-plane 장치로 다루는 편이 맞다.
| 경계 | 맡는 것 | 보호 방식 |
|---|---|---|
| Data plane | 실시간 price update, signed quote 발급 | HSM/KMS/TEE, threshold signer, key rotation, monitoring |
| Control plane | signer set, feed mapping, pause/resume, fallback 전환 | multisig, timelock, guardian, emergency revoke |
| App risk logic | stale check, deviation check, mark price, liquidation guard | onchain rule, conservative mode, event log, alerting |
HyperCore는 Pull이 아니라 consensus-native Push에 가깝다
Hyperliquid의 HyperCore oracle은 이 논의에서 좋은 비교점이다. 결론부터 말하면 HyperCore는 전형적인 Pull oracle이 아니다. 사용자가 signed price를 transaction calldata에 싣고, 컨트랙트가 그 서명을 검증하는 구조가 아니기 때문이다.
HyperCore는 validator들이 가격을 계속 publish하고, 그 값이 consensus-native state와 risk engine으로 이어지는 구조에 가깝다. 공식 문서에 따르면 validator들은 각 perp asset의 spot oracle price를 약 3초마다 publish한다. 최종 oracle price는 validator들이 제출한 값의 stake-weighted median으로 정해진다.
다만 venue basket은 자산별로 달라질 수 있다. HYPE처럼 주요 spot liquidity가 Hyperliquid에 있는 자산은 충분한 유동성이 생기기 전까지 외부 소스를 제외할 수 있고, BTC처럼 주요 spot liquidity가 외부에 있는 자산은 Hyperliquid spot price를 제외할 수 있다. 이 구조는 단일 가격 소스보다 조작 비용을 높일 수 있지만, 그 자체로 완전한 안전을 보장하지는 않는다. oracle price는 funding rate 계산에 쓰이고, margining, liquidations, TP/SL trigger에 쓰이는 mark price의 구성요소가 된다.
중요한 것은 3초라는 숫자 자체가 아니다. validator set이 값을 만들고, stake-weighted median이 집계하고, mark price와 clearinghouse risk engine이 그 값을 받아 청산과 펀딩으로 연결한다는 점이다. 그래서 HyperCore의 3초 Push를 중앙화 서버 하나가 3초마다 updatePrice()를 호출하는 구조와 같은 것으로 보면 안 된다.
특히 HyperCore와 HyperEVM을 혼동하면 안 된다. HyperEVM contract가 precompile로 HyperCore 가격이나 상태를 읽는 것은 기존 HyperCore state를 조회하는 것이다. Pull oracle처럼 사용자가 signed price payload를 제출하고 컨트랙트가 그 서명을 검증하는 구조가 아니다.
HyperEVM precompile read = HyperCore state 조회
Pull oracle = price payload 제출 + onchain 검증
중앙화로 짧은 heartbeat를 만들 수는 있다. 하지만 그러려면 여러 거래소 가격 수집, median 또는 liquidity-aware aggregation, per-update deviation check, updatedAt 기반 stale check, mark/index price 분리, pause/fallback, live signer 보호를 직접 설계해야 한다. 이때도 파라미터는 자산마다 달라야 한다. BTC/ETH처럼 유동성이 깊은 자산과 저유동성 알트는 같은 threshold를 쓸 수 없고, 2x 레버리지와 100x 레버리지도 같은 max staleness를 쓸 수 없다.
용도별로 모델을 다르게 골라야 한다
오라클 선택은 취향이 아니라 사용처의 리스크 모양에 따라 달라진다.
| 용도 | 추천 모델 | 필수 장치 | 이유 |
|---|---|---|---|
| 렌딩 / 담보 평가 | Push 중심 | stale check, conservative pricing, max deviation, pause | 담보 가치는 자주 읽히고, 컨트랙트 인터페이스가 단순해야 한다. |
| 청산 | Push + guard 또는 하이브리드 | stale check, mark price, liquidation delay/guard, circuit breaker | 잘못된 가격은 사용자 손실과 프로토콜 손실로 바로 이어진다. |
| Perp / 레버리지 | Pull 또는 Push + Pull | 짧은 expiry, mark price 분리, push-pull deviation check, funding/liquidation guard | 실행 시점 가격과 latency가 중요하다. |
| 스왑 | Pull 또는 하이브리드 | signed/report price, slippage check, expiry, feedId 검증 | 거래 시점의 가격과 사용자가 허용한 slippage가 핵심이다. |
| RWA / NAV | Push | 업데이트 이력, 운영자 권한 관리, 수동 정정, pause | 원천 데이터 자체가 중앙화되어 있고 초단위 최신성이 덜 중요할 수 있다. |
| 게임 / 포인트 / 내부 시스템 | 중앙화 Push 가능 | updater 권한, event log, stale check | 금전 리스크가 작거나 시스템 경계가 명확하면 단순 구조가 실용적이다. |
렌딩에서는 "늘 최신"보다 "이상 가격을 쓰지 않는 것"이 더 중요할 때가 많다. Perp에서는 최신성도 중요하지만, 최신 가격 하나를 그대로 청산 가격으로 쓰면 또 다른 문제가 생긴다. 스왑에서는 oracle price와 사용자 slippage가 함께 있어야 한다. RWA/NAV에서는 원천 데이터 제공자와 정정 권한이 핵심이다.
즉, 오라클 모델은 프로토콜의 risk engine과 같이 설계해야 한다.
결론
오라클 설계는 데이터를 어떻게 가져올지보다 어떤 가격을 언제까지 믿을지 정하는 일이다. 더 정확히는 언제 멈출지 정하는 일이다.
Push는 기준선을 만들기 좋다. Pull은 실행 시점 가격에 가깝게 갈 수 있다. 하지만 둘 다 stale check, replay 방어, execution-context binding, deviation guard 없이 쓰면 위험하다.
중앙화 오라클은 가능하다. 다만 그 순간 보안은 블록체인이 아니라 오라클 서버, signer key, 운영 절차에 의존한다. data plane signer와 control plane 권한을 나누지 않으면 가격 장애가 운영 장애로 번진다.
실서비스에서 가장 현실적인 구조는 대체로 아래 조합이다.
Push baseline
+ Pull execution price
+ deviation check
+ stale check
+ mark price / risk engine
+ emergency pause
마지막으로 남는 문장은 하나다.
오라클을 잘 설계한다는 것은 더 빠른 가격을 가져오는 일이 아니라,
잘못된 가격을 믿지 않는 경계를 만드는 일이다.
참고한 문서
- Chainlink Docs: Data Feeds
- Chainlink Docs: Developer Responsibilities
- Chainlink Docs: Data Streams
- Pyth Docs: What is a Pull Oracle?
- Pyth Docs: Why Update Prices
- Pyth Docs: Best Practices
- RedStone Docs: Pull model
- RedStone Docs: Push model
- RedStone Docs: Hybrid push + pull
- EIP-712: Typed structured data hashing and signing
- Hyperliquid Docs: Oracle
다음 읽기
이 생각이 이어지는 방향
읽은 뒤의 대화
읽은 뒤의 생각을 이어갑니다
질문, 반론, 조용한 후속 메모를 이 글 아래에 남길 수 있습니다.