Equillar가 투자 계약의 지급 능력을 보장하는 방법
Source: Dev.to
투자 플랫폼에서 가장 중요한 측면 중 하나는 투자자들이 결제를 신뢰할 수 있게 그리고 제때에 받도록 보장하는 것입니다. Equillar에서는 활성화된 각 투자 계약의 결제 능력을 지속적으로 검증하는 자동화 시스템을 구현했습니다. 이 기사에서는 이 메커니즘이 내부적으로 어떻게 작동하는지 살펴보겠습니다.
왜 결제 능력을 검증하는 것이 중요한가?
여러 투자자가 매월 수익을 기다리고 있는 활성 투자 계약이 있다고 가정해 보세요. 계약은 정상적으로 작동하여 자금을 받고 운영을 처리하고 있을 수 있지만, 어느 순간 준비금에 다음 지급을 충당할 충분한 자금이 없게 된다면 어떻게 될까요?
이러한 상황을 가능한 한 피하기 위해 우리는 다음과 같은 접근 방식을 선택했습니다: 문제가 발생하기 전에 이를 감지하여 조직이 제때 시정 조치를 취할 수 있도록 하는 것입니다.
검증 흐름: 3단계 아키텍처
우리의 결제 가능성 검증 시스템은 명령, 비동기 메시지, 구체적인 서비스에 기반한 아키텍처를 사용하여 세 단계로 동작합니다.
1단계: 검증할 계약 식별
모든 것은 정기적으로 실행되는 스케줄 명령으로 시작됩니다:
php bin/console app:contract:check-payment-availability
이 명령(CheckContractPaymentAvailabilityCommand)은 검증 시스템의 진입점입니다. 로직은 단순하고 집중되어 있습니다:
-
검증 대상 계약 선택 – 모든 계약을 지속적으로 검증할 필요는 없습니다. 명령은 운영 상태에 있는 계약만 검색합니다:
ACTIVE– 투자를 받고 있는 활성 계약FUNDS_REACHED– 목표 자금을 달성한 계약PAUSED– 일시 중지 상태이지만 여전히 결제 의무가 있는 계약
-
검증 레코드 생성 – 선택된 각 계약에 대해 시스템은
ContractPaymentAvailability엔티티를 생성합니다. 이 레코드는 검증 “티켓” 역할을 하며 다음을 저장합니다:- 검증할 계약
- 요청이 생성된 시점
- 프로세스 상태(
pending,processed,failed) - 검증 결과
-
작업 큐에 넣기 – 모든 작업을 동기적으로 처리하면(몇 분씩 시스템을 차단할 수 있음) 대신, 명령은 각 검증을 Symfony Messenger 를 통해 비동기 메시지로 큐에 넣습니다.
이 설계는 명령이 빠르게 종료될 수 있게 하면서 무거운 작업은 백그라운드에서 처리되도록 해줍니다. 예를 들어 활성 계약이 50개라면, 명령은 단순히 50개의 “티켓”을 만들고 큐에 넣은 뒤 몇 초 안에 제어를 반환합니다.
2단계: 비동기 처리
메시지가 큐에 들어가면 Symfony Messenger 워커가 작동합니다. CheckContractPaymentAvailabilityMessage 타입의 각 메시지는 해당 핸들러(CheckContractPaymentAvailabilityMessageHandler)에 의해 처리됩니다.
핸들러는 최소한의 코디네이터 역할을 합니다:
public function __invoke(CheckContractPaymentAvailabilityMessage $message): void
{
$contractPaymentAvailability = $this->contractPaymentAvailabilityStorage
->getById($message->contractPaymentAvailabilityId);
try {
$this->contractCheckPaymentAvailabilityService
->checkContractAvailability($contractPaymentAvailability);
} catch (ContractExecutionFailedException $e) {
// 예외는 데이터베이스에 이미 상태가 기록되어 있기 때문에
// 조용히 처리됩니다.
}
}
“전문화된 서비스에 위임”하는 이 패턴은 좋은 실천 사례입니다. 핸들러는 인프라(데이터 조회, 오류 포착)만 담당하고, 비즈니스 로직은 서비스에 존재합니다.
3단계: 블록체인 연동
프로세스 중 가장 흥미로운 부분이 여기서 일어납니다. ContractCheckPaymentAvailabilityService는 검증 시스템의 핵심이며, Stellar 블록체인에 배포된 스마트 계약과 직접 상호작용합니다.
단계별 절차
-
스마트 계약 호출
서비스는 블록체인에 배포된 특정 스마트 계약 함수(
check_reserve_balance)를 호출합니다:$trxResponse = $this->checkContractPaymentAvailabilityOperation ->checkContractPaymentAvailability($contractPaymentAvailability);이 함수는 온체인 계산을 수행합니다: 투자자 수, 다음 대기 중인 결제, 예비 기금 규모를 확인하고, 부족 여부를 계산합니다.
계약 내 이 함수의 코드는 Rust(Soroban)로 작성된 다음과 같습니다:
pub fn check_reserve_balance(env: Env) -> Result { require_admin(&env); let claims_map: Map = get_claims_map_or_new(&env); let project_balances: ContractBalances = get_balances_or_new(&env); let mut min_funds: i128 = 0; for (_addr, next_claim) in claims_map.iter() {
Source: …
if next_claim.is_claim_next(&env) {
min_funds += next_claim.amount_to_pay;
}
- 결과 처리
(원본 텍스트가 여기서 끊겼습니다; 서비스가 반환된 값을 어떻게 해석하고, ContractPaymentAvailability 엔터티를 업데이트하며, 필요한 경고나 교정 조치를 어떻게 트리거하는지에 대한 설명을 계속합니다.)
스마트 계약 준비금 검증 워크플로우
스마트 계약 응답은 XDR 형식(스텔라에서 사용하는 바이너리 형식)으로 도착합니다. 우리는 이를 Soneso PHP Stellar SDK로 디코딩합니다:
$trxResult = $this->scContractResultBuilder
->getResultDataFromTransactionResponse($trxResponse);
$requiredFunds = $this->i128Handler
->fromI128ToPhpFloat(
$trxResult->getLo(),
$trxResult->getHi(),
$contractPaymentAvailability->getContract()
->getToken()
->getDecimals()
);
$requiredFunds 값은 매우 중요합니다:
- 0 – 모든 것이 정상입니다.
- > 0 – 다음 결제를 충당하기 위해 준비금에서 부족한 금액을 정확히 나타냅니다.
3. 트랜잭션 기록
Each blockchain interaction is recorded:
$contractTransaction = $this->contractTransactionEntityTransformer
->fromSuccessfulTransaction(
$contractPaymentAvailability->getContract()->getAddress(),
ContractNames::INVESTMENT->value,
ContractFunctions::check_reserve_balance->name,
[$trxResult],
$trxResponse->getTxHash(),
$trxResponse->getCreatedAt()
);
The resulting ContractTransaction provides:
- 완전한 추적 가능성 – 각 검증을 감사할 수 있습니다.
- 트랜잭션 해시 – Stellar에서 검증 가능.
- 정확한 타임스탬프 – 언제 발생했는지 알 수 있습니다.
- 결과 – 스마트 계약이 반환한 값.
4. 상태 업데이트
If a deficit is detected ($requiredFunds > 0), the system blocks the contract:
if ($requiredFunds > 0) {
$contract = $contractPaymentAvailability->getContract();
$this->contractEntityTransformer->updateContractAsBlocked($contract);
$this->persistor->persist($contract);
}
The contract moves to BLOCKED state, which:
- 관리자를 알립니다.
- UI에 계약이 주의가 필요함을 표시합니다.
- 적립금에 추가해야 하는 정확한 금액을 기록합니다.
UI 경고
In assets/react/controllers/Contract/ViewContract.tsx the warning is displayed as:
{query.data.requiredReserveFunds && query.data.requiredReserveFunds > 0 ? (
⚠️ The contract requires adding {formatCurrencyFromValueAndTokenContract(
query.data.requiredReserveFunds,
query.data.tokenContract
)} to the reserve fund to ensure investor payments.
) : (
✓ The contract has capacity to serve payments.
)}
Future work: 적자가 감지되면 즉시 관리자를 알리는 이메일 알림을 추가합니다.
5. 오류 처리
무언가 실패할 경우(네트워크 장애, 스마트‑컨트랙트 오류 등), 우리는 이를 통제된 방식으로 기록합니다:
catch (TransactionExceptionInterface $ex) {
$contractTransaction = $this->contractTransactionEntityTransformer
->fromFailedTransaction(
$contractPaymentAvailability->getContract()->getAddress(),
ContractNames::INVESTMENT->value,
ContractFunctions::check_reserve_balance->name,
$ex
);
// Mark the verification as failed
$this->contractPaymentAvailabilityTransformer
->updateContractPaymentAvalabilityAsFailed(
$contractPaymentAvailability,
$contractTransaction
);
}
The ContractPaymentAvailability Entity – Historical Record
각 검증은 ContractPaymentAvailability 엔터티를 통해 데이터베이스에 영구 저장됩니다:
class ContractPaymentAvailability
{
private ?int $id;
private ?Contract $contract; // Which contract was verified?
private ?float $requiredFunds; // Missing amount (if any)
private ?ContractTransaction $contractTransaction; // Associated blockchain transaction
private ?\DateTimeImmutable $checkedAt; // When it was verified
private ?\DateTimeImmutable $createdAt; // When it was requested
private ?string $status; // PENDING, PROCESSED, FAILED
}
이 구조를 통해 우리는:
- 계약의 전체 검증 기록을 확인할 수 있습니다.
- 패턴을 식별할 수 있습니다 (예: 자금 부족이 빈번한 경우).
- 처리 시간을 측정할 수 있습니다.
- 보고서와 메트릭을 생성할 수 있습니다.
이 아키텍처의 장점
- Scalability – 비동기 처리 덕분에 메인 애플리케이션에 영향을 주지 않고 많은 계약을 검증할 수 있습니다. 각 검증은 자체 컨텍스트에서 실행됩니다.
- Resilience – 하나의 검증에서 실패가 발생해도 다른 검증에 영향을 주지 않으며, 실패한 작업을 나중에 재시도할 수 있습니다.
- Transparency – 온체인 스마트 계약과 직접 상호작용함으로써 계산을 검증 가능하고 불변하게 만들며, 누구나 로직을 감사할 수 있습니다.
- Separation of Concerns
- Command – 작업을 식별합니다.
- Handler – 메시징 인프라를 처리합니다.
- Service – 비즈니스 로직을 포함합니다.
- Entities – 데이터를 모델링합니다.
각 구성 요소는 명확한 목적을 가지고 있으며 독립적으로 발전할 수 있습니다.
결론
우리는 다음과 같은 하이브리드 오프‑체인/온‑체인 솔루션을 구현했습니다:
- 예비 기금 상태를 지속적으로 모니터링합니다.
- 모든 검증을 블록체인과 데이터베이스에 기록합니다.
- 적자가 감지되면 자동으로 계약을 차단하고 이해관계자에게 알립니다.
이 접근 방식은 확장성, 탄력성, 투명성, 그리고 깔끔한 관심사의 분리를 제공하여 자동 이메일 알림 및 보다 풍부한 보고와 같은 향후 개선을 위한 견고한 기반을 마련합니다.
오프체인 및 온체인 상호작용
이 시스템은 Symfony와 스마트 계약 상호작용을 결합하여 투자 계약의 결제 능력을 확인합니다.
- 오프체인 컴포넌트 – 스케줄링, 큐잉 및 상태 관리를 처리합니다.
- 온체인 컴포넌트 – Stellar 블록체인에서 실제 금융 계산을 수행합니다.
이 접근 방식은 양쪽의 장점을 모두 활용할 수 있게 합니다:
- 비즈니스 로직을 위한 전통적인 PHP 프레임워크의 유연성 및 친숙함.
- 중요한 금융 검증을 위한 블록체인의 투명성 및 불변성.
What’s Next?
In future articles we will explore other aspects of the Equillar platform, such as:
- 결제 처리
- 준비금 관리
- Stellar 네트워크와의 통합
Stay tuned!
Technical note
이 문서는 작성 시점의 시스템 내부 작동 방식을 설명합니다. 구현 세부 사항은 시간이 지나면서 변할 수 있지만, 기본적인 아키텍처 원칙은 그대로 유지됩니다.