Smart Contract Wallet 파헤치기 시리즈 — Dharma 제 1편

Smart Contract로 사용자 경험과 서비스 품질을 개선하는 방법!

--

이번 시리즈는 현재 운용되고 있는 Smart Contract 기반 지갑들을 종합적으로 분석합니다. 기존 블록체인 서비스 사용자의 경험을 기존과 어떻게 다르게 구성하여 개선했는지 알아보고, 어떤 방식으로 Smart Contract를 이용하여 운영하고 있는지 알아보겠습니다.

또한 사용자의 자산이 서비스의 Contract에 예치되는 만큼, 실질적인 자산의 소유주나 권익에 대한 대변을 충분히 하는지 확인할 수 있는 시간이 될 것입니다. 궁극적으로 이러한 아이디어에 영감을 얻어, Ethereum 커뮤니티 내에서 새로운 서비스를 만드는데 도움이 되셨으면 합니다.

<Smart Contract Wallet 파헤치기 시리즈>

<Dharma 제 1편>

<Dharma 제 2편>

<Dharma 제 3편>

🧶 Dharma는 무엇입니까?

Dharma는 현재 Maker의 스테이블 코인인 Sai, Circle과 Coinbase의 USDC를 예치하는 플랫폼입니다. 예치된 금액에 대해서 연간 이자율(APR, Annual Percentage Rate)을 실시간으로 제공해주고 있는 것이 특징입니다. 이 스테이블 코인의 가격은 평균적으로 1달러에 수렴하고 있습니다.

최근에는 기존 Dharma가 개발했던 Lending 플랫폼을 없애고 Compound를 통합한 Dharma V2가 공개되었습니다.

🎭 Dharma Smart Wallet

Dharma는 사용자들이 플랫폼을 잘 이용할 수 있도록 Smart Wallet을 제공합니다.

Smart Wallet은 중요한 정보의 백업을 요구하지 않습니다. 일반적인 웹브라우저로 이용할 수 있으며, Metamask와 같은 브라우저 확장 프로그램을 요구하지 않습니다. 또한 사용자의 자산은 Dharma로 부터 할당된 사용자의 지갑에 보관되며, 사용자는 일체 수수료를 지불하지 않습니다.

Dharma의 Smart Wallet 소개 페이지를 가보면 다음과 같은 특징들이 적혀 있습니다.

Wallet의 모든 Transaction은 사용자의 승인 하에 작동하며, 모든 출금은 Dharma로 부터 검증된다고 합니다. 어떤 웹브라우저에서도 작동 하고. 사용자는 실질적으로 소유하는 것 없이 Smart Wallet에 대한 소유권을 주장할 수 있다는 말인데, Dharma의 말 그대로 Wallet이 작동할까요? 이에 대한 해답을 다음 페이지에서 힌트를 얻을 수 있었습니다.

키는 클라우드가 아니라 각각의 웹브라우저에 기록되며, 웹브라우저에 기록된 키 없이 자산은 어디로든 이동할 수 없다고 합니다. 사용자가 정보를 소유하지 않는 것은 아니었네요.

또한 Dharma는 과거에 Email과 Password를 기반한 회원 가입 과정이 있었지만 V2가 되면서 Coinbase의 OAuth를 주로 제공합니다. 여기까지는 기존 웹 서비스를 사용하는 경험과 동일하네요. 🙃 만약 이러한 장치 없이 사용자를 구분해야 했다면, 브라우저 확장 프로그램이 필요하지 않았을까? 하고 생각하게 됩니다.

🎣 거꾸로 올라가기

이 글을 읽으시는 분들과 동일하게, 주어진 정보에 기초하여 Dharma Smart Wallet이 어떻게 작동하는지 알아보려고 합니다.

원래 Coinbase 계정이 있었기 때문에 간단한 연동 과정을 거쳤더니 이러한 인터페이스가 제공되었습니다.

처참한 APR…😭

여기서 저는 Sai를 입금할 것이기 때문에, Sai 버튼을 누릅니다.

I’ll get a donation. 😉 only for Sai, USDC

방금 가입하여 들어왔는데 주소가 할당되어 있는 것을 볼 수 있습니다. 우선 저에게 할당된 Smart Wallet의 주소는 0xD80a28B303b54bD892EC30a607F60Ff725D25389 였는데, 이때 Etherscan에 조회해본 결과 아무런 정보가 나타나지 않는 평범한 EOA 처럼 나타났습니다. 아직 이 주소와 관련해서 아무런 일이 일어나지 않았다는 것이죠.

아 없어요 없어… ❄️

