RTX
RWA Tokenization Platform
R RTX OP Sepolia
미연결
RWA Tokenization Platform

RWA 토큰화 플랫폼

RTX는 ERC-1155 멀티토큰 표준 기반의 RWA(Real World Asset) 토큰화 플랫폼으로,
실물 자산의 발행·결합·거래·사용까지 전 라이프사이클을 온체인에서 관리합니다.
이종 가맹점 쿠폰의 상호 합의형 결합, 에스크로 기반 사용 처리,
그리고 P2P 거래소를 통한 2차 시장 유통을 지원합니다.

ERC-1155
멀티토큰 표준
OP Sepolia
L2 네트워크
EIP-712
서명 기반 결제
RWA
토큰화 거래소

1. 발명의 개요

📑
발명의 명칭

발명 1. 블록체인 기반 이종 가맹점 쿠폰의 결합과 사용 처리 시스템

발명 2. 블록체인 기반 RWA 토큰화 거래소

📄
종래 기술의 문제점
본 발명이 해결하고자 하는 과제
문제 1
중앙화된 쿠폰 시스템의 위변조 취약성

기존 쿠폰 시스템은 중앙 서버에서 발행 수량, 사용 내역, 유효기간을 관리하므로 운영자가 임의로 발행량을 조작하거나 사용 기록을 변조할 수 있다. 소비자는 쿠폰의 진위 여부나 잔여 수량을 독립적으로 검증할 수 없다.

문제 2
이종 가맹점 간 쿠폰 교차 사용 불가

종래의 쿠폰은 발행 가맹점 내에서만 사용할 수 있어, 서로 다른 가맹점(예: 치킨점과 맥주점)의 쿠폰을 결합하여 새로운 혜택(예: 치맥 세트 할인)을 만드는 것이 구조적으로 불가능하다. 설령 제휴를 한다 하더라도, 일방적 제휴 설정만으로는 상대 가맹점의 동의 없이 결합이 강제될 위험이 있다.

문제 3
결합 쿠폰의 양측 가맹점 사용 보장 어려움

쿠폰이 결합되더라도 양쪽 가맹점 모두에서 순차적으로 사용되어야 하는 경우(AND 모드), 한쪽 가맹점만 사용하고 나머지를 사용하지 않는 상황에서 미사용분 처리 로직이 없어 소비자 피해가 발생할 수 있다.

문제 4
오프라인 결제 시 전체 자산 권한 노출

블록체인 기반 시스템에서 오프라인 매장 결제를 구현할 때, 기존 방식(setApprovalForAll)은 점주에게 소비자의 전체 토큰에 대한 무제한 권한을 부여하게 되어, 특정 쿠폰 1장만 소각하려 해도 모든 자산이 위험에 노출된다.

발명의 해결 수단
각 문제에 대응하는 기술적 해결책
해결 1

ERC-1155 온체인 쿠폰 발행 — 발행 수량(maxSupply), 유효기간(startAt/expiresAt), 메타데이터(IPFS URI)가 스마트 컨트랙트에 불변 기록되어, 누구나 블록체인에서 독립적으로 검증 가능. 만료된 토큰은 _update() 레벨에서 전송 자체가 차단됨.

해결 2

상호 합의형 결합(Hybrid allowMerge) — 양쪽 가맹점이 각각 setAllowMerge()를 호출하여 상호 동의한 경우에만 결합 타입 생성이 가능. 일방적 결합을 원천 방지.

해결 3

AND 모드 에스크로 + 타임아웃 자동 반환 — 첫 번째 가맹점 사용 시 에스크로 생성, 두 번째 가맹점이 타임아웃 내 완료하지 않으면 미사용 소스 토큰을 소비자에게 자동 반환(mint-back). 소비자 보호와 가맹점 간 공정성을 스마트 컨트랙트로 보장.

해결 4

EIP-712 건별 서명 기반 QR 결제 — 소비자가 특정 tokenId, 수량, orderId, deadline에 대해서만 1회성 서명(Redeem 타입)을 생성. 점주는 이 서명으로만 해당 쿠폰을 소각할 수 있으며, 다른 토큰이나 추가 수량에 대한 권한은 전혀 부여되지 않음.

2. 시스템 아키텍처

🛠
기술 스택
검증된 기술로 구축된 아키텍처
Smart Contract
Coupon1155V3 (Solidity 0.8.26)
Token Standard
ERC-1155 Multi-Token
Network
OP Sepolia (L2)
Security
OpenZeppelin v5.0 (Ownable2Step, Pausable, ReentrancyGuard)
Metadata
IPFS / Storacha (CID-addressable)
Signing
EIP-712 Typed Structured Data
Token ID
(merchantIndex << 64) | localId (256-bit)
Merge Namespace
merchantIndex=0 (결합 전용 네임스페이스)
👥
시스템 참여자 및 역할
점주 (Merchant)
  • 쿠폰 발행 — 가맹점 쿠폰 생성 및 블록체인 등록
  • 요청 관리 — 에어드랍 요청 확인 및 safeTransferFrom 전송
  • POS 결제 — QR1 생성 + QR2 스캔 + redeemWithSig 실행
  • 에스크로 — AND 모드 QR 기반 1단계(QR1+QR2 스캔+useAtFirstMerchant) / 2단계(QR1+QR2 스캔+useAtSecondMerchant)
  • 결합 허용 — setAllowMerge로 상대 쿠폰과의 결합 동의
