본문으로 건너뛰기
전체 글

하드포크 없이 소유기간을 기록하다: SecurityToken.sol 설계 리뷰

· 약 9분
Bankware Global Engineering

토큰을 한 달 내내 가진 사람과 정산 직전에 산 사람에게 같은 수익을 나눠도 될까. 앞선 글에서는 이 질문을 소유 이력과 기간가중 권리의 관점에서 살펴봤다. 잔액이 바뀐 지점만 기록하면 그 사이에 얼마를 얼마나 오래 보유했는지 다시 계산할 수 있다는 아이디어였다.

그 글을 쓴 뒤 2023년의 코드를 다시 펼쳤다. 수식보다 먼저 존재했던 구현이었다. nigo-protocol-v0의 SecurityToken.sol은 토큰 잔액의 변경 이력으로 정해진 블록 구간의 수익을 계산하는 프로토타입이다. 기존 체인을 하드포크하지 않고도 이 생각을 스마트컨트랙트로 표현할 수 있었다. 작은 계산부터 따라가면 당시 구현이 보여준 가능성과 다음 설계에 남긴 과제를 함께 볼 수 있다.

이 글은 2023년 5월 프로토타입인 SecurityToken.sol, commit af4cb29을 읽는 회고다. 해당 코드의 전용 테스트·migration, 공개 배포 주소와 보안감사 근거는 확인하지 못했다. implemented는 역사적 코드가 있다는 뜻이며 운영 검증이나 현재 nigo-protocol 탑재 기능을 뜻하지 않는다. 컨트랙트 이름과 법적 증권성도 별개다.

A에게 70, B에게 30을 나누는 예​

총발행량이 기간 내내 100토큰이고, 15번부터 24번 블록까지 발생한 수익 100단위를 나눈다고 하자. 수익은 별도의 지급 자산으로 예치하며, 여기서 1단위는 그 자산의 최소 정수 단위다. 토큰 수와 지급 자산의 단위는 다르다. 양 끝을 포함해 10블록이므로 블록당 나눌 수익은 10이다.

A는 10번 블록부터 100토큰을 가지고 있다가 20번 블록에 60토큰을 B에게 보낸다. 추가 발행이나 소각은 없고, 수익 기간이 끝난 뒤 청구한다고 가정한다.

표의 잔액과 수익은 각각 A, B 순서로 표시했다.

구간 (블록 수)잔액 A · B수익 A · B
15~19 (5)100 · 050 · 0
20~24 (5)40 · 6020 · 30
합계 (10)—70 · 30

첫 5블록의 수익 50은 A가 모두 받고, 뒤의 5블록 수익 50은 A와 B가 40:60으로 나눈다. 합계는 예치액 100과 같다. 마지막 잔액만 보았다면 A=40, B=60으로 나눴겠지만 이 계산은 A가 먼저 보유한 기간도 반영한다.

이 결과를 얻으려고 모든 블록의 잔액 복사본을 저장할 필요는 없다. A의 잔액이 바뀐 두 지점만 있으면 된다.

기록하는 것은 매 블록의 잔액이 아니라 변경점​

체크포인트는 ‘어느 블록에서 잔액이 얼마로 바뀌었는가’라는 기록이다. 예제에서는 A의 기록이 (10, 100), (20, 40)이고 B는 (20, 60)이다. 첫 기록 전의 잔액은 0이다. A가 10~19에는 100토큰, 20부터 다음 변경 전까지는 40토큰을 가졌다고 재구성할 수 있다.

원본은 외부 mint와 전송 경로에서 잔액을 먼저 바꾸고 해당 주소의 블록 높이와 변경 후 잔액을 기록한다. 내부 _burn도 같은 기록 함수를 호출하지만 이를 호출하는 외부 함수는 없다. 상태 모델만 의사코드로 쓰면 다음과 같다.

onBalanceChanged(holder, newBalance):
balances[holder] = newBalance
balanceAt[holder][currentBlock] = newBalance
changedBlocks[holder].append(currentBlock)

상태를 설명하는 코드이며 동일 블록 병합·권한·지급 안전성을 완성한 구현은 아니다. 저장량은 체인의 전체 블록 수보다 주소별 잔액 변경 횟수에 비례한다. 다만 원본은 같은 블록의 반복 변경도 배열에 추가한다.

체크포인트의 잔액은 트랜잭션 처리 뒤의 잔액이다. 같은 블록에서 여러 번 전송하면 블록 끝의 최종 잔액을 쓸지, 거래 순서까지 권리에 반영할지 정해야 한다. 블록 번호 역시 벽시계 시간이 아니라 원장의 순서다. 일·월 단위의 실제 수익을 블록 구간에 연결하는 정책은 별도로 필요하다.

보유 구간과 수익 기간이 겹치는 만큼 계산한다​

원본의 수익 기록 Income에는 시작 블록, 종료 블록, 블록당 수익, 기준 총발행량이 들어간다. 예치액을 DD, 수익 구간의 양 끝을 S,ES,E라 하면 블록당 수익 rr은 다음과 같다.

