Smart Contract
IncrementFiStakingConnectors
A.efa9bd7d1b17f1ed.IncrementFiStakingConnectors
1import DeFiActions from 0x92195d814edf9cb0
2import DeFiActionsUtils from 0x92195d814edf9cb0
3import FungibleToken from 0xf233dcee88fe0abe
4import Staking from 0x1b77ba4b414de352
5import SwapConfig from 0xb78ef7afa52ff906
6import SwapInterfaces from 0xb78ef7afa52ff906
7
8/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
9/// THIS CONTRACT IS IN BETA AND IS NOT FINALIZED - INTERFACES MAY CHANGE AND/OR PENDING CHANGES MAY REQUIRE REDEPLOYMENT
10/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
11///
12/// IncrementFiStakingConnectors
13///
14/// DeFiActions adapter implementations for IncrementFi staking protocols. This contract provides connectors that
15/// integrate with IncrementFi's staking pools, allowing users to stake tokens and claim rewards through the
16/// DeFiActions framework.
17///
18/// The contract contains two main connector types:
19/// - PoolSink: Allows depositing tokens into IncrementFi staking pools
20/// - PoolRewardsSource: Allows claiming rewards from IncrementFi staking pools
21///
22access(all) contract IncrementFiStakingConnectors {
23 /// PoolSink
24 ///
25 /// A DeFiActions.Sink implementation that allows depositing tokens into IncrementFi staking pools.
26 /// This connector accepts tokens of a specific type and stakes them in the designated staking pool.
27 ///
28 access(all) struct PoolSink: DeFiActions.Sink {
29 /// The type of Vault this Sink accepts when performing a deposit
30 access(all) let vaultType: Type
31 /// Address of the user staking in the pool
32 access(self) let staker: Address
33 /// The unique identifier of the staking pool to deposit into
34 access(self) let pid: UInt64
35 /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
36 /// specific Identifier to associated connectors on construction
37 access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
38
39 /// Initializes a new PoolSink
40 ///
41 /// @param pid: The unique identifier of the staking pool to deposit into
42 /// @param staker: Address of the user staking in the pool
43 /// @param uniqueID: Optional identifier for associating connectors in a stack
44 ///
45 init(
46 pid: UInt64,
47 staker: Address,
48 uniqueID: DeFiActions.UniqueIdentifier?
49 ) {
50 let pool = IncrementFiStakingConnectors.borrowPool(pid: pid)
51 ?? panic("Pool with ID \(pid) not found or not accessible")
52
53 self.vaultType = IncrementFiStakingConnectors.tokenTypeIdentifierToVaultType(pool.getPoolInfo().acceptTokenKey)
54 self.staker = staker
55 self.pid = pid
56 self.uniqueID = uniqueID
57 }
58
59 /// Returns a list of ComponentInfo for each component in the stack
60 ///
61 /// @return a list of ComponentInfo for each inner DeFiActions component in the PoolSink
62 ///
63 access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
64 return DeFiActions.ComponentInfo(
65 type: self.getType(),
66 id: self.id() ?? nil,
67 innerComponents: []
68 )
69 }
70 /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
71 /// a DeFiActions stack. See DeFiActions.align() for more information.
72 ///
73 /// @return a copy of the struct's UniqueIdentifier
74 ///
75 access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
76 return self.uniqueID
77 }
78
79 /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
80 /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
81 ///
82 /// @param id: the UniqueIdentifier to set for this component
83 ///
84 access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
85 self.uniqueID = id
86 }
87
88 /// Returns the Vault type accepted by this Sink
89 ///
90 /// @return the type of Vault this Sink accepts when performing a deposit
91 ///
92 access(all) view fun getSinkType(): Type {
93 return self.vaultType
94 }
95
96 /// Returns an estimate of how much of the associated Vault can be accepted by this Sink
97 ///
98 /// @return the minimum capacity available for deposits to this Sink
99 ///
100 access(all) fun minimumCapacity(): UFix64 {
101 if let pool = IncrementFiStakingConnectors.borrowPool(pid: self.pid) {
102 // Get the staking amount for the user in the pool
103 let stakingAmount = pool.getUserInfo(address: self.staker)?.stakingAmount ?? 0.0
104 return pool.getPoolInfo().limitAmount - stakingAmount
105 }
106
107 return 0.0 // no capacity if the staking pool is not available
108 }
109
110 /// Deposits up to the Sink's capacity from the provided Vault
111 ///
112 /// @param from: The vault to withdraw tokens from for staking
113 ///
114 access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
115 let minimumCapacity = self.minimumCapacity()
116 if minimumCapacity == 0.0 {
117 return
118 }
119
120 if let pool: &{Staking.PoolPublic} = IncrementFiStakingConnectors.borrowPool(pid: self.pid) {
121 let depositAmount = from.balance < minimumCapacity
122 ? from.balance
123 : minimumCapacity
124
125 pool.stake(staker: self.staker, stakingToken: <- from.withdraw(amount: depositAmount))
126 }
127 }
128 }
129
130 /// PoolRewardsSource
131 ///
132 /// A DeFiActions.Source implementation that allows claiming rewards from IncrementFi staking pools.
133 /// This connector provides tokens by claiming rewards from the designated staking pool.
134 ///
135 /// NOTE: This connector assumes that the pool has only one reward token type. If the pool has multiple reward
136 /// token types, the connector will panic.
137 ///
138 access(all) struct PoolRewardsSource: DeFiActions.Source {
139 /// The type of Vault this Source provides when claiming rewards
140 access(all) let vaultType: Type
141 /// The unique identifier of the staking pool to claim rewards from
142 access(self) let pid: UInt64
143 /// Capability to access the user's staking certificate
144 access(self) let userCertificate: Capability<&Staking.UserCertificate>
145 /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
146 /// specific Identifier to associated connectors on construction
147 access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
148
149 /// Initializes a new PoolRewardsSource
150 ///
151 /// @param userCertificate: Capability to access the user's staking certificate
152 /// @param pid: The unique identifier of the staking pool to claim rewards from
153 /// @param vaultType: The type of Vault this Source provides when claiming rewards
154 /// @param uniqueID: Optional identifier for associating connectors in a stack
155 ///
156 init(
157 userCertificate: Capability<&Staking.UserCertificate>,
158 pid: UInt64,
159 uniqueID: DeFiActions.UniqueIdentifier?,
160 ) {
161 let pool = IncrementFiStakingConnectors.borrowPool(pid: pid)
162 ?? panic("Pool with ID \(pid) not found")
163 let rewardsInfo = pool!.getPoolInfo().rewardsInfo
164
165 assert(rewardsInfo.keys.length == 1, message: "Pool with ID \(pid) has multiple reward token types, only one is supported")
166 let rewardTokenType = rewardsInfo.keys[0]
167
168 self.pid = pid
169 self.userCertificate = userCertificate
170 self.vaultType = IncrementFiStakingConnectors.tokenTypeIdentifierToVaultType(rewardTokenType)
171 self.uniqueID = uniqueID
172 }
173
174 /// Returns a list of ComponentInfo for each component in the stack
175 ///
176 /// @return a list of ComponentInfo for each inner DeFiActions component in the PoolRewardsSource
177 ///
178 access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
179 return DeFiActions.ComponentInfo(
180 type: self.getType(),
181 id: self.id() ?? nil,
182 innerComponents: []
183 )
184 }
185
186 /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
187 /// a DeFiActions stack. See DeFiActions.align() for more information.
188 ///
189 /// @return a copy of the struct's UniqueIdentifier
190 ///
191 access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
192 return self.uniqueID
193 }
194
195 /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
196 /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
197 ///
198 /// @param id: the UniqueIdentifier to set for this component
199 ///
200 access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
201 self.uniqueID = id
202 }
203
204 /// Returns the Vault type provided by this Source
205 ///
206 /// @return the type of Vault this Source provides when claiming rewards
207 ///
208 access(all) view fun getSourceType(): Type {
209 return self.vaultType
210 }
211
212 /// Returns an estimate of how much of the associated rewards can be claimed from this Source
213 ///
214 /// @return the minimum amount of rewards available for claiming from this Source
215 ///
216 access(all) fun minimumAvailable(): UFix64 {
217 if let address = self.userCertificate.borrow()?.owner?.address {
218 if let pool = IncrementFiStakingConnectors.borrowPool(pid: self.pid) {
219 // Stake an empty vault on behalf of the user to update the pool
220 // The Staking contract does not expose any way to update the unclaimed rewards
221 // field, so staking an empty vault is a workaround to update the unclaimed rewards
222 let emptyVault <- DeFiActionsUtils.getEmptyVault(IncrementFiStakingConnectors.tokenTypeIdentifierToVaultType(pool.getPoolInfo().acceptTokenKey))
223 pool.stake(staker: address, stakingToken: <- emptyVault)
224 if let unclaimedRewards = pool.getUserInfo(address: address)?.unclaimedRewards {
225 // Return the unclaimed rewards for the specific vault type
226 return unclaimedRewards[SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: self.vaultType.identifier)] ?? 0.0
227 }
228 }
229 }
230
231 return 0.0 // no capacity if the staking pool is not available
232 }
233
234 /// Withdraws rewards from the staking pool up to the specified maximum amount
235 /// Overflow rewards are sent to the appropriate overflow sinks if provided
236 ///
237 /// @param maxAmount: The maximum amount of rewards to claim
238 /// @return a Vault containing the claimed rewards
239 ///
240 access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} {
241 let minimumAvailable = self.minimumAvailable()
242 if minimumAvailable == 0.0 {
243 return <- DeFiActionsUtils.getEmptyVault(self.getSourceType())
244 }
245
246 if let pool = IncrementFiStakingConnectors.borrowPool(pid: self.pid) {
247 if let userCertificate = self.userCertificate.borrow() {
248 let withdrawAmount = maxAmount < minimumAvailable
249 ? maxAmount
250 : minimumAvailable
251
252 let rewards <- pool.claimRewards(userCertificate: userCertificate)
253 let targetSliceType = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: self.vaultType.identifier)
254
255 assert(rewards.keys.length <= 1, message: "Pool with ID \(self.pid) has multiple reward token types, only one is supported")
256
257 if rewards.keys.length == 0 {
258 destroy rewards
259 return <- DeFiActionsUtils.getEmptyVault(self.getSourceType())
260 }
261
262 assert(
263 rewards.keys[0] == targetSliceType,
264 message: "Reward token type \(rewards.keys[0]) is not supported by this Source instance (poolID: \(self.pid), instance sourceType: \(self.vaultType.identifier)). This instance can only claim \(targetSliceType) rewards."
265 )
266 let reward <- rewards.remove(key: rewards.keys[0])!
267 destroy rewards
268 return <- reward
269 }
270 }
271
272 return <- DeFiActionsUtils.getEmptyVault(self.getSourceType())
273 }
274 }
275
276 /// Helper function to borrow a reference to the staking pool
277 ///
278 /// @return a reference to the staking pool, or nil if not available
279 ///
280 access(all) fun borrowPool(pid: UInt64): &{Staking.PoolPublic}? {
281 let poolCollectionCap = getAccount(Type<Staking>().address!).capabilities.get<&Staking.StakingPoolCollection>(Staking.CollectionPublicPath)
282 return poolCollectionCap.borrow()?.getPool(pid: pid)
283 }
284
285 /// Helper function to borrow a reference to the pair public interface
286 ///
287 /// @param pid: The pool ID to borrow the pair public interface for
288 /// @return a reference to the pair public interface
289 ///
290 access(all) fun borrowPairPublicByPid(pid: UInt64): &{SwapInterfaces.PairPublic}? {
291 let pool = IncrementFiStakingConnectors.borrowPool(pid: pid)
292 if pool == nil {
293 return nil
294 }
295
296 let pair = getAccount(IncrementFiStakingConnectors.tokenTypeIdentifierToVaultType(pool!.getPoolInfo().acceptTokenKey).address!)
297 .capabilities
298 .borrow<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath)
299
300 return pair
301 }
302
303 /// Helper function to convert a token type identifier to a vault type
304 /// E.g. "A.0x1234567890.USDC" -> Type("A.0x1234567890.USDC.Vault")
305 ///
306 /// @param tokenType: The token type identifier to convert to a vault type
307 /// @return the vault type
308 ///
309 access(all) fun tokenTypeIdentifierToVaultType(_ tokenType: String): Type {
310 return CompositeType(tokenType.concat(".Vault"))!
311 }
312}