SettleLab
전체 코스
LESSON 02RWA Tokenization

ERC-7540 비동기 볼트

심화40분근거 2

학습 결과

  • 비동기 deposit/redeem 요청 모델을 설명한다.
  • 요청, 처리, claim 상태를 제품 흐름으로 설계한다.

선행 조건

  • RWA 학습맵과 ERC-4626 볼트

완료 기준

  • 비동기 vault의 상태를 설명했다.
  • redemption UX를 설계했다.
  • partial 처리 기준을 남겼다.

ERC-7540 비동기 볼트

도입

ERC-4626은 즉시 deposit/redeem이 가능한 vault를 이해하는 데 좋다. 하지만 RWA fund, private credit, T-bill wrapper, 일부 lending vault는 같은 transaction 안에서 입출금을 끝낼 수 없다. cutoff time, NAV 산정, compliance review, liquidity gate, offchain settlement가 중간에 들어가기 때문이다.

ERC-7540은 ERC-4626에 asynchronous request flow를 더한다. 사용자는 먼저 requestDeposit 또는 requestRedeem을 만들고, request가 claimable 상태가 된 뒤 별도 claim function을 호출해 shares 또는 assets를 받는다. 이 두 단계 구조가 RWA redemption UX의 핵심이다.

학습 목표

  • 비동기 deposit/redeem 요청 모델을 설명한다.
  • 요청, 처리, claim 상태를 제품 흐름으로 설계한다.

개념 설명

타임라인가로 스크롤 · 크게 보기 지원
ERC-7540 비동기 볼트 학습 타임라인이 시각화는 RWA vault와 redemption 운영에서 시간 지연, finality, 운영 확인이 어떤 순서로 제품 상태가 되는지를 보여주며, 'ERC-7540 비동기 볼트'에서 남겨야 할 설계 증거를 좁힌다.
1Model

개념 읽기

비동기 deposit/redeem 요청 모델을 설명한다.

2Check

실패 상태 확인

share와 asset 권리가 분리되는가

3Practice

실습 산출물 작성

ERC-7540 비동기 볼트 이해 점검

4Close

완료 기준 대조

비동기 vault의 상태를 설명했다.

크게 보기
1Model

개념 읽기

비동기 deposit/redeem 요청 모델을 설명한다.

2Check

실패 상태 확인

share와 asset 권리가 분리되는가

3Practice

실습 산출물 작성

ERC-7540 비동기 볼트 이해 점검

4Close

완료 기준 대조

비동기 vault의 상태를 설명했다.

ERC-7540의 기본 lifecycle은 Request, Pending, Claimable, Claimed다. 중요한 점은 request가 곧바로 사용자에게 결과물을 push하지 않는다는 것이다. 사용자가 다시 claim function을 호출해 output token을 pull한다.

표 자료가로 스크롤 · 크게 보기 지원
상태의미제품 화면 문구
Request사용자가 deposit 또는 redeem 요청을 제출함요청이 접수됐습니다
Pendingvault가 아직 처리하지 않았음처리 대기 중입니다
Claimablevault가 처리했고 사용자가 claim 가능함수령 가능 상태입니다
Claimed사용자가 shares 또는 assets를 수령함수령 완료
크게 보기
상태의미제품 화면 문구
Request사용자가 deposit 또는 redeem 요청을 제출함요청이 접수됐습니다
Pendingvault가 아직 처리하지 않았음처리 대기 중입니다
Claimablevault가 처리했고 사용자가 claim 가능함수령 가능 상태입니다
Claimed사용자가 shares 또는 assets를 수령함수령 완료

비동기 deposit과 redeem은 ERC-4626 함수의 의미도 바꾼다.

표 자료가로 스크롤 · 크게 보기 지원
흐름request 단계claim 단계주의점
Async depositrequestDeposit(assets, controller, owner)로 asset을 vault에 넣고 pending 기록deposit 또는 mint로 claimable request를 shares로 수령request 시점의 conversion과 claim 시점의 shares가 다를 수 있다
Async redeemrequestRedeem(shares, controller, owner)로 shares를 vault 통제 아래 둠withdraw 또는 redeem으로 claimable request를 assets로 수령pending 기간에 yield 또는 exchange rate가 고정되지 않을 수 있다
크게 보기
흐름request 단계claim 단계주의점
Async depositrequestDeposit(assets, controller, owner)로 asset을 vault에 넣고 pending 기록deposit 또는 mint로 claimable request를 shares로 수령request 시점의 conversion과 claim 시점의 shares가 다를 수 있다
Async redeemrequestRedeem(shares, controller, owner)로 shares를 vault 통제 아래 둠withdraw 또는 redeem으로 claimable request를 assets로 수령pending 기간에 yield 또는 exchange rate가 고정되지 않을 수 있다