r=⌊DE−S+1⌋r=\left\lfloor\frac{D}{E-S+1}\right\rfloor

바닥 기호 ⌊ ⌋\lfloor\ \rfloor는 정수 나눗셈의 버림을 뜻한다. 예제에서는 100/(24−15+1)=10100/(24-15+1)=10이다. 이후 잔액이 일정한 각 구간에서 보유량 bb, 겹치는 블록 수 LL, 고정 기준 공급량 TT를 사용한다.

구간 수익=⌊b r LT⌋\text{구간 수익}=\left\lfloor\frac{b\,r\,L}{T}\right\rfloor

A의 두 구간을 대입하면 같은 70이 나온다.

PA=100×10×5100+40×10×5100=50+20=70\begin{aligned} P_A&=\frac{100\times10\times5}{100} +\frac{40\times10\times5}{100}\\ &=50+20=70 \end{aligned}

원본은 [15,24]처럼 양 끝을 포함하는 블록 구간을 쓴다. 앞선 글의 반열린 구간 [15,25)와 같은 블록들을 가리킨다. 어느 표기를 택하든 끝점을 포함하는지 계산 전체에서 일관되게 지켜야 한다.

나눗셈 순서도 결과의 일부다. 예치액이 100 대신 103최소 단위라면 첫 나눗셈에서 블록당 10만 배정돼 3이 남는다. 보유자별 나눗셈에서도 별도 잔여금(dust)이 생길 수 있다. 최종식에서 한 번만 나누면 원본과 다른 계산이 되므로 남은 금액의 귀속과 이월 정책까지 정의해야 한다.

지급은 보유자가 나중에 자신의 몫을 청구하는 pull 방식이다. 지급자가 모든 주소를 한 번에 열거할 필요가 없다. 보조 컨트랙트 SecurityTokenPublisher.sol은 승인과 초기 청약 배정을 담당하지만 기간 계산의 핵심은 이 체크포인트와 청구 로직이다. 기간가중 표결권이나 기여도 장부는 이 코드의 구현 범위가 아니다.

코드에서 확인한 사실과 다음 구현의 조건​

70을 계산할 수 있다는 사실만으로 정산 프로토콜이 완성되지는 않는다. 실제 자금은 한 번만 지급돼야 하고 실패한 전송이 성공한 정산으로 남아서도 안 된다. 원본의 상태를 보면 어떤 보장이 더 필요한지 구체화된다.

원본에서 관찰한 동작정산에 미치는 영향다음 구현에서 정할 것
저장 offset부터 청구하고 마지막에 incomes.length - 1을 저장마지막 처리 위치와 다음 미처리 위치를 혼동하면 완료 캠페인을 다시 처리할 수 있음nextClaimIndex와 캠페인별 지급 상태
같은 블록의 잔액은 덮어쓰고 블록 번호 배열에는 계속 추가끝이 시작보다 앞선 구간을 만들어 정수 뺄셈에서 청구가 되돌아갈 수 있음블록 종료 잔액 정책이면 마지막 체크포인트 갱신
수익 분모로 호출자의 _totalSupply 입력을 저장실제 적격 공급량과 일치한다는 보장이 별도로 필요고정 공급 검증 또는 공급량 이력
전송 반환값 확인 없이 지급 후 offset 갱신외부 호출·실제 수령액·청구 상태의 일치가 필요안전한 전송, 실제 입금액 회계, 상태 갱신 순서와 재진입 방지

근거는 원본의 _balanceSnapshot, depositIncome, withdrawIncome에서 확인할 수 있다. 표의 개선안은 원본이 이미 제공하는 보장이 아니다.

종료 전 청구를 허용할지도 별도 선택이다. 완료된 기간만 한 번 청구하게 하거나 부분 청구라면 어디까지 지급했는지 보존해야 한다. 캠페인 생성 권한과 중단 정책을 정하고 누적 지급액이 실제 예치액을 넘지 않는다는 불변식을 테스트해야 한다.

비용도 남는다. 원본은 미처리 수익 캠페인마다 보유자의 체크포인트를 순회한다. 캠페인 100개와 체크포인트 1,000개면 약 10만 번의 구간 검사가 필요할 수 있다. 이는 작업량 예이며 가스량 측정 결과는 아니다. pull 방식은 전체 보유자 열거를 피하지만 한 사람의 청구 비용까지 제한하지는 않는다.

컨트랙트 안에서도 더 단단하게 만들 수 있다​

우선 체크포인트에 직전 변경점까지의 누적 잔액 × 블록 수를 함께 기록할 수 있다. 공급량이 고정돼 있다면 시작과 끝의 누적값 차이로 보유량 면적을 구하고 정렬된 체크포인트는 이진 탐색으로 찾는다. 청구할 캠페인 수를 한 번에 제한하고 실제 입금·지급·잔여금 회계를 함께 관리하는 개선도 가능하다.

공급량이 바뀌면 한 단계가 더 필요하다. 다음은 원본 예제와 별개로, 같은 길이의 두 구간에서 블록당 수익은 같고 A의 잔액은 10인데 전체 공급량만 늘어나는 경우다.

