Smart Contract

IncrementPointsV1

A.3ddb31a3880d1d8f.IncrementPointsV1

Deployed

1d ago
Feb 26, 2026, 10:41:48 PM UTC

Dependents

0 imports
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