Sepolia 수동 테스트 계획
이 페이지는 지갑을 갖춘 사람이 Sepolia 테스트 네트워크에서 직접 손으로 수행하는 체크리스트입니다. 각 사례에는 보고서에서 인용할 수 있는 T-01 형식의 고정 번호가 붙습니다.
시작하기 전에. 이 계획은 새 버전을 대상으로 합니다. 저장소의 공개 manifest는 LP 수수료
100인 운영 중인 Sepolia 배포를 설명하며, 이 계획은 그 배포에 적용됩니다. 테스트하는 버전에 해당하지 않는 사례는 “실패”가 아니라 “버전 범위 외”로 기록합니다.
인용한 값은 contracts/src/ 아래의 새 버전 코드와 2026년 9월 14일 설계 결정에서 가져왔습니다. 확인하지 못한 동작에는 “테스트 중 확인 필요” 표시가 붙습니다.
이 계획 사용법
각 섹션은 사례를 6열 표로 제시합니다. 마지막 두 열은 인수 검증 중에 채웁니다.
| 열 | 기록할 내용 |
|---|---|
| 사례 | 고정 식별자, T-01부터 T-110까지 |
| 사전 조건 | 시작 전에 충족되어야 하는 조건 |
| 단계 | 순서대로 수행할 동작 |
| 예상 결과 | 코드와 결정이 예정하는 결과 |
| 관찰 결과 | 실제로 일어난 일, 트랜잭션 hash 또는 읽기 블록 포함 |
| 심각도 | 부합하면 비움, 아니면 차단, 중대, 경미, 외관 중 하나 |
| 심각도 | 기준 |
|---|---|
| 차단 | 자금 손실 또는 동결, 세금 미징수, 매수벽 손실, 원금에서 보상 지급 |
| 중대 | 소스와 반대되는 동작, 누락된 거절, 잘못 표시된 수치 |
| 경미 | 체인에 영향이 없는 표시 차이, 부정확한 메시지 |
| 외관 | 타이포그래피, 레이아웃, 문구 |
거절 사례에 따라 거절된 트랜잭션은 성공입니다. hash는 트랜잭션이 제출되었다는 뜻일 뿐이며 성공 여부는 영수증만 알려 줍니다.
일부 사례는 인터페이스를 거치지 않습니다. 현재 소스에서 dapp은 운영 중인 버전의 ABI를 읽으며, exact-output 주문도, claimTeam()도, 매수벽 상세 읽기도 제공하지 않습니다. 이런 사례에는 “직접 호출” 표시가 붙으며, 테스트하는 배포의 manifest 주소를 대상으로 컨트랙트 호출 도구로 실행합니다.
1. 준비
첫 스왑 전에 다음 항목을 기록하세요. 이후 모든 비교의 기준이 됩니다.
- 네트워크: Sepolia, 체인 ID
11155111. - 테스트하는 배포의 manifest에서 읽은 주소: 토큰, hook, PoolManager,
poolId, 라우터, Lens, V2 레지스트리, Vault, 런처, 그리고 런치패드 추가 후에는 거버넌스 vault와 Forge. 메모에 개인 키를 절대 옮겨 적지 마세요. - 고정 매개변수:
fee,tickSpacing,LAUNCH_ETH,MIN_POOL_SUPPLY,launchTimestamp,moduleRevision. - 초기 상태: Lens의
snapshot(), hook의band(), 토큰의totalSupply()와totalBurned(),pendingFloorEth,pendingAbsorbedTokens,wallCount(),activeWallCount(),teamAccrued,teamPaidCumulative, Vault의rewardReserve(), 그리고 읽기 블록 번호.
교환 금액 외에도 테스트 ETH를 준비하세요. 모든 트랜잭션이 gas를 내며, 여러 사례에서 거절되는 트랜잭션이 필요한데 이것도 gas를 소모합니다.
| 사례 | 사전 조건 | 단계 | 예상 결과 | 관찰 결과 | 심각도 |
|---|---|---|---|---|---|
| T-01 | 지갑 설치됨 | Sepolia 선택, 테스트 버전에 연결된 dapp 열기 | 네트워크가 인식됨, 다른 체인에서 서명하라는 요청 없음 | ||
| T-02 | 새 계정 | Sepolia faucet으로 테스트 ETH 충전 | 새로고침 후 지갑과 dapp에 잔액 표시 | ||
| T-03 | manifest를 옆에 둠 | 표시된 각 주소를 manifest의 주소와 비교 | 식별자 동일, poolId, fee, tickSpacing 일치 |
||
| T-04 | 아직 스왑 없음 | snapshot()을 읽고 블록 기록 |
기준값 기록, 블록 식별 | ||
| T-05 | 직접 호출 | hook의 poolKey() 읽기 |
currency0은 영 주소, fee = 100, tickSpacing = 10, hooks는 hook 주소 |
||
| T-06 | V2 레지스트리 읽기 가능 | moduleRevision()과 모듈 주소 읽기 |
개정 기록, 인수 검증 도중 변경되면 서명마다 다시 확인 필요 |
2. 출시와 밴드
풀 초기화 때 hook은 예치 전액을 BAND_SALT로 식별되는 단일 포지션 [minUsableTick, tickUpper]에 배치합니다. tickUpper는 개시 tick을 간격 단위로 내림한 값이므로 포지션에는 CUBIT만 들어 있습니다. hook은 MIN_POOL_SUPPLY, 즉 공급량의 80 %보다 적은 예치를 거절하고 반올림 잔량을 소각합니다.
전체 출시는 같은 트랜잭션에서 공급량의 20 %를 Vault 준비금에 넣고 0.1 ETH 매수도 실행합니다.
| 사례 | 사전 조건 | 단계 | 예상 결과 | 관찰 결과 | 심각도 |
|---|---|---|---|---|---|
| T-07 | 출시 완료, 직접 호출 | band() 읽기 |
lower = −887 270, upper는 개시 tick을 10의 배수로 내림한 값으로 FDV 3.75 ETH에서 155 390, 유동성은 0이 아님 |
||
| T-08 | 출시 트랜잭션 확인됨 | 그 이벤트 읽기 | BandBootstrapped(lower, upper, liquidity, tokens)가 band() 값과 함께 한 번 발생, tokens는 예치액에서 반올림 잔량을 뺀 값 |
||
| T-09 | 첫 매수 없는 출시 | 스왑 전에 snapshot()에서 bandEth와 bandTokens 읽기 |
bandEth = 0, bandTokens는 반올림 오차 범위에서 예치액과 같음 |
||
| T-10 | 출시 완료, 직접 호출 | token.balanceOf(hook) 읽기 |
제3자의 직접 전송이 없으면 0: 초기화 후 hook은 토큰 그대로의 CUBIT을 보유하지 않음 | ||
| T-11 | 직접 호출 | 풀에 유동성 추가 시도 | ExternalLiquidityForbidden으로 거절: hook이 유일한 유동성 공급자 |
||
| T-12 | 리허설 배포 | MIN_POOL_SUPPLY보다 적은 예치로 풀 초기화 |
SupplyNotDeposited로 거절, 밴드 배치 없음 |
||
| T-13 | 전체 출시 | 출시 트랜잭션 후 Vault의 rewardReserve() 읽기 |
준비금은 공급량의 20 %, 즉 420만 CUBIT, 팀 할당도 에어드롭도 없음 | ||
| T-14 | 전체 출시 | 출시 트랜잭션에서 배포자 매수 읽기 | 0.1 ETH 매수, 팀 몫 3 %가 담긴 BuyTaxed, 받은 CUBIT은 자유롭게 전송 가능 |
||
| T-15 | 매수 확인됨, 직접 호출 | 밴드의 가상 준비금으로 예상 출력 재계산 | 3 % 세금과 LP 수수료 적용 후 x·y=k 곡선과 일치하는 출력, 처음에는 1,680만 CUBIT 대 가상 3 ETH — 테스트 중 확인 필요 | ||
| T-16 | 매수 후 매수한 CUBIT 재매도 | 매수 전, 두 거래 사이, 매도 후에 bandEth 읽기 |
bandEth는 매수 때 늘었다가 시작값 쪽으로 돌아옴, 매수로 실제 들어온 ETH를 넘지 않음 |
3. 매수와 매도 절차
라우터는 swapExactIn(key, zeroForOne, amountIn, amountOutMin, recipient, deadline)과 swapExactOut(key, zeroForOne, amountOut, amountInMax, recipient, deadline)을 제공합니다. zeroForOne = true는 ETH로 CUBIT을 매수합니다.
현재 dapp 소스에서 인터페이스는 exact-input만 사용합니다. 사용자는 항상 지불할 금액을 입력하며 받을 수량은 읽기 전용 필드입니다. 따라서 exact-output 절차는 라우터를 직접 호출해 테스트해야 합니다.
매수는 msg.value로 네이티브 ETH를 보냅니다. 매도는 value 0을 보내며 라우터에 대한 CUBIT ERC-20 승인(approval)이 필요합니다. 인터페이스는 무제한이 아니라 정확한 금액만큼 승인을 요청하므로, 더 큰 매도에는 새 승인이 필요합니다.
라우터는 부분 체결을 거절합니다. 완전히 소모되지 않은 exact-input은 IncompleteInput을, 완전히 제공되지 않은 exact-output은 InsufficientOutput을 발생시킵니다. 세금은 요청 금액 기준으로 산정되므로 취소가 사용자를 보호합니다.
dapp 소스에서 읽은 인터페이스 설정이며 인수 검증 중에 확인해야 합니다. 슬리피지 허용치 기본값 1.0 %, 입력은 소수점 둘째 자리와 0부터 99.99까지의 범위로 제한, 최소 수령량은 정수로 계산해 wei 단위로 올림, 견적 유효 시간 30초, 온체인 기한 = 체인 timestamp + 120초 − 견적 경과 시간.
| 사례 | 사전 조건 | 단계 | 예상 결과 | 관찰 결과 | 심각도 |
|---|---|---|---|---|---|
| T-17 | ETH 잔액 충분 | 소액 매수, 예: 0.001 ETH | 성공 영수증, CUBIT 입금, BuyTaxed 이벤트 발생 |
||
| T-18 | ETH 잔액 많음 | 큰 금액 매수 | 성공 영수증, 가격 영향은 세율이 아니라 견적에 나타남 | ||
| T-19 | CUBIT 보유 | 승인 후 매도 | 별도 트랜잭션 두 건, 순 ETH 수령, SellTaxed 발생 |
||
| T-20 | 이전 매도 확인됨 | 이전보다 큰 금액 매도 | 새 승인 요청: 이전 승인은 정확한 금액에 대한 것이었음 | ||
| T-21 | 스왑 화면 열림 | 제안된 허용치를 건드리지 않고 읽기 | 기본값 1.0 % | ||
| T-22 | 스왑 화면 열림 | 0.005, 100, 음수 값을 차례로 입력 |
0–99.99 범위와 소수점 둘째 자리에 관한 메시지와 함께 입력 거절 | ||
| T-23 | 최신 견적 | 허용치를 허용되는 최저값으로 설정하고 가격 변동을 기다린 뒤 서명 | TooLittleReceived(received, minimum)으로 거절, 토큰 손실 없음, 인터페이스가 보호 해제를 권하지 않음 |
||
| T-24 | 견적 표시됨 | 아무 동작 없이 30초 넘게 기다린 뒤 서명 시도 | 견적이 만료된 것으로 간주되어 서명 전에 다시 계산됨 | ||
| T-25 | 곧 만료될 견적 | 만료 직전에 서명하고 전달된 기한 읽기 | 기한은 120초 빼기 견적 경과 시간: 30초 지난 견적이면 약 90초 남음 | ||
| T-26 | 직접 호출 | 이미 지난 기한으로 swapExactIn 호출 |
Expired로 거절, 자금 이동 없음 |
||
| T-27 | 직접 호출 | 넉넉한 입력 상한으로 매수용 swapExactOut 호출 |
정확한 수량 수령, 남은 ETH는 같은 트랜잭션에서 caller에게 환불 | ||
| T-28 | 직접 호출 | 필요 금액보다 1 wei 낮은 입력 상한으로 swapExactOut 호출 |
TooMuchRequested(required, maximum)으로 거절 |
||
| T-29 | 밴드와 매수벽이 사들일 수 있는 양보다 많은 CUBIT 잔액, 예: 보상으로 받은 CUBIT | 라우터로 이 잔액을 exact-input 매도 | IncompleteInput으로 거절, 세금도 트랜잭션과 함께 취소 — 테스트 중 확인 필요 |
||
| T-30 | 직접 호출 | 장부가 제공할 수 있는 것보다 많은 ETH를 exact-output으로 요청 | InsufficientOutput으로 거절, 부분 정산 없음 |
||
| T-31 | 직접 호출 | 0 금액, 영 주소 수신자, 맞지 않는 msg.value, 다른 풀 key를 차례로 전송 |
각각 InvalidAmount, ZeroRecipient, WrongValue, WrongPool로 거절 |
||
| T-32 | 호환 타사 라우터 | 타사 경로로 매수 후 매도 | hook 세금 적용 | ||
| T-33 | 호출 컨트랙트 또는 트랜잭션 묶음 | 같은 트랜잭션에서 매수 후 매도를 연속 실행 | 두 부분이 각각 과세됨 |
4. 세금과 회계
| 작업 | 기준 | 배분 |
|---|---|---|
| 매수 | 총 ETH 부분의 3 % | 100 % 팀 몫, 매수벽 배분은 명시적으로 0 |
| 매도 | 총 ETH 출력의 15 % | 12 %는 매수벽, 3 %는 팀 |
exact-input에서 매수세는 제공한 금액에 포함되며 wei 단위로 올림합니다. exact-output에서는 풀 부분 위에 더해지므로 총액 대비 세금은 3 %로 유지됩니다.
exact-output 매도의 세금은 ceil(sortie × 1500 / 8500)입니다. 풀은 요청 출력에 세금을 더한 금액을 내놓습니다. exact-input 매도의 세금은 ceil(brut × 15 %)입니다. 배분에서 팀 몫은 내림하고 wei 단위 잔여분은 모두 매수벽에 귀속됩니다.
현재 dapp 소스에서 인터페이스는 스왑을 허용하기 전에 이 세율을 체인에서 읽도록 요구합니다. 확인되기 전까지 버튼은 대기 상태로 남습니다.
| 사례 | 사전 조건 | 단계 | 예상 결과 | 관찰 결과 | 심각도 |
|---|---|---|---|---|---|
| T-34 | 매수 확인됨 | BuyTaxed(ethIn, toFloor, toTeam) 읽기 |
toFloor는 0, toTeam은 총입력의 3 %를 wei 단위로 올림한 값 |
||
| T-35 | 매수 확인됨 | 전후 teamAccrued 비교 |
증가분은 팀 몫과 같음 | ||
| T-36 | 매도 확인됨 | SellTaxed(ethOut, toFloor, toTeam) 읽기 |
toFloor + toTeam은 총액의 15 %, toTeam은 총액의 3 %, 합계는 wei 단위까지 정확 |
||
| T-37 | teamAccrued가 0이 아님, 직접 호출 |
아무 계정에서 claimTeam() 호출 |
자금은 고정된 팀 주소로 전송, TeamPaid(amount, cumulative) 발생, teamAccrued는 0으로 초기화 |
||
| T-38 | teamAccrued가 0, 직접 호출 |
claimTeam() 호출 |
호출은 오류로 되돌려지지 않고 아무것도 전송하지 않음 | ||
| T-39 | 같은 금액의 매수 후 매도 | gas를 제외하고 시작 ETH와 최종 ETH 비교 | 잔존 비율은 0.97 × 0.85 = 0.8245에 가까움, 차이는 LP 수수료, 가격 영향, 반올림으로 설명됨 |
||
| T-40 | 크기가 크게 다른 매도 두 건 | 총액 대비 세금 비교 | 두 경우 모두 세율 15 %, 구간이나 면제 없음 |
5. 자동 매수벽
매도마다, exact-input이든 exact-output이든 hook은 _collectCrossedWalls()를 호출한 다음 _placeWall()을 호출합니다. 먼저 가격이 완전히 통과한 매수벽을 가장 가까운 것부터 가장 먼 것까지 모두 비운 다음, 대기 중인 ETH 전부를 목표 0.4 × prix courant + 0.6 × prix de lancement에 배치합니다. 목표는 매도 후 가격으로 계산하고 tick 단위로 반올림합니다. 출시 가격 이하처럼 이 목표가 풀 tick보다 엄밀히 위에 있지 않으면 매수벽을 현재 가격보다 1 % 낮은 곳에 배치합니다.
비워진 매수벽은 그 CUBIT을 pendingAbsorbedTokens에 더하고, 남은 ETH(수수료와 미세 잔량)를 pendingFloorEth로 돌려보냅니다. 일부만 소모된 매수벽은 그 자리에 남습니다. 유동성을 만들기에 너무 작은 금액과, 가격이 tick 범위의 맨 위에 있어 가격 아래에 매수벽을 둘 자리가 없는 극단적인 경우만 자금을 pendingFloorEth에서 기다리게 하며, 이 이유로 매도가 거절되는 일은 없습니다. 이어서 CUBIT 라우터가 try/catch 안에서 deliverAbsorbed()를 호출합니다.
Lens의 참조값 floorPrice()는 전역 최저가가 아니라 가장 최근에 자금을 조달한 매수벽을 나타냅니다. 통과한 매수벽 하나마다 약 185 000 gas가 들므로, 트랜잭션 하나의 16 777 216 gas 한도 안에서 매도 하나는 최대 약 88개의 매수벽을 통과할 수 있습니다.
| 사례 | 사전 조건 | 단계 | 예상 결과 | 관찰 결과 | 심각도 |
|---|---|---|---|---|---|
| T-41 | 가격이 출시 가격보다 높음 | 매도 후 매수벽 읽기 | WallFunded(id, lower, addedEth, liquidity) 발생, 반올림 오차 범위에서 매도의 12 %를 포함한 대기 ETH로 목표 tick에 매수벽이 생성되거나 두꺼워짐, pendingFloorEth에는 배치되지 않은 잔여분만 남음 |
||
| T-42 | 이전 상황 | 매도 후 가격과 출시 가격으로 목표 재계산 | 매수벽의 lower가 이 가격으로 계산하고 tick 단위로 반올림한 40/60 목표와 일치 |
||
| T-43 | 목표가 같은 tick에 떨어지는 매도 두 건 | wallCount()와 walls(id) 읽기 |
매수벽은 하나: 두 WallFunded의 id가 같고, 유동성은 늘고 새 식별자는 생기지 않음 |
||
| T-44 | 서로 다른 tick의 매수벽 여러 개 | 매도와 매수를 여러 번 반복한 뒤 walls(id) 다시 읽기 |
각 매수벽의 lower는 그대로, 이동한 매수벽 없음 |
||
| T-45 | 가격 아래의 유효 매수벽 | 매수벽을 통과하지 않고 일부 소모하는 금액 매도 | 매수벽은 ETH와 CUBIT을 함께 보유한 유효 상태로 남음, WallAbsorbed 없음, pendingAbsorbedTokens 그대로 |
||
| T-46 | 일부 소모된 매수벽 | 가격이 매수벽 위로 다시 올라갈 때까지 매수 | 매수벽이 CUBIT을 되팔고 ETH를 되찾음, 식별자와 tick은 그대로 | ||
| T-47 | 유효 매수벽, CUBIT 라우터로 매도 | 매수벽을 완전히 통과하는 금액 매도 | 같은 트랜잭션에서 WallAbsorbed(id, cubit, ethRemaining)와 TokensAbsorbed(amount, pendingAbsorbedTokens), 이어서 AbsorbedDelivered(sink, amount)와 RewardReserveFunded 발생, rewardReserve()가 그 CUBIT만큼 증가, pendingAbsorbedTokens는 0으로 돌아감, totalSupply()와 totalBurned()는 그대로 |
||
| T-48 | 가격이 출시 가격에 가깝고 40/60 목표가 시장 아래에 있지 않음 | 체결 가능한 소액 매도 후 매수벽 읽기 | 매도 성공, WallFunded 발생, 매수벽이 매도 후 가격보다 1 % 낮은 곳에 tick 단위로 반올림해 배치됨, pendingFloorEth에는 배치되지 않은 잔여분만 남음 |
||
| T-49 | T-48의 상황 | 체결 가능한 소액을 다시 매도한 후 pendingFloorEth 읽기 |
새 가격보다 1 % 낮은 곳에 WallFunded 발생, pendingFloorEth에는 반올림 미세 잔량만 남음, 매도 사이에 적체가 쌓이지 않음 |
||
| T-50 | 호환 타사 라우터 | 타사 경로로 매수벽을 완전히 통과하며 매도한 뒤 아무 계정에서 deliverAbsorbed() 호출 |
세금 적용, TokensAbsorbed 발생, CUBIT은 호출 때까지 pendingAbsorbedTokens에 남고 호출 시 AbsorbedDelivered 발생 |
||
| T-51 | 자금을 조달한 매수벽 여러 개 | Lens의 floorPrice()와 netFloorPrice()를 읽고, 이어서 wallAmountsPage(0, 500)부터 activeWallCount까지의 페이지를 모두 같은 블록에서 읽기 |
가장 최근에 자금을 조달한 매수벽의 참조값이며 그렇게 표시됨, 매수벽의 CUBIT은 페이지 합계에 pendingAbsorbedTokens를 한 번만 더한 값 |
||
| T-52 | Forge 자식 시장, 완전히 통과된 매수벽 | 자식 hook의 absorbedTokenSink()를 읽은 뒤 매도 이벤트 읽기 |
목적지가 Forge의 governanceVault()와 같음, AbsorbedDelivered 발생, Deposited(token, from, amount, unlockAt) 묶음이 30일간 잠김 |
||
| T-106 | 매도 한 번에 통과할 매수벽이 많음 | 매도의 gas를 추정한 뒤 전송 | 통과한 매수벽 하나마다 약 185 000 gas, 약 88개를 넘으면 매도가 16 777 216 gas를 초과해 손실 없이 실패: 매도를 나눔 | ||
| T-107 | CUBIT 목적지가 전송을 거부하는 리허설 배포 | CUBIT 라우터로 매수벽을 통과하며 매도한 뒤 deliverAbsorbed() 다시 호출 |
매도 성공, CUBIT은 pendingAbsorbedTokens에 남음, 호출은 모든 계정에 열려 있으며 목적지가 거부하는 동안에는 실패 |
6. mCUBIT Vault
Vault는 rewardReserve에서만 떼어 내는 CUBIT 보상을 지급합니다. 보상은 DAILY_REWARD_BPS = 300, 즉 24시간 기간(REWARD_PERIOD)마다 예치액의 3 %이며, 경과 시간에 비례해 계산하고 한 기간으로 상한을 둡니다. 그 이상은 초과분이 소멸합니다. 보상은 준비금 잔액을 절대 넘지 않으며 원금에서 절대 지급되지 않습니다.
예치할 때마다 지갑의 전체 포지션에 24시간 잠금(LOCK_DURATION)이 다시 시작되며, 기한 전 인출은 Locked로 거절됩니다. 예치, 인출, 청구는 먼저 쌓인 보상을 지급하고 기간을 다시 시작합니다. 어떤 계정이든 fundRewardReserve(amount)로 준비금을 채울 수 있습니다. 기한은 브라우저 시계가 아니라 체인 timestamp로 판단합니다.
보상은 CUBIT으로만 지급되며 claimCubit()로 청구합니다. Vault는 WETH 보상 함수를 전혀 제공하지 않습니다.
| 사례 | 사전 조건 | 단계 | 예상 결과 | 관찰 결과 | 심각도 |
|---|---|---|---|---|---|
| T-53 | Vault 이용 가능, CUBIT 보유 | 승인 후 예치 | Staked(user, amount, unlockAt) 발생, unlockAt은 블록 timestamp에 24시간을 더한 값 |
||
| T-54 | 기존 포지션 | 기한 전에 다시 예치 | 전체 포지션의 잠금이 다시 시작됨, 쌓인 보상이 0이 아니면 CubitRewardClaimed와 함께 지급되고 기간이 다시 시작됨 |
||
| T-55 | 잠금 진행 중 | 인출 요청 | Locked로 거절 |
||
| T-56 | 잠금 만료 | 예치액 일부 인출 | 일부 인출 허용, Withdrawn 발생, 쌓인 보상을 먼저 지급, 남은 잔액은 계속 예치됨 |
||
| T-57 | 1 000 CUBIT 예치, 준비금 충분 | 12시간 후, 이어서 24시간 후 pendingCubit 읽기 |
약 15 CUBIT, 이어서 30 CUBIT | ||
| T-58 | 이전 상황 | 청구하지 않고 48시간 기다린 뒤 pendingCubit 읽기 |
여전히 30 CUBIT: 둘째 날분은 소멸 | ||
| T-59 | 보상 쌓임 | claimCubit() 호출 |
CUBIT 전송, CubitRewardClaimed 발생, rewardReserve가 지급액만큼 감소, pendingCubit은 0으로 돌아감 |
||
| T-60 | 준비금이 작은 리허설 배포 | 준비금보다 큰 보상 청구 | 준비금 잔액만 지급, 준비금은 0이 됨, 원금은 건드리지 않음 | ||
| T-61 | 직접 호출 | fundRewardReserve(0)을 호출한 뒤, 아무 계정에서 승인 후 fundRewardReserve(x) 호출 |
InvalidAmount로 거절, 이어서 RewardReserveFunded(from, x) 발생, rewardReserve가 x만큼 증가 |
||
| T-62 | 직접 호출 | 여러 시점에 token.balanceOf(vault)를 totalStaked + rewardReserve와 비교 |
Vault 잔액은 이 합계보다 작아지지 않음 | ||
| T-63 | 0 금액, 직접 호출 | stake(0) 후 withdraw(0) 호출 |
두 경우 모두 InvalidAmount로 거절 |
||
| T-64 | 현재 레지스트리에 연결되지 않은 Vault | 예치 시도 | Inactive로 거절 |
||
| T-65 | 열린 포지션 | 표시된 잠금 기간 읽기 | LOCK_DURATION에서 도출한 시간 단위 표시: 24시간 |
||
| T-66 | dapp 열림 | WETH 보상 절차 찾기 | 없음: CUBIT 보상만 제공 |
7. 런치패드 거버넌스 vault
거버넌스 vault는 Forge 출시 수수료(ETH)와 Forge 자식 시장의 매수벽이 흡수한 토큰을 받습니다. 각 예치분은 자기 수령 시점부터 30일간 잠깁니다(LOCK_DURATION). 배포자만 청구할 수 있고, 이 권리는 영구적이며 양도할 수 없습니다. 날짜가 지난 묶음만 오래된 것부터 최신 순으로 청구합니다. ETH는 키 ETH(), 즉 영 주소로 기록됩니다. 인수 검증 전에 테스트하는 배포의 ABI를 다시 읽으세요. 배포자는 extendLock으로 현재와 향후 모든 예치분의 잠금을 연장할 수 있으며, lockExtension()은 늘어나기만 하고 모든 날짜에 더해집니다.
자식 시장 매수벽에서 자동으로 보내는 기능은 T-52로, 출시 수수료 예치는 T-81로 확인합니다. T-67~T-73 사례에서는 직접 호출로 테스트 토큰을 예치하고, T-108에서는 수수료의 ETH를 청구합니다.
| 사례 | 사전 조건 | 단계 | 예상 결과 | 관찰 결과 | 심각도 |
|---|---|---|---|---|---|
| T-67 | 테스트 토큰, 직접 호출 | 승인 후 deposit(token, amount) 호출 |
Deposited(token, from, amount, unlockAt) 발생, unlockAt은 블록 timestamp에 30일을 더한 값, held(token)이 같은 양만큼 증가 |
||
| T-68 | 30일이 안 된 예치 | 배포자 계정으로 claim(token, n) 호출 |
NothingToClaim으로 거절 |
||
| T-69 | 잠금이 풀린 묶음 | 다른 계정으로 claim(token, n) 호출 |
NotDeployer로 거절 |
||
| T-70 | 7일 동안 하루 한 번 예치 | 첫 예치 후 30일째부터 매일 청구 | 하루에 한 묶음씩 오래된 것부터 풀림, 마지막 묶음은 일곱 번째 예치 30일 후에 풀림, 청구마다 Claimed(token, amount, tranches) 발생 |
||
| T-71 | 잠금이 풀린 묶음 여러 개 | claim(token, 1) 호출 |
한 묶음만 지급, 다음 묶음은 계속 청구 가능 | ||
| T-72 | 단순 전송으로 보낸 토큰 | lockUntracked(token)을 호출한 뒤 새 전송 없이 다시 호출 |
첫 호출부터 30일간 잠기는 새 묶음 생성, 두 번째 호출은 NothingToLock으로 거절 |
||
| T-73 | 잠긴 묶음과 풀린 묶음 | claimable(token)과 locked(token) 읽기 |
청구 가능 금액과 잠긴 금액의 합은 held(token)과 같음 |
||
| T-108 | T-81의 출시 수수료가 예치된 지 30일 초과 | 다른 계정에서, 이어서 배포자에서 claim(address(0), 1) 호출 |
먼저 NotDeployer로 거절, 이어서 ETH가 배포자에게 지급됨, Claimed(address(0), amount, 1) 발생, held(address(0))가 지급액만큼 감소 |
||
| T-109 | 잠긴 묶음 | 다른 계정에서, 이어서 배포자에서 extendLock(extra) 호출 |
먼저 NotDeployer로 거절, 이어서 LockExtended(extra, lockExtension) 발생, tranche(token, i)로 읽은 모든 날짜가 extra만큼 늦춰짐, 잠금을 줄이는 함수 없음 |
8. Forge
Forge는 향후 릴리스로 소개되는 공개 런치패드입니다. CUBIT 출시에 포함되지 않으며, 나중에 거버넌스 vault와 함께 추가됩니다. 런치패드가 추가되면 그 주소를 기록하세요.
모든 계정이 정확한 수수료를 내면 자식을 출시할 수 있습니다. Forge 자식은 공급량 전체를 자기 밴드에 예치하고, Forge는 생성 시 거버넌스 vault 주소를 받으며, 각 출시는 0.005 ETH의 수수료를 이 vault에 납부하며 vault가 이를 보유합니다. 출시자는 이를 결코 돌려받지 못합니다. 배포 salt는 출시하는 계정에 묶입니다.
| 사례 | 사전 조건 | 단계 | 예상 결과 | 관찰 결과 | 심각도 |
|---|---|---|---|---|---|
| T-81 | Forge 이용 가능, 임의의 계정 | 0.005 ETH의 정확한 수수료로 자식 출시 | ChildLaunched(token, hook, launcher, team, fee) 발생, 자식의 BandBootstrapped가 반올림 오차 범위에서 공급량 전체와 같은 예치를 표시, 거버넌스 vault가 Deposited(address(0), forge, fee, unlockAt) 발생(unlockAt은 블록 타임스탬프에 30일과 연장분을 더한 값), 부모 hook의 pendingFloorEth 변화 없음 |
||
| T-82 | Forge 이용 가능 | 잘못된 value, 빈 이름, 0 팀 주소 또는 다른 템플릿으로 출시 시도 | 거절: wrong launch fee, invalid name, invalid team 또는 template mismatch |
||
| T-110 | 다른 계정이 본 출시의 salt | 같은 salt로 두 번째 계정에서 출시 | 첫 출시의 주소를 가져가지 못함: salt는 출시 계정에 묶이며, hook 주소에 권한이 없으면 두 번째 출시는 child deployment failed로 거절됨 |
9. 교체와 권한
레지스트리 authority는 주변 주소 네 개(Vault, 라우터, Lens, Forge)를 언제든 지체 없이 교체할 수 있습니다. 교체마다 ModuleUpdated를 내고 moduleRevision을 증가시키며, 해당 기능(Vault, Momentum, Forge)을 팀이 다시 열 때까지 닫습니다. 라우터 교체로 닫히는 기능은 없습니다. 이미 등록되었거나, 다른 hook이나 토큰에 연결되었거나, 이미 stake가 있는 후보는 InvalidModule로 거절됩니다.
hook에는 관리자가 없으며, 누구도 스왑이나 매수벽 메커니즘을 일시 중지할 수 없습니다. 팀 주소는 영구 권한을 유지합니다. 세금의 팀 몫을 받고 레지스트리 모듈을 교체한 뒤 활성화합니다.
| 사례 | 사전 조건 | 단계 | 예상 결과 | 관찰 결과 | 심각도 |
|---|---|---|---|---|---|
| T-83 | 현재 Vault에 예치와 준비금 있음 | Vault 교체 | 예치 CUBIT, 보상 준비금, 기한은 이전 Vault에 남음, 자금 이동 없음 | ||
| T-84 | Vault 교체됨 | 이전 Vault에서 청구, 인출 후 예치 시도 | 청구와 인출은 계속 가능, 새 예치는 Inactive로 거절 |
||
| T-85 | 이미 등록되었거나 stake가 있는 후보 | 교체 시도 | InvalidModule로 거절 |
||
| T-86 | 교체 완료 | moduleRevision()과 이벤트 읽기, Vault 교체 후에는 새 Vault에 예치 시도 |
개정 증가, ModuleUpdated(module, previous, current, revision) 일치, Vault가 다시 활성화될 때까지 예치는 Inactive로 거절됨 |
||
| T-87 | 이전 라우터에 승인 부여 | 라우터 교체 후 매도 시도 | 이전 승인은 새 spender에 적용되지 않음, 새 승인 요청 | ||
| T-88 | 라우터 교체됨 | 이전 라우터로 스왑 | 스왑은 계속 가능하고 hook 세금 적용, dapp은 새 라우터 사용 | ||
| T-89 | 직접 호출 | 배포된 hook의 ABI 검사 | 스왑이나 매수벽 메커니즘을 중단시키는 함수가 없음, hook에는 관리자가 없음 | ||
| T-90 | 직접 호출 | Lens의 snapshot()을 읽고, 같은 블록에서 매수벽 페이지를 읽기 |
snapshot에는 시장, 밴드, 매수벽, 계정 필드, 총공급량, 보상 준비금, 활성 매수벽 수, 읽기 블록과 최선 매수벽만 있고 매수벽 합계는 없으며, 그 비용은 매수벽 수에 좌우되지 않음, 유통 공급량은 totalSupply에서 매수벽의 CUBIT과 rewardReserve를 뺀 값이고 보유 CUBIT은 그 공급량에서 bandTokens를 뺀 값 |
||
| T-91 | 출시 후 언제든 | 매수와 매도 | 스왑이 정상 동작: 어떤 계정도 스왑을 막을 수 없음 |
10. 예상 거절 사례
이 표는 인수 검증 내내 기준으로 씁니다. 자동 매수벽은 매도에 거절을 추가하지 않습니다. 매수벽을 배치할 수 없으면 자금이 대기 상태로 남고, 실패한 전송은 CUBIT을 대기 상태로 둡니다.
주의할 점. 현재 dapp 소스에서는 컨트랙트 오류가 번역되지 않습니다. 온체인 거절이 원시 메시지로, 잘린 채 표시될 수 있습니다. 발생한 거절마다 표시된 정확한 문구를 기록하고 이해할 수 있는지 판단하세요.
| 오류 | 발생 원인 | 애플리케이션이 보여야 할 내용 |
|---|---|---|
ExternalLiquidityForbidden |
제3자의 유동성 추가 | 작업 불가: 프로토콜이 유일한 유동성 공급자 |
SupplyNotDeposited |
공급량의 80 % 미만 예치로 초기화 | 예치 부족으로 출시 불가 |
Expired |
트랜잭션 기한 초과 | 견적 만료, 다시 계산 필요 |
TooLittleReceived(received, minimum) |
허용 최솟값보다 적은 출력 | 슬리피지 보호 작동 |
TooMuchRequested(required, maximum) |
허용 상한보다 큰 입력 | 입력 상한 보호 작동 |
IncompleteInput |
완전히 소모되지 않은 exact-input | 가용 유동성에 비해 금액이 너무 큼 |
InsufficientOutput |
완전히 제공되지 않은 exact-output | 장부가 이 출력을 제공할 수 없음 |
InvalidAmount, ZeroRecipient, WrongValue, WrongPool |
잘못된 주문 매개변수 또는 Vault에 보낸 0 금액 | 원시 코드 없는 입력 오류 |
Inactive, Locked |
현재 레지스트리에 연결되지 않은 Vault 또는 기한 전 인출 | 모듈 사용 불가 또는 잠금 해제 날짜 |
NotDeployer, NothingToClaim, NothingToLock, NothingToExtend |
제3자의 거버넌스 vault 청구나 연장, 기한 전 청구, 새 잔액 없는 잠금, 0 연장 | 제한된 작업, 청구할 것 없음, 잠글 것 없음 또는 연장할 것 없음 |
NotAuthority, InvalidModule |
제3자의 교체 요청 또는 호환되지 않는 후보 | 교체 거절과 그 이유 |
순수하게 애플리케이션에서 발생하는 거절도 기록합니다: 만료된 견적, 바뀐 스왑 맥락, 다른 체인의 지갑, 바뀐 계정, 바뀐 모듈 개정, 아직 확인되지 않은 세율.
11. 애플리케이션
이 사례는 dapp으로 수행합니다. 인용한 인터페이스 동작은 소스에서 가져온 것이며 인수 검증 중에 확인합니다.
| 사례 | 사전 조건 | 단계 | 예상 결과 | 관찰 결과 | 심각도 |
|---|---|---|---|---|---|
| T-92 | dapp 열림 | 선택기의 10개 언어를 모두 확인 | 각 언어가 누락 문구나 넘침 없이 번역된 내용을 표시, 제품명은 의도적으로 영어로 유지 | ||
| T-93 | 언어 선택됨 | 새로고침 후 창을 640 px 미만으로 줄이기 | 선택이 세션 간에 유지됨, 640 px 미만에서 선택기는 국기만 표시 | ||
| T-94 | 영어 외 언어 | 홈 제목을 영어판과 비교 | 영어 외 언어에서는 제목을 의도적으로 줄임, 넘치거나 잘리면 안 됨 | ||
| T-95 | 약 400 px 화면 | 모든 화면 확인 | 가로 넘침 없음, 넓은 영역은 자체 컨테이너 안에서 스크롤, 버튼은 계속 누를 수 있음 | ||
| T-96 | 768~1279 px 창 | 탐색 메뉴 열기 | 1279 px까지 축소 메뉴 사용, 이동 후 메뉴가 닫힘 | ||
| T-97 | 트랜잭션 확인됨 | 표시된 각 금액을 같은 블록의 온체인 값과 비교 | 금액 일치, 표시 반올림이 서명 금액을 바꾸지 않음 | ||
| T-98 | 작업 준비 중 | 절차 도중 지갑에서 네트워크 변경 | 견적이 무효화되고 예상 네트워크 밖에서는 서명 거절, 버튼은 먼저 네트워크 전환을 제안하고 교환에는 두 번째 동작을 요구 | ||
| T-99 | 작업 준비 중 | 절차 도중 지갑에서 계정 변경 | 새 계정 기준으로 잔액, 승인, 견적 재계산, 이전 계정용으로 준비한 서명은 거절 | ||
| T-100 | 승인 완료, 스왑 미서명 | 그 사이에 모듈 개정이 바뀌게 두기 | 애플리케이션이 맥락을 재검증하며 새 spender로 조용히 넘어가지 않음 | ||
| T-101 | RPC 불가 또는 오래된 읽기 | RPC 접근을 끊고 관찰 | 상태가 미확인으로 표시되고 동작이 비활성화됨 | ||
| T-102 | 트랜잭션 전송됨 | hash에 이어 영수증 추적 | 인터페이스가 “제출됨”과 “성공”을 구분, Sepolia 탐색기에서 이벤트 확인 가능 | ||
| T-103 | 온체인 거절 유발 | 표시된 문구 전체 기록 | 메시지는 사용자가 이해할 수 있어야 함, 원시 기술 코드나 잘린 메시지는 모두 기록 | ||
| T-104 | 중국어, 한국어, 일본어 | 외부 글꼴 서비스 접근 없이 이 언어 표시 | 문자가 올바르게 표시됨: 글꼴은 사이트가 제공 | ||
| T-105 | Proof 화면 열림 | 밴드, 매수벽, 대기 자금 읽기 | bandEth, bandTokens, 매수벽, 대기 ETH, 전송 대기 CUBIT이 따로 표시됨, 어떤 화면에도 래더, keeper, 매수벽 소각이 나오지 않음 |
12. 기록표
앞 섹션의 각 표가 그 섹션의 기록표입니다. 사례를 진행하면서 “관찰 결과”와 “심각도” 열을 채우세요. 관찰 결과에는 최소한 트랜잭션 hash나 읽기 블록을 적고, 이어서 확인한 내용을 적습니다.
보고서에 첨부할 요약:
| 섹션 | 사례 | 부합 | 차이 | 최고 심각도 |
|---|---|---|---|---|
| 1. 준비 | T-01~T-06 | |||
| 2. 출시와 밴드 | T-07~T-16 | |||
| 3. 매수와 매도 | T-17~T-33 | |||
| 4. 세금과 회계 | T-34~T-40 | |||
| 5. 자동 매수벽 | T-41~T-52, T-106, T-107 | |||
| 6. mCUBIT Vault | T-53~T-66 | |||
| 7. 거버넌스 vault | T-67~T-73, T-108, T-109 | |||
| 8. Forge | T-81, T-82, T-110 | |||
| 9. 교체와 권한 | T-83~T-91 | |||
| 10. 거절 사례 | 공통 참조 | |||
| 11. 애플리케이션 | T-92~T-105 |
차이는 스크린샷만이 아니라 반드시 사례 번호에 연결합니다. 네트워크, 배포 주소, 블록, hash, 애플리케이션 버전을 함께 첨부하세요.
이 계획의 한계
이 계획은 새 버전 코드와 2026년 9월 14일 결정이 예정하는 내용을 설명합니다. 이 계획은 검증이 아닙니다. Sepolia 인수 검증에 성공해도 테스트 캠페인을 대체하지 않습니다.
“테스트 중 확인 필요” 항목은 관찰한 뒤 이 페이지에 반영해야 합니다.
출처: contracts/src/CubitHook.sol, CubitLens.sol, interfaces/ICubitHook.sol, interfaces/ICubitLens.sol, libraries/BandLib.sol, libraries/WallLib.sol, periphery/CubitRouter.sol, CubitV2.sol, CubitVault.sol, CubitGovernanceVault.sol, CubitForge.sol, CubitLaunch.sol, dapp/src의 현재 절차, 그리고 contracts/docs/REDESIGN_HANDOFF.md에 기록된 2026년 9월 14일 설계 결정.