Smart Contract

MorphoERC4626SinkConnectors

A.251032a66e9700ef.MorphoERC4626SinkConnectors

Valid From

142,038,823

Deployed

2w ago
Feb 13, 2026, 02:42:35 AM UTC

Dependents

0 imports
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