프로젝트를 시작하기에 앞서 온체인 개발 스택은 다음과 같다.
- On-chain
- Solidity (Smart Contracts)
- Hardhat (Build / Test / Deploy)
- Sepolia (Testnet)
여기서 Hardhat은 스마트컨트랙트 테스트 및 배포 도구이다. 이더리움 네트워크에 스마트컨트렉트가 올라가면 EVM에 의해 실행되는데 이때 EVM은 특정 코드가 어떻게 처리될 지를 정의한 규칙들의 집합이다. hardhat은 컨트랙트의 코드가 의도대로 동작하는지 EVM과 동일한 환경에서 미리 실행·검증하게 해주는 개발 도구라고 보면 된다. 로컬 환경에서의 테스트, 테스트넷 배포, 메인넷 배포까지도 지원한다.
추가로 현재 블록체인 개발에서 노드 접근과 온체인 데이터 통신에 주로 JSON-RPC 기반의 인터페이스를 사용하고 개발 도구와 클라이언트 라이브러리 생태계가 Javascript/Typescript 중심으로 형성되어 있다. 따라서 컨트랙트 외부 프론트나 백엔드 개발에 있어서 React, NestJS 같은 프레임워크가 레퍼런스도 많고 호환성 측면에서 유리하다.
온체인 개발
스마트컨트랙트 개발에 있어 추후 구현할 토큰 관련 로직의 ERC721, ERC20 등의 기본 구조를 상속받을 수 있는 라이브러리를 먼저 설치했다.
npm install @openzeppelin/contracts
스마트컨트랙트는 일단 다음 4개의 파일로 분리했다.
- HouseNFT : 리뷰 권한 Soulbound token(HRT)
- HRTHistory : HRT 권한과 history 기록
- ReviewToken : 리뷰 보상 및 열람 비용 지불 관련 FT(RVT)
- ReviewRegistry : 정의된 HRT의 권한 관리 및 history 기록, RVT 보상 및 차감 관리
HouseNFT

HouseNFT 스마트컨트렉트는 ERC721을 상속받았으므로 _safeMint(to, tokenId)로 발행할 때만 신경쓰면 토큰과 그 주인의 매핑관계는 자동으로 관리된다. SBT 컨셉상 계약에 대해 HRT 발급 여부를 확인하는 minted라는 상태를 추가했고, Ownable에서 상속받은 _beforeTokenTransfer을 override해서 transfer, approve 기반 이동을 전면으로 차단했다.
스마트컨트렉트는 기본적으로 (key, value) 자료구조로 상태를 저장하는데 ERC721을 상속받은 HouseNFT에서 관리되는 주요 상태는 다음과 같다.
| 상태 | 자료구조 | key | value | 설명 |
| 발급 여부 (minted) | mapping | contractIdHash | bool | 계약당 1회 발급 체크 |
| 소유자 (_owners) | mapping | contractIdHash | owner address | HRT 소유자 |
| 소유자 잔액 (_balances) |
mapping | owner address | uint256 | 소유자가 가진 HRT 수 (한 사람이 여러 계약 가능) |
| registry | 변수 | - | address | HRT 발급 권한 컨트랙트 주소 |
| approved / operator | mapping | tokenId / owner | approved address / bool |
ERC721 기본 승인 관리, SBT라 사용 X |
실제 저장되는 형식은 다음과 같다.
minted 상태
| contractIdHash | minted |
| 0x8bc7066b9edae88da34970118d5968285ace40153 | true |
| 0xdeefc17f4163c39aaf0d843ff62f52c5a421cd6e | true |
_owners 상태
| contractIdHash | owner address |
| 0x8bc7066b9edae88da34970118d5968285ace40153 | tenant1 |
| 0xdeefc17f4163c39aaf0d843ff62f52c5a421cd6e | tenant2 |
balance 상태
| owner | balance |
| tenant1 | 1 |
| tenant2 | 1 |

HRTHistory.sol

