Smart Contract
LendingAprSnapshot
A.7bb1e28c69407925.LendingAprSnapshot
1import LendingConfig from 0x2df970b6cdee5735
2import LendingError from 0x2df970b6cdee5735
3import LendingInterfaces from 0x2df970b6cdee5735
4
5pub contract LendingAprSnapshot {
6 pub let AdminStoragePath: StoragePath
7 /// { marketAddr => perMarketAprData}
8 access(self) let _markets: {Address: AprSnapshot}
9
10 /// Reserved parameter fields: {ParamName: Value}
11 access(self) let _reservedFields: {String: AnyStruct}
12
13 pub event MarketDataTracked(market: Address, marketType: String, startTrackingFrom: UFix64)
14 pub event MarketDataErased(market: Address, erasedFrom: UFix64)
15 pub event AprSampled(market: Address, truncatedTimestamp: UInt64, supplyApr: UFix64, borrowApr: UFix64)
16
17 /// Per-snapshot data point
18 pub struct Observation {
19 // Unix timestamp
20 pub let timestamp: UFix64
21 // supplyApr in ufix64
22 pub let supplyApr: UFix64
23 // borrowApr in ufix64 (e.g. 0.12345678 => 12.35%)
24 pub let borrowApr: UFix64
25
26 init (t: UFix64, supplyApr: UFix64, borrowApr: UFix64) {
27 self.timestamp = t
28 self.supplyApr = supplyApr
29 self.borrowApr = borrowApr
30 }
31 }
32
33 /// Per-market snapshot configurations and data points
34 pub struct AprSnapshot {
35 /// Contains functions to query public market data
36 pub let poolPublicCap: Capability<&{LendingInterfaces.PoolPublic}>
37 /// Each sample covers a 6-hour window: i.e. sampleLength = 21600s
38 pub let sampleLength: UInt64
39 /// We store 1 year of apr data in maximum: i.e. numSamples = 1460
40 pub let numSamples: UInt64
41 /// A circular buffer storing apr samples
42 access(self) let aprObservations: [Observation]
43 /// Reserved parameter fields: {ParamName: Value}
44 access(self) let _reservedFields: {String: AnyStruct}
45
46 /// Returns the index into the circular buffer of the given timestamp
47 pub fun observationIndexOf(timestamp: UFix64): UInt64 {
48 return UInt64(timestamp) / self.sampleLength % self.numSamples
49 }
50
51 pub fun sample(): Bool {
52 let now = getCurrentBlock().timestamp
53 let idx = self.observationIndexOf(timestamp: now)
54 let ob = self.aprObservations[idx]
55 let timeElapsed = now - ob.timestamp
56
57 if (UInt64(timeElapsed) > self.sampleLength) {
58 let poolRef = self.poolPublicCap.borrow()
59 ?? panic("cannot borrow reference to lendingPool")
60 let newSupplyApr: UFix64 = LendingConfig.ScaledUInt256ToUFix64(poolRef.getPoolSupplyAprScaled())
61 let newBorrowApr: UFix64 = LendingConfig.ScaledUInt256ToUFix64(poolRef.getPoolBorrowAprScaled())
62 // Truncate timestamp for better plotting in frontend
63 let samplePeriodStart: UInt64 = UInt64(now) / self.sampleLength * self.sampleLength
64 self.aprObservations[idx] = Observation(t: UFix64(samplePeriodStart), supplyApr: newSupplyApr, borrowApr: newBorrowApr)
65 emit AprSampled(market: poolRef.getPoolAddress(), truncatedTimestamp: samplePeriodStart, supplyApr: newSupplyApr, borrowApr: newBorrowApr)
66 return true
67 }
68 return false
69 }
70
71 pub fun queryHistoricalAprData(scale: UInt8, plotPoints: UInt64): [Observation] {
72 let now = getCurrentBlock().timestamp
73 let idxNow: UInt64 = self.observationIndexOf(timestamp: now)
74 var idxPrev: UInt64 = 0
75 switch scale {
76 case 0:
77 // idx for timestamp 1 month ago
78 idxPrev = self.observationIndexOf(timestamp: now - 30.0 * UFix64(self.sampleLength) * 4.0)
79 case 1:
80 // idx for timestamp 6 month ago
81 idxPrev = self.observationIndexOf(timestamp: now - 180.0 * UFix64(self.sampleLength) * 4.0)
82 case 2:
83 // idx for timestamp 1 year ago. (Use 360 instead of 365 for the purpose of exact-division)
84 idxPrev = self.observationIndexOf(timestamp: now - 360.0 * UFix64(self.sampleLength) * 4.0)
85 default:
86 panic("invalid spanning param")
87 }
88 let numSampledPoints = idxPrev < idxNow ? (idxNow - idxPrev + 1) : (self.numSamples + idxNow - idxPrev + 1)
89 assert(
90 plotPoints <= numSampledPoints,
91 message: "invalid plotPoints param: cannot plot due to insufficient samples"
92 )
93 let step: UInt64 = numSampledPoints / plotPoints
94 var res: [Observation] = []
95 var i: UInt64 = 0
96 while i < plotPoints {
97 let ob = self.aprObservations[idxPrev]
98 // Filtering non-meaningful data
99 if (ob.timestamp > 0.0) {
100 res.append(
101 Observation(
102 t: ob.timestamp,
103 supplyApr: ob.supplyApr,
104 borrowApr: ob.borrowApr
105 )
106 )
107 }
108 idxPrev = idxPrev + step
109 if idxPrev >= self.numSamples {
110 idxPrev = idxPrev - self.numSamples
111 }
112 i = i + 1
113 }
114 return res
115 }
116
117 pub fun getLatestData(): Observation {
118 let now = getCurrentBlock().timestamp
119 let idx = self.observationIndexOf(timestamp: now)
120 return self.aprObservations[idx]
121 }
122
123 /// Proposed params: sampleLength = 21600 (6h) && numSamples = 1460 (store 1 year's data)
124 init(poolPublicCap: Capability<&{LendingInterfaces.PoolPublic}>) {
125 self.poolPublicCap = poolPublicCap
126 // 6h
127 self.sampleLength = 30
128 // stores 1 year's data
129 self.numSamples = 1460
130 self.aprObservations = []
131 var i: UInt64 = 0
132 // Init circular buffer
133 while (i < self.numSamples) {
134 self.aprObservations.append(Observation(t: 0.0, supplyApr: 0.0, borrowApr: 0.0))
135 i = i + 1
136 }
137 self._reservedFields = {}
138 }
139 }
140
141 /// sample() is made public so everyone can sample the given market's apr data, as long as it's expired.
142 /// @Returns sampled or not
143 pub fun sample(poolAddr: Address): Bool {
144 pre {
145 self._markets.containsKey(poolAddr) == true:
146 LendingError.ErrorEncode(
147 msg: "Market not tracked yet",
148 err: LendingError.ErrorCode.MARKET_NOT_OPEN
149 )
150 }
151 return self._markets[poolAddr]!.sample()
152 }
153
154/////////////// TODO: Check if it's ok to pull 120 x (3 UFix64) in 1 script?
155 /// A getter function for frontend to query stored samples and plot data.
156 /// @scale: Spanning of time the drawing should cover - 0 (1 month), 1 (6 months), 2 (1 year).
157 /// @plotPoints: Maximum data points the drawing needs, e.g. 120 points in maximum
158 /// @Returns historical apy data in a timestamp-ascending order. Note: only meaningful data is returned, so the length is not guaranteed to be equal to `plotPoints`.
159 pub fun queryHistoricalAprData(poolAddr: Address, scale: UInt8, plotPoints: UInt64): [Observation] {
160 pre {
161 self._markets.containsKey(poolAddr) == true:
162 LendingError.ErrorEncode(
163 msg: "Market not tracked yet",
164 err: LendingError.ErrorCode.MARKET_NOT_OPEN
165 )
166 }
167 return self._markets[poolAddr]!.queryHistoricalAprData(scale: scale, plotPoints: plotPoints)
168 }
169
170 pub fun getLatestData(poolAddr: Address): Observation {
171 return self._markets[poolAddr]!.getLatestData()
172 }
173
174 access(contract) fun trackMarketData(poolAddr: Address) {
175 pre {
176 self._markets.containsKey(poolAddr) == false:
177 LendingError.ErrorEncode(
178 msg: "Market has already been tracked",
179 err: LendingError.ErrorCode.ADD_MARKET_DUPLICATED
180 )
181 }
182 // Start tracking a new market
183 let poolPublicCap = getAccount(poolAddr).getCapability<&{LendingInterfaces.PoolPublic}>(LendingConfig.PoolPublicPublicPath)
184 assert(poolPublicCap.check() == true, message:
185 LendingError.ErrorEncode(
186 msg: "Cannot borrow reference to PoolPublic resource",
187 err: LendingError.ErrorCode.CANNOT_ACCESS_POOL_PUBLIC_CAPABILITY
188 )
189 )
190
191 self._markets[poolAddr] = AprSnapshot(poolPublicCap: poolPublicCap)
192 emit MarketDataTracked(
193 market: poolAddr,
194 marketType: poolPublicCap.borrow()!.getUnderlyingTypeString(),
195 startTrackingFrom: getCurrentBlock().timestamp
196 )
197 }
198
199 access(contract) fun eraseMarketData(poolAddr: Address) {
200 pre {
201 self._markets.containsKey(poolAddr) == true:
202 LendingError.ErrorEncode(
203 msg: "Market not tracked yet",
204 err: LendingError.ErrorCode.MARKET_NOT_OPEN
205 )
206 }
207 self._markets.remove(key: poolAddr)
208 emit MarketDataErased(
209 market: poolAddr,
210 erasedFrom: getCurrentBlock().timestamp
211 )
212 }
213
214 pub resource Admin {
215 pub fun trackMarketData(poolAddr: Address) {
216 LendingAprSnapshot.trackMarketData(poolAddr: poolAddr)
217 }
218 pub fun eraseMarketData(poolAddr: Address) {
219 LendingAprSnapshot.eraseMarketData(poolAddr: poolAddr)
220 }
221 }
222
223 init() {
224 self.AdminStoragePath = /storage/lendingAprSnapshotAdmin
225 self._markets = {}
226 self._reservedFields = {}
227
228 destroy <-self.account.load<@AnyResource>(from: self.AdminStoragePath)
229 self.account.save(<-create Admin(), to: self.AdminStoragePath)
230 }
231}