Smart Contract
MorphoERC4626SinkConnectors
A.251032a66e9700ef.MorphoERC4626SinkConnectors
1import Burner from 0xf233dcee88fe0abe
2import FungibleToken from 0xf233dcee88fe0abe
3import EVM from 0xe467b9dd11fa00df
4import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
5import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141
6import FlowToken from 0x1654653399040a61
7import DeFiActions from 0x6d888f175c158410
8import DeFiActionsUtils from 0x6d888f175c158410
9import EVMTokenConnectors from 0x1a771b21fcceadc2
10import ERC4626Utils from 0x04f5ae6bef48c1fc
11
12/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
13/// THIS CONTRACT IS IN BETA AND IS NOT FINALIZED - INTERFACES MAY CHANGE AND/OR PENDING CHANGES MAY REQUIRE REDEPLOYMENT
14/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
15///
16/// MorphoERC4626SinkConnectors
17///
18access(all) contract MorphoERC4626SinkConnectors {
19
20 /// AssetSink
21 ///
22 /// Deposits assets to a Morpho ERC4626 vault (which accepts the asset as a deposit denomination) to the contained COA's
23 /// vault share balance
24 ///
25 access(all) struct AssetSink : DeFiActions.Sink {
26 /// The asset type serving as the price basis in the ERC4626 vault
27 access(self) let assetType: Type
28 /// The EVM address of the asset ERC20 contract
29 access(self) let assetEVMAddress: EVM.EVMAddress
30 /// The address of the ERC4626 vault
31 access(self) let vaultEVMAddress: EVM.EVMAddress
32 /// The COA capability to use for the ERC4626 vault
33 access(self) let coa: Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>
34 /// The token sink to use for the ERC4626 vault
35 access(self) let tokenSink: EVMTokenConnectors.Sink
36 /// The optional UniqueIdentifier of the ERC4626 vault
37 access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
38
39 init(
40 vaultEVMAddress: EVM.EVMAddress,
41 coa: Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>,
42 feeSource: {DeFiActions.Sink, DeFiActions.Source},
43 uniqueID: DeFiActions.UniqueIdentifier?
44 ) {
45 pre {
46 coa.check():
47 "Provided COA Capability is invalid - need Capability<&EVM.CadenceOwnedAccount>"
48
49 feeSource.getSourceType() == Type<@FlowToken.Vault>():
50 "Invalid feeSource - given Source must provide FlowToken Vault, but provides \(feeSource.getSourceType().identifier)"
51 }
52 self.vaultEVMAddress = vaultEVMAddress
53
54 self.assetEVMAddress = ERC4626Utils.underlyingAssetEVMAddress(vault: self.vaultEVMAddress)
55 ?? panic("Cannot get an underlying asset EVM address from the vault")
56 self.assetType = FlowEVMBridgeConfig.getTypeAssociated(with: self.assetEVMAddress)
57 ?? panic("Underlying asset for vault \(self.vaultEVMAddress.toString()) (asset \(self.assetEVMAddress.toString())) is not associated with a Cadence FungibleToken - ensure the type & underlying asset contracts are associated via the VM bridge")
58 assert(
59 DeFiActionsUtils.definingContractIsFungibleToken(self.assetType),
60 message: "Derived asset type \(self.assetType.identifier) not FungibleToken type"
61 )
62
63 self.coa = coa
64 self.tokenSink = EVMTokenConnectors.Sink(
65 max: nil,
66 depositVaultType: self.assetType,
67 address: coa.borrow()!.address(),
68 feeSource: feeSource,
69 uniqueID: uniqueID
70 )
71 self.uniqueID = uniqueID
72 }
73
74 /// Returns the Vault type accepted by this Sink
75 access(all) view fun getSinkType(): Type {
76 return self.assetType
77 }
78 /// Returns an estimate of how much can be withdrawn from the depositing Vault for this Sink to reach capacity
79 access(all) fun minimumCapacity(): UFix64 {
80 // Check the EVMTokenConnectors Sink has capacity to bridge the assets to EVM
81 // TODO: Update EVMTokenConnector.Sink to return 0.0 if it doesn't have fees to pay for the bridge call
82 let coa = self.coa.borrow()
83 if coa == nil {
84 return 0.0
85 }
86 let tokenSinkCapacity = self.tokenSink.minimumCapacity()
87 return tokenSinkCapacity
88 }
89 /// Deposits up to the Sink's capacity from the provided Vault
90 access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
91 // check capacity & early return if none
92 let capacity = self.minimumCapacity()
93 if capacity == 0.0 || from.balance == 0.0 { return; }
94
95 // withdraw the appropriate amount from the referenced vault & deposit to the EVMTokenConnectors Sink
96 var amount = capacity <= from.balance ? capacity : from.balance
97
98 // TODO: pass from through and skip the intermediary withdrawal
99 // depositCapacity can deposit less than requested (capacity/fees/bridge constraints), and it doesn’t return "actualDeposited". Without the intermediary vault there's no way to safely compute the amount
100 let deposit <- from.withdraw(amount: amount)
101 self.tokenSink.depositCapacity(from: &deposit as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
102 if deposit.balance == amount {
103 // nothing was deposited to the EVMTokenConnectors Sink
104 Burner.burn(<-deposit)
105 return
106 } else if deposit.balance > 0.0 {
107 // update deposit amount & deposit the residual
108 amount = amount - deposit.balance
109 from.deposit(from: <-deposit)
110 } else {
111 // nothing left - burn & execute vault's burnCallback()
112 Burner.burn(<-deposit)
113 }
114
115 // approve the ERC4626 vault to spend the assets on deposit
116 let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(amount, erc20Address: self.assetEVMAddress)
117 let approveRes = self._call(
118 dry: false,
119 to: self.assetEVMAddress,
120 signature: "approve(address,uint256)",
121 args: [self.vaultEVMAddress, uintAmount],
122 gasLimit: 500_000
123 )!
124 if approveRes.status != EVM.Status.successful {
125 // TODO: consider more graceful handling of this error
126 panic(self._approveErrorMessage(ufixAmount: amount, uintAmount: uintAmount, approveRes: approveRes))
127 }
128
129 // deposit the assets to the ERC4626 vault
130 let depositRes = self._call(
131 dry: false,
132 to: self.vaultEVMAddress,
133 signature: "deposit(uint256,address)",
134 args: [uintAmount, self.coa.borrow()!.address()],
135 gasLimit: 1_000_000
136 )!
137 if depositRes.status != EVM.Status.successful {
138 panic(self._depositErrorMessage(ufixAmount: amount, uintAmount: uintAmount, depositRes: depositRes))
139 }
140 }
141 /// Returns a ComponentInfo struct containing information about this component and a list of ComponentInfo for
142 /// each inner component in the stack.
143 access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
144 return DeFiActions.ComponentInfo(
145 type: self.getType(),
146 id: self.id(),
147 innerComponents: [
148 self.tokenSink.getComponentInfo()
149 ]
150 )
151 }
152 /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
153 /// a DeFiActions stack. See DeFiActions.align() for more information.
154 ///
155 /// @return a copy of the struct's UniqueIdentifier
156 ///
157 access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
158 return self.uniqueID
159 }
160 /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
161 /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
162 ///
163 /// @param id: the UniqueIdentifier to set for this component
164 ///
165 access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
166 self.uniqueID = id
167 }
168 /// Performs a dry call to the ERC4626 vault
169 ///
170 /// @param to The address of the ERC4626 vault
171 /// @param signature The signature of the function to call
172 /// @param args The arguments to pass to the function
173 /// @param gasLimit The gas limit to use for the call
174 ///
175 /// @return The result of the dry call or `nil` if the COA capability is invalid
176 access(self)
177 fun _call(dry: Bool, to: EVM.EVMAddress, signature: String, args: [AnyStruct], gasLimit: UInt64): EVM.Result? {
178 let calldata = EVM.encodeABIWithSignature(signature, args)
179 let valueBalance = EVM.Balance(attoflow: 0)
180 if let coa = self.coa.borrow() {
181 return dry
182 ? coa.dryCall(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
183 : coa.call(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
184 }
185 return nil
186 }
187 /// Returns an error message for a failed approve call
188 ///
189 /// @param ufixAmount: the amount of assets to approve
190 /// @param uintAmount: the amount of assets to approve in uint256 format
191 /// @param approveRes: the result of the approve call
192 ///
193 /// @return an error message for a failed approve call
194 ///
195 access(self)
196 fun _approveErrorMessage(ufixAmount: UFix64, uintAmount: UInt256, approveRes: EVM.Result): String {
197 return "Failed to approve ERC4626 vault \(self.vaultEVMAddress.toString()) to spend \(ufixAmount) assets \(self.assetEVMAddress.toString()). "
198 .concat("approvee: \(self.vaultEVMAddress.toString()), amount: \(uintAmount). ")
199 .concat("Error code: \(approveRes.errorCode) Error message: \(approveRes.errorMessage)")
200 }
201 /// Returns an error message for a failed deposit call
202 ///
203 /// @param ufixAmount: the amount of assets to deposit
204 /// @param uintAmount: the amount of assets to deposit in uint256 format
205 /// @param depositRes: the result of the deposit call
206 ///
207 /// @return an error message for a failed deposit call
208 ///
209 access(self)
210 fun _depositErrorMessage(ufixAmount: UFix64, uintAmount: UInt256, depositRes: EVM.Result): String {
211 let coaHex = self.coa.borrow()!.address().toString()
212 return "Failed to deposit \(ufixAmount) assets \(self.assetEVMAddress.toString()) to ERC4626 vault \(self.vaultEVMAddress.toString()). "
213 .concat("amount: \(uintAmount), to: \(coaHex). ")
214 .concat("Error code: \(depositRes.errorCode) Error message: \(depositRes.errorMessage)")
215 }
216 }
217 /// ShareSink
218 ///
219 /// Redeems shares from a Morpho ERC4626 vault to the contained COA's underlying asset balance
220 ///
221 access(all) struct ShareSink : DeFiActions.Sink {
222 /// The share vault type serving as the price basis in the ERC4626 vault
223 access(self) let vaultType: Type
224 /// The EVM address of the asset ERC20 contract
225 access(self) let assetEVMAddress: EVM.EVMAddress
226 /// The address of the ERC4626 vault
227 access(self) let vaultEVMAddress: EVM.EVMAddress
228 /// The COA capability to use for the ERC4626 vault
229 access(self) let coa: Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>
230 /// The token sink to use for the vault shares
231 access(self) let shareSink: EVMTokenConnectors.Sink
232 /// The optional UniqueIdentifier of the ERC4626 vault
233 access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
234
235 init(
236 vaultEVMAddress: EVM.EVMAddress,
237 coa: Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>,
238 feeSource: {DeFiActions.Sink, DeFiActions.Source},
239 uniqueID: DeFiActions.UniqueIdentifier?
240 ) {
241 pre {
242 coa.check():
243 "Provided COA Capability is invalid - need Capability<&EVM.CadenceOwnedAccount>"
244
245 feeSource.getSourceType() == Type<@FlowToken.Vault>():
246 "Invalid feeSource - given Source must provide FlowToken Vault, but provides \(feeSource.getSourceType().identifier)"
247 }
248 self.vaultEVMAddress = vaultEVMAddress
249 self.vaultType = FlowEVMBridgeConfig.getTypeAssociated(with: vaultEVMAddress)
250 ?? panic("Provided ERC4626 Vault \(vaultEVMAddress.toString()) is not associated with a Cadence FungibleToken - ensure the type & ERC4626 contracts are associated via the VM bridge")
251 assert(
252 DeFiActionsUtils.definingContractIsFungibleToken(self.vaultType),
253 message: "Derived vault type \(self.vaultType.identifier) not FungibleToken type"
254 )
255
256 self.assetEVMAddress = ERC4626Utils.underlyingAssetEVMAddress(vault: vaultEVMAddress)
257 ?? panic("Cannot get an underlying asset EVM address from the vault")
258 let assetType = FlowEVMBridgeConfig.getTypeAssociated(with: self.assetEVMAddress)
259 ?? panic("Underlying asset for vault \(self.vaultEVMAddress.toString()) (asset \(self.assetEVMAddress.toString())) is not associated with a Cadence FungibleToken - ensure the type & underlying asset contracts are associated via the VM bridge")
260 assert(
261 DeFiActionsUtils.definingContractIsFungibleToken(assetType),
262 message: "Derived asset type \(assetType.identifier) not FungibleToken type"
263 )
264
265 self.coa = coa
266 self.shareSink = EVMTokenConnectors.Sink(
267 max: nil,
268 depositVaultType: self.vaultType,
269 address: coa.borrow()!.address(),
270 feeSource: feeSource,
271 uniqueID: uniqueID
272 )
273 self.uniqueID = uniqueID
274 }
275
276 /// Returns the Vault type accepted by this Sink
277 access(all) view fun getSinkType(): Type {
278 return self.vaultType
279 }
280 /// Returns an estimate of how much can be withdrawn from the depositing Vault for this Sink to reach capacity
281 access(all) fun minimumCapacity(): UFix64 {
282 // Check the EVMTokenConnectors Sink has capacity to bridge the shares to EVM
283 // TODO: Update EVMTokenConnector.Sink to return 0.0 if it doesn't have fees to pay for the bridge call
284 let coa = self.coa.borrow()
285 if coa == nil {
286 return 0.0
287 }
288 let shareSinkCapacity = self.shareSink.minimumCapacity()
289 return shareSinkCapacity
290 }
291 /// Deposits up to the Sink's capacity from the provided Vault
292 access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
293 // check capacity & early return if none
294 let capacity = self.minimumCapacity()
295 if capacity == 0.0 || from.balance == 0.0 { return; }
296
297 // withdraw the appropriate amount from the referenced vault & deposit to the EVMTokenConnectors Sink
298 var amount = capacity <= from.balance ? capacity : from.balance
299
300 // TODO: pass from through and skip the intermediary withdrawal
301 let deposit <- from.withdraw(amount: amount)
302 self.shareSink.depositCapacity(from: &deposit as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
303 if deposit.balance == amount {
304 // nothing was deposited to the EVMTokenConnectors Sink
305 Burner.burn(<-deposit)
306 return
307 } else if deposit.balance > 0.0 {
308 // update deposit amount & deposit the residual
309 amount = amount - deposit.balance
310 from.deposit(from: <-deposit)
311 } else {
312 // nothing left - burn & execute vault's burnCallback()
313 Burner.burn(<-deposit)
314 }
315
316 let uintShares = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(amount, erc20Address: self.vaultEVMAddress)
317
318 let coa = self.coa.borrow() ?? panic("can't borrow COA")
319
320 // redeem the shares from the ERC4626 vault
321 let redeemRes = self._call(
322 dry: false,
323 to: self.vaultEVMAddress,
324 signature: "redeem(uint256,address,address)",
325 args: [uintShares, coa.address(), coa.address()],
326 gasLimit: 1_000_000
327 )!
328 if redeemRes.status != EVM.Status.successful {
329 // TODO: Consider unwinding the redeem & returning to the from vault
330 // - would require {Sink, Source} instead of just Sink
331 panic(self._redeemErrorMessage(ufixShares: amount, uintShares: uintShares, redeemRes: redeemRes))
332 }
333 }
334 /// Returns a ComponentInfo struct containing information about this component and a list of ComponentInfo for
335 /// each inner component in the stack.
336 access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
337 return DeFiActions.ComponentInfo(
338 type: self.getType(),
339 id: self.id(),
340 innerComponents: [
341 self.shareSink.getComponentInfo()
342 ]
343 )
344 }
345 /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
346 /// a DeFiActions stack. See DeFiActions.align() for more information.
347 ///
348 /// @return a copy of the struct's UniqueIdentifier
349 ///
350 access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
351 return self.uniqueID
352 }
353 /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
354 /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
355 ///
356 /// @param id: the UniqueIdentifier to set for this component
357 ///
358 access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
359 self.uniqueID = id
360 }
361 /// Performs a dry call to the ERC4626 vault
362 ///
363 /// @param to The address of the ERC4626 vault
364 /// @param signature The signature of the function to call
365 /// @param args The arguments to pass to the function
366 /// @param gasLimit The gas limit to use for the call
367 ///
368 /// @return The result of the dry call or `nil` if the COA capability is invalid
369 access(self)
370 fun _call(dry: Bool, to: EVM.EVMAddress, signature: String, args: [AnyStruct], gasLimit: UInt64): EVM.Result? {
371 let calldata = EVM.encodeABIWithSignature(signature, args)
372 let valueBalance = EVM.Balance(attoflow: 0)
373 if let coa = self.coa.borrow() {
374 return dry
375 ? coa.dryCall(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
376 : coa.call(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
377 }
378 return nil
379 }
380 /// Returns an error message for a failed redeem call
381 ///
382 /// @param ufixShares: the amount of shares to redeem
383 /// @param uintShares: the amount of shares to redeem in uint256 format
384 /// @param depositRes: the result of the redeem call
385 ///
386 /// @return an error message for a failed redeem call
387 ///
388 access(self)
389 fun _redeemErrorMessage(ufixShares: UFix64, uintShares: UInt256, redeemRes: EVM.Result): String {
390 let coaHex = self.coa.borrow()!.address().toString()
391 return "Failed to redeem \(ufixShares) shares \(self.vaultEVMAddress.toString()) from ERC4626 vault for \(self.assetEVMAddress.toString()). "
392 .concat("amount: \(uintShares), to: \(coaHex). ")
393 .concat("Error code: \(redeemRes.errorCode) Error message: \(redeemRes.errorMessage)")
394 }
395 }
396}
397