HRTHistory 스마트컨트렉트에서는 HRT에 대해 기록하는 컨트랙트로 오프체인 리뷰의 무결성을 검증하고, HRT가 soulbound로서 권한 제어가 되고있음을 공개한다. HRT가 발급될 때 6개 필드가 모두 초기화되며 리뷰를 작성해야 결정되는 usedAt, reviewHash 필드는 0으로 초기화됐다가 세입자가 리뷰 작성을 하면 올바른 값으로 채워진다. 리뷰 작성시 다음의 조건을 걸어 두번째 리뷰, 허용 기간 외 리뷰를 막았다.
내부적으로 (key, value) 구조인 Record 구조체에서는 HouseNFT와 다르게 key: {value1, value2...} 구조로 상태들이 저장된다.

ReviewToken.sol

ReviewToken은 리뷰를 등록하거나 열람할 때 보상 또는 차감되는 포인트에 해당하는 FT이다. ERC20을 상속받았고 화폐처럼 융통돼서 사용할 수 없고, 정해진 규칙으로 발행과 소각만 가능하도록 구현했다. {토큰: 소유자}, {소유자: 잔액} 2개의 매핑이 필요한 HouseNFT와 다르게 ERC20에서는 {소유자: 잔액} 매핑 하나로 관리한다.

ReviewRegistry.sol
ReviewRegistry는 앞선 3개의 스마트컨트렉트를 통합 관리하는 컨트렉트로서 앞선 세 컨트랙트와 reviewRegistry 필드로 연결되는 제어 컨트렉트이다.
| 기능 | 주체 | 설명 |
| 리뷰 권한 발급 (issueReviewRight) | 관리자 | NFT 발급 + HRTHistory 레코드 생성 (오프체인 검증 완료시) |
| 리뷰 제출 (submitReview) | 세입자 | NFT 존재/소유 확인 → HRTHistory 검증 → 리뷰 기록 → RVT 보상 → NFT 회수 |
| 리뷰 열람 (payToView) | 누구나 | RVT 차감 → off-chain 리뷰 제공 |
| 만료된 리뷰 회수 (revokeExpired) | 관리자 | NFT 회수 (리뷰 작성시, 기간 만료시) |
| 관리용 세팅 | 관리자 | 보상/열람 비용 조정 |

ReviewRegistry가 자체적으로 관리하는 상태는 reviewReward, viewCost, 나머지 컨트랙트들의 주소뿐이지만 위 기능들을 담당하며 나머지 세 컨트렉트의 상태를 폭넓게 관리하는 orchestrator로 볼 수 있다.
로컬에서 테스트하기 위해 다음과 같은 시나리오로 hardhat 배포 스크립트를 작성해봤다.
- Admin: 컨트랙트 배포·설정, ReviewRegistry를 다른 컨트랙트와 연결하여 관리 권한 부여
- Tenant1: house1 계약 → HRT 발급 → 리뷰 제출 → RVT 10개 보상
- Tenant2: house2 계약 → HRT 발급 → 리뷰 제출 → RVT 10개 보상 → house1 리뷰 열람 → RVT 1개 차감
npx hardhat node로 로컬에서 네트워크 환경을 세팅하고 배포 스크립트를 실행한 결과는 다음과 같다.



편의상 리뷰 만료 시각은 발급 후 한 달로 설정하고 record의 timestamp 값들은 가독성 좋게 변환했다. house1의 리뷰를 본 후 Tenant2의 balance가 1 RVT 줄은 9.0 RVT가 되며 의도대로 잘 동작함을 확인할 수 있다.
여러 차례 테스트 후 코드를 수정해서 배포 스크립트를 재실행했다.



수차례 수정하고 배포했던 흔적을 볼 수 있다.
완성된 컨트랙트 배포 후에 Next.js, wagmi를 활용하여 프론트엔드를 구현하고 컨트랙트와 적절히 연결하면 테스트용 UI가 완성된다. NestJS로 오프체인 서버까지 구축하면 Web3 DApp이 완성된다.