키퍼
24시간마다 한 번, 각 트레저리의 전환을 트리거하고, 이어서 그 에어드랍을 트리거하는 오프체인 워커입니다. 아무것도 결정하지 않습니다. 키퍼의 루프는 기본값으로 5분마다 돌며, 2026-10-06부터는 에어드랍 측정 구간마다 한 번, 측정 구간이 닫힌 뒤 루프를 처음 돌 때 볼트의 ETH를 전환합니다: 아래의 "전환 빈도"를 참고하십시오. 에어드랍 단계는 2026-10-05부터 작성되어 있습니다: 아래를 참고하십시오.
하는 일
- 24시간마다 한 번, 1년 내내 미국 개장 전인 기본값 13:00 UTC에 측정 구간이 닫힌 뒤, 전환 임계값(기본값 0.1 ETH) 이상을 보유한 볼트를, 닫힌 뒤 루프를 처음 돌 때 찾아냅니다; 그때 그보다 적은 볼트는 그날 나중에 거래로 임계값을 넘더라도 다음 측정 구간을 기다립니다. 주기의 매수는 이어지는 거래 시간 중에 이루어지며, 그 전송은 기본적으로 그 거래 시간이 끝난 뒤에 이루어집니다. 볼트가 이전 주기에서 아직 보유하고 있는 현금(USDC 또는 USDG)은 임계값과 상관없이 이어서 처리됩니다: 여섯 번째 감사 루프 이후로, 2026-10-06부터, 볼트의 USDC는 그 ETH의 각 전환 뒤에 한 번, 그 밖에는 측정 구간마다 많아야 한 번, 미러 볼트의 USDG는 루프를 돌 때마다 처리됩니다("전환 빈도" 참고)
- 스왑 경로를 제안하고, 최신 호가로 최소 수량을 계산합니다; 2026-10-05부터 볼트는 실제로 도착하는 양에 그 최소 수량을 적용합니다. 2026-10-06부터 Ethereum의 각 볼트는 자신의 라우터, 즉 생성될 때의 라우터에서 견적됩니다
- 거래 장소가 볼트의 한도 안에서 체결할 수 있도록 각 단계의 규모를 정합니다(아래 참고)
- 전환, 브리지 배치, 이어서 원격 체인에서의 매수를 호출합니다; 2026-10-05부터 브리지 배치를 보낼 수
있는 것은 키퍼뿐입니다(
bridgeReady). 정식 브리지 레일에서는 최신 base fee의 두 배를 기준으로 배치의 수수료를 요청하며(quoteBridgeAt), 초과분은 키퍼에게 돌아옵니다. 아홉 번째 감사 루프 이후로, 키퍼가 이전 허브의 견적으로 넘어가는 것은 허브 자체가 그런 견적이 없다고 답할 때뿐이며, 노드가 응답하지 않을 때는 결코 그러지 않습니다: 응답이 없으면 배치는 한 바퀴 미뤄지며, 수수료가 base fee를 따르는 정식 브리지 레일에서는 알림과 함께, USDG 레일에서는 경고만 남깁니다. 열 번째 감사 루프 이후로, 2026-10-06부터, USDG 레일의 배치는 많아야 어댑터의 마켓 수 상한(기본값 17)만큼만 담으며, 키퍼는 배치마다 그 상한을 읽습니다: 가장 오래 기다린 마켓이 먼저 가고, 나머지는 키퍼의 다음 바퀴까지 USDC를 자신의 볼트에 유지하므로, 영원히 기다리는 마켓은 없습니다(Robinhood 레일 참고). 그 전까지는 준비된 모든 마켓이 하나의 배치에 들어갔으며, 큰 배치는 그 고정된 compose 가스를 바닥낼 수 있었습니다 - 새 마켓마다 첫 배치 전에 Robinhood Chain에 그 마켓의 미러 볼트를 미리 배포하며(
predeploy), 2026-10-05부터 이는 키퍼와 StockFun의 오너만 할 수 있습니다. 리모트 허브는 배치를 통해 키퍼를 알게 되므로, 첫 배치 전에는 StockFun의 오너가 첫 마켓의 볼트를 미리 배포합니다. 키퍼는 자신의 Robinhood Chain 키가 리모트 허브가 키퍼로 알고 있는 키이거나 리모트 허브 관리자의 키일 때에만 미리 배포합니다. 키퍼가 볼트를 미리 배포할 수 없는 마켓(첫 배치 전, 리모트 허브가 아직 알지 못하는 키퍼 변경 이후, 또는 미리 배포에 실패한 경우)은 알림과 함께 배치에서 제외되며, 다른 마켓들은 건너갑니다. 키퍼가 그날 미러 볼트를 읽을 수 없는 마켓은 다음 주기를 기다립니다. 확인 없이 보낸 마켓 하나가 배치 전체를 실패하게 만들 수 있기 때문입니다 - 전달을 모니터링합니다: 도착하지 않은 전송, 재실행할 티켓, 아직 배포되지 않은 미러
볼트. 출발 시점에 Robinhood Chain에서 측정하지 못한 전송은 리모트 허브 자체의 이벤트로만 확인되며,
나중에 읽은 잔액으로는 결코 확인되지 않습니다. 2026-10-06부터 정식 브리지 레일에서는 모든 전송이
그렇게 확인됩니다: 리모트 허브는 공동의 현금에서 기록을 지급하므로, 잔액이 다른 배치의 돈으로
늘어날 수 있기 때문입니다. 키퍼는 각 전송의 탐색이 멈춘 곳에서부터, 그 이벤트를 몇 구간씩
읽습니다; 일곱 번째 감사 루프 이후로, 2026-10-06부터, 체인의 최신 블록보다 몇 블록 아래까지만
읽으므로, 최신 블록에 있는 이벤트는 한두 블록 뒤처진 노드 때문에 놓치는 대신 이후 주기에서
읽힙니다. 여덟 번째 감사 루프 이후로 키퍼는 브리지 배치를 담은 블록이 그만큼의 깊이가 된
뒤에만(
KEEPER_LOG_LAG_BLOCKS), 그때 다시 읽은 영수증에 따라 그 배치를 추적합니다: Robinhood Chain에서 배치의 식별자는 그 블록에서 나오며, 배치를 옮긴 Ethereum 최신 블록의 체인 재구성은 키퍼가 결코 존재하지 않을 식별자를 계속 지켜보게 만들었습니다. 열 번째 감사 루프 이후로, 제때 적립되지 않은 전송에 대한 키퍼의 알림은 USDG 레일에서 배치의 메시지가 Robinhood Chain의 엔드포인트에서 어느 단계에 있는지 알려 줍니다: 아직 그곳에 전달되지 않음; compose되었고 적립이 뒤따름; 또는 저장됨. 저장된 경우 그 USDG는 리모트 허브에 있으며 누군가가 더 많은 가스로 마지막 단계를 수동으로 다시 실행할 때까지 어디에도 기록되지 않고, 알림은 저장된 메시지의 해시를 알려 줍니다. 그 전까지 알림은 리모트 허브가 기록하는 것만 알려 주었으며, 가스가 부족한 마지막 단계가 현금을 허브에 남겼을 때 그 0은 "아무것도 도착하지 않음"으로 읽혔습니다 - 모든 볼트, 브리지 허브, 에어드랍 컨트랙트, 리모트 허브, 모든 미러 볼트에서 보이는 모든 긴급
이동을 즉시 자신의 알림 웹훅으로 중계합니다. 열 번째 감사 루프 이후로 키퍼는 감시하는 컨트랙트를
그룹으로 나누어 그룹마다 로그 요청 하나로 지정하며, 각 요청은 많아야
KEEPER_LOG_MAX_SELECTORS개(기본값 1,000)의 주소와 이벤트 값을 지정하고, 블록 구간은 그 모든 그룹을 읽은 뒤에야 넘어갑니다. 그 전까지는 요청 하나가 모두를 지정했습니다: 마켓 레지스트리가 엔드포인트가 받아들이는 수(Robinhood Chain의 노드에서는 주소와 이벤트 값 2,000개, Sepolia의 공개 엔드포인트에서는 주소 9개)를 넘어서면 모든 요청이 거부되어, 어떤 긴급 이동도 다시는 중계되지 않았습니다 - 열 번째 감사 루프 이후로, 각 바퀴에 필요한 모든 마켓의 정보(볼트의 ETH, 그 USDC와 측정 구간, 부채, 마지막 에어드랍)를 Multicall3를 통해 호출 하나에 마켓 100개씩, 각 읽기가 단독으로 쓰던 것과 같은 블록에서 읽습니다; 돌아오지 않은 읽기는 예전처럼 단독으로 이루어지며, 결코 0으로 여겨지지 않습니다. 그 전까지는 지금까지 생성된 모든 마켓이 유휴 상태든 아니든 바퀴마다 하나씩 읽기 비용을 치렀으며, 유휴 마켓 2,000개가 있으면 한 바퀴가 두 바퀴 사이의 간격만큼 오래 걸렸습니다
- 정식 브리지 레일에서는, 여섯 번째 감사 루프 이후로, 각 배치의 티켓 두 개를 그 전송의 적립과
상관없이 각각 실행되었다는 것을 알 때까지, 거래 시간이든 아니든 매 주기 지켜보며, 아직 유효한
티켓을 재실행합니다. 배치가 출발한 지 기본값으로 6시간(
KEEPER_TICKET_ALERT_HOURS)이 지나도 실행되었는지 알 수 없는 티켓에 대해서는 알림이 한 번 가며, 기한에 또는 그 뒤에 실행이 보이지 않은 채 사라진 티켓이나 생성에 실패한 티켓도 마찬가지입니다. 그 전에는 키퍼가 배치의 마켓들이 적립되는 즉시 그 배치의 티켓 감시를 멈췄으므로, 자동 실행을 놓친 입금이 만료될 수 있었습니다. 일곱 번째 감사 루프 이후로 키퍼는 각 티켓을 Arbitrum SDK와 같은 순서로 읽습니다: 먼저 그 생성 영수증, 다음으로 그 자동 실행, 그다음 생성보다 이르지 않은 블록에서 그 티켓이 아직 존재하는지; 여덟 번째 감사 루프 이후로는 최신 블록보다 몇 블록 아래의 블록(KEEPER_REMOTE_LOG_LAG_BLOCKS), 또는 생성 블록이 그보다 나중이면 생성 블록에서 읽으며, 이는 엔드포인트의 모든 노드가 가진 블록입니다. 두 번의 읽기 사이에 생성된 입금이나 한 블록 차이가 나는 두 노드가 본 입금이, 아직 유효한데도 실행된 것으로 여겨지는 일은 결코 없습니다 - 볼트에서 실패가 반복되면 알림을 보내며, 그 볼트의 Ethereum 단계와 Robinhood Chain 단계를 따로 세므로, 한쪽에서 매일 실패하는 볼트가 보고됩니다. 일곱 번째 감사 루프 이후로 키퍼는 알림 웹훅을 최대 10초까지 기다리고 그 응답을 읽습니다: 웹훅이 받지 않은 알림은 상태 파일에 최대 100개까지 보관되어 이후 주기에 다시 보내지므로, 아예 오지 않는 대신 늦게 도착합니다. 메시지에는 Slack과 Discord가 각각 읽는 필드가 담깁니다. 여덟 번째 감사 루프 이후로는 서로 다른 알림마다 항목 하나만 보관됩니다: 기다리는 동안 다시 일어난 알림(매 주기 실패하는 볼트)은 세어질 뿐, 두 번 보관되지 않습니다. 메시지는 알림이 처음과 마지막으로 일어난 시각과 횟수를 알려 주며, 늦게 전달된 알림은 그렇다는 말로 시작합니다. 아홉 번째 감사 루프 이후로 보관된 알림은 마지막으로 일어난 순서대로 나갑니다: 다시 일어난 알림은 다른 알림들 뒤로 옮겨지므로, 어떤 대상에 대해 마지막으로 전해지는 것이 그 최신 상태입니다(웹훅이 다운된 동안 실패하고, 복구되고, 다시 실패한 감시는 마지막에 "실패 중"으로 전달되며, 예전에는 "복구됨"으로 끝났습니다). 100개를 넘으면 가장 먼저 버려지는 것은 그 원인이 이어지는 동안 매 주기 일어나는 알림(전환에 실패하는 볼트, 실패하는 브리지 배치)뿐이므로, 일회성 알림, 예를 들어 긴급 이동, 그리고 각 에피소드의 최신 소식은 결코 밀려나지 않습니다. 열 번째 감사 루프 이후로 오류의 텍스트는 로그, 알림, 상태 파일 어디에서나 RPC 주소의 스킴과 호스트만 담으며, 제공자가 키를 넣는 나머지 부분은 결코 담지 않습니다
- 아홉 번째 감사 루프 이후로, 자신의 트랜잭션 하나가 채굴된 뒤 1분 동안, 그 트랜잭션이 바꾼 것을 최신 블록이 아니라 그 트랜잭션의 블록에서 읽고, 다음에 보낼 것의 가스도 그곳에서 견적합니다. 그 이유는 테스트넷 실행에서 드러났습니다: 같은 바퀴의 전환 직후에 보낸 브리지 배치가, 부하 분산된 엔드포인트 중 전환보다 한 블록 뒤처진 노드에서 시뮬레이션되어, 브리지로 보낼 것이 없다고 보고 거짓 알림을 일으켰습니다. 이제 그 블록보다 뒤처진 노드는 오류로 답하며, 배치는 경고와 함께 한 바퀴 기다립니다. 또한 Ethereum의 Glamsterdam 업그레이드로 트랜잭션 안의 호출이 무거워졌으므로, 키퍼의 모든 트랜잭션은 가스 견적에 1.25를 곱한 값으로 나갑니다
- 아홉 번째 감사 루프 이후로, 어떤 날짜에도 담을 수 없는, 체인에서 읽은 시각(피드의 시작, 측정 구간의 끝, 티켓의 기한)은 그것을 만난 읽기를 실패시키는 대신 "an unknown time"으로 읽힙니다
- 일곱 번째 감사 루프 이후로, 첫 주기 전에 각 RPC가 설정에서 지정한 체인을 서비스하는지 확인하며, 그렇지 않으면 시작을 거부합니다. 나중에 발견된 잘못된 체인은 알림과 함께 그 체인의 작업을 멈추게 하며, 식별자를 읽을 수 없는 체인은 기다리고, 다른 체인의 작업은 계속됩니다. 여덟 번째 감사 루프 이후로는 두 체인 ID를 모두 설정해야 하며(아래 참고), 다른 체인을 위해 쓰인 상태 파일은 키퍼가 자신의 RPC가 서비스하는 체인을 읽을 때까지 손대지 않은 채 남겨 둡니다("재시작" 참고)
- 여덟 번째 감사 루프 이후로, Robinhood Chain에서 미러 볼트 오라클의 두 가격 피드 가드를 읽습니다: 오라클이 시퀀서 때문에 모든 가격을 보류하는 동안 원격 매수는 알림 한 번과 함께 기다리며, 기업 행위 중인 주식은 단독으로 기다립니다("오라클이 가격을 보류할 때" 참고)
- 두 레일 모두에서, 리모트 허브에서 대기 중인 현금을 스윕으로 지급합니다: 토큰이 도착한 기록, 그리고 허브가 일시 중지된 동안 허브에 도착한 것. USDG 레일에서는 리모트 허브가 긴급 이동에 대한 StockFun 오너의 정산을 기다리는 동안 스윕을 보류하고, 이를 한 번 보고하며, 장부가 정산되면 재개합니다. 2026-10-05부터는 미러 볼트가 거부한 몫도, 그 볼트가 다시 현금을 받게 되면 스윕합니다; 정식 브리지 레일에서는 그 스윕이 무언가를 지급할 때에만 이를 보냅니다
- 버너가 매수하기 전에 가져가는, 훅에 적립된 바이백 잔액까지 셈에 넣어
BuybackBurner를 트리거합니다 - 측정 구간마다 한 번, 기본값으로 미국 거래 시간이 끝난 뒤, 각 마켓의 주식을 에어드랍 컨트랙트로 보내되, 배분은 결코 정하지 않습니다: 아래를 참고하십시오
- 2026-10-05부터는 매 주기, 거래 시간과 상관없이, 훅과 유동성 락이 이를 거부한 수령인을 위해
보관하는 것을 지급합니다: 훅이 마켓의 볼트에 지급해야 할 것(
payTreasury), 그리고 락이 볼트나 크리에이터를 위해 보관하는 수수료 수취분의 몫(payOwed). 각 지급은 먼저 시뮬레이션되며, 무언가를 지급할 때에만 전송됩니다. 두 호출 모두 누구에게나 열려 있습니다 - 2026-10-05부터는 LP 수수료와 함께 생성된 풀의 LP 수수료를 거두며(
collectFees, 누구에게나 열려 있음), 풀마다 하루에 많아야 한 번입니다. StockFun 풀은 기본적으로 LP 수수료를 부과하지 않으므로, 기본 상태에서는 거둘 것이 없습니다
전환 빈도
창업자는 2026-09-27에 볼트의 ETH를 일일 주기마다 한 번, 에어드랍 직전에, 그리고 그 확인 시점에 볼트가 임계값을 보유한 경우에만 전환하기로 결정했습니다. 키퍼는 2026-10-06부터 이를 지킵니다; 그 전에는 볼트가 임계값을 보유하는 즉시 거래 시간 중 루프를 돌 때마다 볼트의 ETH를 전환했습니다.
- 측정 구간. 주기는 에어드랍의 측정 구간입니다: 측정 구간은 에어드랍 컨트랙트가 정하는 때에
닫히며(기본값으로 13:00 UTC에 닫히는 24시간),
$STOCKFUN마켓도 포함됩니다; 팩토리가 에어드랍 컨트랙트를 지정하지 않는 동안에는 그 기본 일정을 따릅니다 - 측정 구간마다 한 번의 확인. 루프는 여전히 기본값으로 5분마다 돌며, 루프를 한 번 돌 때마다 주기 보고서로 끝납니다. 임계값은 측정 구간이 닫힌 뒤 그 볼트에 도달하는 첫 바퀴에서 확인됩니다. 임계값 이상이면 ETH는 한 번 전환됩니다: 볼트 자체가 기록한 마지막 ETH 전환이 체인에서 읽혀 이후의 바퀴들에 이를 알려 주므로, 재시작해도 아무것도 바뀌지 않습니다. 그보다 적으면, 그날 나중에 거래로 임계값을 넘더라도 ETH는 다음 측정 구간을 기다립니다; 키퍼는 그 확인을 상태 파일에 보관합니다. 2026-10-06부터는 이를 자신의 시계 시각이 아니라 확인 대상이 된 측정 구간의 끝으로 보관합니다. 마감 무렵에 시계가 조금만 어긋나도 자신의 시계 시각이 체인의 측정 구간에서 벗어날 수 있었기 때문입니다; 이전 버전의 키퍼가 쓴 확인은 인정되지 않으므로, 업그레이드가 이루어진 측정 구간에서 그런 볼트는 다시 확인됩니다. 여섯 번째 감사 루프 이후로 확인은 측정 구간이 닫힌 뒤의 볼트 잔액을 읽습니다: 이전에는 마감 시각에 걸쳐 돈 바퀴가 마감 전에 읽은 잔액을 기록했고, 제때 임계값을 넘은 볼트가 그 측정 구간을 건너뛰었습니다
- 확인 시점에 나갈 수 없는 전환(ETH/USD 피드의 갱신 정지, 볼트의 한도 안에서 체결하지 못하는 거래 장소, 실패한 트랜잭션, 키퍼가 읽을 수 없는 측정 구간)은 같은 측정 구간의 이후 바퀴에서 다시 시도됩니다. 키퍼가 읽을 수 없는 측정 구간은 ETH 단계의 실패로 세어져 실패가 반복되면 알림이 가며, 볼트의 USDC는 그래도 계속 처리됩니다
- 여섯 번째 감사 루프 이후의 USDC. 볼트에 이미 있는 USDC는 임계값과 상관없이 자신의 빈도로 이어서 처리됩니다: Ethereum에서의 그 매수, 또는 브리지 배치로의 그 방출은 볼트의 ETH 전환마다 그 뒤에 한 번, 그 밖에는(기부, 환불, 실패했거나 절반으로 줄어든 단계가 남긴 것) 측정 구간마다 많아야 한 번 나갑니다. 성공한 단계만 인정되며, 실패한 단계는 이후 바퀴에서 다시 시도됩니다. 이렇게 2026-09-27 창업자의 결정이 USDC에도 적용됩니다: 그 전에는 루프를 돌기 전마다 볼트로 보내진 USDC 몇 단위 때문에 키퍼가 루프를 돌 때마다 매수를, 또는 수수료가 붙는 브리지 배치 전체를 보냈습니다
- 루프를 돌 때마다 나가는 것: Robinhood Chain에서의 매수. 미러 볼트의 잔액은 전달과 기부를 구분할 수 없으며, 각 매수의 상한은 큰 전달을 의도적으로 여러 바퀴에 나눕니다. 절반으로 줄어든 전환이 남긴 ETH(거래 장소가 한도 안에서 잔액 전체를 체결하지 못한 것)는 다음 측정 구간을 기다립니다
- 로컬과 테스트넷 실행에서는
KEEPER_CONVERT_ONCE_PER_WINDOW를 false로 두며, 그러면 루프를 돌 때마다 전환, 매수, 브리지를 실행합니다; 그 밖의 모든 곳에서는 기본값대로 켜진 상태로 남습니다
이 페이지의 다른 곳에서 "매 주기"는 주기 보고서에서와 마찬가지로 루프를 돌 때마다를 뜻합니다; 측정 구간인 것은 에어드랍의 일일 주기뿐입니다.
한 번에 하나의 실패
2026-10-05부터 컨트랙트는 레그 하나, 주식 하나, 마켓 하나, 전달 하나가 다른 것들을 멈추지 않고 실패할 수 있게 하며, 무엇이 실패했는지 이벤트로 보고합니다. 키퍼는 그 이벤트를 읽고 계속 남는 실패에 대해 알림을 보냅니다; 키퍼 자신의 작업도 같은 방식으로 나뉘어 있습니다.
- 매수 레그. 각 주식의 레그는 따로 계획됩니다: 계획할 수 없는 레그는 건너뛰고, 다른 레그들은
진행됩니다. 볼트가 실패했다고 보고한 레그(
LegFailed)는 그 현금을 계속 그 주식에 배정된 상태로 유지합니다; 키퍼는 이를 결코 전환된 것으로 세지 않으며, 그 볼트에서 그 주식이 연속 세 번 실패하면 알림을 보냅니다(KEEPER_ALERT_AFTER_FAILURES). 볼트의 어떤 레그도 계획할 수 없을 때는, 이전과 마찬가지로 그 볼트 전체가 실패합니다 - 브리지 배치의 마켓. 브리지 허브가 배치에서 제외한 마켓(
MarketSkipped)은 이후의 배치를 위해 USDC를 자신의 볼트에 유지합니다. 연속 세 배치에서 제외되면(KEEPER_BRIDGE_ALERT_AFTER_SKIPS), 디코딩된 이유와 그 해결 방법과 함께 한 번 알림이 갑니다. 열 번째 감사 루프 이후로 어댑터의 상한 때문에 배치에서 빠진 마켓은 로그에 그 사실이 남은 채 다음 바퀴를 기다리며, 그 배치의 맨 앞에 섭니다; 어댑터가 거부한 배치(상한을 넘었거나, 배치 설정 없이 업그레이드된 어댑터)에 대해서는 그 이유와 그 배치 자체의 마켓들을 밝힌 알림이 갑니다 - 거부된 전달. 현금 토큰이 미러 볼트에 대해 거부한 몫(
DeliveryRefused)에 대해서는, 리모트 허브가 그 마켓에 그 몫 중 지급할 것이 하나도 남지 않을 때까지, 해결 방법과 함께 한 번 알림이 갑니다 - 읽을 수 없는 볼트. 예를 들어 잘못된 업그레이드 이후, 자신의 뷰 함수가 응답하지 않는 볼트는
그 주기에서 제외되며, 다른 볼트들은 전환합니다. 연속 세 주기가 지나면 한 번 알림이
갑니다(
KEEPER_ALERT_AFTER_FAILURES) - 남아 있는 채무. 볼트가, 또는 크리에이터의 몫에 대해서는 훅이, 지급받아야 할 ETH를 계속 거부하는 동안, 키퍼는 그 풀이 StockFun의 오너(수정을 위한 업그레이드)를 기다리고 있다고 에피소드마다 한 번 알림을 보내며, 키퍼든 다른 누구든 채무를 지급하면 에피소드의 종료를 기록합니다
StockFun의 오너만 해결할 수 있는 것은 "관리자 대기"입니다: 에피소드마다 한 번 알림을 보내고, 결코 실패로 세지 않으며, 그동안 나머지는 계속 진행됩니다.
주기 보고서에는 지급된 채무와 아직 남은 채무, 실패한 레그, LP 수수료를 거둔 풀, 그리고 에어드랍
단계에 대해서는 열린 주기, 실행된 따로 보관된 주식 탐색, 전송한 볼트, 아직 이동 중인 전달이
추가됩니다. 2026-10-06부터, 에어드랍 컨트랙트가 따로 보관하는, 적격 보유량이 없는 측정 구간으로의
전송은 전송한 볼트와 함께 세지 않고 따로 셉니다(airdropHeldAside); 정식 브리지 레일에서는 보낸
티켓 재실행(redeemed)과 아직 지켜보는 티켓(ticketsWatched)을 셉니다. 일곱 번째 감사 루프
이후로는 에어드랍 전송이 그 비용만큼의 가치가 될 때까지 기다리는 마켓(airdropBelowCost)과 웹훅을
위해 보관된 알림(alertsUndelivered)도 세며, 체인의 식별자가 아직 확인되지 않았을 때는 그렇다고
알려 줍니다(deferred: chain-unverified).
오라클이 가격을 보류할 때
2026-10-06부터 미러 볼트의 오라클은 가격을 보류할 수 있으며(Robinhood 레일 참고), 여덟 번째 감사 루프 이후로 키퍼는 무엇이든 견적하기 전에 이를 읽습니다:
- 시퀀서. 주기마다 한 번, 키퍼는 오라클에 그 시퀀서 검사가 무엇을 말하는지 묻습니다. 시퀀서가 멈춰 있거나, 복구된 지 유예 기간이 지나지 않았거나, 그 업타임 피드를 읽을 수 없는 동안에는 어떤 주식에도 가격을 매길 수 없습니다: 볼트의 매수는 기다리며, 아무것도 견적하지 않고, 아무것도 실패로 세지 않습니다. 알림 한 번이 그 에피소드를 열며, 재시작으로 다시 일어나지 않도록 상태 파일에 보관되고, 또 한 번의 알림이 매수가 재개되는 때를 알려 줍니다. Chainlink가 Robinhood Chain의 업타임 피드를 공개할 때까지 이 검사는 그 체인에서 꺼져 있으므로, 지금은 이 중 어떤 일도 일어날 수 없습니다
- 기업 행위. 토큰이 자신의 오라클을 일시 중지한 주식은 처음부터 매수에서 빠지며, 로그에 그렇게 남고, 그 USDG는 그 주식을 위해 유지됩니다; 이 주식은 결코 실패 알림에 세어지지 않으며, 바스켓의 다른 주식들은 매수됩니다. 키퍼의 계획과 그 매수 사이에 그 이유로 볼트가 실패했다고 보고한 레그는 예상된 것이며, 이 또한 세지 않습니다
- 이유를 밝힙니다. 가드가 레그의 가격을 보류해서 그 레그의 한도를 읽을 수 없을 때, 키퍼는 어느
체인에서든 그 레그에 가격을 매길 수 없는 볼트가 아니라 그 이유를 로그에 남깁니다. 키퍼가 읽을 수
없는 가드(가드 이전의 오라클)는 아무것도 보류하지 않습니다: 예전처럼 각 레그 자신의 한도가
정합니다. 아홉 번째 감사 루프 이후로는 오라클의 다른 오류도 디코딩됩니다: 오래된 피드는 로그와
실패한 레그의 이유에서, 디코딩되지 않은 코드 대신
StalePrice로 읽힙니다
에어드랍 단계
컨트랙트 쪽은 2026-10-04부터, 키퍼의 단계는 2026-10-05부터 코딩되어 있습니다. 매 주기, 거래 시간을
확인하기 전에, 키퍼는 먼저 이미 보낸 전달을 추적합니다. 그런 다음, 팩토리가 에어드랍 컨트랙트를
지정하고 있는 동안, $STOCKFUN 마켓을 포함해 각 마켓을 차례로 처리합니다:
- 측정 구간마다 한 번. 볼트의 마지막 전송(
lastAirdropAt)이 지금 보내는 전송이 들어가게 될 측정 구간의 끝(currentCycleEnd)과 같거나 그 이후이면, 그 볼트는 그 측정 구간에 대해 끝난 것입니다. 둘 다 체인에서 읽으므로, 재시작해도 아무것도 바뀌지 않습니다. 기본적으로 키퍼는 그날의 미국 거래 시간이 끝난 뒤에만 전송하므로, 그날의 매수가 전송 하나에 담겨 그날 끝난 측정 구간에 들어가며, 볼트가 주식을 보유할 때에만 전송합니다; 일곱 번째 감사 루프 이후로는 보내는 비용만큼의 가치가 있는 주식만 보냅니다(아래 참고). 열 번째 감사 루프 이후로, 이 기본값에서는, 거래 시간이 끝난 뒤 읽었을 때 보낼 것이 없던 볼트는 다음 거래 시간이 열리거나 측정 구간이 바뀔 때에만 다시 읽히며, 이는 매수가 뉴욕 거래 시간을 따르는 레일(Ondo 레일은 제외)에서 그렇습니다: 볼트가 보유하는 것은 거래 시간 중에 이루어지는 그 매수에서 오기 때문입니다. 다만 볼트 자신의 매수가 아직 영수증을 기다리는 동안은 그렇지 않으며, 그 영수증은 저녁에 도착할 수도 있습니다: 그런 볼트는 바퀴마다 읽히며, 그 매수가 가져오는 것은 그날 끝난 측정 구간으로 갑니다 - 보낼 것이 없어도, 따로 보관된 주식 먼저. 마켓이 따로 보관하고 있는 주식이 있으면, 주식이
귀속되거나(귀속되면 주식을 받는 측정 구간의 주기가 열립니다) 확인할 끝난 측정 구간이 남지 않을
때까지
assignUnassigned를 주기당 최대 다섯 번 호출합니다. 2026-10-06부터 키퍼는 볼트에 보낼 것이 있든 없든, 그리고 볼트가 일시 중지되어 있든 아니든 이 탐색을 실행합니다: 그 전에는 전송 전에만 실행되었으므로, 한 번 거래된 뒤 조용해진 마켓은 새 거래가 있거나 누군가 탐색을 직접 실행할 때까지 따로 보관된 첫 에어드랍을 홀더의 손이 닿지 않는 곳에 두었습니다. 끝나지 않은 탐색은 적격 보유량이 없는 측정 구간으로의 전송을 다음 주기로 미룹니다. 여섯 번째 감사 루프 이후로, 볼트에 보낼 것이 없는 동안, 아무것도 귀속하지 않을 탐색(아직 어떤 측정 구간도 주식을 받을 수 없음)은 마켓의 탐색이 최신 측정 구간보다maxWindowsPerAssign개(기본값 30)의 측정 구간만큼 뒤처졌을 때에만 보내집니다: 영원히 하루에 한 번이 아니라 한 달에 한 번의 탐색입니다. 주식을 귀속하는 탐색은 항상 보내집니다. 일곱 번째 감사 루프 이후로 "보낼 것이 없음"은 전송과 같은 규칙으로 보낼 가치가 있는 것이 없음을 뜻합니다: 브리지 레일에서는 OFT가 운반할 수 없어 매 전송이 미러 볼트에 남기는 더스트는 세지 않으며, 그곳에서도 이 제한이 지켜집니다 - 주기, 전송 전에만.
openCycle을 호출하며, 그러면 각 전달은 주기에 더해지기만 합니다; 아무것도 보내지 않는 측정 구간에 대해서는 결코 호출하지 않으며, 일곱 번째 감사 루프 이후로는 개시를 포함해 그 비용만큼의 가치가 있는 전송을 위해서만 호출합니다. 적격 보유량이 없는 측정 구간은 오류가 아닙니다: 그때 주식은 따로 보관됩니다 - 전송. 로컬 레일에서는 마켓의
TreasuryVault에서sendToAirdrop을 호출합니다. 브리지 레일에서는 미러 볼트에서sendToAirdrop을 호출하며, 견적된 LayerZero 수수료(quoteSendToAirdrop)에 기본값 10 %의 여유분을 더해 지불합니다; 초과분은 볼트가 환불합니다. 아홉 번째 감사 루프 이후로 전송과 그 모든 견적은 그 전달들이 Ethereum에서 받는 가스를 담으며, 그 가스는 키퍼가 고릅니다(아래 참고). 그 전송 전에 키퍼는 리모트 허브가 팩토리의 에어드랍 컨트랙트로 보내는지, 각 주식에 어댑터가 있고 그 어댑터가 응답하는지, 그 주식의 Ethereum OFT가 에어드랍 컨트랙트에 등록되어 있는지 확인합니다. 일곱 번째 감사 루프 이후로 전송에는 그 비용만큼의 가치가 있는 주식만 오르며, 그 수수료는 그 주식들에 대해 다시 견적됩니다 - 전달. 브리지 레일에서는 각 주식의 전달을, Ethereum의 LayerZero 엔드포인트가 에어드랍 컨트랙트에 적립하는 마지막 단계를 실행할 때까지 추적합니다. 아홉 번째 감사 루프 이후로 그 엔드포인트에서 멈춘 전달은 키퍼가 직접 다시 실행합니다(아래 참고). 기본값 60분 안에 적립되지 않은 전달은 알림을 한 번 일으키며, 이 알림에는 그 단계들을 수동으로 실행하는 명령이 담겨 있습니다; 누구나 이를 실행할 수 있습니다
각 주식은 따로 진행됩니다: 잔액을 읽을 수 없는 주식(발행사의 동결), 어댑터가 없거나 OFT가 등록되지
않은 주식, 어댑터가 응답하지 않는 주식(peers(): 그 주소에 컨트랙트가 없거나 LayerZero 앱이 아님;
2026-10-06부터, 그 전에는 마켓의 전송 전체를 실패시켰음), 여덟 번째 감사 루프 이후로는 그 어댑터가
LayerZero 수수료를 견적할 수 없는 주식(아래 참고), 그리고 볼트가 보낼 수 없었다고 보고한
주식(AirdropSendFailed)은 한 번의 알림과 함께 제외되며, 나머지는 진행됩니다. 각 마켓도 따로
진행됩니다: 계속 실패하는 마켓은 연속 세 번 실패한 뒤 한 번 알림이 가며, 다음 마켓은 계속
진행됩니다. 2026-10-06부터 계속 실패하는 따로 보관된 주식 탐색에는 자체 알림이 있으며, 그 해결
방법을 알려 줍니다: 누구나 그 마켓에 대해 assignUnassigned를 호출할 수 있습니다.
여기서 "관리자 대기"는 일시 중지된 에어드랍 컨트랙트, 긴급 이동 이후 에어드랍 컨트랙트에 부족한 주식(그 주식은 볼트에서 기다리고, 나머지는 진행됨), 팩토리의 것이 아닌 다른 에어드랍 컨트랙트로 보내는 리모트 허브, 그리고 아홉 번째 감사 루프 이후로는 전달 가스의 범위가 설정되지 않았거나 그 상한이 전달에 필요한 양보다 낮은 리모트 허브를 포함합니다.
보낼 가치가 있는 것
일곱 번째 감사 루프 이후로, 2026-10-06부터, 키퍼는 보내는 비용만큼의 가치가 있는 것만 보냅니다. 그 전까지는 0보다 많은 주식이면 무엇이든 나갔습니다: 아무도 거래하지 않는 마켓의 볼트에 더스트를 조금 넘는 주식을 선물하면, 키퍼가 매일 그 측정 구간의 주기를 열고 LayerZero 메시지, 또는 Ethereum 전송의 비용을 지불했습니다.
- 전송이 담는 것. 잔액을 읽을 수 있고 0보다 많은 주식; 브리지 레일에서는 어댑터와 0보다 큰 자신의 견적도 있어야 합니다. 볼트의 견적은 OFT가 운반할 수 없는 더스트에 대해서도, 어댑터가 그 전송을 견적할 수 없는 주식에 대해서도 똑같이 0이므로, 여덟 번째 감사 루프 이후로 키퍼는 볼트가 만들 전송에 대해 그 주식의 어댑터 자체에 묻습니다: 더스트는 결코 보내지 않고 알림도 보내지 않습니다; 견적이 실패하는 주식, 즉 자신의 전송이 실패할 주식은 이유를 밝히는 알림 한 번과 함께 제외됩니다; 그리고 키퍼가 물을 수 없는 주식은 예전처럼 나가며, 그 전송이 결과를 정합니다. 그 전까지는 그런 주식이 더스트로 여겨져 아무 말 없이 모든 전송에서 빠졌습니다
- 그 가치. 각 주식은 그 볼트 자신의 오라클로, 그 가격 피드의 최신 응답에 따라, 오래되었든 아니든 평가됩니다. 이는 추정치일 뿐 결코 한도가 아니기 때문입니다; ETH로 지불하는 비용은 같은 오라클의 ETH/USD 피드로 달러로 환산됩니다
- 그 자신의 비용. 각 주식은 적어도 자신의 비용만큼의 가치가 있어야 합니다: 각 주식이 별개의 메시지인 브리지 레일에서는 그 LayerZero 수수료, 로컬 레일에서는 전송 가스 중 그 주식의 몫. 자신의 메시지만큼의 가치가 없는 선물은 결코 진짜 전송에 묻어가지 않습니다
- 전송 전체. 통과한 주식들은 합쳐서 전송 전체와, 측정 구간의 주기가 아직 열리지 않았다면 그
개시(
openCycle)만큼의 가치가 있어야 합니다. 로컬 레일에서는 여덟 번째 감사 루프 이후로 개시를 한 번만 셉니다: 주기가 닫혀 있는 동안 얻은 전송 자체의 견적에 이미 그것이 들어 있으므로, 개시 트랜잭션의 기본 비용만 더합니다; 두 번 셌을 때는 비용보다 조금 더 가치가 있는 전송이 며칠씩 보류되었습니다. 그렇지 않으면 아무것도 나가지 않으며, 주기도 열리지 않습니다 - 기다리는 것. 나가지 않은 것은 볼트에 남아, 그 뒤에 쌓이는 것과 함께 이후의 측정 구간에 나갑니다. 전송이 기다리는 마켓은 주기 보고서에 세어지며, 측정 구간마다 한 번 로그에 남습니다
- 읽을 수 없는 것. 키퍼가 읽을 수 없는 가격, 수수료, 가스 비용은 이 규칙 이전처럼 전송을 통과시킵니다: 실패한 읽기가 진짜 전송을 막는 일은 결코 없습니다
KEEPER_AIRDROP_MIN_VALUE_BPS가 그 배수를 정합니다: 기본값은 10,000, 즉 비용의 1배이므로, 적어도
그 비용만큼의 가치가 있는 전송은 그보다 얼마나 크든 나갑니다; 더 큰 값은 더 작은 전송을 그
배수만큼의 가치가 될 때까지 보류하며, 0은 운반되는 것을 모두 보냅니다. 같은 규칙이 위의 따로
보관된 주식 탐색에서 볼트에 보낼 것이 있는지도 정합니다. 감사는 이를 권장 기본값으로 채택했습니다;
이 규칙은 주식이 언제 나가는지만 정할 뿐, 얼마를, 무엇을, 어디로 보낼지는 결코 정하지 않습니다.
Ethereum에서 전달의 가스
2026-10-06에 Sepolia는 Ethereum의 Glamsterdam 업그레이드를 활성화했으며, 이 업그레이드는 새 스토리지 슬롯의 가스를 예전의 약 다섯 배로 만들어, LayerZero 테스트넷 실행의 첫 주기에 있었던 에어드랍 전달은 모두 LayerZero의 익스큐터에서 가스가 바닥났습니다: 전송은 전달의 마지막 단계의 가스만 지불했고, 그 첫 단계의 가스는 주식의 LayerZero 어댑터의 오너(메인넷에서는 발행사)가 강제하는 값 그대로였습니다. 아홉 번째 감사 루프 이후로, 같은 날 창업자의 결정에 따라:
- 키퍼는 전송 전에 Ethereum에서 각 전달을 시뮬레이션합니다: 전달을 받는 주식의 토큰 컨트랙트와, 이를 적립하는 에어드랍 컨트랙트를, 전달이 만나게 될 상태 그대로 시뮬레이션합니다. 키퍼는 가장 무거운 주식의 필요량에 1.25를 곱한 값에서, 그 주식의 LayerZero 어댑터가 첫 단계에 이미 강제하는 가스를 뺀 값을 요청합니다
- 시뮬레이션을 실행할 수 없을 때는 그 마켓의 최근 전달이 Ethereum에서 쓴 값을, 그다음에는 리모트 허브의 기본값을 씁니다
- 리모트 허브가 이를 제한합니다. StockFun의 오너는 두 단계 각각에 대해 기본값, 하한, 상한을 정하며(Robinhood 레일 참고), 키퍼가 요청하는 값은 무엇이든 그 사이로 맞춰집니다. 상한 때문에 전달의 필요량보다 낮게 잘린 요청은 "관리자 대기"입니다: 전송은 그래도 나가며, 멈춘 전달은 다시 실행됩니다(다음 절)
- 홀더에게는 아무것도 바뀌지 않습니다. 키퍼가 정하는 것은 전달에 주는 가스뿐이며, 얼마를 보내는지, 어디로, 누구에게 보내는지는 결코 정하지 않습니다
- 필요할 때만 다시 고릅니다. 모든 주식의 시뮬레이션에서 고른 한 쌍은, 주식, 측정 구간 주기의 상태, 그리고 전달이 다음 측정 구간의 끝 이후에 도착할 수 있는지가 그대로인 동안 그 측정 구간 내내 유지됩니다; 시뮬레이션이 내놓지 못한 쌍은 바퀴마다 다시 고릅니다. 열 번째 감사 루프 이후로, 브리지가 운반할 수 없는 더스트만 담은 전송은 전달 환경의 어떤 것도 읽지 않으며, 측정 구간의 주기가 열린 뒤 시뮬레이션한 쌍은, 그 상태가 결코 되돌아가지 않으므로, 그 상태를 다시 읽지 않고 재사용됩니다
Ethereum에서 멈춘 전달
가스가 부족한 전달은 아무것도 잃지 않습니다: 누군가 더 많은 가스로 다시 실행할 때까지 Ethereum의 LayerZero 엔드포인트에 머물며, 그 첫 단계는 검증되었지만 실행되지 않았거나, 그 마지막 단계는 저장되었지만 실행되지 않은 상태입니다. 아홉 번째 감사 루프 이후로는 키퍼가 직접 이를 합니다:
- 키퍼는 매 주기 엔드포인트에서 각 전달의 단계를 읽습니다. 익스큐터의 차례가 끝나면(그 실패, 단
LayerZero 자체의 익스큐터에서 온 것만 믿음, 또는 10분 경과), 멈춘 단계를 자신의 Ethereum 키에서,
시뮬레이션한 필요량에 1.25를 곱한 가스로, 결코
KEEPER_AIRDROP_REEXECUTION_MAX_GAS(기본값 4,000,000)를 넘지 않게 다시 실행합니다 - 보낼 수 없는 단계(시뮬레이션이 실패하거나, 필요량이 한도를 넘거나, 키에 ETH가 부족함)에는 수동으로 실행하는 명령과 함께 알림이 한 번 가며, 매 주기 다시 시도됩니다
- 체인에서 다시 실패해 단계가 여전히 멈춰 있는 것이 두 번째 실패입니다: 명령과 함께 알림이 가며,
키퍼는 다시는 보내지 않습니다. 키퍼는 응답이 있는 단계 읽기에 근거해서만, 그리고 실패한 실행의
블록이 몇 블록 깊이가 된 뒤에만(
KEEPER_LOG_LAG_BLOCKS) 그렇게 판단하므로, 한 블록 뒤처진 노드도, 그 블록의 체인 재구성도 키퍼가 포기하게 만들 수 없습니다 - 이 실행들의 비용은 키퍼의 Ethereum 키가 지불합니다: Glamsterdam에서는 주기를 여는 마지막 단계에 약 100만 가스가 필요합니다
키퍼가 정하는 것은 시점과, 오너가 정한 범위 안에서의 전달 가스뿐이며, 그 밖에는 아무것도 없습니다. 다음 마감 시각 이후에 도착한 전송은 다음 날의 측정 구간을 기준으로 측정됩니다.
이 단계는 일곱 가지 설정으로 움직입니다: KEEPER_AIRDROP(기본값 켜짐),
KEEPER_AIRDROP_AFTER_SESSION(기본값 켜짐; 거래 시간을 무시하는 테스트넷에서는 꺼짐),
KEEPER_AIRDROP_FEE_MARGIN_BPS(1,000), KEEPER_AIRDROP_TRANSIT_MINUTES(60), 일곱 번째 감사 루프
이후의 KEEPER_AIRDROP_MIN_VALUE_BPS(10,000), 그리고 아홉 번째 이후의
KEEPER_AIRDROP_REEXECUTION_MAX_GAS(4,000,000)와 KEEPER_AIRDROP_LZ_EXECUTOR(비어 있음: 키퍼의
체인에 있는 LayerZero의 익스큐터로, 키퍼가 실패 알림을 믿는 유일한 호출자).
재시작: 상태 파일
2026-10-05부터 키퍼는 주기에서 다음 주기로 추적하는 것을 상태 파일(KEEPER_STATE_DIR)에 씁니다:
이동 중인 에어드랍 전달과 브리지 전송, 영수증을 기다리는 트랜잭션, 알림 에피소드와 연속 실패 기록;
여섯 번째 감사 루프 이후로는 키퍼가 지켜보는 정식 브리지 레일의 티켓, 각 전송의 탐색이 멈춘 곳,
그리고 각 볼트의 USDC가 마지막으로 나간 때도 포함됩니다; 일곱 번째 이후로는 웹훅이 아직 받지 않은
알림도 포함됩니다. 이전 버전의 키퍼가 쓴 파일은 그대로 불러옵니다. 매 주기와 매 전송 뒤에 파일을
쓰고, 시작할 때 한 번 읽습니다. 재시작해도 그중 어느 것도 잊지 않습니다: 재시작 뒤에 마지막 단계가
실패한 전달은 여전히 알림이 가고, 재시작 전에 보낸 트랜잭션은 결코 다시 보내지 않으며, 이미 알림을
보낸 에피소드는 두 번 알리지 않습니다. 다른 팩토리를 위해 쓰인 파일은 한쪽으로 치워 두며, 키퍼는
경고와 함께 빈 상태로 시작합니다. 여덟 번째 감사 루프 이후로, 다른 체인을 위해 쓰인 파일은 키퍼가
자신의 RPC가 서비스하는 체인을 읽을 때까지 먼저 그대로 남겨 두며, 읽지도 덮어쓰지도 않습니다:
RPC가 설정된 체인을 서비스하면 그 파일은 다른 체인의 것이었으므로 그때 한쪽으로 치워 두고; 그렇지
않으면 잘못된 것은 설정된 식별자이므로 키퍼는 시작을 거부하며, 식별자가 바로잡히면 키퍼는 지켜보던
입금을 포함해 자신의 파일을 되찾습니다. 그 전까지는 식별자를 잘못 입력하면 키퍼가 시작을 거부하기
전에 파일을 잃었습니다. 같은 루프 이후로 보관된 알림은 서로 다른 알림마다 하나입니다; 이전 버전의
키퍼가 쓴 파일은 그대로 불러옵니다. 아홉 번째 감사 루프 이후로 파일은 각 에어드랍 전달의 패킷과
키퍼가 그 전달을 실행한 기록, 그 마켓의 최근 전달이 쓴 가스, 그리고 각 정식 티켓에 대한 키퍼의
마지막 재실행도 보관합니다; 이전 버전의 키퍼가 쓴 파일은 여전히 그대로 불러옵니다.
2026-10-06부터:
- 각 전송은 기다리기 전에 디스크에 기록됩니다. 각 트랜잭션은 체인이 그 직전에 키퍼의 주소에 주는 nonce로 나가며, 해시가 돌아오는 즉시, 영수증을 기다리기 전에, 그 해시와 nonce와 함께 상태 파일에 기록됩니다: 기다리는 동안 종료된 키퍼는 재시작할 때 이를 다시 보내는 대신 그 해시로 읽습니다
- 잃어버린 트랜잭션은 포기됩니다. 보낸 뒤
KEEPER_RECEIPT_ALERT_MS의 네 배(기본값 2시간)가 지나도 어떤 노드도 더 이상 알지 못하는 트랜잭션은 알림 한 번과 함께 버려지므로, 더 이상 같은 종류의 전송을 막지 않습니다; 그 nonce는 여전히 비어 있으며, 키퍼의 다음 트랜잭션이 이를 씁니다. 아직 채굴되지 않은 트랜잭션에 대한 알림은 이를 대체하는 방법을 알려 줍니다 - 통째로 기록됩니다. 파일을 받아들일 수 없을 만큼 디스크가 가득 찬 것은 오류이며, 알림이 가고, 마지막 정상 파일이 남습니다; 그 전에는 잘린 사본이 이를 대체할 수 있었습니다
- 폴더마다 키퍼는 하나. 키퍼는 상태 폴더에 잠금 파일을 둡니다(
keeper.lock): 같은 폴더의 두 번째 키퍼는 시작을 거부하고 첫 번째 키퍼를 알려 줍니다. 키퍼가 더 이상 없는 잠금 파일은 넘겨받지만, 다른 호스트의 키퍼가 남긴 잠금 파일은 넘겨받지 않으며, 그 키퍼가 더 이상 실행되지 않을 때 운영자가 이를 삭제합니다. 드라이 런은 잠금 파일을 두지 않습니다
각 단계의 규모 정하기
2026-10-01부터 볼트는 잔액 전체가 아니라 키퍼가 지정한 금액을 전환합니다. 그 규모는 키퍼가 정합니다:
| 단계 | 금액 |
|---|---|
| ETH → USDC | 잔액 전체이며, 호가가 볼트의 한도에 미치지 못하는 동안 절반으로 줄이되, 전환 임계값 밑으로는 줄이지 않음; 2026-10-06부터 나머지는 다음 측정 구간을 기다림 |
| Ethereum의 Uniswap 풀에 있는 주식 | 그 주식의 배정분이며, 최대 네 번까지 절반으로 줄임; 그래도 미치지 못하는 레그는 배정분을 그대로 둔 채 다음 주기를 기다림 |
| 선택 사항인 Ondo 레일의 주식 | 그 주식의 배정분이며, 세션의 명목 금액 한도까지로 제한됨. 2026-10-06부터 Ondo가 볼트의 한도보다 낮게 가격을 매기는 레그는 결코 보내지 않음: 무료 견적을 어떤 어테스테이션보다 먼저 읽으며, 어테스테이션이 한도보다 낮게 돌아오면 다음 측정 구간까지 그 주식에 대해 더 이상 어테스테이션을 요청하지 않음 |
| Robinhood Chain의 주식 | 그 주식의 배정분이며, 주식별 고정 상한으로 제한되고, 풀을 측정할 수 있을 때는 그 풀 깊이의 일정 비율로도 제한됨 |
지출되지 않은 금액은 볼트에서 다음 주기(ETH의 경우 다음 측정 구간)를 기다리며, 주식의 현금은 계속 그 주식에 배정된 상태로 남습니다.
2026-10-01 보안 파이프라인 이후로, 주식을 매수하는 모든 레일에서 각 매수는 n을 주식의 수라고 할 때 바스켓의 마지막 주식에서 n−1 단위를 남겨 둡니다. 볼트는 비중 계산의 반올림 나머지를 마지막 주식에 주므로, 키퍼가 값을 읽은 시점과 트랜잭션 사이에 들어온 현금 몇 단위가 그 한 주식의 배정분을 최대 n−2 단위까지 줄일 수 있습니다. 읽은 금액을 그대로 지출하면 호출 전체가 실패할 것이며, 누구나 더스트 전송으로 그렇게 만들 수 있을 것입니다. 남겨 둔 금액은 다음 호출을 위해 배정된 상태로 남습니다. 볼트를 호출하는 그 밖의 모든 주체도 같은 여유분을 두어야 합니다. 문서화된 배정 규칙을 바꾸는 컨트랙트 측 수정은 오너의 결정을 기다리고 있습니다. 2026-10-06부터, 그 몇 단위의 USDC만 보유한 볼트(바스켓에는 최대 다섯 개의 주식만 담을 수 있으므로 많아야 네 단위)는 더 이상 그것 때문에 처리되지 않습니다: 그 USDC는 그 ETH가 가져올 다음 USDC를 기다립니다.
$STOCKFUN 소각도 같은 방식으로 규모를 정합니다: 가격 영향이 키퍼의 상한을 넘는 소각 분량은
절반으로 줄이되 키퍼의 임계값 밑으로는 줄이지 않으며, 나머지는 이후 주기에 소각됩니다.
2026-10-01 보안 파이프라인 이후로는 $STOCKFUN 풀이 전부 체결할 수 없는 분량도 절반으로
줄입니다. 이전에는 그 주기의 소각이 실패했습니다. 그 밖의 실패는 재시도하지 않습니다.
할 수 없는 일
바스켓 밖의 자산을 고르는 것. 한 주식에 배정된 현금을 다른 주식으로 옮기는 것. 오라클 한도를 완화하는 것. 컨트랙트가 보내는 곳이 아닌 다른 곳으로 자산을 보내는 것. 무엇이든 인출하는 것. 에어드랍을 받을 사람이나 에어드랍이 담는 수량을 고르거나, 자신에게 몫을 배정하는 것.
키퍼의 트리거에 접근 제한을 두는 것은 샌드위치 공격을 막기 위해서이지, 키퍼를 신뢰하기 때문이 아닙니다: 키퍼가 하는 모든 호출은 볼트의 한도 안에서 실행됩니다.
주기
NYSE 캘린더} C -->|아니오| W[대기] C -->|예| P{컨트랙트 일시 중지?} P -->|예| W P -->|아니오| A{이 측정 구간에서 볼트의
첫 확인: ≥ 0.1 ETH?} A -->|아니오, ETH는 다음
측정 구간을 기다림| N[다음 마켓] A -->|예| E[ETH → USDC, 50 bps 한도] E --> B[USDC → USDG → 브리지] B --> R{원격 체인에 도착?} R -->|아니오| M[모니터링, 티켓 재실행] R -->|예| K[주식 매수, 200 bps 한도] K --> AD[에어드랍 컨트랙트로 전송] AD --> N
다이어그램의 시각, 임계값, 한도는 기본 설정입니다. 키퍼의 루프는 5분마다 돕니다; 볼트의 ETH는 측정 구간마다 한 번, 닫힌 뒤 루프를 처음 돌 때 확인되며, 볼트가 이미 보유한 USDC는 각 전환 뒤에 한 번, 그 밖에는 측정 구간마다 많아야 한 번 이어서 처리됩니다. 에어드랍 컨트랙트로의 전송은 측정 구간마다 한 번, 기본값으로 거래 시간이 끝난 뒤 실행됩니다; 채무, LP 수수료, 전달, 그리고 여섯 번째 감사 루프 이후로는 정식 브리지 레일의 티켓이 거래 시간과 상관없이 매 주기 처리됩니다.
설정
모든 것은 환경 변수로 설정합니다: 두 체인의 RPC, 키퍼의 개인 키, 컨트랙트 주소, 실행 간격, 그리고
안전 장치 — 드라이 런, 1회 실행, 시장 개장 시에만 실행. 2026-10-05부터는 다음도 있습니다: 위에서
본 에어드랍 단계의 설정; LP 수수료 수취, KEEPER_COLLECT_FEES(기본값 켜짐),
KEEPER_COLLECT_FEES_HOURS(24), KEEPER_COLLECT_FEES_MIN_WEI(0); 반복된 제외 뒤의 알림,
KEEPER_BRIDGE_ALERT_AFTER_SKIPS(3); 그리고 상태 파일의 폴더, KEEPER_STATE_DIR.
2026-10-06부터는 KEEPER_CONVERT_ONCE_PER_WINDOW(기본값 켜짐)도 있습니다: 볼트의 ETH는 측정
구간마다 한 번 전환되며, 여섯 번째 감사 루프 이후로는 그 USDC가 각 전환 뒤에 한 번, 그 밖에는 측정
구간마다 많아야 한 번 나갑니다; 로컬이나 테스트넷 실행은 이를 끄고, 루프를 돌 때마다 전환, 매수,
브리지를 실행합니다. 여섯 번째 감사 루프 이후로는 KEEPER_TICKET_ALERT_HOURS(6)도 있습니다: 정식
브리지 레일에서, 실행되었는지 알 수 없는 티켓에 대해 알림을 보내기까지의 시간입니다. 팩토리가 새
볼트를 위해 지정하는 라우터인 KEEPER_STOCK_ROUTER는 어느 시장의 거래 시간이 주기를 허용하는지만
정합니다; 각 볼트는 자신의 라우터에서 견적됩니다.
일곱 번째 감사 루프 이후로 체인 ID인 KEEPER_CHAIN_ID와 KEEPER_REMOTE_CHAIN_ID는 첫 주기 전에
RPC와 대조됩니다: 메인넷에서는 1과 4663, 테스트넷에서는 11155111과 46630입니다. 여덟 번째 감사
루프 이후로는 둘 다 설정해야 합니다: KEEPER_CHAIN_ID는 항상, KEEPER_REMOTE_CHAIN_ID는 브리지를
쓸 때 필요하며, 이 값들이 없으면 키퍼는 시작을 거부합니다(그 전까지는 빠져 있으면
KEEPER_CHAIN_ID는 11155111, KEEPER_REMOTE_CHAIN_ID는 4663으로 읽혔습니다). 열 번째 감사 루프
이후로 KEEPER_LOG_MAX_SELECTORS(기본값 1,000, 최소 5)는 로그 요청 하나가 지정하는 주소와 이벤트
값의 수를 Robinhood Chain의 노드가 세는 방식대로 제한합니다; 주소 10개 이상을 거부하는 Sepolia의
공개 엔드포인트 뒤에서는 10으로 설정됩니다. 두 설정이 로그 탐색이 최신 블록보다 몇 블록 아래에서
멈출지를 정합니다: KEEPER_LOG_LAG_BLOCKS(Ethereum에서 2)와
KEEPER_REMOTE_LOG_LAG_BLOCKS(Robinhood Chain에서 12); 여덟 번째 감사 루프 이후로 앞의 것은
키퍼가 브리지 배치를 추적하기 전에 그 블록이 얼마나 깊어야 하는지도 정하며, 뒤의 것은 티켓을 최신
블록보다 몇 블록 아래에서 읽을지도 정합니다. 오라클의 가드에는 설정이 필요 없습니다: 키퍼는 각
미러 볼트의 오라클에서 이를 읽습니다. 비워 둔 정수 설정은 그 기본값을 쓰며, 범위를 벗어난 설정은
시작할 때 키퍼를 멈춥니다.
키퍼의 키는 핫 키이며, 가스비만큼만 자금이 들어 있고, 배포자의 키와는 다릅니다.
한계
- USDG 레일에서는, 키퍼가 시작되기 전이나 멈춰 있는 동안 현금 토큰이 거부한 전달은 스윕할 때 경고로만 나타납니다; 정식 브리지 레일은 그런 몫을 리모트 허브 자체의 상태에서 찾아냅니다
- 아홉 번째 감사 루프 전까지 키퍼는 실패한 전달의 마지막 단계(
lzCompose)를 직접 실행하지 않았습니다; 그 이후로는 멈춘 단계를 한도 안에서 다시 실행하며, 두 번째 실패는 명령과 함께 사람에게 맡겨집니다 - 긴급 감시는 재시작하면 어디까지 읽었는지를 유지하지 않습니다: 키퍼가 멈춰 있는 동안 발생한 이벤트는 스캔되지 않습니다
- 전송과 그 상태 파일 기록 사이의 몇 밀리초 동안 종료된 키퍼는 재시작할 때 그 트랜잭션을 한 번 더 보낼 수 있습니다; 컨트랙트 자체의 검사로 그런 반복은 대부분 실패하며, 그 가스비가 듭니다
- 전송의 도착을 찾는 탐색은 결코 같은 블록을 두 번 읽지 않습니다: 도착을 이미 읽은 블록으로 옮기는 체인 재구성이 일어나면, 그 전송은 이동 중 알림에 맡겨집니다. 일곱 번째 감사 루프 이후로 모든 로그 탐색은 최신 블록보다 몇 블록 아래에서 멈추므로, 적립과 그 이동 중 알림이 그 몇 블록만큼 늦어집니다
- 여덟 번째 감사 루프 이후로 브리지 배치는 그 블록이 몇 블록 깊이가 된 뒤에 추적되며, 이는 예전보다 한 주기 늦고, 다음 배치는 이를 기다립니다; 그보다 깊은 체인 재구성은 로그 탐색과 마찬가지로 다루지 않습니다. 마지막 몇 블록 안에 실행된 티켓은 최신 블록이 그 실행보다 그만큼 지나갈 때까지 그곳에서 여전히 유효하게 읽힙니다: 바쁜 체인에서는 다음 주기, 한산한 체인에서는 더 많은 주기 뒤입니다. 아홉 번째 감사 루프 이후로, 키퍼 자신의 재실행이 삭제한 티켓은 그동안 다시 재실행되지도, 알림을 일으키지도 않으며, 재시작 뒤에도 마찬가지입니다; 그 블록들 안에서 다른 누군가가 재실행한 티켓은 다시 시도되고, 시뮬레이션에서 실패하며, 알림 시간이 지나면 여전히 유효한 것으로 알림이 갈 수 있습니다
- 키퍼가 자신의 마지막 트랜잭션의 블록에서 읽는 그 1분은, 다른 노드들보다 1분 넘게 뒤처진 노드를 다루지 않습니다
- 로컬 레일에서는 측정 구간의 개시를 한 번만 세며, 두 번째 트랜잭션이 되풀이하는 부분(비용의 약 5 %)은 넣지 않습니다: 전송은 그 비용보다 최대 그만큼 낮은 가치로도 나갈 수 있습니다
- 열 번째 감사 루프 이후로, 거래 시간이 끝난 뒤 어떤 매수와도 별개로 볼트에 주어진 주식은 다음 거래 시간의 전송과 함께 더 나중의 측정 구간으로 갑니다: 키퍼는 보낼 것이 없는 볼트를 다음 거래 시간에야 다시 읽기 때문입니다. 키퍼가 거래 시간의 마지막 바퀴에서 영수증을 읽은 매수가, 거래 시간이 끝난 뒤 첫 바퀴에서 그보다 뒤처진 노드에서는 비어 있는 것으로 읽힐 수도 있는데, 그러려면 바퀴 사이의 간격이 그 노드의 지연(Sepolia에서 약 5초)보다 짧아야 하며, 기본값인 5분으로는 결코 그렇지 않습니다
사전 점검
실제 실행 전에, 사전 점검 명령은 트랜잭션을 하나도 쓰지 않고 다음을 확인합니다: 네트워크가 응답하는지, 주소에 코드가 있는지, USDG OFT의 LayerZero 피어가 일치하는지, USDG가 발행사인 Paxos에 의해 일시 중지되지 않았는지, 바스켓 비중이 올바른지.
여덟 번째 감사 루프 이후로 사전 점검은 오라클의 가드도 확인합니다. 그 매니페스트는 시퀀서 검사가
어떻게 설정되는지를 어느 쪽이든 밝혀야 합니다: 업타임 피드도 유예 기간도 없음(지금처럼 검사가
꺼짐), 또는 유예 기간이 있는 피드. 피드가 있으면 이를 읽고, 각 주식 토큰의 오라클 일시 중지를
읽습니다: 기업 행위 중인 토큰은 경고이며, 확인을 실패시키지 않습니다; 그 신호가 없는 토큰은
실패합니다. 배포 뒤에는 배포된 오라클이 매니페스트와 일치하는지, 그 시퀀서 상태가 가격을
통과시키는지, 모든 주식의 일시 중지 검사가 켜져 있는지 확인합니다. 키퍼의 inspect 명령은 같은
가드를 출력합니다: 시퀀서 검사와 그 상태, 그리고 각 주식의 일시 중지 검사와 그 토큰이 지금 일시
중지하는지.
아홉 번째 감사 루프 이후로 사전 점검은 LayerZero 테스트넷 실행의 배포(Sepolia와 Robinhood Chain의 테스트넷)에서도, 그 실행의 두 파일로부터, 그 체인 쌍에 맞게 이름 붙인 RPC로 동작하므로, 메인넷 RPC에 테스트넷에 대해 묻는 일은 결코 없습니다; 섞인 것을 포함해 그 밖의 체인 쌍은 거부됩니다. 테스트넷에서는 그 체인에 없는 Uniswap v3 라우터를 읽지 않으며, ETH/USD 피드를 그 실행의 오라클에 주어진 하트비트에 맞춰 확인합니다. 두 쌍 모두에서, 원격 라우터가 매니페스트에 적힌 v3 라우터를 지정하는지 확인합니다. 테스트넷 배포에서 실행한 결과: 확인 68개, 모두 통과.
RPC가 없으면 거짓 성공을 보고하는 대신 실행을 거부합니다.