[Lido Finance] 2. Lido.sol 컨트랙트 분석

DongHeon Lee
BerryFi
Published in
10 min readJun 3, 2022

[Lido Finance] 1. 전반적인 작동원리와 StETH 컨트랙트 분석에서는 Ethereum Deposit Contract와 Lido Finance의 관계를 살펴본 뒤, Lido Finance에 Ether를 예치하고 받을 수 있는 StETH의 로직을 분석해 보았다. 이번 아티클에서는 유저가 Lido에 예치한 이더가 Ethereum Deposit Contract에 예치되는 과정과 보상 분배 방식에 대해 알아볼 것이다.

Author: 이동헌

Audited By: 이태헌, 표정우, 김홍욱

Lido에 예치된 Ether의 라이프사이클

  1. Buffered

사용자가 Lido finance에 Ether를 예치하면, 먼저 Lido.sol 컨트랙트에 임시로 보관되는데 이를 Buffered Ether라고 한다.

  1. Transient

Buffered Ether는 DAO에 의해 지정된 특정 지갑의 함수 호출에 의해 Ethereum Deposit Contract에 예치된다. 다만 Ethereum Deposit Contract는 비콘체인이 아닌 이더리움 메인넷에 존재하므로 Ethereum Deposit Contract에 예치된 Ether가 비콘체인에서 나타나기까지는 약간의 기간이 존재한다. 이 기간에 있는 Ether를 Transient 단계에 있다고 표현한다.

  1. Staked

Ethereum Deposit Contract에 예치되어 비콘체인의 Validator를 활성화하고, 비콘체인에서도 밸런스가 확인되는 Ether를 Staked Ether라고 한다.

  1. Withdraw

Merge가 완료된 이후에는 StETH를 redeem함으로써 비콘체인의 Ether를 인출할 수 있게 된다. 아직 Merge가 진행되지 않았으므로 Withdraw는 불가능하다.

Node Operator란 무엇인가?

Node Operator는 Lido가 스테이킹한 Ether로 인해 활성화되는 Validator를 직접 운영하는 주체로써, 2022년 5월 현재 22개의 Node Operator가 활성화되어 있다. Node Operator와 이들이 운영하는 Validator key는 NodeOperatorsRegistry.sol에 의해 관리된다.

Node Operator는 id와 보상을 받을 주소를 입력함으로써 추가된다. 보상은 이더리움 메인넷에서 StETH로 주어진다. 하나의 Node Operator는 여러 개의 Validator를 운영할 수 있는데, 이들이 운영할 Validator의 공개키와 서명은 DAO에 의해 NodeOperatorsRegistry.sol에 미리 저장되어 있다가 Ethereum Deposit Contract에 Ether가 예치될 때마다 하나씩 할당되어 Validator를 활성화시키게 된다.

Lido.sol 의 데이터 저장 구조

1편에서 언급하였듯 Lido Finance는 업그레이드 가능성을 위해 proxy 패턴을 따르고 있다. 이에 따라 Lido.sol의 bytes32 변수에는 UnstructuredStorage 라이브러리가 적용되며, 주요 변수들은 다음과 같이 Struct 구조체를 활용하지 않고 커스텀 포인터가 참조하는 위치에 저장되어 있다.

Lido Finance에 ETH 예치

사용자가 Lido Finance에 ETH를 예치하면, Lido.sol 의 submit 함수를 호출하게 된다. 이는 Lido.sol이 상속하는 StETH.sol의 internal 함수인 _mintShares 를 호출하여 StETH를 msg.sender 에게 발행하고, BUFFERED_ETH_POSITIONmsg.value를 더한다. BUFFERED_ETH_POSITION은 유저들이 예치하였으나 아직 Ethereum Deposit Contract에 예치되지 않고 Lido.sol 컨트랙트가 보유하고 있는 buffered ether의 수량을 나타내는 constant 변수이다. _mintShares로 발행된 StETH의 수량은 사용자가 예치한 ETH의 수량과 일치한다.

Buffered Ether를 ETH2 Deposit Contract에 예치

DEPOSIT_ROLE로 지정된 지갑은 depositBufferedEther 함수를 호출하여 Buffered Ether를 이더리움 메인넷의 Ethereum Deposit Contract에 예치한다. depositBufferedEther에는 auth(DEPOSIT_ROLE) 이라는 modifier가 적용되어 있는데, 이는 Lido.sol이 상속하는 AragonApp의 커스텀 modifier로서 DEPOSIT_ROLE로 지정된 지갑만이 이 함수를 호출할 수 있음을 의미한다.

depositBufferedEther가 호출하는 internal 함수인 _depositBufferedEtherBuffered EtherDEPOSIT_SIZE로 나눈 값인 numDeposits_ETH2Deposit 함수의 인자로 넘겨준다. DEPOSIT_SIZE는 32 ether로 하드코딩 되어 있다.

_ETH2Deposit은 인자로 받은 numDepositsNodeOperatorsRegistry.sol 에 정의된 함수인 assignNextSigningKeys 의 인자로 넘겨준다. Buffered Ether는 32 ether의 배수가 아닌 경우가 대부분이므로 numDeposit은 대부분의 경우 정수가 아니다. assignNextSigningKeys는 루프 구문을 사용하여 numDeposit 이하의 모든 정수에 대해 가동할 수 있는 Validator의 공개키와 서명을 찾아 반환한다. _ETH2Deposit 함수는 각각의 공개키와 서명에 대해 _stake 함수를 호출하여 32 ether 씩 Ethereum Deposit Contract에 예치한다. 예치되지 않은 나머지 Ether는 Lido.sol에 buffered 된 상태로 남아있게 된다.