ERC-7540은 request를 requestIdcontroller로 식별한다. 같은 requestId가 여러 요청에 쓰일 수 있으므로 product DB는 requestId만 저장하면 부족하다. controller, owner, request type, assets/shares, submittedAt, claimableAt, claimedAt, exchangeRate policy를 함께 저장해야 한다.

흐름도가로 스크롤 · 크게 보기 지원
강의 흐름도상태, 책임, 검증 지점을 순서대로 읽기 위한 다이어그램이다.
크게 보기

코드로 확인하기

비동기 vault의 핵심은 요청과 실행 사이에 시간이 있다는 점이다. 코드는 "사용자가 요청했다"와 "vault가 처리했다"를 같은 상태로 취급하지 않아야 한다.

컨트랙트비동기 redemption 요청 큐

CODE SURFACEsolidity
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";using SafeERC20 for IERC20;struct RedemptionRequest {    address owner;    uint256 shares;    uint64 epoch;    bool claimed;}IERC20 public immutable asset;mapping(uint256 requestId => RedemptionRequest) public redemptionRequests;mapping(uint64 epoch => uint256 assetsPerShare) public navByEpoch;uint256 public nextRequestId;uint64 public currentEpoch;event RedeemRequested(uint256 indexed requestId, address indexed owner, uint256 shares, uint64 epoch);event RedeemClaimed(uint256 indexed requestId, address indexed owner, uint256 assets, uint64 epoch);error ZeroShares();error NotRequestOwner();error NotClaimable();error AlreadyClaimed();function requestRedeem(uint256 shares) external returns (uint256 requestId) {    if (shares == 0) revert ZeroShares();    requestId = ++nextRequestId;    redemptionRequests[requestId] = RedemptionRequest({        owner: msg.sender,        shares: shares,        epoch: currentEpoch,        claimed: false    });    _transfer(msg.sender, address(this), shares);    emit RedeemRequested(requestId, msg.sender, shares, currentEpoch);}function claimRedeem(uint256 requestId) external returns (uint256 assets) {    RedemptionRequest storage request = redemptionRequests[requestId];    if (request.owner != msg.sender) revert NotRequestOwner();    if (request.claimed) revert AlreadyClaimed();    uint256 price = navByEpoch[request.epoch];    if (price == 0) revert NotClaimable();    request.claimed = true;    assets = request.shares * price / 1e6; // NAV fixed-point scale: 1 USDC = 1_000_000    asset.safeTransfer(msg.sender, assets);    emit RedeemClaimed(requestId, msg.sender, assets, request.epoch);}

요청 시점에는 asset을 바로 돌려주지 않는다. 대신 share를 vault 안으로 잠그고, 어떤 epoch의 NAV로 처리할지 기록한다. 이 기록이 없으면 나중에 "어느 가격으로 환매됐는가"를 설명할 수 없다.

백엔드NAV 확정 후 claim 가능 여부 계산

CODE SURFACEtypescript
type RedemptionRequest = {  id: bigint;  owner: `0x${string}`;  shares: bigint;  epoch: number;  claimed: boolean;};const NAV_SCALE = 1_000_000n; // NAV는 USDC 6 decimals 기준 assets-per-share fixed point로 저장한다.function claimableAssetAmount(request: RedemptionRequest, navByEpoch: Map<number, bigint>) {  const sharePrice = navByEpoch.get(request.epoch);  if (!sharePrice) {    return { ready: false, assets: 0n };  }  const assets = request.shares * sharePrice / NAV_SCALE;  return { ready: !request.claimed, assets };}

운영자는 이 계산 결과를 dashboard에 노출한다. NAV_SCALE은 "share 1개당 asset 수량"을 USDC 6자리 fixed point로 저장하기 위한 단위다. 사용자는 "대기 중", "NAV 확정", "claim 가능", "claim 완료"를 구분해서 볼 수 있어야 한다.

강의 포인트

표 자료가로 스크롤 · 크게 보기 지원
관점확인할 질문증거로 남길 것
request identityrequestId와 controller를 함께 저장했는가request key schema
user custodyrequest 단계에서 asset 또는 share가 어디에 있는가custody transition 표
claim UX사용자가 언제 무엇을 직접 claim해야 하는가status copy와 CTA
partial 처리같은 requestId의 일부가 claimable이면 어떤 pro-rata 정책을 쓸 것인가partial fulfillment policy
stale request오래 pending인 request를 어떻게 review하고 해소할 것인가manual resolution runbook
크게 보기
관점확인할 질문증거로 남길 것
request identityrequestId와 controller를 함께 저장했는가request key schema
user custodyrequest 단계에서 asset 또는 share가 어디에 있는가custody transition 표
claim UX사용자가 언제 무엇을 직접 claim해야 하는가status copy와 CTA
partial 처리같은 requestId의 일부가 claimable이면 어떤 pro-rata 정책을 쓸 것인가partial fulfillment policy
stale request오래 pending인 request를 어떻게 review하고 해소할 것인가manual resolution runbook

