SecureChain AI에서 풀스택 DeFi 스테이킹 플랫폼 구축하기 — Web3 인턴십 4주 차 여정
요약
SecureChain AI 메인넷에 배포된 풀스택 DeFi 스테이킹 플랫폼 구축 과정을 다룹니다. MetaMask, RainbowKit, Ethers.js v6를 활용하여 스마트 컨트랙트와 프론트엔드를 연결하는 설계 및 구현 방법을 설명합니다.
핵심 포인트
- ERC20 토큰, 스테이킹 컨트랙트, 프론트엔드의 유기적 연결 구조
- Ethers.js v6를 이용한 스마트 컨트랙트 상호작용 구현
- 지갑 연결성 및 UI 상태 관리의 중요성
- 토큰 승인, 보상 수학, 보안 고려 사항
MetaMask, RainbowKit, 그리고 Ethers.js v6를 사용하여 두 개의 스마트 컨트랙트를 실제 스테이킹 dApp에 어떻게 설계, 배포 및 연결했는지에 대하여
서론 (Introduction)
EtherAuthority Web3 인턴십 4주 차에 접어들며, 제가 가장 자랑스럽게 생각하는 프로젝트를 소개합니다. 바로 **SecureChain AI (SCAI) 메인넷 (Mainnet)**에 실제로 배포된 완전한 기능의 **DeFi 스테이킹 플랫폼 (DeFi Staking Platform)**입니다.
스테이킹 프로토콜을 사용해 본 적이 있다면 핵심 개념을 알고 계실 것입니다. 토큰을 예치하고, 시간이 지남에 따라 수익을 얻으며, 원할 때 언제든 출금하는 것입니다. 겉보기에는 간단해 보이지만, 처음부터 끝까지 모든 과정을 직접 구축하는 것은 완전히 다른 이야기입니다. 토큰 승인 (token approvals), 보상 수학 (reward math), UI 상태 관리 (UI state management), 지갑 연결성 (wallet connectivity), 그리고 보안 (security)을 동시에 고려해야 합니다.
이 포스트에서는 제가 구축한 모든 것, 제가 내린 모든 결정, 그리고 제가 배운 모든 교훈을 상세히 설명합니다. 글을 다 읽을 때쯤이면 제가 무엇을 만들었는지뿐만 아니라, 왜 그런 방식으로 만들었는지도 이해하게 될 것입니다.
라이브 데모 (Live Demo): https://de-fi-staking-platform-vert.vercel.app
GitHub: https://github.com/feudcommon/DeFi-staking-platform
DeFi 스테이킹이란 무엇인가? (What Is DeFi Staking?)
코드로 들어가기 전에, 우리가 같은 이해를 하고 있는지 확인해 봅시다.
DeFi에서의 **스테이킹 (Staking)**이란 보상(보통 더 많은 토큰)을 받는 대가로 스마트 컨트랙트(smart contract)에 토큰을 잠그거나(locking) 예치(depositing)하는 것을 의미합니다. 보상률은 일반적으로 APR (연간 이율, Annual Percentage Rate) 또는 APY (연간 복리 수익률, Annual Percentage Yield, 복리 계산 포함)로 표현됩니다.
모든 스테이킹 플랫폼의 핵심 구성 요소는 다음과 같습니다:
- ERC20 토큰 — 사용자가 스테이킹하는 토큰
- 스테이킹 컨트랙트 (staking contract) — 예치된 토큰을 보유하고 보상을 계산 및 분배함
- 프론트엔드 (frontend) — 사용자가 스테이킹, 출금 및 클레임(claim)을 하기 위해 상호작용하는 UI
이 세 가지 요소가 모두 원활하게 함께 작동해야 합니다. 그것이 바로 이 프로젝트의 핵심입니다.
시스템 아키텍처 (System Architecture)
모든 것이 어떻게 연결되는지에 대한 상위 수준의 그림은 다음과 같습니다:
사용자 (User)
↓
MetaMask 지갑 (트랜잭션 서명)
...
프론트엔드 (Frontend)는 사용자와 블록체인 사이의 가교 역할을 합니다. 스테이킹 (Staking), 출금 (Withdrawing), 클레임 (Claiming) 등 사용자가 수행하는 모든 동작은 서명된 트랜잭션 (Signed transaction)으로 변환되어 Ethers.js를 통해 적절한 스마트 컨트랙트 (Smart contract)로 전송됩니다.
스마트 컨트랙트 (Smart Contracts)
컨트랙트 1: ERC20 토큰 컨트랙트 (ERC20 Token Contract)
배포 주소: 0x4EF03D37c441BcF78D61367f4EE709027632d929
이것은 사용자가 스테이킹하는 토큰입니다. 표준 ERC20 인터페이스 (Interface)를 따르며, 다음과 같은 기능을 포함합니다:
balanceOf(address)— 지갑이 보유한 토큰 수량 확인transfer(to, amount)— 다른 주소로 토큰 전송approve(spender, amount)— 다른 컨트랙트가 사용자의 토큰을 사용할 수 있도록 권한 부여transferFrom(from, to, amount)— 스테이킹 컨트랙트가 사용자로부터 토큰을 가져올 때 사용
approve → transferFrom 패턴은 DeFi에서 이해해야 할 매우 중요한 부분입니다. 스마트 컨트랙트는 권한 없이 사용자의 지갑에서 토큰을 가져올 수 없기 때문에, 먼저 토큰 컨트랙트에서 approve()를 호출하여 스테이킹 컨트랙트가 사용자의 토큰을 이동시킬 수 있도록 승인해야 합니다. 그 후에야 스테이킹 컨트랙트가 transferFrom()을 호출하여 실제로 토큰을 이동시킬 수 있습니다.
이 2단계 프로세스는 많은 사용자에게 혼란을 줍니다. 프론트엔드에서 이 문제를 어떻게 처리했는지는 나중에 다시 다루겠습니다.
컨트랙트 2: 스테이킹 컨트랙트 (Staking Contract)
배포 주소: 0x9aab06FAE31e082c26979afca9E53897dB57D50C
이것이 플랫폼의 핵심입니다. 토큰 수령, 누가 얼마를 스테이킹했는지 추적, 보상 계산, 출금 처리 등 모든 것을 관리합니다.
전체 공개 인터페이스 (Public interface)는 다음과 같습니다:
// 스테이킹 컨트랙트로 ERC20 토큰 입금
function stake(uint amount) external;
...
각 함수를 자세히 살펴보겠습니다:
stake(uint amount)
사용자가 이 함수를 호출하면 컨트랙트는 다음을 수행합니다:
transferFrom(msg.sender, address(this), amount)를 호출하여 사용자의 지갑에서 토큰을 가져옵니다.- 매핑 (Mapping)에서 사용자의 스테이킹 잔액을 업데이트합니다.
- 타임스탬프 (Timestamp)를 기록합니다 (나중에 보상 계산에 사용됨).
Withdraw(uint amount)
이를 통해 사용자는 언제든지 토큰을 회수할 수 있습니다 — 잠금 기간 (lock-up period)이 없습니다. 컨트랙트는 다음과 같이 동작합니다:
- 사용자가 충분한 양을 스테이킹했는지 확인합니다.
- 출금 전 미지급된 보상 (pending rewards)을 계산합니다 (중요 — 사용자가 받지 않은 보상을 놓치게 해서는 안 됩니다).
- 요청된 금액을 사용자에게 다시 전송합니다.
claimRewards()
마지막 클레임(claim) 이후에 획득한 보상을 계산하여 사용자에게 전송합니다. 보상은 일반적으로 다음을 기준으로 계산됩니다:
- 사용자가 스테이킹한 금액
- 스테이킹을 유지한 기간
- 보상률 (APR)
간단한 보상 공식은 다음과 같습니다:
rewards = stakedAmount × APR × (timeElapsed / 365 days)
getDashboardData(address)
이것은 프론트엔드에 필요한 모든 정보를 단 한 번의 호출로 반환하는 view 함수 (가스 비용이 들지 않으며 트랜잭션이 필요 없음)입니다:
- 현재 스테이킹된 금액
- 미지급 보상 (Pending rewards)
- 현재 APR
이 정보들을 세 번의 개별 호출 대신 하나의 호출로 배치(Batching)하는 것은 작지만 의미 있는 최적화입니다. 이는 RPC 호출을 줄이고 UI 로딩 속도를 높여줍니다.
왜 잠금 기간(Lock-Up Period)이 없는가?
여기서 저는 의도적인 설계 선택을 했습니다: 사용자는 언제든지 출금할 수 있습니다. 많은 스테이킹 프로토콜은 더 긴 약정을 유도하기 위해 잠금 기간을 강제합니다. 저는 몇 가지 이유로 이를 선택하지 않았습니다:
- 감사(Audit)와 이해가 더 쉽습니다.
- 사용자에게 자금에 대한 완전한 통제권을 부여합니다.
- 더 정직합니다 — 사람들의 토큰을 가두지 않습니다.
보안: 스마트 컨트랙트 감사 (Smart Contract Audit)
두 컨트랙트 모두 EtherAuthority (https://etherauthority.io)로부터 감사를 받았습니다.
감사가 중요한 몇 가지 핵심 이유는 다음과 같습니다:
- 재진입 공격 (Reentrancy attacks) — 악의적인 컨트랙트가 첫 번째 실행이 끝나기 전에 귀하의 컨트랙트를 다시 호출하여 자금을 탈취하는 전형적인 DeFi 취약점 공격입니다.
- 정수 오버플로/언더플로 (Integer overflow/underflow) — Solidity 0.8+ 버전에서는 이를 기본적으로 처리하지만, 여전히 검증할 가치가 있습니다.
- 액세스 제어 (Access control) — 오직 권한이 있는 주소만이 관리자 함수를 호출할 수 있는지 확인합니다.
- 보상 조작 (Reward manipulation) — 수학적 계산을 악용하여 이득을 취할 수 없도록 보장합니다.
사용자의 자금을 중요하게 생각한다면, 서비스를 출시하기 전 감사를 받는 것은 선택 사항이 아닙니다. 이는 기본 중의 기본(table stakes)입니다.
프론트엔드 (Frontend): React + TypeScript + Ethers.js v6
기술 스택 (Tech Stack)
| 계층 (Layer) | 기술 (Technology) |
|---|---|
| 프레임워크 (Framework) | React 18 |
| ... |
프로젝트 구조 (Project Structure)
src/
├── contracts/
│ └── config.ts # ABIs 및 컨트랙트 주소
...
config.ts 파일은 컨트랙트 주소와 ABI를 위한 단일 진실 공급원 (single source of truth)입니다. 이를 중앙 집중화해 두면, 컨트랙트를 재배포할 때 단 하나의 파일만 업데이트하면 됩니다.
SCAI 메인넷 (SCAI Mainnet) 연결하기
SCAI 메인넷은 MetaMask의 기본 네트워크가 아니므로 사용자가 직접 추가해야 합니다. 네트워크 상세 정보는 다음과 같습니다:
| 필드 (Field) | 값 (Value) |
|---|---|
| 네트워크 이름 (Network Name) | SCAI Mainnet |
| ... |
프론트엔드는 연결된 체인 ID (chain ID)를 감지하며, 사용자가 잘못된 네트워크에 있을 경우 전환을 요청합니다. 즉, 오류가 조용히 발생하는 일(silent failures)은 없습니다.
RainbowKit을 이용한 멀티 지갑 지원 (Multi-Wallet Support)
지갑 연결 기능을 처음부터 직접 만드는 대신, 저는 RainbowKit을 사용했습니다. RainbowKit을 사용하면 세련된 UI와 함께 MetaMask, WalletConnect, Coinbase Wallet 지원을 즉시 사용할 수 있습니다.
import { RainbowKitProvider, getDefaultWallets } from '@rainbow-me/rainbowkit';
import { configureChains, createConfig, WagmiConfig } from 'wagmi';
RainbowKit은 지갑 모달, 연결 상태, 체인 전환을 처리합니다. 개발자는 프로바이더 (provider)를 배치하고 훅 (hooks)을 사용하기만 하면 됩니다.
자동 승인 흐름 (The Auto-Approval Flow) — 가장 중요한 UX 결정
UX 관점에서 제가 가장 자랑스럽게 생각하는 부분입니다.
문제점: 토큰을 스테이킹 (stake)하려면 사용자는 다음 단계를 거쳐야 합니다:
- ERC20 컨트랙트에서
approve()를 호출하여 스테이킹 컨트랙트에 권한을 부여합니다. - 그 다음 스테이킹 컨트랙트에서
stake()를 호출합니다.
만약 이 과정을 두 개의 별개 버튼으로 노출한다면, 사용자는 혼란을 느끼고, 트랜잭션 (transaction)은 실패하며, 고객 지원 업무는 늘어날 것입니다.
해결책: 백그라운드에서 이 과정을 자동으로 처리합니다.
const handleStake = async (amount: string) => {
const parsedAmount = ethers.parseUnits(amount, 18);
...
사용자 관점에서는 그저 "Stake"를 클릭하고 한두 개의 MetaMask 팝업을 승인하기만 하면 됩니다. approve가 무엇을 의미하는지, 혹은 왜 그것을 먼저 수행해야 하는지에 대해 혼란을 느낄 필요가 없습니다.
이것이 사람들이 실제로 사용하는 dApp(탈중앙화 애플리케이션)과 사용자가 즉시 이탈해버리는 dApp 사이의 차이점입니다.
라이브 대시보드 (The Live Dashboard)
대시보드는 프론트엔드(Frontend)의 핵심입니다. 대시보드는 다음 항목들을 보여줍니다:
- Staked Amount (스테이킹된 금액) — 사용자가 예치한 토큰 수량
- Available Rewards (수령 가능한 보상) — 현재까지 누적된 미청구 수익
- APR (연간 이율) — 현재의 연간 이율
- APY (연간 수익률) — 복리(Compounding)가 반영된 연간 수익률
- Wallet Balance (지갑 잔액) — 사용자가 보유한 토큰 수량 (아직 스테이킹되지 않은 금액)
이 모든 정보는 getDashboardData()를 호출하는 단 한 번의 호출로 가져옵니다:
const fetchDashboardData = async () => {
const [staked, rewards, apr] = await stakingContract.getDashboardData(userAddress);
const balance = await tokenContract.balanceOf(userAddress);
...
대시보드는 모든 트랜잭션(Transaction) 이후에 자동으로 새로고침되어 사용자가 항상 최신 수치를 확인할 수 있도록 합니다.
트랜잭션 해시 링크 (Transaction Hash Links)
모든 트랜잭션은 블록 익스플로러(Block Explorer)로 연결되는 링크를 노출합니다:
const tx = await stakingContract.stake(parsedAmount);
console.log(`Transaction: https://explorer.securechain.ai/tx/${tx.hash}`);
이는 신뢰를 구축합니다. 사용자는 UI의 말만 믿는 것이 아니라, 자신의 트랜잭션이 실제로 온체인(On-chain)에서 발생했는지 직접 확인할 수 있습니다.
잘못된 네트워크 감지 (Wrong Network Detection)
사용자가 잘못된 체인(Chain)에 연결된 경우, 앱은 이를 감지하고 프롬프트(Prompt)를 표시합니다:
const { chain } = useNetwork();
const SCAI_CHAIN_ID = 34;
...
조용히 실패하는 일도, 혼란스러운 에러가 발생하는 일도 없습니다. 그저 "Please switch to SCAI Mainnet."라는 명확한 메시지만을 보여줍니다.
배포 (Deployment)
스마트 컨트랙트 — Hardhat + SCAI 메인넷
컨트랙트는 Hardhat을 사용하여 컴파일 및 배포되었습니다:
npx hardhat compile
npx hardhat run scripts/deploy.js --network scai
hardhat.config.js에는 RPC URL과 배포자 개인키(환경 변수로부터 가져옴 — 키를 코드에 직접 작성(Hardcode)해서는 절대 안 됨)를 포함한 SCAI 네트워크 설정이 포함되어 있습니다.
프론트엔드 (Frontend) — Vercel
React 앱은 별도의 설정 없이 Vercel에 배포되었습니다. main 브랜치로 푸시(Push)하면 즉시 배포됩니다. 라이브 URL은 다음과 같습니다:
https://de-fi-staking-platform-vert.vercel.app
로컬에서 실행하는 방법
# 1. 저장소(repo) 클론
git clone https://github.com/feudcommon/DeFi-staking-platform.git
cd DeFi-staking-platform
...
http://localhost:3000을 열고, 지갑을 연결한 뒤, SCAI 메인넷(Mainnet)으로 전환하면 바로 실행됩니다.
개선하고 싶은 점
시간적 여유가 더 있다면 다음과 같은 부분들을 개선하고 싶습니다:
- 시간 가중 보상 모델 (Time-weighted rewards model) 추가 — 현재 보상은 선형적(linear)입니다. 시간 가중 모델을 도입하면 장기 스테이커(staker)에게 더 많은 보상을 줄 수 있으며, 이는 더 나은 경제적 모델이 될 것입니다.
- 스테이킹 티어 (Staking tiers) — 더 큰 스테이킹 금액이나 더 긴 약정 기간에 대해 더 높은 연간 수익률 (APR) 제공
- 모바일 반응형 (Mobile responsiveness) — UI가 데스크톱에서는 잘 작동하지만, 모바일에서는 더 정교하게 다듬어질 필요가 있습니다.
- 이벤트 기반 UI 업데이트 (Event-based UI updates) — 새로운 데이터를 위해 폴링(polling)하는 대신, 컨트랙트 이벤트(contract events)를 리스닝(listen)하여 지속적인 RPC 호출 없이 실시간 업데이트 구현
핵심 요약 (Key Takeaways)
1. approve → stake 흐름은 대부분의 dApp UX가 깨지는 지점입니다. 승인(approval) 호출을 백그라운드에서 숨기고 자동화한 것이 사용자 경험의 매끄러움을 완전히 바꾸어 놓았습니다. 컨트랙트가 무엇을 요구하는지가 아니라, 사용자가 실제로 무엇을 해야 하는지를 항상 생각해야 합니다.
2. view 함수는 비용이 들지 않습니다 — 아낌없이 사용하세요. 대시보드의 모든 데이터를 세 번의 개별 호출로 처리하는 대신 getDashboardData() 하나로 배치(Batching) 처리함으로써 UI 속도가 눈에 띄게 빨라졌습니다.
3. 잘못된 네트워크 감지는 타협할 수 없는 요소입니다. 사용자가 잘못된 체인에 접속한 상태에서 컨트랙트와 상호작용하려고 하면 혼란스러운 에러가 발생합니다. 이를 조기에 감지하고 명확하게 사용자에게 알려주어야 합니다.
4. 라이브 서비스 전에는 항상 감사를 수행하세요. 특히 사용자의 자금이 연관된 경우라면 더욱 그렇습니다. 감사(Audit)는 단순히 버그를 찾는 과정이 아니라, 신뢰를 구축하는 과정입니다.
결론
두 개의 컨트랙트, 전체 React 프론트엔드, 멀티 지갑(multi-wallet) 지원, 그리고 라이브 배포까지 — 이 스테이킹 플랫폼을 처음부터 구축하는 것은 제가 지금까지 출시한 Web3 프로젝트 중 가장 완성도 높은 프로젝트입니다.
EtherAuthority 인턴십은 매주 실제로 무언가를 구축하고 배포하도록 저를 몰아붙였습니다. 이것이 이러한 기술들을 배우는 유일한 방법입니다. 튜토리얼은 문법(syntax)을 가르쳐 줄 수 있습니다. 하지만 실제로 제품을 출시(Shipping)하는 것은 그 외의 모든 것을 가르쳐 줍니다.
EtherAuthority Web3 인턴십 기간 중 구축됨 — 4주 차
SecureChain AI (SCAI) 메인넷에 배포됨
토큰 컨트랙트 (Token Contract): 0x4EF03D37c441BcF78D61367f4EE709027632d929
스테이킹 컨트랙트 (Staking Contract): 0x9aab06FAE31e082c26979afca9E53897dB57D50C
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기