ERC4626 Tokenized VaultΒΆ
Warning
This is example code for learning purposes. Do not use in production without thorough review and testing.
ERC4626 standardizes yield-bearing vaults. Users deposit assets and receive shares representing their portion of the vault.
1#pragma version >0.3.10
2
3# NOTE: Copied from https://github.com/fubuloubu/ERC4626/blob/1a10b051928b11eeaad15d80397ed36603c2a49b/contracts/VyperVault.vy
4
5# example implementation of an ERC4626 vault
6
7###########################################################################
8## THIS IS EXAMPLE CODE, NOT MEANT TO BE USED IN PRODUCTION! CAVEAT EMPTOR!
9###########################################################################
10
11from ethereum.ercs import IERC20
12from ethereum.ercs import IERC4626
13
14implements: IERC20
15implements: IERC4626
16
17##### ERC20 #####
18
19totalSupply: public(uint256)
20balanceOf: public(HashMap[address, uint256])
21allowance: public(HashMap[address, HashMap[address, uint256]])
22
23NAME: constant(String[10]) = "Test Vault"
24SYMBOL: constant(String[5]) = "vTEST"
25DECIMALS: constant(uint8) = 18
26
27##### ERC4626 #####
28
29asset: public(IERC20)
30
31
32@deploy
33def __init__(asset: IERC20):
34 self.asset = asset
35
36
37@view
38@external
39def name() -> String[10]:
40 return NAME
41
42
43@view
44@external
45def symbol() -> String[5]:
46 return SYMBOL
47
48
49@view
50@external
51def decimals() -> uint8:
52 return DECIMALS
53
54
55@external
56def transfer(receiver: address, amount: uint256) -> bool:
57 self.balanceOf[msg.sender] -= amount
58 self.balanceOf[receiver] += amount
59 log IERC20.Transfer(sender=msg.sender, receiver=receiver, value=amount)
60 return True
61
62
63@external
64def approve(spender: address, amount: uint256) -> bool:
65 self.allowance[msg.sender][spender] = amount
66 log IERC20.Approval(owner=msg.sender, spender=spender, value=amount)
67 return True
68
69
70@external
71def transferFrom(sender: address, receiver: address, amount: uint256) -> bool:
72 self.allowance[sender][msg.sender] -= amount
73 self.balanceOf[sender] -= amount
74 self.balanceOf[receiver] += amount
75 log IERC20.Transfer(sender=sender, receiver=receiver, value=amount)
76 return True
77
78
79@view
80@external
81def totalAssets() -> uint256:
82 return staticcall self.asset.balanceOf(self)
83
84
85@view
86@internal
87def _convertToAssets(shareAmount: uint256) -> uint256:
88 totalSupply: uint256 = self.totalSupply
89 if totalSupply == 0:
90 return 0
91
92 # NOTE: `shareAmount = 0` is extremely rare case, not optimizing for it
93 # NOTE: `totalAssets = 0` is extremely rare case, not optimizing for it
94 return shareAmount * staticcall self.asset.balanceOf(self) // totalSupply
95
96
97@view
98@external
99def convertToAssets(shareAmount: uint256) -> uint256:
100 return self._convertToAssets(shareAmount)
101
102
103@view
104@internal
105def _convertToShares(assetAmount: uint256) -> uint256:
106 totalSupply: uint256 = self.totalSupply
107 totalAssets: uint256 = staticcall self.asset.balanceOf(self)
108 if totalAssets == 0 or totalSupply == 0:
109 return assetAmount # 1:1 price
110
111 # NOTE: `assetAmount = 0` is extremely rare case, not optimizing for it
112 return assetAmount * totalSupply // totalAssets
113
114
115@view
116@external
117def convertToShares(assetAmount: uint256) -> uint256:
118 return self._convertToShares(assetAmount)
119
120
121@view
122@external
123def maxDeposit(owner: address) -> uint256:
124 return max_value(uint256)
125
126
127@view
128@external
129def previewDeposit(assets: uint256) -> uint256:
130 return self._convertToShares(assets)
131
132
133@external
134def deposit(assets: uint256, receiver: address=msg.sender) -> uint256:
135 shares: uint256 = self._convertToShares(assets)
136 extcall self.asset.transferFrom(msg.sender, self, assets)
137
138 self.totalSupply += shares
139 self.balanceOf[receiver] += shares
140 log IERC4626.Deposit(sender=msg.sender, owner=receiver, assets=assets, shares=shares)
141 return shares
142
143
144@view
145@external
146def maxMint(owner: address) -> uint256:
147 return max_value(uint256)
148
149
150@view
151@external
152def previewMint(shares: uint256) -> uint256:
153 assets: uint256 = self._convertToAssets(shares)
154
155 # NOTE: Vyper does lazy eval on `and`, so this avoids SLOADs most of the time
156 if assets == 0 and staticcall self.asset.balanceOf(self) == 0:
157 return shares # NOTE: Assume 1:1 price if nothing deposited yet
158
159 return assets
160
161
162@external
163def mint(shares: uint256, receiver: address=msg.sender) -> uint256:
164 assets: uint256 = self._convertToAssets(shares)
165
166 if assets == 0 and staticcall self.asset.balanceOf(self) == 0:
167 assets = shares # NOTE: Assume 1:1 price if nothing deposited yet
168
169 extcall self.asset.transferFrom(msg.sender, self, assets)
170
171 self.totalSupply += shares
172 self.balanceOf[receiver] += shares
173 log IERC4626.Deposit(sender=msg.sender, owner=receiver, assets=assets, shares=shares)
174 return assets
175
176
177@view
178@external
179def maxWithdraw(owner: address) -> uint256:
180 return max_value(uint256) # real max is `self.asset.balanceOf(self)`
181
182
183@view
184@external
185def previewWithdraw(assets: uint256) -> uint256:
186 shares: uint256 = self._convertToShares(assets)
187
188 # NOTE: Vyper does lazy eval on and, so this avoids SLOADs most of the time
189 if shares == assets and self.totalSupply == 0:
190 return 0 # NOTE: Nothing to redeem
191
192 return shares
193
194
195@external
196def withdraw(assets: uint256, receiver: address=msg.sender, owner: address=msg.sender) -> uint256:
197 shares: uint256 = self._convertToShares(assets)
198
199 # NOTE: Vyper does lazy eval on `and`, so this avoids SLOADs most of the time
200 if shares == assets and self.totalSupply == 0:
201 raise # Nothing to redeem
202
203 if owner != msg.sender:
204 self.allowance[owner][msg.sender] -= shares
205
206 self.totalSupply -= shares
207 self.balanceOf[owner] -= shares
208
209 extcall self.asset.transfer(receiver, assets)
210 log IERC4626.Withdraw(sender=msg.sender, receiver=receiver, owner=owner, assets=assets, shares=shares)
211 return shares
212
213
214@view
215@external
216def maxRedeem(owner: address) -> uint256:
217 return max_value(uint256) # real max is `self.totalSupply`
218
219
220@view
221@external
222def previewRedeem(shares: uint256) -> uint256:
223 return self._convertToAssets(shares)
224
225
226@external
227def redeem(shares: uint256, receiver: address=msg.sender, owner: address=msg.sender) -> uint256:
228 if owner != msg.sender:
229 self.allowance[owner][msg.sender] -= shares
230
231 assets: uint256 = self._convertToAssets(shares)
232 self.totalSupply -= shares
233 self.balanceOf[owner] -= shares
234
235 extcall self.asset.transfer(receiver, assets)
236 log IERC4626.Withdraw(sender=msg.sender, receiver=receiver, owner=owner, assets=assets, shares=shares)
237 return assets
238
239
240@external
241def DEBUG_steal_tokens(amount: uint256):
242 # NOTE: This is the primary method of mocking share price changes
243 # do not put in production code!!!
244 extcall self.asset.transfer(msg.sender, amount)
The vault implements:
deposit/withdraw: Exchange assets for sharesmint/redeem: Exchange shares for assetsShare price calculation based on
totalAssets / totalSupply
Note
The DEBUG_steal_tokens function is for testing share price changes.
Do not include in production code.