Smart Contract
SafeBox
A.08dd120226ec2213.SafeBox
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