소비자 (Consumer)
  • 쿠폰 받기 — 에어드랍 신청 후 토큰 수령
  • 내 쿠폰 — 보유 쿠폰 목록 및 메타데이터 확인
  • 쿠폰 결합 — mergeTo(A+B 소각 → C 발행) / unmerge(C 소각 → A+B 복원)
  • 쿠폰 사용 — QR1 스캔 → EIP-712 서명 → QR2 제시
  • 에스크로 확인 — AND 모드 1단계(승인+QR2) / 2단계(에스크로 ID 입력+QR2) / 에스크로 ID 조회
  • 거래소 — 쿠폰 판매(ETH) / 쿠폰 스왑(교환) / 에스크로 기반 안전 거래
관리자 (Admin/Issuer)
  • 결합 정의 — createMergeType (OR/AND 모드 지정)
  • registerMerchant — 가맹점 등록 (merchantIndex ↔ address 매핑)
  • setIssuer — 발행자 권한 부여/회수
  • pause / unpause — 컨트랙트 긴급 정지
  • setDefaultEscrowTimeout — 에스크로 기본 타임아웃 설정

3. 핵심 기능 상세

각 기능의 전제 조건, 처리 흐름, 구체적 실시 예를 기술합니다.

3-1. 쿠폰 발행 (createCoupon)
가맹점 단위 쿠폰을 블록체인에 발행하는 기능

전제 조건

  • 가맹점이 registerMerchant(merchantIndex, address)로 사전 등록되어 있어야 한다.
  • 호출자(msg.sender)는 해당 가맹점의 소유자(merchantOwner)이거나 관리자(owner/issuer)여야 한다.
  • 메타데이터 URI(IPFS CID)가 비어 있지 않아야 한다.
  • maxSupply가 0이 아닌 경우, initialSupply는 maxSupply를 초과할 수 없다.

토큰 ID 생성 규칙

토큰 ID는 256비트 정수로, 상위 192비트에 merchantIndex(uint32), 하위 64비트에 localId(uint64)를 배치하여 생성된다.

tokenId = (uint256(merchantIndex) << 64) | uint256(localId)

이 구조로 인해 가맹점 간 토큰 ID 충돌이 원천 방지되며, 토큰 ID에서 가맹점을 역산할 수 있다.

처리 흐름

1
점주가 가맹점명, 쿠폰명, 이미지, 수량, 유효기간을 입력한다.
2
이미지와 메타데이터(JSON)가 IPFS(Storacha)에 업로드되어 CID가 생성된다.
3
스마트 컨트랙트의 createCoupon(merchantIndex, tokenUri, startAt, expiresAt, maxSupply, initialSupply)가 호출된다.
4
컨트랙트는 nextLocalId[merchantIndex]를 자동 증가시키고, _makeId()로 고유 tokenId를 생성한다.
5
initialSupply만큼 점주 지갑으로 토큰이 민팅(mint)된다. CouponCreated 이벤트가 발생한다.

구체적 실시 예

예시: 치킨 가맹점의 할인 쿠폰 발행
가맹점구로통닭 (merchantIndex = 1)
점주 지갑0x1234...abcd
쿠폰명치킨 5,000원 할인권
발행 수량1,000장 (maxSupply = 1,000)
유효기간2025-01-01 ~ 2025-12-31
생성된 tokenId(1 << 64) | 1 = 18446744073709551617
메타데이터ipfs://bafk...xyz (이미지, 이름, 설명, 혜택 포함)
3-2. 에어드랍 및 수령 (safeTransferFrom)
소비자가 쿠폰을 신청하고 점주가 승인·전송하는 기능

전제 조건

  • 소비자가 EVM 호환 지갑 주소를 보유하고 있어야 한다.
  • 점주가 해당 tokenId의 토큰을 1개 이상 보유하고 있어야 한다.
  • 해당 쿠폰이 만료(expiresAt)되지 않았어야 한다.

처리 흐름

1
소비자가 카탈로그에서 원하는 쿠폰을 선택하고 수량을 입력하여 에어드랍을 신청한다. (오프체인 API: POST /api/claims)
2
점주가 요청 관리 페이지에서 요청 목록을 확인한다. 각 요청에는 요청자 지갑, tokenId, 수량, 자신의 잔고가 표시된다.
3
점주가 "전송" 버튼을 누르면 ERC-1155의 safeTransferFrom(merchant, consumer, tokenId, amount, 0x)가 호출된다.
4
트랜잭션 완료 후 요청 상태가 "sent"로 갱신되고 txHash가 기록된다.
3-3. 이종 가맹점 쿠폰의 상호 합의형 결합 (Hybrid Merge)
본 발명의 핵심 청구 대상
신규성 포인트: 서로 다른 가맹점(merchantIndex가 다른)의 쿠폰 토큰을, 양쪽 가맹점이 각각 독립적으로 setAllowMerge()를 호출하여 상호 합의한 경우에만, 소비자가 두 토큰을 동시 소각(burn)하고 새로운 결합 토큰을 발행(mint)받을 수 있도록 한 시스템. 결합된 토큰은 다시 원래 토큰으로 분리(unmerge)할 수 있어 가역성을 보장한다.