보상 및 수수료 배분

Lido의 수수료 배분은 stETH의 Balance 계산에 사용하는 shares를 추가로 발행하여 Treasury, Insurance fund, Node Operators에 배분하는 방식으로 이루어진다. 기존 stETH 홀더에게는 shares가 추가로 발행되지 않으므로, 이것은 기존 stETH 홀더의 지분 희석을 통해 수수료를 수취하는 것이나 마찬가지이다. 수수료 부과 대상이 되는 것은 비콘체인의 채굴 보상이다.

다음 그림은 이해를 돕기 위한 것으로, A와 B 두 명의 홀더가 존재하고 하나의 Node Operator가 수수료를 받는 상황을 가정하고 있다.

1편에서 설명했듯, StETH는 Lido Finance에 예치된 Ether의 총량에 해당하는 TotalPooledEther에 지분율을 곱해 수량이 결정되는 리베이스 토큰이다. 홀더 A와 B가 t=0에서 전체 100의 shares 가운데 각각 40, 60의 지분을 갖고 있고, 이들이 예치한 Ether의 총량이 100이라고 가정하자. 산식에 따라 A와 B의 StETH Balance는 각각 40과 60이다.

t=1에는 비콘체인의 채굴 보상으로 인해 TotalPooledEther가 110으로 증가했다고 가정하자. Node Opreator는 그동안 Validator를 유지한 것에 대한 수수료를 5 shares를 추가로 발행받는 방식으로 수취하게 되었다. 이 경우 TotalShares는 105가 되고, A와 B의 지분은 다소 희석된다. 산식에 따라 A와 B의 지갑에 들어 있는 StETH의 양은 각각 41.90, 62.86으로 늘어나 채굴 보상을 분배받게 되고, Node Operator는 수수료로 5.24 StETH를 갖게 된다. 만약 t=1이 이더리움 Merge 이후라면, 이들은 각각 자신이 보유한 StETH를 상환하여 정확히 1:1로 대응되는 수량의 ETH를 받을 수 있게 된다.

이제 보상 배분 체계가 컨트랙트에서 어떻게 구현되어 있는지 살펴보자. Lido.solpushBeacon 함수는 Oracle에 의해 주기적으로 호출되어 비콘체인에 나타나는 Validator의 수와 Ether Balance를 업데이트한다. 이 함수 내에서 비콘체인에서의 총 채굴 보상에 해당하는 rewards가 다음과 같이 계산된다.

rewards = 새롭게 보고된 Beacon Balance — (새로 추가된 Validator의 수 * 32 ether + 기존의 Beacon Balance)

기존의 Beacon Balance는 이전 호출 시점 이후로 비콘체인에서 채굴된 Ether 양을 포함하지 않고, 새로 추가된 Validator의 수 * 32 ether는 비콘체인에 새로 추가된 Ether 양(즉, 아직 채굴 보상을 받지 않은 Ether의 양)을 의미한다. 따라서 rewards는 이전 호출 시점 이후로 비콘체인에서 채굴된 Ether 양을 의미한다.

rewards는 수수료 분배 로직을 담고 있는 distributeRewards 함수의 인자로 전달된다. distributeRewards 는 발행해야 할 shares의 갯수를 계산하여 Lido.sol의 주소에 발행한 뒤, 이를 수수료율에 따라 Treasury, Insurance fund, Node Operators에 배분한다.

발행해야 할 shares의 갯수인 shares2mint에, share 발행 이후 share의 개당 가치(newShareCost)를 곱한 것은 rewards 가운데 fee로 수취되는 총량과 같아야 한다. 총 수수료율에 해당하는 feeBasis는 BP(0.01%)단위로 표현되고, 2022년 5월 현재 값은 10%에 해당하는 1000이다.

shares2mint * newShareCost = (rewards * feeBasis) / 10000

share 하나의 가치는 Buffered Ether, Transient Ether, Staked Ether의 총합인 TotalPooledEther를 발행된 shares의 갯수로 나눈 것과 같다. 따라서 share 의 개당 가치는 다음과 같이 표현된다.

newShareCost = newTotalPooledEther / (prevTotalShares + shares2mint)

위 두 식을 풀면 shares2mint는 다음과 같이 계산된다.

shares2mint = rewards * feeBasis * prevTotalShares / (newTotalPooledEther * 10000) — (feeBasis * rewards)

이렇게 발행된 shares는 treasuryFeeBasisPoints,insuranceFeeBasisPoints, operatorsFeeBasisPoints 에 따라 Treasury, Insurance fund, Node Operators에게 배분된다. 2022년 5월 현재 각각의 값은 0, 5000, 5000이며, 단위는 BP이다. feeBasis가 1000BP(10%)이고 이 중 Insurance fund와 Node Operators가 5000BP(50%)를 가져가므로 이들에게 비콘체인 채굴 보상 중 5%씩이 각각 수수료로 지급되고 있는 것이다.

--

--