Solidity Smart Contracts
프록시 시스템
PortfolioProxy
역할
Portfolio 생태계에서 사용하는 프록시 컨트랙트입니다. ERC1967 표준을 따르며, 구현 컨트랙트를 가리키는 포인터 역할을 합니다. 프록시를 통해 컨트랙트 업그레이드가 가능하며, 사용자는 항상 동일한 주소로 상호작용합니다.
핵심 책임
- 구현 컨트랙트 주소 저장
- 모든 호출을 구현 컨트랙트로 위임
- 인터페이스 지원 확인 (supportsInterface)
- 업그레이드 시 주소 유지
ERC1967Proxy
OpenZeppelin의 표준 프록시 컨트랙트를 상속받습니다.
- • 표준 스토리지 슬롯 사용
- • Fallback 함수로 모든 호출 위임
- • 구현 주소 안전하게 저장
주요 함수
Interface
역할
ERC165 인터페이스 지원을 확인하기 위한 간단한 인터페이스입니다. 프록시 컨트랙트가 구현 컨트랙트의 supportsInterface를 호출할 때 사용됩니다.
ERC165란?
컨트랙트가 특정 인터페이스를 구현했는지 확인하는 표준입니다.
- • 런타임 인터페이스 검증
- • 호환성 확인
- • 안전한 상호작용
프록시에서의 역할
프록시는 자체 로직이 없으므로, 구현 컨트랙트의 인터페이스를 조회해야 합니다.
- • 스토리지 슬롯에서 구현 주소 조회
- • 구현 컨트랙트의 supportsInterface 호출
- • 결과 반환
프록시 패턴
프록시 컨트랙트
사용자가 상호작용하는 고정 주소입니다.
- • 주소 불변
- • 로직 없음
- • delegatecall로 위임
구현 컨트랙트
실제 로직을 포함하는 컨트랙트입니다.
- • 주소 변경 가능
- • 업그레이드 가능
- • 로직만 포함
스토리지
프록시 컨트랙트에 저장됩니다.
- • 데이터 보존
- • 업그레이드 후에도 유지
- • ERC1967 슬롯 사용
업그레이드 프로세스
1. 배포
프록시와 구현 컨트랙트 배포
2. 초기화
프록시를 통해 initialize 호출
3. 업그레이드
새 구현 컨트랙트 배포 및 주소 변경
4. 데이터 유지
기존 데이터는 모두 보존됨
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
interface Interface {
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}Portfolio 권한 관리 시스템
PortfolioAdmin
역할
Portfolio 생태계에서 Super Operator 정보를 중앙 집중식으로 저장하는 컨트랙트입니다. 문자열 기반의 권한 이름(operator)과 주소 매핑을 관리하며, 다른 컨트랙트들이 권한을 조회할 수 있는 단일 진실 소스(Single Source of Truth) 역할을 합니다.
핵심 책임
- Super Operator 정보 저장 및 관리
- 권한 이름(string)과 주소 매핑
- 권한 조회 인터페이스 제공
- Owner에 의한 권한 설정/해제
주요 기능
PortfolioSuperOperatorsUpgradeable
역할
다른 컨트랙트가 상속을 통해 권한 검증 기능을 사용할 수 있도록 제공하는 추상 컨트랙트입니다. PortfolioAdmin에 저장된 권한 정보를 조회하여 모디파이어를 통해 자동으로 검증합니다. 또한 Immutable X의 Allowlist 시스템을 통합하여 NFT 거래 시 승인된 주소만 상호작용할 수 있도록 제한합니다.
핵심 책임
- PortfolioAdmin과 연동하여 권한 검증
- 모디파이어 제공으로 간편한 권한 체크
- NFT 전송/승인 시 Allowlist 검증
- EOA와 컨트랙트 구분 검증
제공 모디파이어
시스템 구조
1. 권한 저장
Owner가 PortfolioAdmin에 Super Operator 정보를 저장합니다.
2. 컨트랙트 상속
다른 컨트랙트가 PortfolioSuperOperatorsUpgradeable을 상속받습니다.
3. 권한 검증
모디파이어가 PortfolioAdmin에서 권한을 자동 조회하여 검증합니다.
시스템 장점
- 중앙 집중 관리: 권한 정보를 한 곳에서 관리
- 재사용성: 모디파이어를 상속으로 간편하게 사용
- 동적 변경: PortfolioAdmin만 수정하면 모든 컨트랙트에 반영
- Allowlist 통합: NFT 거래소 호환성 확보
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
contract PortfolioAdmin is Initializable, OwnableUpgradeable, UUPSUpgradeable, ERC165 {
event SetSuperOperator(string operator, address superOperator, bool enabled);
// operator => who => valid
struct SuperOperators {
mapping(address => bool) who;
string operator;
}
mapping(string => SuperOperators) public _superOperators;
constructor() {
_disableInitializers();
}
function initialize() public initializer {
__UUPSUpgradeable_init();
__Ownable_init();
}
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC165) returns (bool) {
return super.supportsInterface(interfaceId);
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
function setSuperOperator(string memory operator, address[] memory _operatorAddress, bool enabled) external onlyOwner {
for (uint8 i = 0; i < _operatorAddress.length;) {
_superOperators[operator].operator = operator;
_superOperators[operator].who[_operatorAddress[i]] = enabled;
emit SetSuperOperator(operator, _operatorAddress[i], enabled);
unchecked { ++i; }
}
}
function setSuperOperators(string[] memory operators, address[][] memory _operatorAddresses, bool enabled) external onlyOwner {
for (uint8 i = 0; i < operators.length; i++) {
_superOperators[operators[i]].operator = operators[i];
for (uint8 j = 0; j < _operatorAddresses[i].length; j++) {
_superOperators[operators[i]].who[_operatorAddresses[i][j]] = enabled;
emit SetSuperOperator(operators[i], _operatorAddresses[i][j], enabled);
}
}
}
function isSuperOperator(string memory operator, address who) public view returns (bool) {
return _superOperators[operator].who[who];
}
uint256[30] private __PortfolioAdminGap;
}Portfolio 서비스 점검 관리 시스템
PortfolioService
역할
Portfolio 생태계에서 서비스 점검 상태 정보를 중앙 집중식으로 저장하는 컨트랙트입니다. 컨트랙트별 점검(Inspection) 상태를 관리하며, 다른 컨트랙트들이 서비스 가능 여부를 조회할 수 있는 단일 진실 소스(Single Source of Truth) 역할을 합니다.
핵심 책임
- 점검 상태 정보 저장 및 관리
- 컨트랙트별 점검 여부 매핑
- 서비스 상태 조회 인터페이스 제공
- Super Operator에 의한 점검 설정/해제
주요 기능
PortfolioLive
역할
다른 컨트랙트가 상속을 통해 서비스 상태 검증 기능을 사용할 수 있도록 제공하는 추상 컨트랙트입니다. PortfolioService에 저장된 점검 상태 정보를 조회하여 모디파이어를 통해 자동으로 검증합니다. 점검 중인 컨트랙트의 기능 실행을 차단하여 안전한 유지보수를 가능하게 합니다.
핵심 책임
- PortfolioService와 연동하여 서비스 상태 검증
- 모디파이어 제공으로 간편한 점검 제어
- 점검 중인 컨트랙트 기능 차단
- 실시간 서비스 상태 확인
제공 모디파이어
시스템 구조
1. 점검 상태 저장
Super Operator가 PortfolioService에 점검 상태 정보를 저장합니다.
2. 컨트랙트 상속
다른 컨트랙트가 PortfolioLive를 상속받습니다.
3. 서비스 검증
모디파이어가 PortfolioService에서 점검 상태를 자동 조회하여 차단합니다.
시스템 장점
- 중앙 집중 관리: 점검 상태를 한 곳에서 관리
- 재사용성: 모디파이어를 상속으로 간편하게 사용
- 동적 변경: PortfolioService만 수정하면 모든 컨트랙트에 반영
- 안전한 유지보수: 점검 중 기능 실행 차단으로 안전성 확보
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../../utils/PortfolioSuperOperatorsUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract PortfolioService is Initializable, PortfolioSuperOperatorsUpgradeable, UUPSUpgradeable {
mapping(address => bool) isInspection;
event Inspection(address contractAddress, uint256 timestamp, bool live);
constructor() {
_disableInitializers();
}
function initialize(string memory operator, address portfolioAdmin) public initializer {
__UUPSUpgradeable_init();
__PortfolioSuperOperators_init(operator, portfolioAdmin);
}
function _authorizeUpgrade(address newImplementation) internal override onlySuperOperator {}
function isLive(address contractAddress) public view returns (bool) {
return !isInspection[contractAddress];
}
function setInspection(address[] memory contractAddresses, bool _isInspection) external onlySuperOperator {
for (uint8 i = 0; i < contractAddresses.length;) {
isInspection[contractAddresses[i]] = _isInspection;
emit Inspection(contractAddresses[i], block.timestamp, _isInspection);
unchecked { ++i; }
}
}
uint256[30] private __LuxonServiceGap;
}Portfolio 주소 레지스트리 시스템
DataAddress
역할
Portfolio 생태계에서 컨트랙트 주소 정보를 중앙 집중식으로 저장하는 레지스트리 컨트랙트입니다. 문자열 이름으로 컨트랙트 주소를 조회할 수 있어, 하드코딩된 주소 대신 유연한 주소 참조가 가능하며, 다른 컨트랙트들이 필요한 주소를 조회할 수 있는 단일 진실 소스(Single Source of Truth) 역할을 합니다.
핵심 책임
- 컨트랙트 주소 정보 저장 및 관리
- 이름(string)과 주소 매핑
- 주소 조회 인터페이스 제공
- Super Operator에 의한 주소 설정/업데이트
주요 기능
PortfolioDataUpgradeable
역할
다른 컨트랙트가 상속을 통해 주소 조회 기능을 사용할 수 있도록 제공하는 추상 컨트랙트입니다. DataAddress에 저장된 주소 정보를 조회하는 편리한 인터페이스를 제공하여, 컨트랙트들이 다른 컨트랙트의 주소를 쉽게 찾을 수 있게 합니다.
핵심 책임
- DataAddress와 연동하여 주소 조회
- 간편한 주소 조회 함수 제공
- 하드코딩된 주소 의존성 제거
- 동적 주소 관리 지원
제공 함수
시스템 구조
1. 주소 저장
Super Operator가 DataAddress에 컨트랙트 주소 정보를 저장합니다.
2. 컨트랙트 상속
다른 컨트랙트가 PortfolioDataUpgradeable을 상속받습니다.
3. 주소 조회
함수를 통해 DataAddress에서 필요한 주소를 조회합니다.
시스템 장점
- 중앙 집중 관리: 모든 주소를 한 곳에서 관리
- 재사용성: 조회 함수를 상속으로 간편하게 사용
- 동적 변경: DataAddress만 수정하면 모든 컨트랙트에 반영
- 유연성: 하드코딩 없이 주소 변경 가능
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../../utils/PortfolioSuperOperatorsUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract DataAddress is Initializable, PortfolioSuperOperatorsUpgradeable, UUPSUpgradeable {
event SetDataAddress(string indexed name, address indexed dataAddress, bool indexed isValid);
struct DataAddressInfo {
string name;
address dataAddress;
bool isValid;
}
mapping(string => DataAddressInfo) private dataAddresses;
constructor() {
_disableInitializers();
}
function initialize(string memory operator, address portfolioAdmin) public initializer {
__UUPSUpgradeable_init();
__PortfolioSuperOperators_init(operator, portfolioAdmin);
}
function _authorizeUpgrade(address newImplementation) internal override onlySuperOperator {}
function getDataAddress(string memory _name) public view returns (address) {
require(dataAddresses[_name].isValid, "this data address is not valid");
return dataAddresses[_name].dataAddress;
}
function setDataAddress(DataAddressInfo memory _dataAddressInfo) external onlySuperOperator {
dataAddresses[_dataAddressInfo.name] = _dataAddressInfo;
emit SetDataAddress(_dataAddressInfo.name, _dataAddressInfo.dataAddress, _dataAddressInfo.isValid);
}
function setDataAddresses(DataAddressInfo[] memory _dataAddressInfos) external onlySuperOperator {
for (uint8 i = 0; i < _dataAddressInfos.length;) {
dataAddresses[_dataAddressInfos[i].name] = _dataAddressInfos[i];
emit SetDataAddress(_dataAddressInfos[i].name, _dataAddressInfos[i].dataAddress, _dataAddressInfos[i].isValid);
unchecked { ++i; }
}
}
uint256[30] private __DataAddressGap;
}Portfolio NFT 보관 시스템
PortfolioCenter
역할
Portfolio 생태계에서 ERC1155 NFT를 안전하게 보관하는 중앙 허브 역할을 합니다. ERC1155Holder를 상속받아 NFT를 수신하고 보관할 수 있으며, 에어드랍, 스테이킹, 마켓플레이스 등 다양한 용도로 활용됩니다.
핵심 책임
- ERC1155 NFT 안전 보관
- NFT 수신 기능 제공
- 배치 NFT 수신 지원
- 중앙 집중식 NFT 관리 허브
상속 구조
ERC1155Holder
역할
OpenZeppelin에서 제공하는 ERC1155 NFT 수신 기능을 구현한 컨트랙트입니다. NFT 전송 시 안전성을 보장하는 콜백 함수들을 제공하여, NFT가 잘못된 주소로 전송되어 손실되는 것을 방지합니다.
제공 콜백
활용 사례
시스템 장점
- 안전한 수신: ERC1155 표준 준수로 NFT 손실 방지
- 중앙 집중: 모든 NFT를 한 곳에서 관리
- 배치 지원: 여러 NFT를 한 번에 처리
- 감사 추적: 블록체인에 모든 전송 기록 보존
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
contract PortfolioCenter is ERC1155Holder {
function name() public pure returns (string memory) {
return "Portfolio Center";
}
}에어드랍 시스템
AdminAirdropCoin
역할
Portfolio 생태계에서 네이티브 코인(MATIC, ETH)을 여러 사용자에게 한 번에 분배하는 에어드랍 컨트랙트입니다. Super Operator 권한을 통해 안전하게 코인을 관리하고, 배치 처리로 효율적인 대량 분배를 지원합니다.
핵심 책임
- 코인 보관 및 관리
- 배치 에어드랍 실행
- 잔액 관리 (입금/출금)
- Super Operator 권한 검증
주요 기능
에어드랍 프로세스
실행 단계
보안 메커니즘
- ReentrancyGuard: 재진입 공격 방지
- onlySuperOperator: 권한 검증
- require 검증: 모든 입력값 검증
- 잔액 확인: 전송 전 충분한 잔액 확인
제한사항
- • 최대 수신자: 500명
- • 최소 금액: 0 초과
- • 권한: Super Operator 필요
시스템 장점
- 배치 처리: 한 번의 트랜잭션으로 여러 주소에 전송
- 가스 최적화: unchecked 증가 연산으로 가스 절약
- 안전성: ReentrancyGuard로 재진입 공격 방지
- 유연성: ETH 단위 입력, Wei로 자동 변환
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../../utils/PortfolioSuperOperatorsUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract AdminAirdropCoin is Initializable, ReentrancyGuardUpgradeable, PortfolioSuperOperatorsUpgradeable, UUPSUpgradeable {
event AirdropCoin(address indexed receiver, uint256 amount);
event Withdraw(address indexed to, uint256 amount);
event Deposit(address indexed from, uint256 amount);
constructor() {
_disableInitializers();
}
function initialize(string memory operator, address portfolioAdmin) public initializer {
__ReentrancyGuard_init();
__UUPSUpgradeable_init();
__PortfolioSuperOperators_init(operator, portfolioAdmin);
}
struct AirdropInfo {
address receiver;
uint256 amount; // ETH 단위 (Wei로 자동 변환됨)
}
function airdrop(AirdropInfo[] memory airdropInfos) external onlySuperOperator nonReentrant {
uint256 length = airdropInfos.length;
require(length > 0, "Empty array");
require(length <= 500, "Too many recipients");
uint256 totalAmount = 0;
// 1단계: 유효성 검증 및 총액 계산
for (uint256 i = 0; i < length; ) {
require(airdropInfos[i].receiver != address(0), "Invalid address");
require(airdropInfos[i].amount > 0, "Invalid amount");
uint256 amountInWei = airdropInfos[i].amount * 10 ** 18;
totalAmount += amountInWei;
unchecked { ++i; }
}
require(address(this).balance >= totalAmount, "Insufficient balance");
// 2단계: 전송 실행
for (uint256 i = 0; i < length; ) {
address payable recipient = payable(airdropInfos[i].receiver);
uint256 amountInWei = airdropInfos[i].amount * 10 ** 18;
(bool success, ) = recipient.call{value: amountInWei}("");
require(success, "Transfer failed");
emit AirdropCoin(recipient, amountInWei);
unchecked { ++i; }
}
}
function charging() external payable onlySuperOperator {
require(msg.value > 0, "Invalid amount");
emit Deposit(msg.sender, msg.value);
}
function withdraw() external onlySuperOperator nonReentrant {
uint256 balance = address(this).balance;
require(balance > 0, "No balance");
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Transfer failed");
emit Withdraw(msg.sender, balance);
}
function withdrawAmount(uint256 amount) external onlySuperOperator nonReentrant {
require(amount > 0, "Invalid amount");
require(address(this).balance >= amount, "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
emit Withdraw(msg.sender, amount);
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
receive() external payable {
emit Deposit(msg.sender, msg.value);
}
function _authorizeUpgrade(address newImplementation) internal override onlySuperOperator {}
uint256[30] private __AdminAirdropCoinGap;
}NFT 데이터 관리 시스템
GachaData
역할
가챠 티켓 NFT의 메타데이터를 저장하고 관리하는 컨트랙트입니다. 각 가챠 티켓의 타입(Shape/Number), 등급 확률(tierRatio), 유효성 등을 저장하며, 가챠 실행 시 필요한 정보를 제공합니다.
핵심 책임
- 가챠 티켓 정보 저장 및 관리
- 가챠 타입 관리 (Shape/Number)
- 등급 확률(tierRatio) 저장
- 가챠 정보 조회 인터페이스 제공
주요 기능
GachaDto
역할
가챠 티켓의 데이터 구조를 정의하는 구조체입니다. 토큰 ID, 이름, 가챠 타입, 등급 확률 등 가챠 티켓에 필요한 모든 정보를 표준화합니다.
데이터 구조
GachaType Enum
시스템 장점
- 중앙 집중 관리: 모든 가챠 티켓 정보를 한 곳에서 관리
- 등급 확률 관리: tierRatio로 유연한 확률 설정
- 유효성 검증: 등록된 가챠 티켓만 사용 가능
- 타입별 분리: Shape와 Number 가챠를 구분하여 관리
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
enum GachaType {
None,
Shapes,
Number
}
struct GachaDto {
uint256 tokenId;
string name;
GachaType gachaType;
uint32[] tierRatio;
bool isValid;
}ERC721
Shape
역할
Portfolio 생태계에서 Shape NFT(ERC721)를 발행하고 관리하는 컨트랙트입니다. 각 NFT는 고유한 tokenId와 shapeId를 가지며, shapeId는 ShapeData에서 관리하는 메타데이터를 참조합니다. PortfolioERC721Upgradeable을 상속받아 Immutable X 호환 기능, 로열티, Permit 등을 지원합니다.
핵심 책임
- Shape NFT 발행 (ERC721)
- tokenId와 shapeId 매핑 관리
- 배치 발행 및 소각
- ShapeData 메타데이터 참조
주요 기능
PortfolioERC721Upgradeable
역할
Portfolio 생태계의 표준 ERC721 NFT 베이스 컨트랙트입니다. OpenZeppelin ERC721을 확장하여 ERC2981 로열티, EIP-2612 Permit, 가스 최적화(ERC721Psi) 등 다양한 기능을 통합 제공합니다.
통합 기능
- ERC2981: 로열티 표준
- EIP-2612 Permit: 가스리스 승인
- ERC721Psi: 가스 최적화 발행
추가 기능
상속 구조
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
struct TransferRequest {
address from;
address[] tos;
uint256[] tokenIds;
}
struct IDBurn {
address owner;
uint256[] tokenIds;
}
struct Mint {
address to;
uint256 quantity;
}
struct IDMint {
address to;
uint256[] tokenIds;
}
struct ShapeStruct {
uint256 tokenId;
uint32 shapeId;
}ERC1155
Gacha & Number
역할
Portfolio 생태계에서 사용되는 두 가지 ERC1155 NFT 컨트랙트입니다. Gacha는 가챠 티켓 NFT를, Number는 숫자 기반 아이템 NFT를 관리합니다. 동일한 PortfolioERC1155Upgradeable을 상속받아 일관된 인터페이스를 제공합니다.
Gacha NFT
- 가챠 티켓 역할
- Shape 또는 Number 가챠에 사용
- 소모성 아이템 (사용 시 소각)
- 등급별 확률 설정 가능
Number NFT
- 게임 내 숫자 아이템
- 등급(tier)별 분류
- 가챠로 획득
- 에어드랍 가능
공통 특징
- ERC1155 표준 준수
- UUPS 업그레이드 가능
- Super Operator 권한 관리
- 로열티 지원 (ERC2981)
PortfolioERC1155Upgradeable
역할
Portfolio 생태계의 표준 ERC1155 NFT 베이스 컨트랙트입니다. OpenZeppelin ERC1155를 확장하여 ERC2981 로열티, EIP-2612 Permit, Super Operator 권한 관리 등 다양한 기능을 통합 제공합니다.
통합 기능
- ERC2981: 로열티 표준
- EIP-2612 Permit: 가스리스 승인
- Super Operator: 권한 관리
- 배치 발행/소각: 효율적인 처리
주요 기능
상속 구조
ERC1155 vs ERC721
ERC1155 (Gacha, Number)
- 멀티 토큰: 하나의 컨트랙트로 여러 토큰 타입 관리
- 수량 관리: 각 토큰마다 수량(amount) 보유
- 배치 전송: 여러 토큰을 한 번에 전송
- 가스 효율: 동일 토큰 대량 발행 시 유리
- 사용 사례: 가챠 티켓, 게임 아이템
ERC721 (Shape)
- 고유 토큰: 각 NFT가 고유한 ID
- 단일 소유: 각 NFT는 1개만 존재
- 개별 메타데이터: 각 NFT마다 다른 속성
- 소유권 명확: 고유 자산 증명
- 사용 사례: 캐릭터, 아트워크, 부동산
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IErc1155Token {
function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data) external;
function mint(address account, uint256 id, uint256 amount, bytes memory data) external;
function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) external;
function balanceOf(address account, uint256 id) external returns (uint256);
function burn(address account, uint256 id, uint256 value) external;
function burnBatch(address account, uint256[] memory ids, uint256[] memory values) external;
function safeMint(address to, uint256 id, uint256 value, bytes memory data) external;
}가챠 시스템
GachaMachine
역할
사용자가 가챠 티켓을 사용하여 Shape 또는 Number NFT를 랜덤하게 획득하는 가챠 시스템입니다. 가챠 타입에 따라 다른 랜덤 알고리즘을 사용하며, 티켓 소각 후 NFT를 발행합니다.
핵심 책임
- 가챠 티켓 소각 처리
- 가챠 타입별 NFT 발행
- 등급 확률 기반 랜덤 추출
- 블랙리스트 및 서비스 상태 확인
Shape 가챠
- Shape NFT (ERC721) 발행
- shapeId는 나중에 설정 (VRF 대기)
- Chainlink VRF 연동 가능 (현재 비활성)
Number 가챠
- Number NFT (ERC1155) 발행
- 온체인 난수 사용 (blockhash 기반)
- 즉시 토큰 ID 결정
- 최대 100개까지 한 번에 가챠
ShapeGachaMachine
역할
Chainlink VRF로부터 받은 난수를 사용하여 Shape NFT의 shapeId를 결정하는 컨트랙트입니다. 가챠 실행 후 VRF 난수가 도착하면 Super Operator가 이 컨트랙트를 통해 Shape NFT의 속성(shapeId)을 설정합니다.
핵심 책임
- VRF 난수 기반 shapeId 결정
- 등급(tier) 추출
- 등급별 Shape 랜덤 선택
- Shape NFT에 shapeId 설정
주요 기능
처리 흐름
통합 가챠 프로세스
1. 티켓 사용
사용자가 가챠 티켓 소각
2. 타입 확인
Shape 또는 Number 가챠 구분
3. NFT 발행
타입에 맞는 NFT 발행
4. 속성 결정
Number는 즉시, Shape는 VRF 후
5. 완료
ShapeGachaMachine이 shapeId 설정
난수 생성 방식
Shape 가챠 (VRF)
- Chainlink VRF 난수 사용
- 검증 가능한 랜덤
- 조작 불가능
- 2단계 처리 (발행 → 속성 설정)
Number 가챠 (온체인)
- blockhash, timestamp 기반
- 즉시 처리
- 가스 비용 절감
- 1단계 처리 (발행 + 속성 동시)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../admin/management/DataAddress.sol";
import "../data/gacha/GachaData.sol";
import "../data/number/NumberData.sol";
import "../token/erc1155/IErc1155Token.sol";
import "../token/erc721/IShapeToken.sol";
import "../utils/PortfolioBlacklist.sol";
import "../utils/PortfolioDataUpgradeable.sol";
import "../utils/PortfolioLive.sol";
import "../utils/PortfolioSuperOperatorsUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
contract GachaMachine is Initializable, ReentrancyGuardUpgradeable, ERC1155Holder, PortfolioLive, PortfolioDataUpgradeable, PortfolioSuperOperatorsUpgradeable, PortfolioBlacklist, UUPSUpgradeable {
event GachaShape(address indexed owner, uint256 indexed gachaTokenId, uint256 indexed tokenId, uint256 requestId);
event GachaNumber(address indexed owner, uint256 indexed gachaTokenId, uint256 indexed tokenId);
struct UsedGoods {
uint256 tokenId;
uint256 amount;
}
address private shapeAddress;
address private numberAddress;
address private gachaAddress;
address private gachaDataAddress;
address private numberDataAddress;
function gachaByGachaToken(UsedGoods memory _gachaInfo) external nonReentrant isLive isBlacklist {
GachaDto memory gachaDto = mintPay(_gachaInfo);
if (gachaDto.gachaType == GachaType.Shapes) {
mintShape(_gachaInfo);
} else if (gachaDto.gachaType == GachaType.Number) {
mintNumber(_gachaInfo);
}
}
function mintPay(UsedGoods memory _gachaInfo) private returns (GachaDto memory) {
GachaDto memory gachaDto = GachaData(gachaDataAddress).getGachaInfo(_gachaInfo.tokenId);
require(gachaDto.isValid, "not valid token id");
IErc1155Token(gachaAddress).burn(msg.sender, _gachaInfo.tokenId, _gachaInfo.amount);
return gachaDto;
}
function mintShape(UsedGoods memory _gachaInfo) private {
IShapeToken(shapeAddress).mintByQuantity(msg.sender, _gachaInfo.amount);
uint256 lastTokenId = IShapeToken(shapeAddress).nextTokenId() - 1;
for (uint256 i = 0; i < _gachaInfo.amount;) {
uint256 tokenId = lastTokenId - _gachaInfo.amount + i + 1;
uint256 requestId = 0; // Chainlink VRF 사용 시 활성화
emit GachaShape(msg.sender, _gachaInfo.tokenId, tokenId, requestId);
unchecked { ++i; }
}
}
function mintNumber(UsedGoods memory _gachaInfo) private {
require(_gachaInfo.amount <= 100, "Exceed max gacha amount");
(uint32[] memory _tierRatio, uint32 _tierRatioSum) = GachaData(gachaDataAddress).getGachaTierRatio(_gachaInfo.tokenId);
for (uint32 i = 0; i < _gachaInfo.amount;) {
bytes32 combinedSeed = keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp, block.number, msg.sender, tx.gasprice, i));
uint8 tier = uint8(randomNumber(combinedSeed, i, _tierRatio, _tierRatioSum, "number") + 1);
uint256 count = NumberData(numberDataAddress).getNumberInfoByTireCount(tier);
uint32 index = uint32(uint256(keccak256(abi.encodePacked(combinedSeed, i, "index"))) % count);
uint256 tokenId = NumberData(numberDataAddress).getNumberInfoByTireAndIndex(tier, index);
IErc1155Token(numberAddress).mint(msg.sender, tokenId, 1, "");
emit GachaNumber(msg.sender, _gachaInfo.tokenId, tokenId);
unchecked { ++i; }
}
}
function randomNumber(bytes32 _seed, uint256 _tokenId, uint32[] memory _ratio, uint32 _ratioSum, string memory _type) private pure returns (uint32 index) {
uint32 ratio = uint32(uint256(keccak256(abi.encodePacked(_seed, _tokenId, _type))) % _ratioSum) + 1;
index = 0;
uint32 ratioSum = 0;
for (uint8 i = 0; i < _ratio.length;) {
ratioSum += _ratio[i];
if (ratio <= ratioSum) {
break;
}
index++;
unchecked { ++i; }
}
}
}