3-3-1. 결합 허용 설정 (setAllowMerge)

  • 호출자는 자기 토큰의 가맹점 소유자이거나 관리자여야 한다.
  • 자기 토큰(myToken)이 이미 결합 토큰(isMerged=true)이 아니어야 한다.
  • 양쪽 토큰이 모두 존재(exists=true)해야 한다.
  • 양방향 합의 필수: A 가맹점이 mergeAllowed[tokenA][tokenB] = true로 설정하고, B 가맹점도 mergeAllowed[tokenB][tokenA] = true로 설정해야만 isMergePairAllowed()가 true를 반환한다.

3-3-2. 결합 타입 생성 (createMergeType)

  • 관리자(owner/issuer)만 호출 가능하다.
  • 소스 토큰 A, B가 서로 다른 가맹점이어야 한다 (SameMerchant 방지).
  • isMergePairAllowed(sourceIdA, sourceIdB)가 true여야 한다.
  • OR 또는 AND 모드를 지정한다.

결합 토큰의 merchantIndex는 0(MERGE_NAMESPACE)으로 설정되어, 일반 가맹점 토큰과 구분된다. 유효기간은 두 소스 토큰 중 더 이른 만료일(_minExpiry)이 적용되고, 시작일은 더 늦은 시작일(_maxStart)이 적용된다.

allowedMerchantA/B에는 소스 토큰들의 merchantIndex가 정렬(lo/hi)되어 저장되며, 이는 이후 OR/AND 모드에서 해당 가맹점만 사용 처리를 할 수 있도록 제한하는 근거가 된다.

3-3-3. 결합 실행 (mergeTo)

1
소비자가 보유한 토큰 A 1장 + 토큰 B 1장의 잔고를 확인한다.
2
소비자가 mergeTo(sourceIdA, sourceIdB, mergeId)를 호출한다.
3
컨트랙트가 소스 토큰 매칭, 잔고, 만료 여부를 검증한다.
4
검증 통과 시: _burn(user, sourceIdA, 1) + _burn(user, sourceIdB, 1) + _mint(user, mergeId, 1)가 원자적(atomic)으로 실행된다.

3-3-4. 분리 (unmerge) — 가역성 보장

  • 소비자가 결합 토큰을 1개 이상 보유하고 있어야 한다.
  • 해당 사용자+결합토큰에 대해 활성 에스크로가 없어야 한다 (activeEscrow[user][mergeId] == 0).

unmerge(mergeId) 호출 시: 결합 토큰 1장을 소각하고, 원래의 소스 토큰 A, B를 각 1장씩 소비자에게 재발행(mint)한다. 이로써 소비자는 결합 결정을 철회하고 원래 쿠폰으로 되돌릴 수 있다.

구체적 실시 예

예시: 치킨+맥주 결합 쿠폰
소스 A구로통닭 - 치킨 5,000원 할인권 (merchantIndex=1, tokenId=18446744073709551617)
소스 B골목맥주 - 맥주 3,000원 할인권 (merchantIndex=2, tokenId=36893488147419103233)
결합 전구로통닭 점주가 setAllowMerge(tokenA, tokenB, true) 호출
골목맥주 점주가 setAllowMerge(tokenB, tokenA, true) 호출
모드OR — 어느 한 가맹점에서 1회 사용
결합 결과치맥 결합권 (merchantIndex=0, tokenId=1)
allowedMerchantA=1(구로통닭), allowedMerchantB=2(골목맥주)
결합 실행소비자가 mergeTo 호출 → 치킨권 1장 소각 + 맥주권 1장 소각 → 치맥 결합권 1장 발행
분리소비자가 unmerge 호출 → 치맥 결합권 소각 → 치킨권 1장 + 맥주권 1장 복원
3-4. OR 모드 — 단일 가맹점 사용
결합 쿠폰을 어느 한쪽 가맹점에서 1회 소각하여 사용하는 방식

전제 조건

  • 결합 토큰의 mergeMode가 OR이어야 한다.
  • 사용 처리를 실행하는 점주의 merchantIndex가 allowedMerchantA 또는 allowedMerchantB 중 하나여야 한다.

처리 흐름

OR 모드 결합 쿠폰은 일반 쿠폰과 동일한 방식으로 사용된다. 기존의 redeemWithSig() (QR 서명 기반) 또는 redeemFrom() (승인 기반)을 그대로 사용하며, _validateMerchantForToken()에서 결합 토큰인 경우 allowedMerchantA/B 중 하나와 매칭되는지를 검증한다.

예시: 치맥 결합권(OR)을 구로통닭에서 사용

소비자가 구로통닭 매장에서 치맥 결합권을 제시 → 점주가 redeemWithSig로 소각 → 결합권 1장 소각됨 (맥주점 사용 불가 — 이미 소각되었으므로). 또는 골목맥주에서 사용하면 동일하게 1회 소각으로 완료된다.

3-5. AND 모드 — 에스크로 기반 2단계 사용
양쪽 가맹점 모두에서 순차적으로 사용해야 완료되는 방식
진보성 포인트: 결합 쿠폰을 양쪽 가맹점에서 순차적으로 사용해야 하는 경우, 첫 번째 사용 시 결합 토큰을 소각하고 에스크로를 생성하여, 두 번째 가맹점이 타임아웃 내에 완료하지 않으면 미사용 측의 소스 토큰만 선택적으로 소비자에게 자동 반환하는 메커니즘. 이로써 소비자가 부당하게 양쪽 혜택을 모두 잃는 것을 방지한다.

