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 shares

  • mint / redeem: Exchange shares for assets

  • Share price calculation based on totalAssets / totalSupply

Note

The DEBUG_steal_tokens function is for testing share price changes. Do not include in production code.