구간A 잔액총발행량A 지분
앞 절반1010010%
뒤 절반102005%

구간별 지분의 평균은 7.5%다. 그러나 누적 잔액과 누적 공급량을 각각 더한 뒤 나누면 (10+10)/(100+200)≈6.67%(10+10)/(100+200)\approx6.67\%가 된다. 따라서 누적값 두 개를 나누는 것으로 일반화할 수 없다. 잔액과 공급량의 변경 구간을 맞춰 지분을 계산하거나 보상이 생길 때 단위 지분당 누적 보상을 갱신하는 별도 인덱스가 필요하다. 이 역시 다음 구현을 위한 선택지다.

하드포크 없이 가능했기에 원장에 묻게 된 것​

사용한 것은 EVM의 일반 저장소, block.number와 ERC-20 호출이다. 새로운 명령이나 합의 규칙 없이 신규 토큰에 권리 계산을 넣어 실험할 수 있다는 점은 컨트랙트 계층의 장점이었다.

이미 배포된 ERC-20 전체에 자동 적용되는 것은 아니다. 업그레이드·이주·래핑 같은 통합이 필요하고 통합 이전 이력이 이 컨트랙트에 저절로 생기지도 않는다. 기록되는 것도 주소별 잔액이지 수탁사 하위 장부의 고객 지분이나 법적 실소유자까지는 아니다. 여기서 소유기간의 proof는 새로운 암호학적 증명이 아니라 온체인 기록으로 보유 이력을 감사 가능하게 재구성한다는 뜻이다.

이렇게 자산별로 이력을 기록하다 보면 여러 토큰이 같은 저장 구조와 조회 규칙을 반복한다는 점도 보인다. 이것을 원장의 공통 기능으로 다루면 무엇이 달라질까.

항목컨트랙트 구현원장 공통 기능
도입기존 EVM에 신규 토큰으로 배포프로토콜 변경 또는 새 네트워크 필요
정책자산별로 빠르게 실험범용적이고 보수적인 규칙 필요
저장토큰별 체크포인트 구조공통 기록 기능으로 최적화할 여지
조회자산마다 계산·인덱싱 설계표준 과거 잔액 조회·증명 제공 가능
실패 범위주로 해당 컨트랙트합의와 다른 자산에도 영향 가능

공통 기능이 된다고 자산별 데이터 자체가 사라지지는 않는다. 저장량·조회 비용 개선도 실제 측정해야 한다. 블록 종료 상태를 쓸지, 어느 최종 확정 높이부터 인정할지, 오래된 이력을 정리한 뒤 누가 원자료와 증명을 보관·제공할지까지 책임이 원장 쪽으로 옮겨간다.

더 나은 컨트랙트나 오프체인 인덱서도 대안이다. 이 기능 하나가 새 블록체인을 필연적으로 요구하지는 않는다. 다만 컨트랙트로 직접 표현해 봤기 때문에 “모든 토큰이 반복해서 다루는 이력을 원장이 이해하면 어떨까”라는 다음 질문을 구체적으로 할 수 있었다.

이 문제의식은 훗날 nigo-protocol을 구상한 여러 계기 중 하나가 됐다. 이번 회고가 기록하려는 것은 현재의 기능이나 일정이 아니라 작은 상태 모델이 공통 인프라에 대한 질문으로 이어진 그 과정이다.

부록: 구간 계산과 검증 범위​

일반식과 남아 있는 검증 과제

보유 구간을 [sj,ej][s_j,e_j], 수익 구간을 [S,E][S,E]라 두면 겹치는 블록 수 LjL_j는 다음과 같다.

Lj=max⁡(0,min⁡(ej,E)−max⁡(sj,S)+1)PA=∑j⌊bj r LjT⌋\begin{aligned} L_j&=\max\bigl(0,\min(e_j,E)\\ &\qquad-\max(s_j,S)+1\bigr)\\ P_A&=\sum_j\left\lfloor\frac{b_j\,r\,L_j}{T}\right\rfloor \end{aligned}

이 식은 고정 기준 공급량과 양 끝을 포함하는 구간을 사용한다. 총 보유 면적을 먼저 더해 한 번만 나누면 원본의 구간별 정수 버림과 차이가 날 수 있다. 예제에서는 나눗셈이 모두 나누어떨어져 차이가 없다.

2026-09-07 보강에서는 고정 commit의 소스와 A=70·B=30·합계=100, 103단위 예치 시 첫 단계 잔여금 3, 공급 변화 반례를 대조했다. 컨트랙트를 배포·실행하거나 가스·운영 보안을 검증한 것은 아니다. 실제 개선 구현에는 동일 블록 전송, 종료 전·반복 청구, 공급량 변화, 전송 실패와 실제 입금 차이, 청구 분할의 경계 테스트가 필요하다.

오프체인 계산 후 Merkle root에 결과를 확정하고 각 보유자가 자신의 포함 증명으로 청구하게 할 수도 있다. 계산 비용을 옮기는 대신 원자료 공개, 계산 재현과 이의제기 절차를 설계해야 한다. 어느 계층을 골라도 정산의 책임은 남는다.