Smart Contract
ERC4626SinkConnectors
A.04f5ae6bef48c1fc.ERC4626SinkConnectors
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