그래서 화면에 나타난 대로 Sai를 전송했습니다. 곧이어 환영 메시지와 함께 APR에 따라 제 Sai의 잔액이 증가하는 것을 볼 수 있었습니다. 아래와 같이요!

🔥 Live Earning!! 🔥

이후에 Etherscan을 확인 해 본 결과,

Sai를 입금한 첫 번째 Transaction과, Internal Transaction으로 해당 주소에 Contract를 생성하는 두 번째 Transaction이 있었습니다.

두 번째 Transaction을 좀 더 상세하게 보면 다음과 같습니다.

2nd Transaction

여기서 Transaction을 발생 시키는 From Address는 Dharma에서 관리하는 단순한 EOA였습니다. 해당 Transaction의 대상인 0xfc00c80b0000007f73004edb00094cad80626d8d에서 함수 newSmartWallet(address)를 실행한 것을 볼 수 있습니다.

이후에 저에게 할당된 Wallet Address인 0xD80a28B303b54bD892EC30a607F60Ff725D25389를 etherscan을 통해 확인해 본 결과 UpgradeBeaconProxyV1라는 Contract가 배포되어 있는 것을 확인했습니다. 또한 Sai와 동일한 가치의 cSai또한 Wallet에 잘 보관되어 있었습니다.

이 말인 즉 Smart Wallet이 생성되는 작업과 동시에 Sai가 Compound로 전송되어 동일한 가치의 cSai가 Wallet으로 입금되었다는 말이 됩니다.

🤨 어떻게 한 거야?

기존에 사용하던 방법으로 Contract를 배포하기 전에 Address를 계산하는 방법은, Transaction 발생자의 Address와 nonce를 해시화 한 것이 Contract Address였기 때문에 생성할 수 있는 모든 Contract Address를 목록화 할 수 있었습니다. 하지만, 이는 정말 안 좋은 사용자 경험을 만들어낼 수 밖에 없는 형태인데 왜 그런지 한 번 알아보겠습니다.

우선 첫째로, 사용자들은 서비스를 이용하기 위해서 Contract가 만들어 질 때 까지 기다려야 합니다. Dharma와 같이 Contract를 사용자마다 만들어 주어야 한다면, 평균 초당 8개의 Transaction을 처리하는 이더리움에서 굉장히 오랜 시간 걸릴 것으로 예상됩니다. 가입된 사용자가 만 명이라고 가정했을때, 대략 한 블럭에 3명 분의 Contract가 생성된다고 하여도 대략 13 시간이나 소요됩니다. 😱

두 번째로, 서비스를 가입만 하는 사용자가 있는 경우에 Contract를 만드는 것은 대기하고 있는 사용자들의 대기 시간을 더욱 늘려줄 뿐이며, 서비스를 운영하는 입장에서도 가스비를 무작정 사용하게 되기 때문에 좋은 방법이라고 볼 수 없습니다.

그렇다면, Dharma는 어떻게 했을까요? 위에서 호출된 newSmartWallet(address) 함수의 원형을 보겠습니다.

newSmartWallet()

Smart Wallet을 생성하는 newSmartWallet(address)함수입니다. 가장 첫 라인에, initialzationCalldata를 구성하는 데이터로 _INITIALIZER Contract의 initialize()의 Selector를 사용하게 됩니다. userSigningKey로 사용되는 주소는 어디서 넣어주는 것인지 확인하지 못했습니다.

initialize()

initialize(address)의 원형은 DharmaSmartWalletInitializer에 위치해 있으며, 인자의 타입은 address이기 때문에, newSmartWallet()에서 넘어온 매개변수인 userSigningKey을 이용해서 initializationCallData를 만든 것을 볼 수 있습니다.

newSmartWallet()의 다음 라인을 보면 _deployUpgradeBeaconProxyInstance함수에 만든 데이터를 넣어, wallet 주소를 반환 받습니다. 아래에는 이벤트만 실행되기 때문에 배포 함수만을 보도록 하겠습니다.

_deployUpgradeBeaconProxyInstance()

5~8 라인을 보면, 신기하게도 type(C)라는 구문을 볼 수 있습니다. 이는 코드 자체에서 Contract의 이름, 코드, 생성자를 포함한 코드를 반환받을 수 있습니다. type(C) 표현식에 대한 추가적인 정보는 링크를 확인하세요.

type(c)를 이용하여 UpgradeBeaconProxyV1의 바이너리 코드를 반환 받았으며, 생성자에 들어갈 데이터로 initilaizationCallData를 넣어 주었습니다. 결과물로 나오는 initCode는 UpgradeBeaconProxyV1 바이너리 코드를 배포할 수 있는 코드가 되었습니다.