에스크로 상태 머신

NONE
→ useAtFirstMerchant
A_USED
→ useAtSecondMerchant
COMPLETE
A_USED
→ timeout 경과 + expireEscrow
EXPIRED
→ 미사용 소스 토큰 mint-back

Step 1: 첫 번째 가맹점 사용 (QR 기반)

  • 결합 토큰의 mergeMode가 AND이어야 한다.
  • 호출자(점주A)가 allowedMerchantA 또는 allowedMerchantB의 가맹점 소유자여야 한다.
  • 소비자(user)가 호출자에게 setApprovalForAll을 부여했어야 한다.
  • 소비자가 결합 토큰을 1개 이상 보유하고 있어야 한다.
  • 동일 user + mergeTokenId에 대해 기존 활성 에스크로가 없어야 한다.
QR1
점주A → 소비자: 점주A가 에스크로 관리 화면에서 결합 쿠폰과 자신의 가맹점을 선택하여 에스크로 요청(QR1)을 생성한다. QR1에는 요청 ID(rid)와 결제 URL이 인코딩된다.
승인
소비자 지갑: 소비자가 에스크로 확인 페이지에서 QR1을 스캔하면, 요청 상세(가맹점명, 결합 토큰 ID, 잔액)가 표시된다. 소비자가 "사용 확인"을 누르면 점주A 지갑에 대한 setApprovalForAll(merchantWallet, true)이 먼저 실행된다.
QR2
소비자 → 점주A: 승인 완료 후 소비자 화면에 QR2가 생성된다. QR2에는 { type: "escrow-confirm", rid, wallet, mergeTokenId, balance }가 포함된다. 소비자가 이 QR2를 점주에게 제시한다.
실행
점주A 온체인: 점주A가 QR2를 스캔하면 소비자 지갑, 잔액 등이 화면에 표시된다. 점주A가 확인 후 useAtFirstMerchant(user, mergeTokenId, merchantIndex)를 호출한다.
결과
결합 토큰 1장이 소각(_burn)되고, 에스크로 레코드가 생성된다: { state: A_USED, user, mergeTokenId, usedMerchantIndex, timeout }. 에스크로 ID가 화면에 크게 표시되며, 소비자에게 이 ID를 전달한다. activeEscrow[user][mergeTokenId] = escrowId로 기록되어 중복 에스크로가 방지된다.

소비자의 에스크로 ID 조회

소비자는 에스크로 확인 페이지의 "내 에스크로 ID 조회" 기능을 통해, 결합 토큰 ID를 입력하면 activeEscrow(myAddress, mergeTokenId)를 온체인으로 조회하여 자신의 활성 에스크로 ID를 확인할 수 있다. 이 에스크로 ID는 Step 2에서 B 가맹점 방문 시 필요하다.

Step 2: 두 번째 가맹점 완료 (QR 기반)

  • 에스크로 상태가 A_USED여야 한다.
  • 호출자(점주B)의 merchantIndex가 첫 번째 사용 가맹점(usedMerchantIndex)과 달라야 한다 (AlreadyUsedAt 방지).
  • 타임아웃이 경과하지 않았어야 한다.
QR1
점주B → 소비자: 점주B가 에스크로 관리 화면의 "2단계" 섹션에서 자신의 가맹점을 선택하고 QR1을 생성한다. QR1에는 2단계 요청 ID(step2 rid)가 포함된다. 점주B는 에스크로 ID를 알 필요가 없다.
입력
소비자: 소비자가 에스크로 확인 페이지에서 QR1을 스캔하면 2단계 모드로 전환된다. 에스크로 ID 입력 패널이 표시되고, 소비자가 Step 1에서 받은 에스크로 ID를 입력한다. (또는 "내 에스크로 ID 조회"로 자동 확인 가능)
QR2
소비자 → 점주B: 소비자가 "2단계 확인"을 누르면 QR2가 생성된다. QR2에는 { type: "escrow-step2-confirm", rid, wallet, escrowId }가 포함된다. 핵심적으로 에스크로 ID가 소비자에 의해 QR2에 포함되어 점주B에게 전달된다.
실행
점주B 온체인: 점주B가 QR2를 스캔하면 소비자 지갑과 에스크로 ID가 화면에 표시된다. 점주B가 확인 후 useAtSecondMerchant(escrowId, merchantIndex)를 호출한다.
결과
에스크로 상태가 COMPLETE로 변경되고, activeEscrow[user][mergeTokenId]가 0으로 초기화된다. 양쪽 가맹점 모두에서 쿠폰 사용이 완료된다.

에스크로 ID 전달 흐름의 설계 의도

B 가맹점(점주B)은 소비자의 에스크로 ID를 사전에 알 수 없다. 에스크로 ID는 Step 1에서 온체인으로 생성되며, 소비자만이 자신의 에스크로 ID를 알고 있다. 따라서 소비자가 QR2를 통해 에스크로 ID를 점주B에게 전달하는 구조를 채택하였다. 이로써:

  • 점주B는 에스크로 ID를 직접 입력하거나 조회할 필요가 없어 UX가 간소화된다.
  • 소비자가 자신의 에스크로를 능동적으로 제시하므로, 잘못된 에스크로가 처리될 위험이 없다.
  • QR 기반 대면 인증으로 소비자 본인 확인이 이루어진다.

