Smart Contract

ActionRouterV2

A.79f5b5b0f95a160b.ActionRouterV2

Valid From

125,677,988

Deployed

1w ago
Feb 19, 2026, 08:25:23 PM UTC

Dependents

1 imports
1import FlowToken from 0x1654653399040a61
2import stFlowToken from 0xd6f80565193ad727
3import LiquidStaking from 0xd6f80565193ad727
4import FungibleToken from 0xf233dcee88fe0abe
5
6access(all) contract ActionRouterV2 {
7    
8    access(all) let AdminStoragePath: StoragePath
9    access(all) let FlowVaultStoragePath: StoragePath
10    access(all) let StFlowVaultStoragePath: StoragePath
11    
12    // Changed: Store Ethereum addresses as strings instead of Flow addresses
13    access(self) var authorizedEVMCallers: {String: Bool}
14    access(self) var isActive: Bool
15    access(self) var minStakeAmount: UFix64
16    access(self) var maxStakeAmount: UFix64
17    access(self) var maxOperationsPerBlock: UInt64
18    access(self) var currentBlockOperations: UInt64
19    access(self) var lastBlockHeight: UInt64
20    
21    access(all) var totalStakeOperations: UInt64
22    access(all) var totalUnstakeOperations: UInt64
23    access(all) var totalFlowStaked: UFix64
24    access(all) var totalStFlowMinted: UFix64
25    
26    // Updated events to use String for EVM addresses
27    access(all) event StakeExecuted(amount: UFix64, recipient: String, stFlowReceived: UFix64, exchangeRate: UFix64, requestId: String)
28    access(all) event UnstakeExecuted(amount: UFix64, recipient: String, flowReceived: UFix64, requestId: String)
29    access(all) event EVMCallerAuthorized(caller: String)
30    access(all) event EVMCallerRevoked(caller: String)
31    access(all) event RouterConfigUpdated(minStake: UFix64, maxStake: UFix64)
32    
33    // Struct definitions moved inside the contract
34    access(all) struct StakeResult {
35        access(all) let flowAmount: UFix64
36        access(all) let stFlowReceived: UFix64
37        access(all) let exchangeRate: UFix64
38        access(all) let requestId: String
39        access(all) let success: Bool
40        
41        init(flowAmount: UFix64, stFlowReceived: UFix64, exchangeRate: UFix64, requestId: String, success: Bool) {
42            self.flowAmount = flowAmount
43            self.stFlowReceived = stFlowReceived
44            self.exchangeRate = exchangeRate
45            self.requestId = requestId
46            self.success = success
47        }
48    }
49
50    access(all) struct UnstakeResult {
51        access(all) let stFlowAmount: UFix64
52        access(all) let flowReceived: UFix64
53        access(all) let requestId: String
54        access(all) let success: Bool
55        
56        init(stFlowAmount: UFix64, flowReceived: UFix64, requestId: String, success: Bool) {
57            self.stFlowAmount = stFlowAmount
58            self.flowReceived = flowReceived
59            self.requestId = requestId
60            self.success = success
61        }
62    }
63
64    access(all) struct RouterStats {
65        access(all) let totalStakeOps: UInt64
66        access(all) let totalUnstakeOps: UInt64
67        access(all) let totalFlowStaked: UFix64
68        access(all) let totalStFlowMinted: UFix64
69        access(all) let currentFlowBalance: UFix64
70        access(all) let currentStFlowBalance: UFix64
71        access(all) let isActive: Bool
72        
73        init(
74            totalStakeOps: UInt64,
75            totalUnstakeOps: UInt64,
76            totalFlowStaked: UFix64,
77            totalStFlowMinted: UFix64,
78            currentFlowBalance: UFix64,
79            currentStFlowBalance: UFix64,
80            isActive: Bool
81        ) {
82            self.totalStakeOps = totalStakeOps
83            self.totalUnstakeOps = totalUnstakeOps
84            self.totalFlowStaked = totalFlowStaked
85            self.totalStFlowMinted = totalStFlowMinted
86            self.currentFlowBalance = currentFlowBalance
87            self.currentStFlowBalance = currentStFlowBalance
88            self.isActive = isActive
89        }
90    }
91    
92    init() {
93        self.AdminStoragePath = /storage/ActionRouterV2Admin
94        self.FlowVaultStoragePath = /storage/ActionRouterV2FlowVault
95        self.StFlowVaultStoragePath = /storage/ActionRouterV2StFlowVault
96        
97        self.authorizedEVMCallers = {}
98        self.isActive = true
99        self.minStakeAmount = 1.0
100        self.maxStakeAmount = 10000.0
101        self.maxOperationsPerBlock = 10
102        self.currentBlockOperations = 0
103        self.lastBlockHeight = getCurrentBlock().height
104        
105        self.totalStakeOperations = 0
106        self.totalUnstakeOperations = 0
107        self.totalFlowStaked = 0.0
108        self.totalStFlowMinted = 0.0
109        
110        let admin <- create Admin()
111        self.account.storage.save(<-admin, to: self.AdminStoragePath)
112        
113        let flowVault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
114        self.account.storage.save(<-flowVault, to: self.FlowVaultStoragePath)
115        
116        // Only create the capability if it doesn't already exist
117        if self.account.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver).check() == false {
118            let flowReceiverCap = self.account.capabilities.storage.issue<&{FungibleToken.Receiver}>(self.FlowVaultStoragePath)
119            self.account.capabilities.publish(flowReceiverCap, at: /public/flowTokenReceiver)
120        }
121    }
122    
123    access(all) resource Admin {
124        
125        // Updated: Accept Ethereum address as string
126        access(all) fun authorizeEVMCaller(caller: String) {
127            pre {
128                caller.length > 0: "Caller address cannot be empty"
129                caller.slice(from: 0, upTo: 2) == "0x": "Caller must be a valid Ethereum address starting with 0x"
130                caller.length == 42: "Ethereum address must be 42 characters long"
131            }
132            ActionRouterV2.authorizedEVMCallers[caller] = true
133            emit EVMCallerAuthorized(caller: caller)
134        }
135        
136        access(all) fun revokeEVMCaller(caller: String) {
137            ActionRouterV2.authorizedEVMCallers.remove(key: caller)
138            emit EVMCallerRevoked(caller: caller)
139        }
140        
141        access(all) fun setActive(active: Bool) {
142            ActionRouterV2.isActive = active
143        }
144        
145        access(all) fun updateLimits(minStake: UFix64, maxStake: UFix64, maxOpsPerBlock: UInt64) {
146            pre {
147                minStake > 0.0: "Minimum stake must be positive"
148                maxStake >= minStake: "Maximum stake must be >= minimum"
149                maxOpsPerBlock > 0: "Max operations per block must be positive"
150            }
151            ActionRouterV2.minStakeAmount = minStake
152            ActionRouterV2.maxStakeAmount = maxStake
153            ActionRouterV2.maxOperationsPerBlock = maxOpsPerBlock
154            emit RouterConfigUpdated(minStake: minStake, maxStake: maxStake)
155        }
156        
157        access(all) fun emergencyWithdraw(amount: UFix64, recipient: Address) {
158            pre {
159                amount > 0.0: "Amount must be positive"
160            }
161            
162            let flowVaultRef = ActionRouterV2.account.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: ActionRouterV2.FlowVaultStoragePath)
163                ?? panic("Could not borrow FlowToken vault")
164            
165            let withdrawnFlow <- flowVaultRef.withdraw(amount: amount)
166            
167            let recipientAccount = getAccount(recipient)
168            let recipientFlowVault = recipientAccount.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver).borrow()
169                ?? panic("Could not borrow recipient FlowToken receiver")
170            
171            recipientFlowVault.deposit(from: <-withdrawnFlow)
172        }
173    }
174    
175    access(self) fun checkRateLimit() {
176        let currentBlock = getCurrentBlock().height
177        
178        if currentBlock > self.lastBlockHeight {
179            self.currentBlockOperations = 0
180            self.lastBlockHeight = currentBlock
181        }
182        
183        assert(
184            self.currentBlockOperations < self.maxOperationsPerBlock,
185            message: "Rate limit exceeded for this block"
186        )
187        
188        self.currentBlockOperations = self.currentBlockOperations + 1
189    }
190    
191    // Updated: Accept Ethereum address as string
192    access(all) fun stakeFlow(amount: UFix64, recipient: String, requestId: String): StakeResult {
193        pre {
194            self.isActive: "Router is not active"
195            self.authorizedEVMCallers[recipient] == true: "Caller not authorized"
196            amount >= self.minStakeAmount: "Amount below minimum"
197            amount <= self.maxStakeAmount: "Amount above maximum"
198        }
199        
200        self.checkRateLimit()
201        
202        let flowVaultRef = self.account.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: self.FlowVaultStoragePath)
203            ?? panic("Could not borrow FlowToken vault")
204        
205        assert(flowVaultRef.balance >= amount, message: "Insufficient FLOW balance in router")
206        
207        let flowToStake <- flowVaultRef.withdraw(amount: amount) as! @FlowToken.Vault
208        let exchangeRateBefore = 1.0 // Placeholder
209        let stFlowVault <- LiquidStaking.stake(flowVault: <-flowToStake)
210        let stFlowAmount = stFlowVault.balance
211        let exchangeRateAfter = 1.0 // Placeholder
212        
213        let existingStFlowVaultRef = self.account.storage.borrow<&stFlowToken.Vault>(from: self.StFlowVaultStoragePath)
214        if existingStFlowVaultRef != nil {
215            existingStFlowVaultRef!.deposit(from: <-stFlowVault)
216        } else {
217            self.account.storage.save(<-stFlowVault, to: self.StFlowVaultStoragePath)
218        }
219        
220        self.totalStakeOperations = self.totalStakeOperations + 1
221        self.totalFlowStaked = self.totalFlowStaked + amount
222        self.totalStFlowMinted = self.totalStFlowMinted + stFlowAmount
223        
224        emit StakeExecuted(
225            amount: amount,
226            recipient: recipient,
227            stFlowReceived: stFlowAmount,
228            exchangeRate: exchangeRateAfter,
229            requestId: requestId
230        )
231        
232        return StakeResult(
233            flowAmount: amount,
234            stFlowReceived: stFlowAmount,
235            exchangeRate: exchangeRateAfter,
236            requestId: requestId,
237            success: true
238        )
239    }
240    
241    // Updated: Accept Ethereum address as string
242    access(all) fun unstakeFlow(stFlowAmount: UFix64, recipient: String, requestId: String): UnstakeResult {
243        pre {
244            self.isActive: "Router is not active"
245            self.authorizedEVMCallers[recipient] == true: "Caller not authorized"
246            stFlowAmount > 0.0: "Amount must be positive"
247        }
248        
249        self.checkRateLimit()
250        
251        let stFlowVaultRef = self.account.storage.borrow<auth(FungibleToken.Withdraw) &stFlowToken.Vault>(from: self.StFlowVaultStoragePath)
252            ?? panic("Could not borrow stFlowToken vault")
253        
254        assert(stFlowVaultRef.balance >= stFlowAmount, message: "Insufficient stFLOW balance")
255        
256        let stFlowToUnstake <- stFlowVaultRef.withdraw(amount: stFlowAmount) as! @stFlowToken.Vault
257        let withdrawVoucher <- LiquidStaking.unstake(stFlowVault: <-stFlowToUnstake)
258        
259        let flowAmount = 0.0 // Placeholder
260        destroy withdrawVoucher
261        
262        self.totalUnstakeOperations = self.totalUnstakeOperations + 1
263        
264        emit UnstakeExecuted(
265            amount: stFlowAmount,
266            recipient: recipient,
267            flowReceived: flowAmount,
268            requestId: requestId
269        )
270        
271        return UnstakeResult(
272            stFlowAmount: stFlowAmount,
273            flowReceived: flowAmount,
274            requestId: requestId,
275            success: true
276        )
277    }
278    
279    access(all) view fun getExchangeRate(): UFix64 {
280        return 1.0 // Placeholder
281    }
282    
283    access(all) view fun getStFlowBalance(): UFix64 {
284        let stFlowVaultRef = self.account.storage.borrow<&stFlowToken.Vault>(from: self.StFlowVaultStoragePath)
285        if stFlowVaultRef != nil {
286            return stFlowVaultRef!.balance
287        }
288        return 0.0
289    }
290    
291    access(all) view fun getFlowBalance(): UFix64 {
292        let flowVaultRef = self.account.storage.borrow<&FlowToken.Vault>(from: self.FlowVaultStoragePath)
293        if flowVaultRef != nil {
294            return flowVaultRef!.balance
295        }
296        return 0.0
297    }
298    
299    access(all) fun getStats(): RouterStats {
300        return RouterStats(
301            totalStakeOps: self.totalStakeOperations,
302            totalUnstakeOps: self.totalUnstakeOperations,
303            totalFlowStaked: self.totalFlowStaked,
304            totalStFlowMinted: self.totalStFlowMinted,
305            currentFlowBalance: self.getFlowBalance(),
306            currentStFlowBalance: self.getStFlowBalance(),
307            isActive: self.isActive
308        )
309    }
310    
311    // Updated: Accept Ethereum address as string
312    access(all) view fun isAuthorized(caller: String): Bool {
313        return self.authorizedEVMCallers[caller] == true
314    }
315}