여기에서 create2가 등장합니다. 앞서 설명하였듯, 일반적으로 Contract의 Address가 결정되기 위해서는 keccak256(sender + nonce)[12:]을 따릅니다.

미래의 주소가 계산 가능하지만, nonce가 지극히 낮은 경우 계산된 주소를 생성하기 까지 오랜 시간이 걸릴 수 있습니다. 하지만, create2는 이런 방식을 따르지 않습니다.

14~22번째 라인을 보시면, create2(0, code, code size, salt) 으로 create2를 이용하는 것을 볼 수 있습니다. 모든 조건은 동일하며, salt만 사용자 마다 다르게 설정되는 것이 특징입니다. 같은 코드를 배포하더라도 다른 주소를 반환 받을 수 있습니다.

create2를 이용해서 주소를 확정하는 방식은 다음과 같습니다. keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:] 이 공식을 이용하면, 해당 주소에서 배포될 Contract의 Address를 미리 계산할 수 있습니다.

🎈 Wallet은 만들었지만, cToken은?

그렇다면 이제, 배포된 Contract에서 initializationCalldata로 어떤 작업이 수행되었는지 파악하여야 합니다. 할당된 0xD80a28B303b54bD892EC30a607F60Ff725D25389를 확인해보면, UpgradeBeaconProxyV1가 배포되어 있는 것을 확인할 수 있습니다. 해당 Contract의 생성자를 살펴보면 다음과 같은 형태를 가지고 있습니다.

UpgradeBeaconProxyV1 Contract

〰️ 2~4번째 줄을 보면 주소가 하나 하드 코딩 되어 있습니다. 주소가 좀 특이한 형태인데 이런 주소 형태를 Vanity Address라고 부릅니다. 일반적으로 Address는 난수에서 만들어지기 때문에 사용자를 나타내지 않습니다. [0~9] 그리고 [a~f]로 구성된 것으로 사용자가 원하는 형태를 가지기는 어렵습니다.

하지만, 주소는 확률적으로 정해지는 것이다 보니, 난수를 지속적으로 생성하다보면 저런 형태의 주소가 나타날 수 있습니다. 이는 컴퓨터의 연산량을 얼마나 투입하는지에 따라 평균적인 시간이 소요됩니다. 이러한 주소를 생성하고 싶으시다면, https://vanity-eth.tk 를 확인해보세요.

〰️ 6~17번째 줄은 생성자인데, 앞서 만들어진 initializationCalldata가 들어옵니다. 내부에서는 _implementation() 함수에서 delegatecall 호출하는데, 함수가 주소를 반환하는 것을 알 수 있습니다. 그렇다면, 이 함수가 어떤 주소를 반환하는지 알아야 할 필요가 있습니다.

〰️ 24~33번째 줄은 _implementation()의 구현체인데 _UPGRADE_BEACON에서 staticcall을 호출하고 있습니다. 이는 _UPGRADE_BEACON의 fallback 함수를 호출한 것과 같습니다.

🥓_UPGRADE_BEACON과 휴식을 가져봅시다.

정말 Beacon의 기능은 간단합니다. Vanity Address가 하나 _CONTROLLER라고 지정되어 있고, 외부에서 호출되는 모든 것에 대한 대응으로, fallback 함수만 하나 존재하고 있습니다.

fallback 함수의 내부를 보았을 때, Contract를 호출한 당사자가 _CONTROLLER 주소가 아닌 경우 지역 변수인 _implementation을 반환하도록 되어 있고. 호출자가 _CONTROLLER 인 경우에는 지역 변수인 _implementation을 변경하도록 되어 있습니다. 따라서 일반적인 UpgradeBeaconProxyV1 Contract가 호출하는 경우에는 _implementation 주소를 반환받는다고 볼 수 있겠습니다. 다시 UpgradeBeaconProxyV1으로 돌아가 보죠! 😂

결과적으로 _implementation() 함수가 반환하는 주소는 Dharma Smart Wallet의 구현된 주소라는 것을 알 수 있으며, delegatecall로 구현체를 직접 호출하는 효과를 가져옵니다. 앞서 우리는 initializationCalldatainitialize(address) 함수를 호출하는 바이트 코드라는 것을 알 고 있기 때문에, 구현체에 구현된 initialize(address) 함수가 UpgradeBeaconProxyV1 에서 실행될 것입니다.

Dharma의 경우 사용자들에게 발급한 지갑을 업그레이드 하기 위해서는 각 Wallet의 implementation을 변경하여도 되지만, 사용자 수 만큼의 Transaction을 발생시켜야 하는 불편함이 따릅니다. 그렇기 때문에 Beacon 역할을하는 Contract를 이용하여 일괄적으로 지갑의 업그레이드를 할 수 있도록 하였습니다.

