-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathVestingEscrow4626.vy
More file actions
336 lines (267 loc) · 10.1 KB
/
Copy pathVestingEscrow4626.vy
File metadata and controls
336 lines (267 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
#pragma version 0.4.3
#pragma evm-version prague
"""
@title ERC-4626 Vesting Escrow
@author Yearn Finance
@license MIT
@notice Vests underlying-asset principal while holding and paying ERC-4626 shares
"""
interface IERC4626:
def asset() -> address: view
def convertToAssets(shares: uint256) -> uint256: view
def convertToShares(assets: uint256) -> uint256: view
def balanceOf(account: address) -> uint256: view
def transfer(receiver: address, shares: uint256) -> bool: nonpayable
event PrincipalClaim:
receiver: indexed(address)
principal_assets: uint256
shares: uint256
event YieldClaim:
recipient: indexed(address)
shares: uint256
event Revoked:
recipient: indexed(address)
revoker: indexed(address)
receiver: indexed(address)
unvested_principal_assets: uint256
shares: uint256
ts: uint256
event RevocationRenounced:
revoker: indexed(address)
event PermissionlessClaimsSet:
enabled: bool
MAX_PRINCIPAL: constant(uint256) = 2**128 - 1
MAX_DURATION: constant(uint256) = 2**64 - 1
recipient: public(address)
vault: public(IERC4626)
start_time: public(uint256)
end_time: public(uint256)
cliff_length: public(uint256)
principal_assets: public(uint256)
claimed_principal_assets: public(uint256)
# Zero until revocation; active escrows use end_time as their effective stop.
disabled_at: public(uint256)
claims_closed: bool
revoker: public(address)
yield_recipient: public(address)
@deploy
def __init__():
# Prevent initialization of the implementation itself.
self.recipient = self
@external
def initialize(
revoker: address,
vault: IERC4626,
recipient: address,
principal_assets: uint256,
start_time: uint256,
end_time: uint256,
cliff_length: uint256,
permissionless_claims: bool,
yield_recipient: address,
) -> bool:
"""Initialize one funded ERC-4626 minimal proxy."""
assert self.recipient == empty(address) # dev: can only initialize once
assert principal_assets > 0 # dev: principal must be > 0
assert principal_assets <= MAX_PRINCIPAL # dev: principal too large
assert revoker != self # dev: invalid revoker
assert recipient not in [empty(address), self, vault.address, revoker] # dev: invalid recipient
assert end_time > block.timestamp and end_time > start_time # dev: invalid vesting period
duration: uint256 = end_time - start_time
assert duration <= MAX_DURATION # dev: duration too long
assert cliff_length <= duration # dev: invalid cliff
funded_shares: uint256 = staticcall vault.balanceOf(self)
assert funded_shares > 0 # dev: escrow not funded
asset_token: address = staticcall vault.asset()
assert asset_token.is_contract # dev: invalid asset
assert yield_recipient not in [
empty(address),
self,
vault.address,
asset_token,
] # dev: invalid yield recipient
roundtrip_shares: uint256 = staticcall vault.convertToShares(principal_assets)
assert roundtrip_shares > 0 and roundtrip_shares <= funded_shares # dev: invalid conversion
assert staticcall vault.convertToAssets(funded_shares) >= principal_assets # dev: insufficient principal backing
self.revoker = revoker
self.vault = vault
self.recipient = recipient
self.start_time = start_time
self.end_time = end_time
self.cliff_length = cliff_length
self.principal_assets = principal_assets
self.claims_closed = not permissionless_claims
self.yield_recipient = yield_recipient
return True
@internal
@view
def _vesting_end() -> uint256:
disabled_at: uint256 = self.disabled_at
if disabled_at == 0:
return self.end_time
return disabled_at
@internal
@view
def _vested_principal_assets(time: uint256) -> uint256:
start_time: uint256 = self.start_time
if time < start_time + self.cliff_length:
return 0
end_time: uint256 = self.end_time
if time >= end_time:
return self.principal_assets
return self.principal_assets * (time - start_time) // (end_time - start_time)
@internal
@view
def _remaining_principal_assets() -> uint256:
return self._vested_principal_assets(self._vesting_end()) - self.claimed_principal_assets
@internal
@view
def _claimable_principal_assets(time: uint256) -> uint256:
return self._vested_principal_assets(time) - self.claimed_principal_assets
@internal
@view
def _split_principal_and_yield(remaining_assets: uint256) -> (uint256, uint256):
balance: uint256 = staticcall self.vault.balanceOf(self)
if remaining_assets == 0:
return 0, balance
value: uint256 = staticcall self.vault.convertToAssets(balance)
if value <= remaining_assets:
return balance, 0
principal_shares: uint256 = staticcall self.vault.convertToShares(remaining_assets)
if staticcall self.vault.convertToAssets(principal_shares) < remaining_assets:
principal_shares += 1
return principal_shares, balance - principal_shares
@internal
@pure
def _payout_shares(
principal_shares: uint256,
principal_before: uint256,
principal_after: uint256,
) -> uint256:
"""Pay shares while rounding the remaining reserve up."""
if principal_after == 0:
return principal_shares
whole: uint256 = principal_shares // principal_before
remainder: uint256 = principal_shares % principal_before
scaled_remainder: uint256 = remainder * principal_after
reserve: uint256 = whole * principal_after + scaled_remainder // principal_before
if scaled_remainder % principal_before > 0:
reserve += 1
return principal_shares - reserve
@internal
@view
def _preview_principal_claim(time: uint256, max_principal_assets: uint256) -> (uint256, uint256):
remaining_assets: uint256 = self._remaining_principal_assets()
claimable_assets: uint256 = min(self._claimable_principal_assets(time), max_principal_assets)
if claimable_assets == 0:
return 0, 0
principal_shares: uint256 = 0
ignored_yield: uint256 = 0
principal_shares, ignored_yield = self._split_principal_and_yield(remaining_assets)
return (
claimable_assets,
self._payout_shares(
principal_shares,
remaining_assets,
remaining_assets - claimable_assets,
),
)
@external
@view
def claimable_principal_assets() -> uint256:
return self._claimable_principal_assets(min(block.timestamp, self._vesting_end()))
@external
@view
def preview_principal_claim(max_principal_assets: uint256) -> (uint256, uint256):
return self._preview_principal_claim(
min(block.timestamp, self._vesting_end()),
max_principal_assets,
)
@external
@view
def claimable_yield_shares() -> uint256:
ignored_principal: uint256 = 0
yield_shares: uint256 = 0
ignored_principal, yield_shares = self._split_principal_and_yield(self._remaining_principal_assets())
return yield_shares
@external
@view
def permissionless_claims() -> bool:
return not self.claims_closed
@external
@nonreentrant
def claim_principal(
receiver: address,
max_principal_assets: uint256,
) -> uint256:
"""Claim up to a requested amount of currently vested principal."""
recipient: address = self.recipient
assert receiver not in [empty(address), self] # dev: invalid receiver
assert msg.sender == recipient or not self.claims_closed and receiver == recipient # dev: not authorized
claimable_assets: uint256 = 0
shares: uint256 = 0
claimable_assets, shares = self._preview_principal_claim(
min(block.timestamp, self._vesting_end()),
max_principal_assets,
)
assert claimable_assets == 0 or shares > 0 # dev: claim too small
self.claimed_principal_assets += claimable_assets
if shares > 0:
assert extcall self.vault.transfer(receiver, shares, default_return_value=True)
log PrincipalClaim(receiver=receiver, principal_assets=claimable_assets, shares=shares)
return shares
@external
@nonreentrant
def claim_yield() -> uint256:
"""Send current yield shares to the fixed yield recipient."""
yield_recipient: address = self.yield_recipient
assert self.recipient != empty(address) # dev: not initialized
ignored_principal: uint256 = 0
yield_shares: uint256 = 0
ignored_principal, yield_shares = self._split_principal_and_yield(self._remaining_principal_assets())
if yield_shares > 0:
assert extcall self.vault.transfer(yield_recipient, yield_shares, default_return_value=True)
log YieldClaim(recipient=yield_recipient, shares=yield_shares)
return yield_shares
@external
@nonreentrant
def revoke(receiver: address):
"""Stop vesting and return unvested principal shares."""
revoker: address = self.revoker
assert msg.sender == revoker # dev: not revoker
assert receiver not in [empty(address), self] # dev: invalid receiver
assert block.timestamp < self.end_time # dev: vesting complete
remaining_assets: uint256 = self.principal_assets - self.claimed_principal_assets
recipient_assets: uint256 = self._vested_principal_assets(block.timestamp) - self.claimed_principal_assets
principal_shares: uint256 = 0
ignored_yield: uint256 = 0
principal_shares, ignored_yield = self._split_principal_and_yield(remaining_assets)
unvested_shares: uint256 = self._payout_shares(
principal_shares,
remaining_assets,
recipient_assets,
)
unvested_assets: uint256 = remaining_assets - recipient_assets
self.disabled_at = block.timestamp
self.revoker = empty(address)
if unvested_shares > 0:
assert extcall self.vault.transfer(receiver, unvested_shares, default_return_value=True)
log Revoked(
recipient=self.recipient,
revoker=revoker,
receiver=receiver,
unvested_principal_assets=unvested_assets,
shares=unvested_shares,
ts=block.timestamp,
)
@external
def renounce_revocation():
revoker: address = self.revoker
assert msg.sender == revoker # dev: not revoker
self.revoker = empty(address)
log RevocationRenounced(revoker=revoker)
@external
def set_permissionless_claims(enabled: bool):
assert msg.sender == self.recipient # dev: not recipient
self.claims_closed = not enabled
log PermissionlessClaimsSet(enabled=enabled)