타임아웃 및 자동 반환 (expireEscrow)

  • 에스크로 상태가 A_USED이고, block.timestamp > timeout이어야 한다.
  • 누구나(anyone) 호출할 수 있다 — 탈중앙화 실행.

타임아웃 시 에스크로가 EXPIRED로 전환된다. 이때 첫 번째 가맹점이 이미 사용한 소스 토큰은 소각된 것으로 유지되고, 두 번째 가맹점에 해당하는 미사용 소스 토큰만 소비자에게 반환(mint-back)된다.

반환 토큰 결정 로직:
if (sourceIdA의 merchantIndex == usedMerchantIndex) → sourceIdB를 반환
else → sourceIdA를 반환

이 메커니즘으로 첫 번째 가맹점의 혜택 제공은 유효하게 인정하면서, 두 번째 가맹점의 미제공 혜택에 해당하는 원래 쿠폰은 소비자에게 되돌려 준다.

예시: 치맥 결합권(AND) 사용 시나리오
상황소비자가 치맥 결합권(AND 모드) 1장을 보유. 구로통닭(치킨)과 골목맥주(맥주)에서 모두 사용해야 완료됨.
Step 1구로통닭 매장 방문 → 점주A가 QR1 생성 → 소비자가 스캔, setApprovalForAll 승인, QR2 생성 → 점주A가 QR2 스캔 → useAtFirstMerchant 호출 → 치맥 결합권 1장 소각 → 에스크로 생성 (escrowId=1, state=A_USED, timeout=30일후) → 소비자에게 에스크로 ID=1 전달
에스크로 ID 조회소비자가 에스크로 확인 페이지에서 "내 에스크로 ID 조회" → 결합 토큰 ID 입력 → activeEscrow 조회 → 에스크로 ID=1 확인
Step 2 (정상)30일 내 골목맥주 매장 방문 → 점주B가 2단계 QR1 생성 → 소비자 스캔, 에스크로 ID=1 입력, QR2 생성 → 점주B가 QR2 스캔 → useAtSecondMerchant(escrowId=1, merchantIndex=2) 호출 → 에스크로 COMPLETE
Step 2 (타임아웃)30일 경과 후 소비자가 골목맥주를 방문하지 않음 → 누군가가 expireEscrow(escrowId=1) 호출 → 에스크로 EXPIRED → 맥주 할인권(sourceIdB) 1장이 소비자에게 자동 반환됨 (치킨은 이미 사용되었으므로 반환되지 않음)
📷
3-6. EIP-712 서명 기반 QR 오프라인 결제
건별 서명으로 최소 권한 원칙을 구현한 오프라인 사용 프로토콜
보안 특징: 소비자는 특정 tokenId, 수량, merchantIndex, orderId, deadline에 한정된 1회성 서명만 생성한다. 점주는 이 서명으로 해당 건만 처리할 수 있으며, 다른 토큰이나 추가 수량에 대한 권한은 전혀 획득하지 못한다. setApprovalForAll()과 달리 전체 자산 노출 위험이 원천 차단된다.

EIP-712 서명 구조체

Redeem(address user, uint256 tokenId, uint256 amount, uint32 merchantIndex, bytes32 orderId, uint256 nonce, uint256 deadline)

QR 2단계 프로토콜

QR1
점주 → 소비자: 점주 POS에서 merchantIndex, tokenId, amount, orderId, deadline을 포함한 결제 요청 QR을 생성하여 소비자에게 제시한다.
서명
소비자 지갑: 소비자가 QR1을 스캔하면 MetaMask에서 EIP-712 타입 구조 서명(signTypedData_v4)을 생성한다. 이 서명은 해당 컨트랙트 주소, 체인ID, 위 구조체의 값에 대해서만 유효하다.
QR2
소비자 → 점주: 서명값(sig)과 파라미터를 담은 QR2를 점주 POS에 제시한다.
소각
점주 온체인: 점주가 redeemWithSig(user, tokenId, amount, merchantIndex, orderId, nonce, deadline, sig)를 호출. 컨트랙트가 서명 검증 후 쿠폰을 소각한다.

nonce를 통한 리플레이 방지

각 사용자(user)별로 redeemNonces[user]가 관리된다. 서명에 포함된 nonce와 현재 저장된 nonce가 일치해야만 유효하며, 사용 후 nonce가 +1 증가한다. 따라서 동일 서명을 재사용(replay)하는 것이 불가능하다.

예시: 치킨 할인 쿠폰 QR 결제
QR1 생성구로통닭 점주가 POS에서 { merchantIndex:1, tokenId:18446744073709551617, amount:1, orderId:0xabc..., deadline:now+300초 } QR 생성
소비자 서명소비자가 QR1 스캔 → MetaMask에서 EIP-712 서명 → sig(65 bytes) 생성
QR2 제시소비자 화면에 QR2 표시 (sig + user + nonce 포함)
소각 처리점주가 QR2 스캔 → redeemWithSig 호출 → 서명 검증(ECDSA.recover) → nonce 확인 → 쿠폰 1장 소각
🔒
3-7. 만료 토큰 전송 차단 (_update 레벨)
컨트랙트 레벨에서 만료·미시작 토큰의 유통을 원천 차단

