블록체인 기반 기부 플랫폼을 위한 스마트 컨트랙트 모음입니다. CampaignFactory는 UUPS 프록시로 배포되고, 각 캠페인은 EIP-1167 클론 패턴과 OpenZeppelin 업그레이드 라이브러리를 활용해 안전·가스 효율적으로 실행됩니다.
- ✅ UUPS 업그레이드 프록시 기반으로 배포되어 장기 운영 중에도 안전하게 기능 확장 가능
- ✅ 단일
Campaign구현체를 배포하고 EIP-1167 클론으로 각 캠페인을 가스 효율적으로 생성 - ✅ Basis Point(만 분율) 기반 플랫폼 수수료율/수령자 관리 (기본 2.5%, 최대 10%)
- ✅
allowedTokens화이트리스트와setTokenAllowanceBatch지원으로 허용 자산을 중앙에서 제어 - ✅
campaignById,campaignsByBeneficiary, 페이지네이션 가능한getCampaigns로 백엔드 연동 최적화 - ✅
getCampaignDetails,version등 뷰 함수 제공으로 온체인 상태 조회 간소화 - ✅ 배포 스크립트가 자동으로
deployments/<network>.json을 생성해 주소/설정 이력을 추적
- ✅ 네이티브 ETH와 단일 ERC-20 토큰 중 하나만 허용하여 기부 경로를 명확히 분기
- ✅ 기부자별
contributions와donors배열을 관리해 개별/배치 환불(refundAll) 모두 지원 - ✅
platformFeeRate·platformFeeRecipient를 캠페인별 스냅샷으로 저장하여 출금 시 자동 정산 - ✅
ReentrancyGuardUpgradeable,SafeERC20, 체크-이펙트-상호작용, Pull over Push 패턴으로 안전성 확보 - ✅
canRefund,canPayout,isExpired,balance등 뷰 함수로 프론트엔드 UX 개선 - ✅
RefundBatchProcessed등 상세 이벤트로 모금/환불 상황을 실시간 추적 가능
contracts/
├── CampaignFactory.sol # 캠페인 생성·관리, UUPS 프록시
└── Campaign.sol # 개별 캠페인 로직 (EIP-1167 클론)
캠페인 생성 전용 컨트랙트로, 관리자(Owner)만 호출 가능합니다.
주요 상태 값
campaignImplementation: 클론에 사용되는Campaign구현체 주소platformFeeRate/platformFeeRecipient: 플랫폼 수수료 구성allowedTokens: 허용된 토큰 화이트리스트 (기본으로 ETH 허용)campaigns,campaignById,campaignsByBeneficiary: 조회 및 백엔드 매핑 용도
핵심 함수
initialize(uint256,address): 프록시 초기화 (수수료율 ≤ 1000 = 10%)createCampaign(...): 검증된 파라미터로 새로운 클론 생성updateCampaignImplementation(address): 새로운Campaign구현체 등록updatePlatformFeeRate(uint256)/updatePlatformFeeRecipient(address): 운영 정책 변경setTokenAllowance(address,bool)및setTokenAllowanceBatch(...): 토큰 화이트리스트 관리getCampaigns,getCampaignsByBeneficiary,getCampaignDetails,getCampaignAddress,version_authorizeUpgrade: UUPS 업그레이드 권한을 owner로 제한
클론으로 배포되는 개별 캠페인 인스턴스입니다.
상태 변수
beneficiary,goal,deadline,raised,acceptedToken,paidOutplatformFeeRate,platformFeeRecipientcontributions,donors,donorRegistered,refundCursor
주요 함수
initialize(...): 프록시/클론 초기화donateNative(),donate(uint256): ETH/토큰 기부payout(): 목표 달성 시 누구나 호출 가능한 출금refund(): 기부자 개별 환불refundAll(uint256 batchSize): 관리자가 배치 환불 진행isGoalReached(),balance(),isExpired(),canRefund(),canPayout(): 조회 함수
이벤트
CampaignCreated,TokenAllowanceUpdated,PlatformFeeRateUpdated,PlatformFeeRecipientUpdated,CampaignImplementationUpdated(Factory)Donated,GoalReached,PaidOut,Refunded,RefundBatchProcessed(Campaign)
npm install.env 파일을 생성하고 다음 값을 입력하세요.
# RPC URLs
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
# Private Key (배포용 계정)
PRIVATE_KEY=your_private_key_here
# Etherscan API Key (컨트랙트 검증용)
ETHERSCAN_API_KEY=your_etherscan_api_key_herenpm run compile- Hardhat 노드 실행:
npx hardhat node
- 다른 터미널에서 배포:
npm run deploy:localhost
npm run deploy:sepolianpx hardhat run scripts/deploy.js --network mainnetscripts/deploy.js는 네트워크별 주소 및 설정을 deployments/<network>.json에 저장합니다.
{
"network": "sepolia",
"contracts": {
"CampaignFactoryProxy": "0x...",
"CampaignFactoryImplementation": "0x...",
"CampaignImplementation": "0x..."
},
"config": {
"platformFeeRate": 250,
"platformFeeRecipient": "0x..."
}
}이 파일을 백엔드/프론트엔드에서 참조하면 일관된 주소를 유지할 수 있습니다.
const factory = await ethers.getContractAt("CampaignFactory", factoryAddress);
const tx = await factory.createCampaign(
campaignId,
beneficiary,
goal,
deadline,
acceptedToken // address(0) => ETH
);
const receipt = await tx.wait();
const event = receipt.logs.find((log) => log.fragment?.name === "CampaignCreated");
const campaignAddress = event.args.campaignAddress;await factory.setTokenAllowanceBatch(
[ethers.ZeroAddress, usdcAddress],
[true, true] // ETH + USDC 허용
);ETH 기부
const campaign = await ethers.getContractAt("Campaign", campaignAddress);
await campaign.donateNative({ value: ethers.parseEther("1") });ERC-20 기부
const token = await ethers.getContractAt("IERC20", tokenAddress);
await token.approve(campaignAddress, amount);
const campaign = await ethers.getContractAt("Campaign", campaignAddress);
await campaign.donate(amount);const campaign = await ethers.getContractAt("Campaign", campaignAddress);
if (await campaign.canPayout()) {
await campaign.payout();
}개별 환불
if (await campaign.canRefund()) {
await campaign.refund(); // 호출자 본인 환불
}배치 환불
// batchSize 만큼 donors 배열을 순회하며 환불 처리
await campaign.refundAll(50);const details = await factory.getCampaignDetails(campaignAddress);
const [
beneficiary,
goal,
deadline,
raised,
acceptedToken,
paidOut,
isGoalReached,
canRefund,
canPayout
] = details;
const campaign = await ethers.getContractAt("Campaign", campaignAddress);
const currentBalance = await campaign.balance();scripts/deploy.js: UUPS 프록시 배포 +deployments/<network>.json생성scripts/create-campaign-example.js: 최신 배포 정보를 읽어 샘플 캠페인을 생성npx hardhat run scripts/create-campaign-example.js --network localhost
scripts/donate-example.js: 지정 캠페인에 ETH 기부npx hardhat run scripts/donate-example.js --network localhost -- <캠페인주소> 1
- 컨트랙트 수정 후 컴파일
npm run compile
- 업그레이드 안전성 검증
npm run validate-upgrade -- --network sepolia
- UUPS 업그레이드 실행
npm run upgrade:sepolia
- 스크립트가 자동으로 새로운 구현체 주소와 버전을
deployments/<network>.json에 반영합니다.
추가로, updateCampaignImplementation을 이용하면 Factory 자체 업그레이드 없이도 새로운 Campaign 구현체를 등록할 수 있습니다.
백엔드와의 연동 흐름:
사용자 → 백엔드 → 스마트 컨트랙트
↓
검증 & DB 저장
↓
Factory.createCampaign()
↓
캠페인 주소 반환
↓
DB에 주소 저장
사용자 → 프론트엔드 → 스마트 컨트랙트
↓
Campaign.donate()
↓
이벤트 발생
↓
백엔드 ← 이벤트 리스닝
↓
DB 업데이트
CampaignFactory
event CampaignCreated(
uint256 indexed campaignId,
address indexed campaignAddress,
address indexed beneficiary,
uint256 goal,
uint256 deadline,
address acceptedToken,
uint256 timestamp
);
event CampaignImplementationUpdated(address oldImplementation, address newImplementation);
event PlatformFeeRateUpdated(uint256 oldRate, uint256 newRate);
event PlatformFeeRecipientUpdated(address oldRecipient, address newRecipient);
event TokenAllowanceUpdated(address indexed token, bool allowed);Campaign
event Donated(address indexed donor, uint256 amount, uint256 timestamp);
event GoalReached(uint256 total, uint256 timestamp);
event PaidOut(address indexed beneficiary, uint256 amount, uint256 platformFee, uint256 timestamp);
event Refunded(address indexed donor, uint256 amount, uint256 timestamp);
event RefundBatchProcessed(uint256 processedCount, uint256 nextCursor, uint256 timestamp);- ReentrancyGuardUpgradeable: 재진입 공격 방지
- 체크-이펙트-상호작용: 외부 호출 전에 상태 확정
- Pull over Push: 수혜자·기부자가 직접 호출하는 안전한 송금 방식
- SafeERC20: ERC-20 토큰 처리를 안전하게 래핑
- Ownable + UUPS: 관리자 전용 권한 및 업그레이드 통제
- 커스텀 에러: 가스 절감과 명확한 실패 원인 제공
- ✅ 배포 전 충분한 테스트와 감사(Audit) 진행
- ✅ 멀티시그 지갑으로 Factory 소유권 관리
- ✅
upgrade.js실행 전validate-upgrade.js로 저장소 충돌 검증 - ✅ 이벤트 리스너를 운영해 실시간 모니터링 및 환불 배치 자동화
- ✅ 필요한 경우
refundAll을 백엔드 잡으로 호출해 기부자 UX 개선
- EIP-1167 클론으로 캠페인 생성 비용 ~90% 절감
- 플랫폼 수수료를 Basis Point로 저장해 부동소수점 연산 회피
- 배치 환불(
refundAll)로 단일 트랜잭션 당 처리량 제어 - View 함수(
getCampaigns,getCampaignDetails)로 오프체인 조회 비용 최소화 - 업그레이드 대비
__gap슬롯 확보로 재배포 없이 확장 가능
npm test