구현체를 별도로 가지고 delegatecall을 통해 Contract의 기능 자체를 지속적으로 업그레이드하는 개념은 이전에도 있었습니다. 다만 매번 Transaction을 발생 시킬 때 Gas가 많이 소모되는 문제가 있습니다. 그리고, 다양한 Contract가 구현체로 등록되면 Contract의 저장소 영역이 변질되는 문제가 발생합니다. Dharma도 이러한 저장소 변질 문제에서 자유로울 수 없기 때문에 다음과 같은 장치를 통해 저장소를 관리합니다.

DharmaSmartWalletImplementationV5

Dharma의 Smart Wallet의 경우 현재 V5까지 업그레이드 되었는데, 위와 같은 주의 문구를 이용하여 저장소를 관리합니다. 이것은 완벽한 해결책으로 보이진 않네요. 🤔

어쨌든, initialize() 함수를 살펴 보겠습니다. 현재 Dharma는 Compound를 통해 Sai와 USDC의 Lending을 지원하고 있으며, 코드 내부에서 Sai와 USDC에 해당하는 Token을 가지고 있을 때, Compound Contract의 함수를 이용해서 cToken으로 변환된다는 것을 예상할 수 있습니다.

🤪 너~~~무 복잡해!

그렇…죠. Transaction 하나에 많은 호출들이 있었고, 지금 알고 있는 정보들은 코드를 한 줄씩 보았기 때문에 얻을 수 있었던 것입니다. 그렇기 때문에 더 나아가기 전에 지금까지의 여정을 정리해 보겠습니다.

하… 🤦‍♀️

Mermaid를 이용해서 Transaction 별, 일어나는 일들을 정리했습니다. 진한 화살표선은 표면적으로 드러나는 함수이고, 점선들은 함수 내부에서 일어나는 Internal Transaction입니다.

정리하고 보면 꽤 간단해 보입니다. Sai 또는 USDC의 입금을 기다리고 있을 때, 두 번째 Transaction을 발생시키는 것은 Dharma의 Off-chain 로직입니다. Transaction 사이에서 접점이 없어보이지만, 이러한 장치는 시리즈 내내 나타나며, Dharma가 주도적으로 Transaction을 생성합니다. 이런 특징은 Dharma가 모든 전권을 가지고 있는 것으로 해석될 수 있습니다.

다음 글에서는 Dharma가 사용자에게 자산의 권한을 어떻게 주는지 알아보도록 하겠습니다.

👒 마치며

이번 글에서 집중한 점은 명확합니다. Smart Contract를 업그레이드 할 때 어떤 방법으로 하였는가? Off-chain의 로직은 어떤 역할을 하는가? 그리고 다음 글을 위해서라면, Smart Contract의 소유권은 누구에게 있는가? 하는 점이었습니다.

Dharma는 개인적으로 정리하는데 거의 2주가 걸렸습니다. (Dharma 측에서 정리하던 도중에 새로운 Wallet 구현체를 지속적으로 배포하더라구요.) 그래서 서비스가 어떻게 변화해 나가는지 알 수 있었고, 이를 다음 글에서 알려드릴 생각을 하니 기대가 됩니다.

[About Us]
HAECHI AUDIT은 글로벌 블록체인 업계를 선도하는 스마트 컨트랙트 보안 감사 및 개발 전문 기업입니다. 다년간 블록체인 기술 연구 개발 경험을 보유하고 있는 전문가들로 구성되어 있으며, 가장 신뢰할 수 있는 스마트 컨트랙트 보안 감사 및 개발 서비스를 제공합니다.

대표적인 포트폴리오로는 SK텔레콤, Kakao 블록체인 자회사인 Ground X, Carry 프로토콜 등이 있으며, 약 50여 곳 이상의 글로벌 프로젝트들을 대상으로 보안 감사를 진행한 경험을 보유하고 있습니다.

또한, 우수한 기술력을 인정받아 삼성전자, 서울시, KB금융그룹, 신한은행, 한화그룹 등의 지원을 받고 있으며, 이더리움 재단으로부터 개발 장려금을 수여한 바 있습니다.

--

--

𝚢𝚘𝚘𝚗𝚜𝚞𝚗𝚐.𝚎𝚝𝚑
HAECHI AUDIT

𝙱𝚕𝚘𝚌𝚔𝚌𝚑𝚊𝚒𝚗 𝚋𝚕𝚊𝚑 𝚋𝚕𝚊𝚑 🍻 𝙾𝚙𝚒𝚗𝚒𝚘𝚗𝚜 𝚊𝚛𝚎 𝚖𝚢 𝚘𝚠𝚗.