Folks Finance사의 개발문서를 보고 Cross-Chain Lending Protocol이 어떻게 이루어졌고 구상되었는지 알아보기 위해 읽고 정리해보았다.
Cross-Chain Messaging Architecture
Cross-Chain Lending Protocol은 Wormhole 또는 Chainlink CCIP를 통해 메세지를 전송하는 것을 지원한다.
사용자가 Spoke 체인에서 거래를 시작할 때, 메세지를 중계할 GMP(Generic Messaging Protocol)를 지정할 수 있다. 일부 GMP는 모든 작업은 지원하지 않을 수 있고, 이 경우 사용자는 선택에 제한을 받게 된다. 프론트엔드는 사용자 경험이 더 많은 GMP를 기본적으로 선택하도록 한다고 한다.
설계는 유연성을 갖추어 최신 버전의 GMP로 업그레이드하고, 취약한 GMP를 제거하고 새로운 GMP를 추가할 수 있도록 했다. 또한 통합의 용이성도 고려되었다.
BridgeMessenger
추상 계약인 BridgeMessenger는 상속자들이 메시지를 송수신할 수 있게 한다. Spoke/Hub 스마트 계약은 메시지가 어떻게 전달되거나 수신되는지에 대한 내부 세부 사항을 알 필요 없이 사용에만 집중할 수 있다.
BridgeRouter
BridgeRouter는 사용 가능한 GMP(현재는 Wormhole과 Chainlink CCIP) 간의 차이점을 추상화한다. 이 계약은 GMP의 매핑을 포함하며, 새로운 GMP를 추가하고 기존 GMP를 제거하는 것을 지원한다.
BridgeRouter의 책임은 다음과 같다:
- 요청된 메시지를 지정된 GMP를 통해 보낸다.
- 수신된 메시지를 원하는 스마트 계약으로 전달한다.
- 실패한 메시지를 저장하여 재시도/역전시킨다.
IBridgeAdapter
인터페이스 IBridgeAdapter는 BridgeRouter에서 호출할 수 있도록 필요한 GMP의 구현을 지정한다. 각 GMP는 자신의 필요에 맞는 어댑터를 갖게 되며, 데이터만 보내거나 데이터와 토큰을 함께 보내는 등의 다양한 기능을 지원하기 위해 여러 어댑터를 가질 수 있다.
WormholeAdapter
WormholeAdapter는 IBridgeAdapter와 IWormholeReceiver를 구현한다. 이 어댑터는 BridgeRouter에서 보내는 메시지를 받아 Wormhole Automatic Relayer를 사용해 전송한다. 또한 다른 체인에서 보내진 메시지를 수신하고, 데이터를 BridgeRouter의 공통 형식으로 변환한다.
CCIPAdapter
CCIPAdapter는 IBridgeAdapter를 구현하고 CCIPReceiver로부터 상속받는다. 이 어댑터는 BridgeRouter에서 보내는 메시지를 받아 Chainlink Relayer를 사용해 전송한다. 또한 다른 체인에서 보내진 메시지를 수신하고, 데이터를 BridgeRouter의 공통 형식으로 변환한다.
Sending a Message
메시지를 보내기 위해서는 BridgeMessenger를 구현하고 sendMessage 함수를 호출해야 한다. 이 함수는 다음과 같은 매개변수를 가진다:
params | MessageParams | 메시지를 어떻게 보내고 어떤 설정을 사용할지에 관한 매개변수. |
sender | bytes32 | 메시지를 보낸 출처 주소. 일반적으로 Spoke나 Hub 계약 주소일 것이다. |
destinationChainId | uint64 | 목적지 체인의 식별자. 각 GMP는 자체 사전(dictionary)에서 chainIds를 가지고 있으며, BridgeRouter는 이러한 chainIds와 각 GMP의 chainIds 간의 매핑을 정의한다. |
handler | address | 목적지 체인에서 수신된 메시지를 처리할 주소. 수신자와는 다른 주소다. |
payload | bytes | 전송할 데이터. 호출된 작업에 따라 다르다. |
finalityLevel | uint8 | 메시지가 목적지 체인에서 수신되기 전에 기다려야 하는 최종성 수준. 해당 기능을 지원하지 않는 어댑터를 사용하는 경우 무시된다. |
extraArgs | bytes | 업그레이드를 위해 설정 가능한 매개변수. 토큰 전송 시 값이 설정된다. |
메시지 전송 예시
메시지가 내부적으로 어떻게 전송되는지 이해하기 위해 메시지 lifecycle 예시를 살펴보자.
Invite Address 작업 예시
사용자가 Spoke 체인에서 "Invite Address" 거래를 제출하고, CCIP를 사용하여 크로스체인 통신을 위한 의도를 전달한다고 가정하자.
"Invite Address" 작업의 구현은 Hub 체인으로 메시지를 보내야 한다. 따라서 Spoke는 다음 매개변수로 BridgeRouter를 호출한다:
params | MessageParams | - adapterId: uint16: CCIP 어댑터 식별자 - returnAdapterId: uint16: 0 - receiverValue: uint256: 0 - gasLimit: uint256: 지정된 가스 한도 - returnGasLimit: uint256: 0 |
sender | bytes32 | Spoke 스마트 계약 주소 |
destinationChainId | uint64 | Hub 체인 식별자 |
handler | bytes | Hub 스마트 계약 주소 |
payload | bytes | 사용자가 자신의 계정에 X 주소를 추가하는 요청을 인코딩한 데이터 |
finalityLevel | uint8 | CCIP가 이 기능을 지원하지 않기 때문에 무시됨 |
extraArgs | bytes | 빈 값 |
동작 과정
- BridgeRouter: 요청을 수신하고 원본 발신자가 유효한지 확인한다. 그 후, 메시지를 CCIPAdapter로 전달한다.
- CCIPAdapter: 전달받은 메시지를 수신하고 발신자가 BridgeRouter임을 확인한다. 그런 다음, 메시지를 CCIP에 필요한 형식으로 변환하여 Hub 체인의 해당 CCIP 어댑터에 메시지를 보낸다.
Receiving a Message
메시지를 수신하기 위해서는 BridgeMessenger를 구현하고 _receiveMessage 함수를 호출해야 한다. 이 함수는 수신된 메시지를 처리하는 로직을 인코딩하며, 다음과 같은 매개변수를 가진다:
messageId | bytes32 | 어댑터와 결합된 메시지의 고유 식별자. |
sourceChainId | uint16 | 메시지가 전송된 체인. |
sourceAddress | bytes32 | 메시지가 전송된 주소. |
handler | bytes32 | 메시지를 처리할 스마트 계약의 주소. 컨텍스트 스마트 계약 주소와 동일해야 한다. |
payload | bytes | 수신된 데이터. 호출된 작업에 따라 다르다. |
returnAdapterId | uint16 | 적용 가능한 경우, 반환 메시지를 라우팅할 어댑터. |
returnGasLimit | uint256 | 적용 가능한 경우, 반환 메시지의 가스 한도. |
메시지 수신 예시
메시지가 내부적으로 어떻게 수신되는지 이해하기 위해 메시지 lifecycle 예시를 살펴보자.
Invite Address 작업 예시
Spoke 체인에서 전송된 "Invite Address" 거래는 Hub 체인에서 수신된다. Chainlink Relayer가 CCIPAdapter의 ccipReceive(...) 함수를 호출하여 메시지 발신자가 유효한지 확인한다. 발신자는 해당 Spoke 체인의 어댑터에서 오는 경우 유효한 것으로 간주된다. 어댑터는 메시지를 BridgeRouter에 필요한 공통 형식으로 변환한 후 receiveMessage(...) 함수를 호출한다.
messageId | bytes32 | 어댑터와 결합된 메시지의 고유 식별자. |
sourceChainId | uint16 | 메시지가 전송된 체인. |
sourceAddress | bytes32 | 메시지가 전송된 주소. |
handler | bytes32 | 메시지를 처리할 스마트 계약의 주소. 컨텍스트 스마트 계약 주소와 동일해야 한다. |
payload | bytes | 수신된 데이터. 호출된 작업에 따라 다르다. |
returnAdapterId | uint16 | 적용 가능한 경우, 반환 메시지를 라우팅할 어댑터. |
returnGasLimit | uint256 | 적용 가능한 경우, 반환 메시지의 가스 한도. |
동작 과정
- BridgeRouter: 수신된 메시지가 알려진 어댑터에서 온 것인지 확인한다. 전달하기 전에 메시지 식별자와 메시지를 매핑에 저장한다. 이는 두 가지 목적을 가진다:
- 동일한 메시지가 두 번 수신되지 않도록 보장한다.
- 실패한 메시지를 나중에 다시 시도하거나 복구하기 위해 기록한다.
- BridgeMessenger: BridgeRouter의 호출을 확인하고, _receiveMessage 함수를 내부적으로 호출한다. 이 함수는 메시지를 적절하게 처리할 수 있도록 자유롭게 구현된다.
Round Trip Messages
GMPs(Generic Messaging Protocols)는 메시지를 보낼 때 수수료를 청구한다. 이 수수료에는 다음과 같은 고려 사항이 포함된다:
- 메시지를 보내는 기본 수수료.
- 자동 릴레이어를 사용하는 경우, 메시지 수신 시 가스 비용에 대한 수수료.
- 자동 릴레이어를 사용하고 명시된 경우, 메시지 수신 시 가치 부착에 대한 수수료. 수수료는 일반적으로 소스 체인의 네이티브 가스 토큰으로 지불된다.
일부 작업은 메시지의 왕복이 필요하다. 다음은 그 과정이다:
- Spoke 체인에서 작업이 시작된다.
- Spoke 체인에서 Hub 체인으로 메시지가 전송된다.
- Hub 체인에서 메시지가 수신된다.
- Hub 체인에서 Spoke 체인으로 메시지가 다시 전송된다.
- Spoke 체인에서 메시지가 수신된다.
단계 4에서 발생하는 수수료는 어떻게 지불할 것인?
답은 단계 2에서 더 높은 수수료를 지불하여 단계 3에서 가치가 첨부되도록 하는 것이다. 이 메커니즘은 Wormhole에서 지원되지만 CCIP에서는 지원되지 않는다.
각 사용자는 Hub 체인에 메시지를 Spoke 체인으로 다시 보낼 수 있는 잔액을 가지게 된다. Spoke 체인에서 작업을 시작할 때, 사용자는 Hub 체인의 잔액을 늘리기 위해 추가 수수료를 지불할 옵션이 있다. 대안으로, 기존 잔액을 사용하거나 추가 수수료를 일부 또는 전혀 지불하지 않고 둘을 조합해서 사용할 수 있다.
또한, 사용자가 필요한 수수료를 과대 추정할 경우 추가 가치를 저장하여 나중에 사용하거나 인출할 수 있도록 Spoke 체인에 사용자 잔액을 정의한다.
Messaging Schema
메시지를 수신할 때, GMP는 메시지 식별자, 소스 체인 및 메시지의 소스 주소와 같은 몇 가지 공통 매개변수를 제공한다. 이러한 정보는 이미 포함되어 있으므로 메시징 스키마에 명시적으로 통합하지 않는다.
메시지의 첫 두 바이트는 메시지와 관련된 작업 유형을 인코딩한다. 다음 32 바이트는 메시지와 관련된 계정 ID를 나타낸다. 그 다음 32 바이트는 메시지와 관련된 사용자 주소를 나타낸다. 마지막으로, 다음 언타임드(untyped) 바이트 집합은 작업에 특정한 세부 사항을 인코딩하는 데 사용된다.
USDC Transfers
"To Bridge or Not To Bridge" 섹션에서 토큰 전송에 대한 하이브리드 접근 방식에 대해 설명했다. USDC와 기타 크로스체인 네이티브 토큰은 Hub 체인으로 브리지되고, 다른 모든 토큰은 각각의 Spoke 체인에 남아 있게 된다.
Circle CCTP를 사용하여 크로스체인 네이티브 USDC 전송을 지원한다. Circle CCTP의 depositForBurn 함수는 소스 체인에서 USDC를 소각한 후 목적지 체인에서 USDC를 발행한다. 발행된 USDC는 mintRecipient로 전송된다. depositForBurnWithCaller는 destinationCaller 주소만 목적지 체인에서 USDC를 발행할 수 있도록 제한한다.
프로토콜에서 USDC 전송은 프로토콜의 작업과 결합된다. 예를 들어, USDC 예치 작업이 있다. 따라서 우리는 작업을 인코딩한 메시지 수신과 USDC 발행의 원자적 실행을 원한다. 이것이 depositForBurnWithCaller 함수를 사용하여 destinationCaller를 GMP 메시지 핸들러와 동일한 주소로 지정하는 이유다.
Chainlink CCIP는 USDC 전송을 내부적으로 자동 처리하므로 다른 토큰 전송과 마찬가지로 네이티브 지원을 사용할 수 있다.
GMP 메시지와 CCTP 메시지를 모두 수신하므로, 메시지를 중계하기 전에 둘 모두에 대해 최종성을 기다려야 한다 (CCTP에 대한 증명).
Adapters
어댑터는 특정 GMP에서 메시지를 송수신하는 역할을 한다. 각 GMP를 지원하는 어댑터가 있으며, 이는 메시지의 소스 체인과 목적지 체인 모두에 배포되어야 한다.
GMP는 특정 작업이나 기능을 지원하기 위해 여러 어댑터를 가질 수 있다. 예를 들어, 사용자가 Wormhole 메시지를 Circle CCTP 전송과 결합하여 중계하려는 경우 해당 작업을 구현하는 특정 어댑터를 호출할 수 있다.
어댑터는 메시지가 알려진 소스 체인과 주소에서 온 것인지 확인하고, 형식이 올바른지 확인한 다음 메시지를 BridgeRouter로 전달한다.
CCIP Adapter
- CCIPAdapter 계약은 CCIPReceiver로부터 상속받으며, _ccipReceive(...) 함수를 오버라이드한다.
- 이 함수는 Client.Any2EVMMessage 형식으로 메시지를 수신한 후, BridgeRouter에 필요한 공통 형식으로 변환한다.
Wormhole Adapter
- WormholeAdapter 계약은 IWormholeReceiver를 구현하고, receiveWormholeMessages(...) 함수를 구현한다.
- 이 함수는 바이트 형식으로 메시지를 수신한 후, BridgeRouter에 필요한 공통 형식으로 변환한다.
- 또한, WormholeAdapter는 기다려야 하는 최종성 수준을 지정할 수 있는 기능이 추가되었다.
- "USDC Transfers" 섹션에서 설명한 바와 같이 CCTP 원자적 전송을 지원하기 위해 두 번째 WormholeAdapter 버전도 제공된다.
Hub Adapter
- HubAdapter는 메시지 송수신 동작을 모방한다. 메시지의 소스 및 목적지 체인이 모두 Hub 체인을 가리킬 때 사용된다.
- HubAdapter의 목적은 작업이 어디에서 오는지에 관계없이 Hub Contracts와의 상호작용이 동일하게 유지되도록 하는 것이다.
Rate Limit on Cross-Chain Value Transferred
위험을 최소화하고 잠재적인 손실을 제한하기 위해 일정 기간 내에 프로토콜에서 인출할 수 있는 최대 금액에 대해 한도를 설정한다. RateLimited 스마트 계약이 각 Spoke 체인에 배포되어 지원되는 각 토큰에 대한 한도를 설정한다.
매개변수 정의
period | uint32 | 현재 기간 번호. 예를 들어, 기간 길이가 하루로 정의된 경우 각 새로운 날은 기간 번호를 1씩 증가시킨다. |
limit | uint128 | 기간당 소비할 수 있는 최대 값. |
capacity | uint128 | 새 기간마다 한도로 재설정되는 실제 용량. |
Rate Limiting Mechanism
예를 들어, 다음은 하루 동안의 기간과 20 ETH의 최대 한도를 나타낸다:
- Repay: 5 ETH 반환 -> 용량: 25 ETH
- Borrow: 20 ETH 대출 -> 용량: 5 ETH (추가 대출 불가)
- Withdraw: 10 ETH 인출 -> 용량: 10 ETH (이후 대출 가능)
고려 사항
- 서비스 거부 공격 방지: 공격자가 새로운 기간이 시작되기 전에 악의적으로 예치하고, 새로운 기간이 시작되면 즉시 인출하여 서비스를 거부할 수 있다. 이를 방지하기 위해 한도를 충분히 높게 설정하고, "핫 월렛"을 통해 현재 기간 동안 용량을 일시적으로 증대시킬 수 있다.
- 토큰 가격 인식 부족: RateLimiter는 토큰 가격을 인식하지 못하므로, 주기적으로 각 버킷의 매개변수를 조정하여 사용성과 보안 간의 적절한 균형을 맞춘다.
Account Management
사용자의 Folks Finance 계정을 정의하며, 이 계정은 사용자의 자산을 바탕으로 모든 정보를 포함하고 대출 기능을 가능하게 한다. 계정은 블록체인 주소를 사용하여 관리된다. 각 계정에는 고유한 bytes32 식별자가 있다.
크로스체인 계정 관리
크로스체인 환경에서 계정을 관리하는 것은 어떻게 하는가? 사용자는 여러 블록체인을 사용하여 자신의 계정을 관리하기를 원한다.
이는 주소를 (체인 식별자, 체인 주소) 튜플을 사용하여 식별한다. 이 두 속성의 조합은 중복 없이 단일 엔터티를 고유하게 식별한다. 모든 주소 형식을 지원하기 위해, 우리는 address 대신 bytes32 형식을 사용한다.
사용자 경험을 단순화하기 위해, 계정 당 Spoke 체인에 최대 하나의 주소만 등록할 수 있도록 제한한다. 따라서 사용자가 예를 들어 ETH를 대출하는 경우, 사용자에게 등록된 Ethereum 주소로 ETH를 받을 것이라고 가정할 수 있다.
또한 (체인 식별자, 체인 주소) 쌍을 여러 계정에 등록할 수 없도록 제한한다. 이는 사용자가 다른 계정에 있다는 것을 인식하지 못한 채 프로토콜과 상호작용하는 상황을 방지하기 위함이다. 하나의 주소 당 하나의 계정은 사용자가 제어하는 주소를 자신의 계정에 추가하여 동일한 계정을 자신에게 사용하지 못하게 하는 서비스 거부 공격 가능성을 없앤다. 따라서 초대와 수락 개념을 사용한다. 기존 계정의 등록된 주소는 다른 Spoke 체인의 주소를 초대할 수 있다. 초대받은 주소는 계정에 등록되기 전에 초대를 수락해야 한다.
다이어그램 설명
- T1: Chain B에서 주소 0x3Da가 주소 0xJT5를 초대한다.
- T2: Chain A에서 주소 0xJT5가 초대를 수락한다.
- Hub Chain: Bob의 계정에는 초대된 주소(Chain B의 0x3Da)와 수락된 주소(Chain A의 0xJT5)가 포함된다.
Delegatable
크로스체인 대출 프로토콜 위에 다른 프로토콜이 구축될 수 있도록, 계정을 관리할 수 있는 또 다른 주소 집합을 활성화한다. 이를 "delegated address"라고 한다. 이들은 Hub 체인에서만 사용할 수 있으며, 계정은 원하는 만큼 많이 추가할 수 있다. 동일한 주소가 여러 계정에 delegate수도 있다.
이 주소들은 크로스체인 대출 프로토콜을 내부적으로 사용하는 Hub 체인의 다른 스마트 계약이 될 것이다.
'Blockchain' 카테고리의 다른 글
Blockchain CTF Template 구현 (0) | 2024.08.14 |
---|---|
간단한 개념 정리 (1) | 2024.07.25 |
Cross-Chain Lending Protocol: 유동성 확보와 편의성을 위한 설계 및 비교 분석 - Part 1 (0) | 2024.07.19 |
Uniswap이 뭘까? (2) | 2024.07.18 |
Damn Vulnerable DeFi Challenge #7 - Compromised (0) | 2024.07.06 |