Smart Contract
IncrementPointsV1
A.3ddb31a3880d1d8f.IncrementPointsV1
1/**
2
3#
4# Author: Increment Labs
5
6*/
7
8import FungibleToken from 0xf233dcee88fe0abe
9// Lending
10import LendingInterfaces from 0x2df970b6cdee5735
11import LendingConfig from 0x2df970b6cdee5735
12// Swap
13import SwapConfig from 0xb78ef7afa52ff906
14import SwapInterfaces from 0xb78ef7afa52ff906
15// Liquid Staking
16import LiquidStaking from 0xd6f80565193ad727
17// Farm
18import Staking from 0x1b77ba4b414de352
19// Oracle
20import PublicPriceOracle from 0xec67451f8a58216a
21
22
23pub contract IncrementPointsV1 {
24 // TODO snapshot POINTS
25 access(self) let _balancesHistorySnapshot: {Address: UFix64}
26
27 access(all) var _totalSupply: UFix64
28 access(self) let _balances: {Address: UFix64}
29 access(self) let _blackList: {Address: UFix64}
30
31 access(self) let _swapPoolWhitelist: {Address: Bool} // {PoolAddress}
32
33 // 为方便扩展和修改,采用 dict 方式存储
34 // 当前的 rate 如下:
35 /* {
36 "LendingSupply": {
37 0.0 : 0.001, // 0.0 ~ 1000.0 -> 0.001
38 1000.0 : 0.002, // 1000.0 ~ 10000.0 -> 0.002
39 10000.0 : 0.003 // 10000.0 ~ Max -> 0.003
40 }
41 }
42 */
43 access(self) let _pointsRatePerDay: {String: AnyStruct}
44
45 // TODO 黑名单 只是 0 不能让tx失败
46 access(self) fun mintPoints(targetAddr: Address, amount: UFix64) {
47 // mint points
48 if self._balances.containsKey(targetAddr) == false {
49 self._balances[targetAddr] = 0.0
50 }
51 self._balances[targetAddr] = self._balances[targetAddr]! + amount
52
53 // referral boost
54 let refereeAddr: Address = 0x00
55 let refereeAmount: UFix64 = 0.0
56 if self._balances.containsKey(targetAddr) == false {
57 self._balances[refereeAddr] = 0.0
58 }
59
60 // update total supply
61 self._totalSupply = self._totalSupply + amount + refereeAmount;
62 }
63
64 //access(self) let _leaderBoard: [AnyStruct]
65
66 access(self) let _userStates: {Address: {String: UFix64}}
67
68 access(self) let _secondsPerDay: UFix64
69
70 // 用于统计 Swap Volume 的锁
71 access(self) var _swapPoolAddress: Address
72 access(self) var _swapVolumeTrackingTimestamp: UFix64
73 access(self) var _swapPoolReserve0: UFix64
74 access(self) var _swapPoolReserve1: UFix64
75
76
77 access(self) let _reservedFields: {String: AnyStruct}
78
79 /// Events
80 access(all) event PointsMinted(userAddr: Address, amount: UFix64, source: String, param: {String: String})
81 access(all) event PointsBurned(userAddr: Address, amount: UFix64)
82 access(all) event StateUpdated(userAddr: Address, state: {String: UFix64})
83 access(all) event PointsRateChanged(source: String, ori: UFix64, new: UFix64)
84 access(all) event PointsTierRateChanged(source: String, ori: {UFix64: UFix64}, new: {UFix64: UFix64})
85
86
87 access(all) fun updateUserState(userAddr: Address) {
88 //
89 let lastMintTimestamp = self._userStates.containsKey(userAddr)? self._userStates[userAddr]!["LastMintTimestamp"]! : 0.0
90 let currMintTimestamp = getCurrentBlock().timestamp
91
92 // Mint Points
93 if lastMintTimestamp != 0.0 && currMintTimestamp - lastMintTimestamp > 0.0 {
94 let duration = currMintTimestamp - lastMintTimestamp
95 let timeRate = duration
96 var amountToMint = 0.0
97 // Lending Points
98 let currTotalSupplyAmountInUsd = self._userStates[userAddr]!["LendingTotalSupplyInUsd"]!
99 let currTotalBorrowAmountInUsd = self._userStates[userAddr]!["LendingTotalBorrowInUsd"]!
100 let mintAmountByLendingSupply = currTotalSupplyAmountInUsd * self.getPointsRatePerDay_LendingSupply(amount: currTotalSupplyAmountInUsd) / self._secondsPerDay * timeRate
101 let mintAmountbyLendingBorrow = currTotalBorrowAmountInUsd * self.getPointsRatePerDay_LendingBorrow(amount: currTotalBorrowAmountInUsd) / self._secondsPerDay * timeRate
102 if mintAmountByLendingSupply > 0.0 {
103 emit PointsMinted(userAddr: userAddr, amount: mintAmountByLendingSupply, source: "LendingSupply", param: {"SupplyUsdValue": currTotalSupplyAmountInUsd.toString(), "Duration": duration.toString()})
104 }
105 if mintAmountbyLendingBorrow > 0.0 {
106 emit PointsMinted(userAddr: userAddr, amount: mintAmountbyLendingBorrow, source: "LendingBorrow", param: {"SupplyUsdValue": currTotalBorrowAmountInUsd.toString(), "Duration": duration.toString()})
107 }
108
109 // Mint
110 let mintAmount = mintAmountByLendingSupply + mintAmountbyLendingBorrow
111 if (mintAmount > 0.0) {
112 self.mintPoints(targetAddr: userAddr, amount: mintAmount)
113 }
114
115 }
116
117
118 // Update Oracle Price
119 let oraclePrices: {String: UFix64} = { // OracleAddress -> Token Price
120 "Flow": PublicPriceOracle.getLatestPrice(oracleAddr: 0xe385412159992e11),
121 "stFlow": PublicPriceOracle.getLatestPrice(oracleAddr: 0x031dabc5ba1d2932),
122 "USDC": PublicPriceOracle.getLatestPrice(oracleAddr: 0xf5d12412c09d2470)
123 }
124
125 // Lending State
126 let lendingComptrollerRef = getAccount(0xf80cb737bfe7c792).getCapability<&{LendingInterfaces.ComptrollerPublic}>(LendingConfig.ComptrollerPublicPath).borrow()!
127 let marketAddrs: [Address] = lendingComptrollerRef.getAllMarkets()
128 let lendingOracleRef = getAccount(0x72d3a05910b6ffa3).getCapability<&{LendingInterfaces.OraclePublic}>(LendingConfig.OraclePublicPath).borrow()!
129 var totalSupplyAmountInUsd = 0.0
130 var totalBorrowAmountInUsd = 0.0
131 for poolAddr in marketAddrs {
132 let poolRef = lendingComptrollerRef.getPoolPublicRef(poolAddr: poolAddr)
133 let poolOraclePrice = lendingOracleRef.getUnderlyingPrice(pool: poolAddr)
134 let res: [UInt256; 5] = poolRef.getAccountRealtimeScaled(account: userAddr)
135 let supplyAmount = SwapConfig.ScaledUInt256ToUFix64(res[0] * res[1] / SwapConfig.scaleFactor)
136 let borrowAmount = SwapConfig.ScaledUInt256ToUFix64(res[2])
137 totalSupplyAmountInUsd = totalSupplyAmountInUsd + supplyAmount * poolOraclePrice
138 totalBorrowAmountInUsd = totalBorrowAmountInUsd + borrowAmount * poolOraclePrice
139 }
140
141 // Liquid Staking State
142
143
144 //
145
146 // Update State
147 if self._userStates.containsKey(userAddr)
148 || totalSupplyAmountInUsd > 0.0 || totalBorrowAmountInUsd > 0.0 {
149 if self._userStates.containsKey(userAddr) == false {
150 self._userStates[userAddr] = {
151 "LastMintTimestamp": 0.0,
152 "LendingTotalSupplyInUsd": 0.0,
153 "LendingTotalBorrowInUsd": 0.0
154 }
155 }
156
157 self._userStates[userAddr]!.insert(key: "LendingTotalSupplyInUsd", totalSupplyAmountInUsd)
158 self._userStates[userAddr]!.insert(key: "LendingTotalBorrowInUsd", totalBorrowAmountInUsd)
159
160 self._userStates[userAddr]!.insert(key: "LastMintTimestamp", currMintTimestamp)
161
162 //
163 emit StateUpdated(userAddr: userAddr, state: self._userStates[userAddr]!)
164 }
165
166 }
167
168 access(all) fun beginVolumeTracking(swapPoolAddr: Address) {
169 // 判断是否是Increment的池子
170 if self._swapPoolWhitelist.containsKey(swapPoolAddr) == false { return }
171
172 let poolInfo: [AnyStruct] = getAccount(swapPoolAddr).getCapability<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath).borrow()!.getPairInfo()
173 self._swapPoolReserve0 = poolInfo[2] as! UFix64
174 self._swapPoolReserve1 = poolInfo[3] as! UFix64
175
176 self._swapVolumeTrackingTimestamp = getCurrentBlock().timestamp
177 self._swapPoolAddress = swapPoolAddr
178 }
179
180 access(all) fun endVolumeTrackingAndMintPoints(userAddr: Address) {
181 if self._swapVolumeTrackingTimestamp != getCurrentBlock().timestamp {
182 return
183 }
184 self._swapVolumeTrackingTimestamp = 0.0
185
186 let poolInfo: [AnyStruct] = getAccount(self._swapPoolAddress).getCapability<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath).borrow()!.getPairInfo()
187 let reserve0Token = poolInfo[0] as! String
188 let reserve1Token = poolInfo[1] as! String
189 let curReserve0 = poolInfo[2] as! UFix64
190 let curReserve1 = poolInfo[3] as! UFix64
191
192 // Add/Remove Lp won't mint any points
193 if curReserve0 >= self._swapPoolReserve0 && curReserve1 >= self._swapPoolReserve1 { return }
194 if curReserve0 <= self._swapPoolReserve0 && curReserve1 <= self._swapPoolReserve1 { return }
195 var amountIn = 0.0
196 var amountOut = 0.0
197 var tokenInKey = ""
198 var tokenOutKey = ""
199 // Swap A to B
200 if curReserve0 > self._swapPoolReserve0 && curReserve1 < self._swapPoolReserve1 {
201 amountIn = curReserve0 - self._swapPoolReserve0
202 tokenInKey = reserve0Token
203 amountOut = self._swapPoolReserve1 - curReserve1
204 tokenOutKey = reserve1Token
205 }
206 // Swap B to A
207 if curReserve0 < self._swapPoolReserve0 && curReserve1 > self._swapPoolReserve1 {
208 amountIn = curReserve1 - self._swapPoolReserve1
209 tokenInKey = reserve1Token
210 amountOut = self._swapPoolReserve0 - curReserve0
211 tokenOutKey = reserve0Token
212 }
213
214 // Cal volume
215 let usdcPrice = PublicPriceOracle.getLatestPrice(oracleAddr: 0xf5d12412c09d2470)
216 let flowPrice = PublicPriceOracle.getLatestPrice(oracleAddr: 0xe385412159992e11)
217 let stflowPrice = PublicPriceOracle.getLatestPrice(oracleAddr: 0x031dabc5ba1d2932)
218 var volumeUsd = 0.0
219 if tokenInKey == "A.b19436aae4d94622.FiatToken" { volumeUsd = amountIn * usdcPrice }
220 else if tokenInKey == "A.1654653399040a61.FlowToken" { volumeUsd = amountIn * flowPrice }
221 else if tokenInKey == "A.d6f80565193ad727.stFlowToken" { volumeUsd = amountIn * stflowPrice }
222 else if tokenOutKey == "A.b19436aae4d94622.FiatToken" { volumeUsd = amountOut * usdcPrice }
223 else if tokenOutKey == "A.1654653399040a61.FlowToken" { volumeUsd = amountOut * flowPrice }
224 else if tokenOutKey == "A.d6f80565193ad727.stFlowToken" { volumeUsd = amountOut * stflowPrice }
225
226 // Mint points
227 let mintAmountBySwapVolume = volumeUsd * self.getPointsRatePerDay_SwapVolume()
228 if mintAmountBySwapVolume > 0.0 {
229 emit PointsMinted(userAddr: userAddr, amount: mintAmountBySwapVolume, source: "SwapVolume", param: {"TokenInKey": tokenInKey, "TokenOutKey": tokenOutKey, "AmountIn": amountIn.toString(), "AmountOut": amountOut.toString(), "VolumeUsd": volumeUsd.toString()})
230 self.mintPoints(targetAddr: userAddr, amount: mintAmountBySwapVolume)
231 }
232 }
233
234 // 计算符合条件的Lp的Price
235 // 符合条件:包含Flow, stFlow, USDC,并且tvl > 2k
236 access(all) fun calValidLpPrice(pairInfo: [AnyStruct], oraclePrices: {String: UFix64}): UFix64 {
237 // TODO 或者命中白名单
238
239 var reserveAmount = 0.0
240 var reservePrice = 0.0
241 var lpPrice = 0.0
242 if pairInfo[0] as! String == "A.b19436aae4d94622.FiatToken" {reserveAmount = pairInfo[2] as! UFix64; reservePrice = oraclePrices["USDC"]!}
243 else if pairInfo[1] as! String == "A.b19436aae4d94622.FiatToken" {reserveAmount = pairInfo[3] as! UFix64; reservePrice = oraclePrices["USDC"]!}
244 else if pairInfo[0] as! String == "A.1654653399040a61.FlowToken" {reserveAmount = pairInfo[2] as! UFix64; reservePrice = oraclePrices["Flow"]!}
245 else if pairInfo[1] as! String == "A.1654653399040a61.FlowToken" {reserveAmount = pairInfo[3] as! UFix64; reservePrice = oraclePrices["Flow"]!}
246 else if pairInfo[0] as! String == "A.d6f80565193ad727.stFlowToken" {reserveAmount = pairInfo[2] as! UFix64; reservePrice = oraclePrices["stFlow"]!}
247 else if pairInfo[1] as! String == "A.d6f80565193ad727.stFlowToken" {reserveAmount = pairInfo[3] as! UFix64; reservePrice = oraclePrices["stFlow"]!}
248 if reservePrice > 0.0 && reserveAmount > 1000.0 {
249 lpPrice = reserveAmount * reservePrice * 2.0 / (pairInfo[5] as! UFix64)
250 }
251 return lpPrice
252 }
253 pub fun type2address(_ type: String) : Address {
254 let address = type.slice(from: 2, upTo: 18)
255 var r: UInt64 = 0
256 var bytes = address.decodeHex()
257 while bytes.length>0{
258 r = r + (UInt64(bytes.removeFirst()) << UInt64(bytes.length*8))
259 }
260 return Address(r)
261 }
262
263 access(all) fun getSwapPoolWhiltlist(): {Address: Bool} { return self._swapPoolWhitelist }
264
265 access(all) fun getPointsRatePerDay_LiquidStaking(amount: UFix64): UFix64 {
266 return self.calculateTierRateByAmount(amount: amount, tier: self._pointsRatePerDay["LiquidStaking"]! as! {UFix64: UFix64})
267 }
268 access(all) fun getPointsRatePerDay_LendingSupply(amount: UFix64): UFix64 {
269 return self.calculateTierRateByAmount(amount: amount, tier: self._pointsRatePerDay["LendingSupply"]! as! {UFix64: UFix64})
270 }
271 access(all) fun getPointsRatePerDay_LendingBorrow(amount: UFix64): UFix64 {
272 return self.calculateTierRateByAmount(amount: amount, tier: self._pointsRatePerDay["LendingBorrow"]! as! {UFix64: UFix64})
273 }
274 access(all) fun getPointsRatePerDay_SwapLP(): UFix64 { return self._pointsRatePerDay["SwapLP"]! as! UFix64 }
275 access(all) fun getPointsRatePerDay_SwapVolume(): UFix64 { return self._pointsRatePerDay["SwapVolume"]! as! UFix64 }
276
277 access(all) fun calculateTierRateByAmount(amount: UFix64, tier: {UFix64: UFix64}): UFix64 {
278 var rate = 0.0
279 var maxThreshold = 0.0
280 for threshold in tier.keys {
281 if amount >= threshold && threshold >= maxThreshold {
282 rate = tier[threshold]!
283 maxThreshold = threshold
284 }
285 }
286 return rate
287 }
288
289 /// Admin
290 ///
291 access(all) resource Admin {
292 access(all) fun setPointsRatePerDay_LiquidStakingTier(tierRate: {UFix64: UFix64}) {
293 emit PointsTierRateChanged(source: "LiquidStaking", ori: IncrementPointsV1._pointsRatePerDay["LiquidStaking"]! as! {UFix64: UFix64}, new: tierRate)
294 IncrementPointsV1._pointsRatePerDay["LiquidStaking"] = tierRate
295 }
296 access(all) fun setPointsRatePerDay_LendingSupplyTier(tierRate: {UFix64: UFix64}) {
297 emit PointsTierRateChanged(source: "LendingSupply", ori: IncrementPointsV1._pointsRatePerDay["LendingSupply"]! as! {UFix64: UFix64}, new: tierRate)
298 IncrementPointsV1._pointsRatePerDay["LendingSupply"] = tierRate
299 }
300 access(all) fun setPointsRatePerDay_LendingBorrowTier(tierRate: {UFix64: UFix64}) {
301 emit PointsTierRateChanged(source: "LendingBorrow", ori: IncrementPointsV1._pointsRatePerDay["LendingBorrow"]! as! {UFix64: UFix64}, new: tierRate)
302 IncrementPointsV1._pointsRatePerDay["LendingBorrow"] = tierRate
303 }
304 access(all) fun setPointsRatePerDay_SwapLP(rate: UFix64) {
305 emit PointsRateChanged(source: "SwapLP", ori: IncrementPointsV1._pointsRatePerDay["SwapLP"]! as! UFix64, new: rate)
306 IncrementPointsV1._pointsRatePerDay["SwapLP"] = rate
307 }
308 access(all) fun setPointsRatePerDay_SwapVolume(rate: UFix64) {
309 emit PointsRateChanged(source: "SwapVolume", ori: IncrementPointsV1._pointsRatePerDay["SwapVolume"]! as! UFix64, new: rate)
310 IncrementPointsV1._pointsRatePerDay["SwapVolume"] = rate
311 }
312
313 // Add Swap Pool in Whiltelist
314 access(all) fun addSwapPoolInWhiltelist(poolAddr: Address) {
315 IncrementPointsV1._swapPoolWhitelist[poolAddr] = true
316 }
317 // Remove Swap Pool in Whitelist
318 access(all) fun removeSwapPoolInWhiltelist(poolAddr: Address) {
319 IncrementPointsV1._swapPoolWhitelist.remove(key: poolAddr)
320 }
321 }
322
323 init() {
324 self._totalSupply = 0.0
325 self._secondsPerDay = 86400.0
326 self._balances = {}
327 self._balancesHistorySnapshot = {}
328 self._pointsRatePerDay = {
329 "LiquidStaking": {
330 0.0: 0.001,
331 1000.0: 0.002,
332 10000.0: 0.003
333 },
334 "LendingSupply": {
335 0.0: 0.001,
336 1000.0: 0.002,
337 10000.0: 0.003
338 },
339 "LendingBorrow": {
340 0.0: 0.002,
341 1000.0: 0.004,
342 10000.0: 0.006
343 },
344 "SwapLP": 0.01,
345 "SwapVolume": 1.0
346 }
347 self._swapPoolWhitelist = {
348 0xfa82796435e15832: true, // FLOW-USDC
349 0xcc96d987317f0342: true, // FLOW-ceWETH
350 0x09c49abce2a7385c: true, // FLOW-ceWBTC
351 0x396c0cda3302d8c5: true, // FLOW-stFLOW v1
352 0xc353b9d685ec427d: true, // FLOW-stFLOW stable
353 0xa06c38beec9cf0e8: true, // FLOW-DUST
354 0xbfb26bb8adf90399: true // FLOW-SLOPPY
355 }
356
357 self._userStates = {}
358 self._blackList = {}
359
360 self._swapPoolAddress = 0x00
361 self._swapVolumeTrackingTimestamp = 0.0
362 self._swapPoolReserve0 = 0.0
363 self._swapPoolReserve1 = 0.0
364
365
366 self._reservedFields = {}
367
368 destroy <-self.account.load<@AnyResource>(from: /storage/pointsAdmin1)
369 self.account.save(<-create Admin(), to: /storage/pointsAdmin1)
370 }
371}
372