실무 예시

투자자가 RWA vault share를 redeem하고 싶다고 가정한다. 즉시 asset을 받을 수 없다. fund는 cutoff 이후 NAV를 계산하고, liquidity를 확보한 다음 redeem request를 claimable로 바꾼다.

표 자료가로 스크롤 · 크게 보기 지원
시간사용자 상태vault/accounting 상태운영 확인
T0redeem 요청 제출shares locked 또는 burned policy 적용requestId, controller, owner
T1처리 대기pendingRedeemRequest 증가cutoff batch 포함 여부
T2수령 가능claimableRedeemRequest 증가exchange rate, fee, available assets
T3claim 완료assets transferred, request consumedclaim tx, final amount
크게 보기
시간사용자 상태vault/accounting 상태운영 확인
T0redeem 요청 제출shares locked 또는 burned policy 적용requestId, controller, owner
T1처리 대기pendingRedeemRequest 증가cutoff batch 포함 여부
T2수령 가능claimableRedeemRequest 증가exchange rate, fee, available assets
T3claim 완료assets transferred, request consumedclaim tx, final amount

이 표가 없으면 사용자는 "출금 버튼을 눌렀는데 왜 돈이 바로 안 오나"라고 느낀다. RWA UX는 지연을 숨기는 것이 아니라 지연의 이유와 다음 행동을 명확히 알려야 한다.

흔한 오해와 실패 시나리오

표 자료가로 스크롤 · 크게 보기 지원
오해실제로 확인할 것
request가 성공하면 deposit/redeem이 끝났다고 본다.request와 claim은 별도 단계이며 사용자가 다시 claim해야 할 수 있다.
preview 함수가 비동기 흐름에서도 항상 유효하다고 본다.async deposit/redeem에서는 preview가 revert하거나 의미가 달라질 수 있다.
requestId만 저장하면 충분하다고 본다.controller와 requestId를 함께 봐야 하며 requestId 0 aggregation도 고려한다.
pending 기간의 exchange rate가 고정된다고 가정한다.표준은 pending redemption request가 yield-bearing이거나 fixed rate라고 보장하지 않는다.
operator approval을 편의 기능으로만 본다.operator는 controller 대신 request를 관리할 수 있으므로 권한 탈취 리스크를 검토해야 한다.
크게 보기
오해실제로 확인할 것
request가 성공하면 deposit/redeem이 끝났다고 본다.request와 claim은 별도 단계이며 사용자가 다시 claim해야 할 수 있다.
preview 함수가 비동기 흐름에서도 항상 유효하다고 본다.async deposit/redeem에서는 preview가 revert하거나 의미가 달라질 수 있다.
requestId만 저장하면 충분하다고 본다.controller와 requestId를 함께 봐야 하며 requestId 0 aggregation도 고려한다.
pending 기간의 exchange rate가 고정된다고 가정한다.표준은 pending redemption request가 yield-bearing이거나 fixed rate라고 보장하지 않는다.
operator approval을 편의 기능으로만 본다.operator는 controller 대신 request를 관리할 수 있으므로 권한 탈취 리스크를 검토해야 한다.

실습 과제

  1. ERC-7540 비동기 볼트 이해 점검: requestId, controller, owner, asset, shares, pending, claimable, claimed 필드를 포함한 request record를 설계한다.
  2. redemption UX 설계: Requested, Pending, Claimable, Claimed, ManualReview, Canceled 상태별 사용자 문구와 CTA를 작성한다.
  3. partial 처리 기준 작성: 같은 requestId가 일부 claimable이 될 때 pro-rata 처리, rounding, fee, disclosure 기준을 정한다.
  4. operator risk 검토: controller가 operator를 승인했을 때 어떤 함수 호출이 허용되는지와 revoke UX를 정리한다.

완료 기준

  1. 비동기 vault의 상태를 설명했다.
  2. redemption UX를 설계했다.
  3. partial 처리 기준을 남겼다.

근거 자료

Final checkpoint

읽기를 마쳤다면 여기서 기록한다

아래 버튼은 읽기 진도를 저장한다. 체크리스트, 과제, 랩 산출물은 위 Workbook에서 따로 관리한다.

  • 비동기 vault의 상태를 설명했다.
  • redemption UX를 설계했다.
  • partial 처리 기준을 남겼다.

학습 자료 근거

ERC7540 비동기 볼트
이 LMS 레슨의 개념, 예시, 과제 구성을 잡는 데 사용한 근거 문서.
내부 참고 문서
ERC-7540: Asynchronous ERC-4626 Tokenized Vaults
https://eips.ethereum.org/EIPS/eip-7540