ERC-1155의 _update() 내부 함수를 오버라이드하여, 모든 토큰 전송(transfer, safeTransfer, batch) 시점에 다음을 검증한다:

  • 만료 차단: expiresAt != 0 && block.timestamp > expiresAt && to != address(0) → 전송 거부 (Expired). 단, to == address(0)(소각)은 허용하여 만료 쿠폰의 정리를 가능하게 한다.
  • 미시작 차단: startAt != 0 && block.timestamp < startAt && from != address(0) → 전송 거부 (NotStarted). 단, from == address(0)(민팅)은 허용하여 사전 발행을 가능하게 한다.
  • 일시정지: whenNotPaused 수정자로 긴급 정지 시 모든 전송이 차단된다.

이 설계로 인해 만료된 쿠폰이 2차 시장에서 거래되거나, 시작 전 쿠폰이 유출되는 것을 온체인 레벨에서 방지한다.

3-8. P2P 쿠폰 거래소 (CouponExchange)
에스크로 기반 안전 거래 및 원자적 스왑
확장성 포인트: ERC-1155 토큰화된 쿠폰이 2차 시장에서 거래될 수 있는 온체인 거래소. 판매(쿠폰→ETH)와 스왑(쿠폰↔쿠폰) 두 가지 모드를 지원하며, Coupon1155V3의 _update() 오버라이드로 만료 토큰의 거래소 예치가 자동 차단된다. 향후 RWA(Real World Asset) 토큰 거래로 확장 가능한 구조이다.

거래소 아키텍처

CouponExchange는 Coupon1155V3와 별도로 배포되는 독립 컨트랙트이다. ERC1155Holder를 상속하여 ERC-1155 토큰을 에스크로로 보유할 수 있으며, 판매자/제안자의 토큰을 컨트랙트에 예치한 후 구매자/수락자의 행동에 의해 원자적으로 정산한다.

3-8-1. 판매 모드 (Sell Order: 쿠폰 → ETH)

  • 판매자가 거래소 컨트랙트에 setApprovalForAll을 부여해야 한다.
  • 쿠폰이 만료되지 않았어야 한다 (만료 시 safeTransferFrom이 revert).
  • 판매 가격(priceWei)이 0보다 커야 한다.
1
판매 등록: 판매자가 createSellOrder(tokenId, amount, priceWei)를 호출. 쿠폰이 판매자 지갑에서 거래소 컨트랙트로 에스크로 전송된다.
2
구매: 구매자가 buyCoupon(orderId)를 ETH와 함께 호출. 수수료(feeBps, 기본 2.5%)가 공제된 후 판매대금이 판매자에게, 쿠폰이 구매자에게 원자적으로 전달된다.
취소
판매자 본인만 cancelSellOrder(orderId)로 취소 가능. 에스크로된 쿠폰이 판매자에게 반환된다.

3-8-2. 스왑 모드 (Swap Order: 쿠폰 ↔ 쿠폰)

  • 제안자가 제공할 쿠폰(offerToken)과 원하는 쿠폰(wantToken)을 지정한다.
  • 제안자의 쿠폰이 거래소에 에스크로된다.
  • 수락자는 원하는 쿠폰을 보유하고 있어야 하며, 거래소에 setApprovalForAll을 부여해야 한다.
1
스왑 제안: 제안자가 createSwapOrder(offerTokenId, offerAmt, wantTokenId, wantAmt)를 호출. 제공 쿠폰이 에스크로된다.
2
스왑 수락: 수락자가 acceptSwap(orderId)를 호출. 수락자의 쿠폰이 제안자에게, 에스크로된 제안자의 쿠폰이 수락자에게 원자적(atomic)으로 교환된다. 한쪽만 전달되는 상황이 불가능하다.
취소
제안자 본인만 cancelSwapOrder(orderId)로 취소 가능.

3-8-3. 만료 토큰 거래 자동 차단

Coupon1155V3의 _update() 오버라이드로 인해, 만료된 쿠폰을 거래소 컨트랙트로 전송(safeTransferFrom)하는 것 자체가 revert된다. 따라서 거래소 컨트랙트에 별도의 만료 검증 로직을 구현할 필요가 없으며, 쿠폰 컨트랙트의 보안 정책이 거래소에도 투명하게 적용된다. 이는 ERC-1155 표준의 _update() 레벨 제어가 생태계 전반에 파급되는 설계적 이점이다.

3-8-4. 호가 거래 모드 (Order Book Trading)

현재 마켓(게시판) 모드에 더하여, 대량 발행된 동일 토큰에 대해 호가(Order Book) 방식의 거래도 지원할 수 있도록 설계되어 있다. ERC-1155의 특성상 동일 tokenId의 토큰은 대체 가능(Fungible)하므로, 매수/매도 호가를 매칭하는 연속 거래가 가능하다.

3-8-4-1. 듀얼 모드 아키텍처

  • 마켓 모드 (현재 구현): 개별 쿠폰, 한정판, 결합 쿠폰 등 소량/다종 거래에 적합. 판매자가 가격을 지정하고 구매자가 수락하는 게시판 방식.
  • 호가 모드 (설계 완료): 정액 상품권, 포인트 토큰, RWA 자산 등 대량/동종 거래에 적합. 매수/매도 호가가 자동 매칭되는 연속 경매(Continuous Double Auction) 방식.

3-8-4-2. 호가 매칭 엔진 설계

