Smart Contract

SafeBox

A.08dd120226ec2213.SafeBox

Deployed

1w ago
Feb 15, 2026, 03:03:12 PM UTC

Dependents

105 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import cBridge from 0x08dd120226ec2213
3import PbPegged from 0x08dd120226ec2213
4// DelayedTransfer must from same account as addXfer is limited to access(account) to avoid spam
5import DelayedTransfer from 0x08dd120226ec2213
6import VolumeControl from 0x08dd120226ec2213
7
8// user deposit into SafeBox, cBridge will mint corresponding ERC20 tokens on specified dest chain.
9// when user burn ERC20 tokens, cBridge will withdraw FungibleTokens to user specified receiver address
10access(all) contract SafeBox {
11  // path for admin resource
12    access(all) let AdminPath: StoragePath
13  // ========== events ==========
14  access(all) event Deposited(
15    depoId: String,
16    depositor: Address,
17    token: String,
18    amount: UFix64,
19    mintChId: UInt64,
20    mintAddr: String,
21    nonce: UInt64
22  )
23
24  access(all) event Withdrawn(
25    wdId: String,
26    receiver: Address,
27    token: String,
28    amount: UFix64,
29    refChId: UInt64,
30    burnAddr: String,
31    refId: String
32  )
33
34  // ========== structs ==========
35  // token vault type identifier string to its config so we can borrow for deposit/withdraw
36  access(all) struct TokenCfg {
37    access(all) let vaultPub: PublicPath
38    access(all) let vaultSto: StoragePath
39    access(all) let minDepo: UFix64
40    access(all) let maxDepo: UFix64
41    // if withdraw amount > delayThreshold, put into delayed transfer map
42    access(all) let delayThreshold: UFix64
43    // used for volume controller
44    access(all) let cap: UFix64
45
46    init(vaultPub:PublicPath, vaultSto: StoragePath, minDepo: UFix64, maxDepo: UFix64, delayThreshold: UFix64, cap: UFix64) {
47      self.vaultPub = vaultPub
48      self.vaultSto = vaultSto
49      self.minDepo = minDepo
50      self.maxDepo = maxDepo
51      self.delayThreshold = delayThreshold
52      self.cap = cap
53    }
54  }
55  // info about one user deposit
56  access(all) struct DepoInfo {
57    access(all) let amt: UFix64
58    access(all) let mintChId: UInt64
59    access(all) let mintAddr: String
60    access(all) let nonce: UInt64
61
62    init(amt: UFix64, mintChId: UInt64, mintAddr: String, nonce: UInt64) {
63      self.amt = amt
64      self.mintChId = mintChId
65      self.mintAddr = mintAddr
66      self.nonce = nonce
67    }
68  }
69
70  // ========== contract states and maps ==========
71  // unique chainid required by cbridge system
72  access(all) let chainID: UInt64
73  // domainPrefix to ensure no replay on co-sign msgs
74  access(contract) let domainPrefix: [UInt8]
75  // similar to solidity pausable
76  access(all) var isPaused: Bool
77
78  // key is token vault identifier, eg. A.1122334455667788.ExampleToken.Vault
79  access(account) var tokMap: {String: TokenCfg}
80  // save for each deposit/withdraw to avoid duplicated process
81  // key is calculated depoId or wdId
82  access(account) var records: {String: Bool}
83
84  access(all) view fun getTokenConfig(identifier: String): TokenCfg {
85    let tokenCfg = self.tokMap[identifier]!
86    return tokenCfg
87  }
88
89  access(all) view fun recordExist(id: String): Bool {
90    return self.records.containsKey(id)
91  }
92
93  // ========== resource ==========
94  access(all) resource SafeBoxAdmin {
95    access(all) fun addTok(identifier: String, tok: TokenCfg) {
96      assert(!SafeBox.tokMap.containsKey(identifier), message: "this token already exist")
97      SafeBox.tokMap[identifier] = tok
98    }
99
100    access(all) fun rmTok(identifier: String) {
101      assert(SafeBox.tokMap.containsKey(identifier), message: "this token do not exist")
102      SafeBox.tokMap.remove(key: identifier)
103    }
104
105    access(all) fun pause() {
106      SafeBox.isPaused = true
107      DelayedTransfer.pause()
108    }
109    access(all) fun unPause() {
110      SafeBox.isPaused = false
111      DelayedTransfer.unPause()
112    }
113
114    access(all) fun createSafeBoxAdmin(): @SafeBoxAdmin {
115        return <-create SafeBoxAdmin()
116    }
117  }
118
119  // ========== functions ==========
120  // chainid must be same as cbridge common proto FLOW_MAINNET = 12340001; FLOW_TEST = 12340002;
121  init(chID:UInt64) {
122    self.chainID = chID
123    // domainPrefix is chainID big endianbytes followed by "A.xxxxxx.SafeBox".utf8, xxxx is this contract account
124    self.domainPrefix = chID.toBigEndianBytes().concat(self.getType().identifier.utf8)
125    self.isPaused = false
126
127    self.records = {}
128    self.tokMap = {}
129
130    self.AdminPath = /storage/SafeBoxAdmin
131    self.account.storage.save<@SafeBoxAdmin>(<- create SafeBoxAdmin(), to: self.AdminPath)
132  }
133
134  access(all) fun deposit(from: auth(FungibleToken.Withdraw)&{FungibleToken.Provider}, info:DepoInfo) {
135    pre {
136      !self.isPaused: "contract is paused"
137    }
138    let user = from.owner!.address
139    let tokStr = from.getType().identifier
140    let tokenCfg = self.tokMap[tokStr]!
141    assert(info.amt >= tokenCfg.minDepo, message: "deposit amount less than min deposit")
142    if tokenCfg.maxDepo > 0.0 {
143      assert(info.amt < tokenCfg.maxDepo, message: "deposit amount larger than max deposit")
144    }
145    // calculate depoId
146    let concatStr = user.toString().concat(tokStr).concat(info.amt.toString()).concat(info.nonce.toString())
147    let depoId = String.encodeHex(HashAlgorithm.SHA3_256.hash(concatStr.utf8))
148    assert(!self.records.containsKey(depoId), message: "depoId already exists")
149    self.records[depoId] = true
150
151    let recev = self.account.capabilities.borrow<&{FungibleToken.Receiver}>(tokenCfg.vaultPub)
152                      ?? panic("Could not borrow a reference to the receiver")
153    recev.deposit(from: <-from.withdraw(amount: info.amt))
154    emit Deposited(
155      depoId: depoId,
156      depositor: user,
157      token: tokStr,
158      amount: info.amt,
159      mintChId: info.mintChId,
160      mintAddr: info.mintAddr,
161      nonce: info.nonce
162    )
163  }
164
165  // we can also use recipient: &AnyResource{FungibleToken.Receiver} to do deposit.
166  // but now, we use the tokCfg pubPath to get the Receiver first.
167  access(all) fun withdraw(token: String, wdmsg: [UInt8], sigs: [cBridge.SignerSig]) {
168    pre {
169      !self.isPaused: "contract is paused"
170    }
171    // calculate correct data by prefix domain, sgn needs to sign the same way
172    let domain = self.domainPrefix.concat("Withdraw".utf8)
173    assert(cBridge.verify(data: domain.concat(wdmsg), sigs: sigs), message: "verify sigs failed")
174    let wdInfo = PbPegged.Withdraw(wdmsg)
175    assert(wdInfo.eqToken(tkStr: token), message: "mismatch token string")
176    // calculate wdId and check records map
177    // withdraw from self storage
178    // and deposit into receiver
179    // emit Withdrawn
180    let tokCfg = SafeBox.tokMap[token] ?? panic("token not support in contract")
181    VolumeControl.updateVolume(token: token, amt: wdInfo.amount, cap: tokCfg.cap)
182    let wdId = String.encodeHex(HashAlgorithm.SHA3_256.hash(wdmsg))
183
184    assert(!self.records.containsKey(wdId), message: "wdId already exists")
185    self.records[wdId] = true
186    let receiverCap = getAccount(wdInfo.receiver).capabilities.get<&{FungibleToken.Receiver}>(tokCfg.vaultPub)
187    let capability = self.account.capabilities.storage.issue<auth(FungibleToken.Withdraw)&{FungibleToken.Provider}>(tokCfg.vaultSto)
188    
189    let vaultRef = capability.borrow() ?? panic("Could not borrow reference to the owner's Vault!")
190    // vault that holds to deposit ft
191    let vault <- vaultRef.withdraw(amount: wdInfo.amount)
192
193    // vault type validation
194    let receiverRef = receiverCap.borrow() ?? panic("Could not borrow a reference to the receiver")
195    assert(receiverRef.isSupportedVaultType(type: vault.getType()), message: "unsupported vault type")
196
197    if wdInfo.amount > tokCfg.delayThreshold {
198        // add to delayed xfer
199        DelayedTransfer.addDelayXfer(id: wdId, receiverCap: receiverCap, from: <- vault)
200    } else {
201        // deposit into receiver
202        receiverRef.deposit(from: <- vault)
203    }
204
205    // emit withdrawn even added to delay, to be consistent with solidity
206    emit Withdrawn(
207      wdId: wdId,
208      receiver: wdInfo.receiver,
209      token: token,
210      amount: wdInfo.amount,
211      refChId: wdInfo.refChainId,
212      burnAddr: wdInfo.burnAccount,
213      refId: wdInfo.refId
214    )
215  }
216
217  access(all) fun executeDelayedTransfer(wdId: String) {
218    pre {
219      !self.isPaused: "contract is paused"
220    }
221    DelayedTransfer.executeDelayXfer(wdId)
222  }
223}
224