Smart Contract
LiquidStaking
A.d6f80565193ad727.LiquidStaking
1/**
2
3# Liquid Staking
4
5# Author: Increment Labs
6
7*/
8
9import FlowToken from 0x1654653399040a61
10import FungibleToken from 0xf233dcee88fe0abe
11
12import FlowIDTableStaking from 0x8624b52f9ddcd04a
13import FlowEpoch from 0x8624b52f9ddcd04a
14
15import stFlowToken from 0xd6f80565193ad727
16import LiquidStakingConfig from 0xd6f80565193ad727
17import LiquidStakingError from 0xd6f80565193ad727
18import DelegatorManager from 0xd6f80565193ad727
19
20access(all) contract LiquidStaking {
21
22 /// Paths
23 access(all) let WithdrawVoucherCollectionPath: StoragePath
24 access(all) let WithdrawVoucherCollectionPublicPath: PublicPath
25
26 /// Events
27 access(all) event Stake(flowAmountIn: UFix64, stFlowAmountOut: UFix64, currProtocolEpoch: UInt64)
28 access(all) event Unstake(stFlowAmountIn: UFix64, lockedFlowAmount: UFix64, currProtocolEpoch: UInt64, unlockProtocolEpoch: UInt64, voucherUUID: UInt64)
29 access(all) event UnstakeQuickly(stFlowAmountIn: UFix64, flowAmountOut: UFix64, currProtocolEpoch: UInt64)
30 access(all) event MigrateDelegator(uuid: UInt64, migratedFlowIn: UFix64, stFlowOut: UFix64)
31 access(all) event BurnWithdrawVoucher(uuid: UInt64, amountFlowCashedout: UFix64, currProtocolEpoch: UInt64)
32
33 /// Reserved parameter fields: {ParamName: Value}
34 access(self) let _reservedFields: {String: AnyStruct}
35
36 /// A voucher indicating the amount of locked $flow will be redeemable after certain protocol epoch (inclusive)
37 access(all) resource WithdrawVoucher {
38
39 /// Flow token amount to be unlocked
40 access(all) let lockedFlowAmount: UFix64
41
42 /// Unlock epoch
43 access(all) let unlockEpoch: UInt64
44
45 init(lockedFlowAmount: UFix64, unlockEpoch: UInt64) {
46 self.lockedFlowAmount = lockedFlowAmount
47 self.unlockEpoch = unlockEpoch
48 }
49 }
50
51 /// Deposit and stake $flow in exchange for $stFlow token
52 access(all) fun stake(flowVault: @FlowToken.Vault): @stFlowToken.Vault {
53 pre {
54 // Pause check
55 LiquidStakingConfig.isStakingPaused == false: LiquidStakingError.ErrorEncode(msg: "Staking is paused", err: LiquidStakingError.ErrorCode.STAKE_NOT_OPEN)
56 // Flow blockchain staking state check
57 FlowIDTableStaking.stakingEnabled(): LiquidStakingError.ErrorEncode(msg: "Cannot stake as not in flowchain's staking auction period", err: LiquidStakingError.ErrorCode.STAKING_AUCTION_NOT_IN_PROGRESS)
58 // Protocol epoch sync check
59 FlowEpoch.currentEpochCounter == DelegatorManager.quoteEpochCounter: LiquidStakingError.ErrorEncode(msg: "Cannot stake until protocol epoch syncs with chain epoch", err: LiquidStakingError.ErrorCode.QUOTE_EPOCH_EXPIRED)
60 // Min staking amount check
61 flowVault.balance >= LiquidStakingConfig.minStakingAmount: LiquidStakingError.ErrorEncode(msg: "Stake amount must be greater than ".concat(LiquidStakingConfig.minStakingAmount.toString()), err: LiquidStakingError.ErrorCode.INVALID_PARAMETERS)
62 // Staking amount cap check
63 LiquidStakingConfig.stakingCap >= flowVault.balance + DelegatorManager.getTotalValidStakingAmount(): LiquidStakingError.ErrorEncode(msg: "Exceed stake cap: ".concat(LiquidStakingConfig.stakingCap.toString()), err: LiquidStakingError.ErrorCode.EXCEED_STAKE_CAP)
64 }
65
66 let flowAmountToStake = flowVault.balance
67 let stFlowAmountToMint = self.calcStFlowFromFlow(flowAmount: flowAmountToStake)
68
69 // Stake to committed tokens
70 DelegatorManager.depositToCommitted(flowVault: <-flowVault)
71
72 // Mint stFlow
73 let stFlowVault <- stFlowToken.mintTokens(amount: stFlowAmountToMint)
74
75 emit Stake(flowAmountIn: flowAmountToStake, stFlowAmountOut: stFlowAmountToMint, currProtocolEpoch: DelegatorManager.quoteEpochCounter)
76
77 return <-stFlowVault
78 }
79
80 /// To unstake (normally) that needs to wait for several epochs before finally withdrawing $flow (principal + interests) from the protocol
81 /// Returns a ticket indicating the amount of $flow redeemable after certain protocol epoch (so you won't get $flow back immediately)
82 access(all) fun unstake(stFlowVault: @stFlowToken.Vault): @WithdrawVoucher {
83 pre {
84 // Pause check
85 LiquidStakingConfig.isUnstakingPaused == false: LiquidStakingError.ErrorEncode(msg: "Unstaking is paused", err: LiquidStakingError.ErrorCode.UNSTAKE_NOT_OPEN)
86 // Unstaking amount check
87 stFlowVault.balance > 0.0: LiquidStakingError.ErrorEncode(msg: "Unstake amount must be greater than 0.0", err: LiquidStakingError.ErrorCode.INVALID_PARAMETERS)
88 }
89
90 let stFlowAmountToBurn = stFlowVault.balance
91 let flowAmountToUnstake = self.calcFlowFromStFlow(stFlowAmount: stFlowAmountToBurn)
92
93 // Burn stFlow
94 stFlowToken.burnTokens(from: <-stFlowVault)
95
96 // Request unstake from staked & committed tokens
97 DelegatorManager.requestWithdrawFromStaked(amount: flowAmountToUnstake)
98
99 let currentBlockView = getCurrentBlock().view
100 let stakingEndView = FlowEpoch.getEpochMetadata(FlowEpoch.currentEpochCounter)!.stakingEndView
101 let protocolEpoch = DelegatorManager.quoteEpochCounter
102
103 // In most of the time, the voucher will be redeemable after 2 protocol epochs
104 var unlockEpoch = protocolEpoch + 2
105 if FlowEpoch.currentEpochCounter > DelegatorManager.quoteEpochCounter {
106 // If unstake() happenes in the short period of time that chain epoch has advanced already but the protocol is waiting to collect
107 // delegators and update $stFlow price. For safety purpose the unlockEpoch is postponed for an additional epoch
108 unlockEpoch = protocolEpoch + 3
109 } else {
110 if FlowIDTableStaking.stakingEnabled() == false {
111 // Otherwise if unstake() happenes in flowchain's epoch setup & commit stage, no "real" action is allowed in the underlying layer
112 // So for safety purpose it's postponed for an extra epoch, too
113 // In this case the waiting time <= (2 epochs + epoch setup & commit time). Epoch setup & commit time usually takes less than half a day
114 // See https://developers.flow.com/nodes/staking/schedule#schedule for more details
115 unlockEpoch = protocolEpoch + 3
116 } else if currentBlockView + LiquidStakingConfig.windowSizeBeforeStakingEnd > stakingEndView {
117 // Before staking auction ends, a window of processing time is reserved to handle unprocessed unstake requests
118 // Any unstake() happening within this short period of time will be postponed with an extra epoch for safety purpose
119 // However, even in this case the waiting time <= (2 epochs + safetyWindowSize)
120 unlockEpoch = protocolEpoch + 3
121 }
122 }
123
124 let unstakeVoucher <- create WithdrawVoucher(
125 lockedFlowAmount: flowAmountToUnstake,
126 unlockEpoch: unlockEpoch
127 )
128
129 emit Unstake(stFlowAmountIn: stFlowAmountToBurn, lockedFlowAmount: flowAmountToUnstake, currProtocolEpoch: protocolEpoch, unlockProtocolEpoch: unlockEpoch, voucherUUID: unstakeVoucher.uuid)
130
131 return <- unstakeVoucher
132 }
133
134 /// To unstake (quickly) from liquid staking protocol's default staking node, without waiting for several epochs
135 /// You'll immediately get $flow back, as long as the default staking node has enough newly committed tokens from this epoch; otherwise reverts internally.
136 access(all) fun unstakeQuickly(stFlowVault: @stFlowToken.Vault): @FlowToken.Vault {
137 pre {
138 // Pause check
139 LiquidStakingConfig.isUnstakingPaused == false: LiquidStakingError.ErrorEncode(msg: "Unstaking is paused", err: LiquidStakingError.ErrorCode.UNSTAKE_NOT_OPEN)
140 // Flow chain unstaking state check
141 FlowIDTableStaking.stakingEnabled(): LiquidStakingError.ErrorEncode(msg: "Cannot unstake as not in flowchain's staking auction period", err: LiquidStakingError.ErrorCode.STAKING_AUCTION_NOT_IN_PROGRESS)
142 // Protocol epoch sync check
143 FlowEpoch.currentEpochCounter == DelegatorManager.quoteEpochCounter: LiquidStakingError.ErrorEncode(msg: "Cannot unstake until protocol epoch syncs with chain epoch", err: LiquidStakingError.ErrorCode.QUOTE_EPOCH_EXPIRED)
144 // Unstaking amount check
145 stFlowVault.balance > 0.0: LiquidStakingError.ErrorEncode(msg: "Unstake amount must be greater than 0.0", err: LiquidStakingError.ErrorCode.INVALID_PARAMETERS)
146 }
147
148 let stFlowAmountToBurn = stFlowVault.balance
149 let flowAmountToUnstake = self.calcFlowFromStFlow(stFlowAmount: stFlowAmountToBurn)
150
151 // Burn stFlow
152 stFlowToken.burnTokens(from: <-stFlowVault)
153
154 // Withdraw from default staking node's committed vault
155 let flowVault <- DelegatorManager.withdrawFromCommitted(amount: flowAmountToUnstake)
156
157 // Deduct fast unstake protocol fee
158 let feeVault <- flowVault.withdraw(amount: LiquidStakingConfig.quickUnstakeFee * flowAmountToUnstake)
159 DelegatorManager.depositToProtocolFees(flowVault: <-feeVault, purpose: "fast unstake fee -> protocol fee")
160
161 emit UnstakeQuickly(stFlowAmountIn: stFlowAmountToBurn, flowAmountOut: flowVault.balance, currProtocolEpoch: DelegatorManager.quoteEpochCounter)
162
163 return <-flowVault
164 }
165
166 /// Cashout WithdrawVoucher by burning it for FlowToken
167 access(all) fun cashoutWithdrawVoucher(voucher: @WithdrawVoucher): @FlowToken.Vault {
168 pre {
169 DelegatorManager.quoteEpochCounter >= voucher.unlockEpoch: LiquidStakingError.ErrorEncode(msg: "Voucher is not redeemable yet", err: LiquidStakingError.ErrorCode.CANNOT_CASHOUT_WITHDRAW_VOUCHER)
170 }
171 let cashoutAmount = voucher.lockedFlowAmount
172
173 let flowVault <-DelegatorManager.withdrawFromUnstaked(amount: cashoutAmount)
174
175 emit BurnWithdrawVoucher(uuid: voucher.uuid, amountFlowCashedout: cashoutAmount, currProtocolEpoch: DelegatorManager.quoteEpochCounter)
176
177 destroy voucher
178
179 return <-flowVault
180 }
181
182 /// Migrate staked NodeDelegator resources to get it managed by the liquid staking protocol, and mint corresponding $stFlow back
183 /// This is a useful feature for users who have already staked $flow to nodes. To get the benefits of $stFlow and its ecosystem, they do not have to
184 /// first unstake from nodes (as it usually takes 1~2 epochs and lose staking rewards meanwhile); users can just migrate their NodeDelegator resources
185 /// (represents their staked positions) in and get $stFlow immediately.
186 access(all) fun migrate(delegator: @FlowIDTableStaking.NodeDelegator): @stFlowToken.Vault {
187 pre {
188 // Pause check
189 LiquidStakingConfig.isMigratingPaused == false: LiquidStakingError.ErrorEncode(msg: "Migrating is paused", err: LiquidStakingError.ErrorCode.MIGRATE_NOT_OPEN)
190 // Flowchain staking state check
191 FlowIDTableStaking.stakingEnabled(): LiquidStakingError.ErrorEncode(msg: "Cannot migrate as not in flowchain's staking auction period", err: LiquidStakingError.ErrorCode.STAKING_AUCTION_NOT_IN_PROGRESS)
192 // Protocol epoch sync check
193 FlowEpoch.currentEpochCounter == DelegatorManager.quoteEpochCounter: LiquidStakingError.ErrorEncode(msg: "Cannot migrate until protocol epoch syncs with chain epoch", err: LiquidStakingError.ErrorCode.QUOTE_EPOCH_EXPIRED)
194 }
195
196 let delegatroInfo = FlowIDTableStaking.DelegatorInfo(nodeID: delegator.nodeID, delegatorID: delegator.id)
197
198 assert(LiquidStakingConfig.stakingCap >= delegatroInfo.tokensStaked + DelegatorManager.getTotalValidStakingAmount(), message: "Exceed stake cap")
199 assert(delegatroInfo.tokensUnstaking == 0.0, message: "Wait for the previous unstaking processing to complete")
200 assert(delegatroInfo.tokensRewarded == 0.0, message: "Please claim the reward before migrating")
201 assert(delegatroInfo.tokensUnstaked == 0.0, message: "Please withdraw the unstaked tokens before migrating")
202 assert(delegatroInfo.tokensRequestedToUnstake == 0.0, message: "Please cancel the unstake requests before migrating")
203 assert(delegatroInfo.tokensCommitted == 0.0, message: "Please cancel the stake requests before migrating")
204 assert(delegatroInfo.tokensStaked > 0.0, message: "No staked tokens need to be migrated.")
205
206 let stakedFlowToMigrate = delegatroInfo.tokensStaked
207 let stFlowAmountToMint = self.calcStFlowFromFlow(flowAmount: stakedFlowToMigrate)
208
209 emit MigrateDelegator(uuid: delegator.uuid, migratedFlowIn: stakedFlowToMigrate, stFlowOut: stFlowAmountToMint)
210
211 // Migrate NodeDelegator resource to make it managed by the liquid staking protocol
212 DelegatorManager.migrateDelegator(delegator: <-delegator)
213
214 // Mint stFlow
215 return <-stFlowToken.mintTokens(amount: stFlowAmountToMint)
216 }
217
218 /// WithdrawVoucher collection
219 access(all) resource interface WithdrawVoucherCollectionPublic {
220 access(all) view fun getVoucherInfos(): [AnyStruct]
221 access(all) fun deposit(voucher: @WithdrawVoucher)
222 }
223
224 access(all) resource WithdrawVoucherCollection: WithdrawVoucherCollectionPublic {
225 /// A list of withdraw vouchers
226 access(self) var vouchers: @[WithdrawVoucher]
227
228 access(all) fun deposit(voucher: @WithdrawVoucher) {
229 self.vouchers.append(<-voucher)
230 }
231
232 access(FungibleToken.Withdraw) fun withdraw(uuid: UInt64): @WithdrawVoucher {
233 var findIndex: Int? = nil
234 var index = 0
235 while index < self.vouchers.length {
236 if self.vouchers[index].uuid == uuid {
237 findIndex = index
238 break
239 }
240 index = index + 1
241 }
242
243 assert(findIndex != nil, message: "Cannot find voucher with uuid ".concat(uuid.toString()))
244 return <-self.vouchers.remove(at: findIndex!)
245 }
246
247 access(all) view fun getVoucherInfos(): [AnyStruct] {
248 var voucherInfos: [AnyStruct] = []
249 var index = 0
250 while index < self.vouchers.length {
251 voucherInfos = voucherInfos.concat([
252 {
253 "uuid": self.vouchers[index].uuid,
254 "lockedFlowAmount": self.vouchers[index].lockedFlowAmount,
255 "unlockEpoch": self.vouchers[index].unlockEpoch
256 }
257 ])
258 index = index + 1
259 }
260 return voucherInfos
261 }
262
263 init() {
264 self.vouchers <- []
265 }
266 }
267
268 access(all) fun createEmptyWithdrawVoucherCollection(): @WithdrawVoucherCollection {
269 return <-create WithdrawVoucherCollection()
270 }
271
272 /// Calculate exchange amount from Flow to stFlow
273 access(all) view fun calcStFlowFromFlow(flowAmount: UFix64): UFix64 {
274 let currentEpochSnapshot = DelegatorManager.borrowCurrentQuoteEpochSnapshot()
275 let scaledFlowPrice = currentEpochSnapshot.scaledQuoteFlowStFlow
276 let scaledFlowAmount = LiquidStakingConfig.UFix64ToScaledUInt256(flowAmount)
277
278 let stFlowAmount = LiquidStakingConfig.ScaledUInt256ToUFix64(
279 scaledFlowPrice * scaledFlowAmount / LiquidStakingConfig.scaleFactor
280 )
281 return stFlowAmount
282 }
283
284 /// Calculate exchange amount from stFlow to Flow
285 access(all) view fun calcFlowFromStFlow(stFlowAmount: UFix64): UFix64 {
286 let currentEpochSnapshot = DelegatorManager.borrowCurrentQuoteEpochSnapshot()
287 let scaledStFlowPrice = currentEpochSnapshot.scaledQuoteStFlowFlow
288 let scaledStFlowAmount = LiquidStakingConfig.UFix64ToScaledUInt256(stFlowAmount)
289
290 let flowAmount = LiquidStakingConfig.ScaledUInt256ToUFix64(
291 scaledStFlowPrice * scaledStFlowAmount / LiquidStakingConfig.scaleFactor
292 )
293 return flowAmount
294 }
295
296 init() {
297 self.WithdrawVoucherCollectionPath = /storage/liquid_staking_withdraw_voucher_collection
298 self.WithdrawVoucherCollectionPublicPath = /public/liquid_staking_withdraw_voucher_collection
299 self._reservedFields = {}
300 }
301}
302