호가 거래 모드는 가격-시간 우선(Price-Time Priority) 원칙에 따라 주문을 매칭한다. 온체인 가스 비용을 최적화하기 위해 하이브리드 매칭 구조를 채택한다.

  • 온체인 정산: 매칭된 주문의 토큰 이전과 ETH 정산은 스마트 컨트랙트에서 원자적으로 실행. 부분 체결(Partial Fill) 지원.
  • 오프체인 매칭 (옵션): 대량 주문 시 매칭 연산을 오프체인에서 수행하고 결과만 온체인에 제출하는 릴레이어 구조. zkProof로 매칭 정당성을 검증할 수 있다.
  • 주문 유형: 지정가(Limit Order) — 특정 가격 지정, 시장가(Market Order) — 현재 최우선 호가로 즉시 체결.
  • 호가창 구조: 매도 호가(Ask)는 낮은 가격순, 매수 호가(Bid)는 높은 가격순으로 정렬. 스프레드(Spread)는 최우선 매도가와 최우선 매수가의 차이.

3-8-4-3. RWA(Real World Asset) 확장

ERC-1155 기반 토큰화 인프라와 호가 거래소를 결합하면, 쿠폰을 넘어 다양한 실물 자산(RWA)의 토큰화와 2차 시장 거래가 가능하다. 동일 플랫폼에서 발행-유통-거래의 전 라이프사이클을 관리할 수 있는 것이 핵심 강점이다.

  • 부동산 분할 소유권: 건물/토지의 소유권을 N개 토큰으로 분할 발행. 투자자 간 호가 거래로 유동성 확보. 임대 수익은 토큰 보유 비율에 따라 자동 분배.
  • 상품권/포인트: 정액 상품권, 기프트카드, 포인트를 토큰화. 시세에 따른 할인/프리미엄 거래. 만료 기능이 그대로 적용되어 유효기간 자동 관리.
  • 탄소 크레딧: 탄소 배출권 1톤 단위 토큰화. 기업 간 대량 호가 거래. 온체인 투명성으로 이중 사용 방지.
  • 공연/이벤트 티켓: 좌석별 토큰 발행. 2차 시장 리세일 시 원작자 로열티 자동 정산. 전매 가격 상한 설정 가능.
  • 멤버십/구독권: 기간제 멤버십을 토큰화. 잔여 기간에 따라 가치가 변동하는 감가 거래.
RWA 호가 거래 시나리오
부동산A 건물 100분의 1 소유권 토큰(tokenId=100) 1,000개 발행 → 투자자들이 호가 거래소에서 매수/매도 → 0.5 ETH에 체결 → 분기별 임대수익 자동 분배
상품권5만원 상품권 토큰(tokenId=200) 10,000장 발행 → 호가창에서 0.025 ETH(액면가 대비 5% 할인)에 거래 → 시장 수급에 따라 가격 변동
탄소크레딧1톤 크레딧 토큰(tokenId=300) → 기업 A가 100톤 매수 주문(0.01 ETH/톤) → 기업 B가 매도 수락 → 즉시 온체인 정산
예시: 쿠폰 거래소 사용 시나리오
판매사용자 A가 치킨 5,000원 할인권 1장을 0.001 ETH에 판매 등록 → 쿠폰이 거래소에 에스크로 → 사용자 B가 0.001 ETH 지불 → 쿠폰이 B에게, ETH가 A에게 원자적 정산 (수수료 2.5% 공제)
스왑사용자 A가 "치킨 할인권 1장 ↔ 맥주 할인권 1장" 스왑 제안 → 치킨 할인권 에스크로 → 사용자 C(맥주 할인권 보유)가 수락 → 치킨권이 C에게, 맥주권이 A에게 원자적 교환
만료 차단사용자 D가 만료된 쿠폰을 판매 등록 시도 → safeTransferFrom에서 Expired revert → 등록 자체 불가

4. 전체 서비스 흐름도

🚀
쿠폰 라이프사이클
발행부터 사용까지의 전체 과정
1
가맹점 등록 및 쿠폰 발행
관리자가 registerMerchant로 가맹점을 등록한다. 점주가 createCoupon으로 쿠폰을 블록체인에 발행한다. 메타데이터와 이미지는 IPFS에 영구 저장된다.
2
에어드랍(배포) 및 수령
소비자가 쿠폰 수령을 신청하면, 점주가 safeTransferFrom으로 토큰을 전송한다. 소비자 지갑에서 보유 현황을 확인할 수 있다.
3
쿠폰 결합 (선택사항)
양쪽 가맹점이 setAllowMerge로 상호 합의 → 관리자가 createMergeType으로 결합 타입 생성 → 소비자가 mergeTo로 A+B 소각하여 결합 쿠폰 C를 획득. 원하면 unmerge로 되돌릴 수 있다.
4
쿠폰 사용 (OR/AND)
일반 쿠폰 및 OR 모드: EIP-712 서명 QR 프로토콜(QR1 → 서명 → QR2 → redeemWithSig)로 1회 소각.
AND 모드: 점주A가 QR1 생성 → 소비자가 승인+QR2 제시 → 점주A가 useAtFirstMerchant로 에스크로 생성 → 소비자가 에스크로 ID 확인 → 점주B가 2단계 QR1 생성 → 소비자가 에스크로 ID 입력+QR2 제시 → 점주B가 useAtSecondMerchant로 완료. 타임아웃 시 미사용분 자동 반환.
5
쿠폰 거래소 (2차 시장)
판매: 소비자가 보유 쿠폰을 ETH 가격으로 거래소에 등록(에스크로). 다른 소비자가 ETH로 구매하면 원자적 정산.
스왑: 소비자 간 쿠폰↔쿠폰 교환 제안/수락. 이종 가맹점 쿠폰도 자유롭게 교환 가능.
만료된 쿠폰은 거래소 등록 자체가 차단됨.

