Smart Contract

ERC4626SinkConnectors

A.04f5ae6bef48c1fc.ERC4626SinkConnectors

Valid From

142,103,887

Deployed

2w ago
Feb 13, 2026, 05:25:40 PM 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 DeFiActions from 0x6d888f175c158410
7import EVMTokenConnectors from 0x1a771b21fcceadc2
8import ERC4626Utils from 0x04f5ae6bef48c1fc
9
10/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
11/// THIS CONTRACT IS IN BETA AND IS NOT FINALIZED - INTERFACES MAY CHANGE AND/OR PENDING CHANGES MAY REQUIRE REDEPLOYMENT
12/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
13///
14/// ERC4626SinkConnectors
15///
16access(all) contract ERC4626SinkConnectors {
17
18    /// AssetSink
19    ///
20    /// Deposits assets to an ERC4626 vault (which accepts the asset as a deposit denomination) to the contained COA's
21    /// vault share balance
22    ///
23    access(all) struct AssetSink : DeFiActions.Sink {
24        /// The asset type serving as the price basis in the ERC4626 vault
25        access(self) let asset: Type
26        /// The EVM address of the asset ERC20 contract
27        access(self) let assetEVMAddress: EVM.EVMAddress
28        /// The address of the ERC4626 vault
29        access(self) let vault: EVM.EVMAddress
30        /// The COA capability to use for the ERC4626 vault
31        access(self) let coa: Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>
32        /// The token sink to use for the ERC4626 vault
33        access(self) let tokenSink: EVMTokenConnectors.Sink
34        /// The optional UniqueIdentifier of the ERC4626 vault
35        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
36
37        init(
38            asset: Type,
39            vault: EVM.EVMAddress,
40            coa: Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>,
41            feeSource: {DeFiActions.Sink, DeFiActions.Source},
42            uniqueID: DeFiActions.UniqueIdentifier?
43        ) {
44            pre {
45                asset.isSubtype(of: Type<@{FungibleToken.Vault}>()):
46                "Provided asset \(asset.identifier) is not a Vault type"
47                coa.check():
48                "Provided COA Capability is invalid - need Capability<&EVM.CadenceOwnedAccount>"
49            }
50            self.asset = asset
51            self.assetEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: asset)
52                ?? panic("Provided asset \(asset.identifier) is not associated with ERC20 - ensure the type & ERC20 contracts are associated via the VM bridge")
53            self.vault = vault
54            self.coa = coa
55            self.tokenSink = EVMTokenConnectors.Sink(
56                max: nil,
57                depositVaultType: asset,
58                address: coa.borrow()!.address(),
59                feeSource: feeSource,
60                uniqueID: uniqueID
61            )
62            self.uniqueID = uniqueID
63        }
64
65        /// Returns the Vault type accepted by this Sink
66        access(all) view fun getSinkType(): Type {
67            return self.asset
68        }
69        /// Returns an estimate of how much can be withdrawn from the depositing Vault for this Sink to reach capacity
70        access(all) fun minimumCapacity(): UFix64 {
71            // Check the EVMTokenConnectors Sink has capacity to bridge the assets to EVM
72            // TODO: Update EVMTokenConnector.Sink to return 0.0 if it doesn't have fees to pay for the bridge call
73            let coa = self.coa.borrow()
74            if coa == nil {
75                return 0.0
76            }
77            let tokenSinkCapacity = self.tokenSink.minimumCapacity()
78            if tokenSinkCapacity == 0.0 {
79                return 0.0
80            }
81            // Check the ERC4626 vault has capacity to deposit the assets
82            let max = ERC4626Utils.maxDeposit(vault: self.vault, receiver: coa!.address())
83            let vaultCapacity = max != nil
84                ? FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(max!, erc20Address: self.assetEVMAddress)
85                : 0.0
86            if vaultCapacity == 0.0 {
87                return 0.0
88            }
89            return tokenSinkCapacity <= vaultCapacity ? tokenSinkCapacity : vaultCapacity
90        }
91        /// Deposits up to the Sink's capacity from the provided Vault
92        access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
93            // check capacity & early return if none
94            let capacity = self.minimumCapacity()
95            if capacity == 0.0 || from.balance == 0.0 { return; }
96
97            // withdraw the appropriate amount from the referenced vault & deposit to the EVMTokenConnectors Sink
98            var amount = capacity <= from.balance ? capacity : from.balance
99
100            // TODO: pass from through and skip the intermediary withdrawal
101            let deposit <- from.withdraw(amount: amount)
102            self.tokenSink.depositCapacity(from: &deposit as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
103            if deposit.balance == amount {
104                // nothing was deposited to the EVMTokenConnectors Sink
105                Burner.burn(<-deposit)
106                return
107            } else if deposit.balance > 0.0 {
108                // update deposit amount & deposit the residual
109                amount = amount - deposit.balance
110                from.deposit(from: <-deposit)
111            } else {
112                // nothing left - burn & execute vault's burnCallback()
113                Burner.burn(<-deposit)
114            }
115
116            // approve the ERC4626 vault to spend the assets on deposit
117            let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(amount, erc20Address: self.assetEVMAddress)
118            let approveRes = self._call(
119                    dry: false,
120                    to: self.assetEVMAddress,
121                    signature: "approve(address,uint256)",
122                    args: [self.vault, uintAmount],
123                    gasLimit: 500_000
124                )!
125            if approveRes.status != EVM.Status.successful {
126                // TODO: consider more graceful handling of this error
127                panic(self._approveErrorMessage(ufixAmount: amount, uintAmount: uintAmount, approveRes: approveRes))
128            }
129
130            // deposit the assets to the ERC4626 vault
131            let depositRes = self._call(
132                dry: false,
133                to: self.vault,
134                signature: "deposit(uint256,address)",
135                args: [uintAmount, self.coa.borrow()!.address()],
136                gasLimit: 1_000_000
137            )!
138            if depositRes.status != EVM.Status.successful {
139                // TODO: Consider unwinding the deposit & returning to the from vault
140                //      - would require {Sink, Source} instead of just Sink
141                panic(self._depositErrorMessage(ufixAmount: amount, uintAmount: uintAmount, depositRes: depositRes))
142            }
143        }
144        /// Returns a ComponentInfo struct containing information about this component and a list of ComponentInfo for
145        /// each inner component in the stack.
146        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
147            return DeFiActions.ComponentInfo(
148                type: self.getType(),
149                id: self.id(),
150                innerComponents: [
151                    self.tokenSink.getComponentInfo()
152                ]
153            )
154        }
155        /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
156        /// a DeFiActions stack. See DeFiActions.align() for more information.
157        ///
158        /// @return a copy of the struct's UniqueIdentifier
159        ///
160        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
161            return self.uniqueID
162        }
163        /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
164        /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
165        ///
166        /// @param id: the UniqueIdentifier to set for this component
167        ///
168        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
169            self.uniqueID = id
170        }
171        /// Performs a dry call to the ERC4626 vault
172        ///
173        /// @param to The address of the ERC4626 vault
174        /// @param signature The signature of the function to call
175        /// @param args The arguments to pass to the function
176        /// @param gasLimit The gas limit to use for the call
177        ///
178        /// @return The result of the dry call or `nil` if the COA capability is invalid
179        access(self)
180        fun _call(dry: Bool, to: EVM.EVMAddress, signature: String, args: [AnyStruct], gasLimit: UInt64): EVM.Result? {
181            let calldata = EVM.encodeABIWithSignature(signature, args)
182            let valueBalance = EVM.Balance(attoflow: 0)
183            if let coa = self.coa.borrow() {
184                return dry
185                    ? coa.dryCall(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
186                    : coa.call(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
187            }
188            return nil
189        }
190        /// Returns an error message for a failed approve call
191        ///
192        /// @param ufixAmount: the amount of assets to approve
193        /// @param uintAmount: the amount of assets to approve in uint256 format
194        /// @param approveRes: the result of the approve call
195        ///
196        /// @return an error message for a failed approve call
197        ///
198        access(self)
199        fun _approveErrorMessage(ufixAmount: UFix64, uintAmount: UInt256, approveRes: EVM.Result): String {
200            return "Failed to approve ERC4626 vault \(self.vault.toString()) to spend \(ufixAmount) assets \(self.assetEVMAddress.toString()). "
201                .concat("approvee: \(self.vault.toString()), amount: \(uintAmount). ")
202                .concat("Error code: \(approveRes.errorCode) Error message: \(approveRes.errorMessage)")
203        }
204        /// Returns an error message for a failed deposit call
205        ///
206        /// @param ufixAmount: the amount of assets to deposit
207        /// @param uintAmount: the amount of assets to deposit in uint256 format
208        /// @param depositRes: the result of the deposit call
209        ///
210        /// @return an error message for a failed deposit call
211        ///
212        access(self)
213        fun _depositErrorMessage(ufixAmount: UFix64, uintAmount: UInt256, depositRes: EVM.Result): String {
214            let coaHex = self.coa.borrow()!.address().toString()
215            return "Failed to deposit \(ufixAmount) assets \(self.assetEVMAddress.toString()) to ERC4626 vault \(self.vault.toString()). "
216                .concat("amount: \(uintAmount), to: \(coaHex). ")
217                .concat("Error code: \(depositRes.errorCode) Error message: \(depositRes.errorMessage)")
218        }
219    }
220}
221