5. 컨트랙트 정보

배포된 스마트 컨트랙트
Contract NameCoupon1155V3
Address
NetworkOP Sepolia (Chain ID: 11155420)
Solidity^0.8.26
DependenciesOpenZeppelin v5.0 (ERC1155, Ownable2Step, Pausable, ReentrancyGuard, EIP712, ECDSA)
거래소 스마트 컨트랙트
Contract NameCouponExchange
Address(미배포)
기능판매(ETH), 스왑(쿠폰 교환), 수수료, 에스크로
DependenciesOpenZeppelin v5.0 (ERC1155Holder, Ownable, ReentrancyGuard)
i
점주 지갑과 동일한 계정으로 MetaMask에 연결한 상태에서 쿠폰을 발행하세요. 발행된 쿠폰은 자동으로 카탈로그에 등록됩니다.
쿠폰 정보 입력
가맹점과 쿠폰 상세 정보를 입력하세요

        
i
결합 쿠폰을 만들려면 양쪽 가맹점이 각각 자기 지갑으로 allowMerge를 실행해야 합니다. 자신의 쿠폰과 상대 쿠폰을 선택하고 허용 버튼을 누르세요.
Merge 허용 설정
각 가맹점 지갑으로 연결하여 상대 쿠폰과의 결합을 허용하세요
-

        
i
MergeType은 서로 다른 두 쿠폰(예: 치킨+맥주)을 소각하여 새로운 결합 쿠폰(치맥)을 생성하는 정의입니다. 양쪽 가맹점이 먼저 결합 허용 메뉴에서 allowMerge를 완료해야 합니다.
결합 쿠폰 설정
소스 쿠폰과 결합 쿠폰 정보를 입력하세요

        
🎁
쿠폰 선택
카탈로그에서 쿠폰을 선택하세요
이미지
-

-

URI: -

점주 재고: -


        
📋
에어드랍 요청 목록
ID가맹점요청자Token ID수량내 재고상태액션

      
지갑을 연결하고 쿠폰을 불러오세요

      
🃏
내 보유 쿠폰
(empty)
MergeType 선택
결합할 쿠폰 조합을 선택하세요
A(source)-
B(source)-
Merge(결합권)-

A 1장 + B 1장 보유 시 결합 가능

이미지

URI: -

-


        
i
AND 모드 결합 쿠폰은 양쪽 가맹점에서 모두 사용해야 완료됩니다. POS와 동일하게 QR 기반으로 처리됩니다.
1단계: 첫 번째 가맹점 사용
QR로 소비자를 확인하고 에스크로를 생성합니다
QR1 - 소비자에게 보여주세요
rid: -
QR2 스캔 - 소비자 확인

소비자가 #에스크로 확인 페이지에서 QR2를 보여줍니다.

2단계: 두 번째 가맹점 완료
QR로 소비자를 확인하고 에스크로를 완료합니다
QR1 - 소비자에게 보여주세요 (2단계)
rid: -
QR2 스캔 - 소비자 확인 (2단계)

소비자가 에스크로 확인 페이지에서 QR2를 보여줍니다.

🕑
에스크로 조회 / 만료 처리

에스크로 ID를 입력하고 조회하세요.


      
📷
요청 불러오기
점주의 QR1을 스캔하거나 rid를 입력하세요
rid-
가맹점-
점주 지갑-
결합 토큰 ID-
내 잔액-
만료-
QR2 - 점주에게 보여주세요
확인 정보
(empty)

점주가 QR2를 스캔하면 에스크로가 생성됩니다.


        
🔍
내 에스크로 ID 조회
결합 토큰 ID로 내 활성 에스크로를 조회합니다

1단계 완료 후 여기서 에스크로 ID를 확인하여 2단계에서 사용하세요.

💳
결제 설정
가맹점, 쿠폰, 수량을 선택하세요
QR1 - 소비자에게 보여주세요
rid: -
url: -
QR2 스캔 - 소비자 서명 확인

QR2가 복잡할 수 있으니 카메라 초점을 맞춰주세요.


      
i
거래소에 쿠폰을 등록하면 컨트랙트에 에스크로로 예치됩니다. 만료된 쿠폰은 등록 자체가 불가합니다.
주문 목록
ETH로 구매할 수 있는 쿠폰

불러오는 중...

주문 등록
보유 쿠폰을 ETH로 판매합니다
📋 내 주문 관리


        
📷
요청 불러오기
점주의 QR1을 스캔하거나 rid를 입력하세요
rid-
가맹점-
점주 지갑-
merchantIndex-
orderId-
tokenId-
수량-
deadline-

1회 소각 권한만 서명합니다 (전체 권한 아님)

nonce
-
QR2 - 점주 POS에서 스캔
서명 Payload
(empty)

QR2가 복잡하면 화면 밝기를 높이고 거리를 띄워주세요.