Smart Contract
PegBridge
A.08dd120226ec2213.PegBridge
1/// PegBridge support mint via co-signed message, and burn, will call corresponding PegToken contract
2/// Account of this contract must has Minter/Burner resource for corresponding PegToken
3/// interfaces/resources in FTMinterBurner are needed to avoid token specific types
4import FungibleToken from 0xf233dcee88fe0abe
5import cBridge from 0x08dd120226ec2213
6import PbPegged from 0x08dd120226ec2213
7import DelayedTransfer from 0x08dd120226ec2213
8import VolumeControl from 0x08dd120226ec2213
9// FTMinterBurner is needed for mint/burn
10import FTMinterBurner from 0x08dd120226ec2213
11
12access(all) contract PegBridge {
13 // path for admin resource
14 access(all) let AdminPath: StoragePath
15 // path for FTMinterBurnerMap resource
16 access(all) let FTMBMapPath: StoragePath
17
18 // ========== events ==========
19 access(all) event Mint(
20 mintId: String,
21 receiver: Address,
22 token: String,
23 amount: UFix64,
24 refChId: UInt64,
25 refId: String,
26 depositor: String
27 )
28
29 access(all) event Burn(
30 burnId: String,
31 burner: Address,
32 token: String,
33 amount: UFix64,
34 toChain: UInt64,
35 toAddr: String,
36 nonce: UInt64
37 )
38
39 // ========== structs ==========
40 // token vault type identifier string to its config so we can borrow to deposit minted token
41 access(all) struct TokenCfg {
42 access(all) let vaultPub: PublicPath
43 access(all) let minBurn: UFix64
44 access(all) let maxBurn: UFix64
45 // if mint amount > delayThreshold, put into delayed transfer map
46 access(all) let delayThreshold: UFix64
47 // used for volume controller
48 access(all) let cap: UFix64
49
50 init(vaultPub:PublicPath, minBurn: UFix64, maxBurn: UFix64, delayThreshold: UFix64, cap: UFix64) {
51 self.vaultPub = vaultPub
52 self.minBurn = minBurn
53 self.maxBurn = maxBurn
54 self.delayThreshold = delayThreshold
55 self.cap = cap
56 }
57 }
58
59 // info about one user burn
60 access(all) struct BurnInfo {
61 access(all) let amt: UFix64
62 access(all) let toChId: UInt64
63 access(all) let toAddr: String
64 access(all) let nonce: UInt64
65
66 init(amt: UFix64, toChId: UInt64, toAddr: String, nonce: UInt64) {
67 self.amt = amt
68 self.toChId = toChId
69 self.toAddr = toAddr
70 self.nonce = nonce
71 }
72 }
73
74 // ========== contract states and maps ==========
75 // unique chainid required by cbridge system
76 access(all) let chainID: UInt64
77 // domainPrefix to ensure no replay on co-sign msgs
78 access(contract) let domainPrefix: [UInt8]
79 // similar to solidity pausable
80 access(all) var isPaused: Bool
81
82 // key is token vault identifier, eg. A.1122334455667788.ExampleToken.Vault
83 access(account) var tokMap: {String: TokenCfg}
84 // save for each mint/burn to avoid duplicated process
85 // key is calculated mintID or burnID
86 access(account) var records: {String: Bool}
87
88 access(all) view fun getTokenConfig(identifier: String): TokenCfg {
89 let tokenCfg = self.tokMap[identifier]!
90 return tokenCfg
91 }
92
93 access(all) view fun recordExist(id: String): Bool {
94 return self.records.containsKey(id)
95 }
96
97 // ========== resources ==========
98 access(all) resource PegBridgeAdmin {
99 access(all) fun addTok(identifier: String, tok: TokenCfg) {
100 assert(!PegBridge.tokMap.containsKey(identifier), message: "this token already exist")
101 PegBridge.tokMap[identifier] = tok
102 }
103
104 access(all) fun rmTok(identifier: String) {
105 assert(PegBridge.tokMap.containsKey(identifier), message: "this token do not exist")
106 PegBridge.tokMap.remove(key: identifier)
107 }
108
109 access(all) fun pause() {
110 PegBridge.isPaused = true
111 DelayedTransfer.pause()
112 }
113 access(all) fun unPause() {
114 PegBridge.isPaused = false
115 DelayedTransfer.unPause()
116 }
117
118 access(all) fun createPegBridgeAdmin(): @PegBridgeAdmin {
119 return <-create PegBridgeAdmin()
120 }
121 }
122
123 // token admin must create minter/burner resource and call add
124 access(all) resource interface IAddMinter {
125 access(all) fun addMinter(minter: @{FTMinterBurner.Minter})
126 }
127 access(all) resource interface IAddBurner {
128 access(all) fun addBurner(burner: @{FTMinterBurner.Burner})
129 }
130
131 /// MinterBurnerMap support public add minter/burner by token admin,
132 /// del minter/burner by account, and mint/burn corresponding ft
133 /// when called by this contract
134 access(all) resource MinterBurnerMap: IAddMinter, IAddBurner {
135 // map from token vault identifier to minter or burner resource
136 access(account) var hasMinters: @{String: {FTMinterBurner.Minter}}
137 access(account) var hasBurners: @{String: {FTMinterBurner.Burner}}
138
139 // called by token admin
140 access(all) fun addMinter(minter: @{FTMinterBurner.Minter}) {
141 let idStr = minter.getType().identifier
142 let newIdStr = idStr.slice(from: 0, upTo: idStr.length - 6).concat("Vault")
143 // only supported token minter can be added
144 assert(PegBridge.tokMap.containsKey(newIdStr), message: "this token not support")
145
146 let oldMinter <- self.hasMinters[newIdStr] <- minter
147 destroy oldMinter
148 }
149 access(all) fun addBurner(burner: @{FTMinterBurner.Burner}) {
150 let idStr = burner.getType().identifier
151 let newIdStr = idStr.slice(from: 0, upTo: idStr.length - 6).concat("Vault")
152 // only supported token burner can be added
153 assert(PegBridge.tokMap.containsKey(newIdStr), message: "this token not support")
154 let old <- self.hasBurners[newIdStr] <- burner
155 destroy old
156 }
157
158 // func in this contract or func of other contract in this account can call this as not exposed by public path, other contracts under
159 access(account) fun delMinter(idStr: String) {
160 let minter <- self.hasMinters.remove(key: idStr) ?? panic("missing Minter")
161 destroy minter
162 }
163 access(account) fun delBurner(idStr: String) {
164 let burner <- self.hasBurners.remove(key: idStr) ?? panic("missing Burner")
165 destroy burner
166 }
167
168 // for extra security, only this contract can call mint/burn
169 access(contract) fun mint(id:String, amt: UFix64): @{FungibleToken.Vault} {
170 let minter = (&self.hasMinters[id] as &{FTMinterBurner.Minter}?)!
171 return <- minter.mintTokens(amount: amt)
172 }
173 access(contract) fun burn(id:String, from: @{FungibleToken.Vault}) {
174 let burner = (&self.hasBurners[id] as &{FTMinterBurner.Burner}?)!
175 burner.burnTokens(from: <- from)
176 }
177 init(){
178 self.hasMinters <- {}
179 self.hasBurners <- {}
180 }
181 }
182
183 // ========== functions ==========
184 init(chID:UInt64) {
185 self.chainID = chID
186 // domainPrefix is chainID big endianbytes followed by "A.xxxxxx.PegBridge".utf8, xxxx is this contract account
187 self.domainPrefix = chID.toBigEndianBytes().concat(self.getType().identifier.utf8)
188 self.isPaused = false
189
190 self.records = {}
191 self.tokMap = {}
192
193 self.AdminPath = /storage/PegBridgeAdmin
194 self.account.storage.save<@PegBridgeAdmin>(<- create PegBridgeAdmin(), to: self.AdminPath)
195
196 self.FTMBMapPath = /storage/FTMinterBurnerMap
197 // needed for minter/burner
198 self.account.storage.save(<-create MinterBurnerMap(), to: self.FTMBMapPath)
199 // anyone can call /public/AddMinter to add a minter to map
200 let addMinterCapability = self.account.capabilities.storage.issue<&{IAddMinter}>(self.FTMBMapPath)
201 self.account.capabilities.publish(addMinterCapability, at: /public/AddMinter)
202 let addBurnerCapability = self.account.capabilities.storage.issue<&{IAddBurner}>(self.FTMBMapPath)
203 self.account.capabilities.publish(addBurnerCapability, at: /public/AddBurner)
204 }
205
206 access(all) fun mint(token: String, pbmsg: [UInt8], sigs: [cBridge.SignerSig]) {
207 pre {
208 !self.isPaused: "contract is paused"
209 }
210 let domain = self.domainPrefix.concat("Mint".utf8)
211 assert(cBridge.verify(data: domain.concat(pbmsg), sigs: sigs), message: "verify sigs failed")
212 let mintInfo = PbPegged.Mint(pbmsg)
213 assert(mintInfo.eqToken(tkStr: token), message: "mismatch token string")
214
215 let tokCfg = PegBridge.tokMap[token] ?? panic("token not support in contract")
216 VolumeControl.updateVolume(token: token, amt: mintInfo.amount, cap: tokCfg.cap)
217
218 let mintId = String.encodeHex(HashAlgorithm.SHA3_256.hash(pbmsg))
219 assert(!self.records.containsKey(mintId), message: "mintId already exists")
220 self.records[mintId] = true
221 //FIX
222 let receiverCap = getAccount(mintInfo.receiver).capabilities.get<&{FungibleToken.Receiver}>(tokCfg.vaultPub)
223 let capability = self.account.capabilities.storage.issue<&MinterBurnerMap>(self.FTMBMapPath)
224 let minterMap = capability.borrow()!
225 let mintedVault: @{FungibleToken.Vault} <- minterMap.mint(id: token, amt: mintInfo.amount)
226
227 if mintInfo.amount > tokCfg.delayThreshold {
228 // add to delayed xfer
229 DelayedTransfer.addDelayXfer(id: mintId, receiverCap: receiverCap, from: <- mintedVault)
230 } else {
231 let receiverRef = receiverCap.borrow() ?? panic("Could not borrow a reference to the receiver")
232 receiverRef.deposit(from: <- mintedVault)
233 }
234
235 emit Mint(
236 mintId: mintId,
237 receiver: mintInfo.receiver,
238 token: token,
239 amount: mintInfo.amount,
240 refChId: mintInfo.refChainId,
241 refId: mintInfo.refId,
242 depositor: mintInfo.depositor
243 )
244 }
245 //
246 access(all) fun burn(from: auth(FungibleToken.Withdraw)&{FungibleToken.Provider}, info:BurnInfo) {
247 pre {
248 !self.isPaused: "contract is paused"
249 }
250 let user = from.owner!.address
251 let tokStr = from.getType().identifier
252 let tokenCfg = self.tokMap[tokStr]!
253 assert(info.amt >= tokenCfg.minBurn, message: "burn amount less than min burn")
254 if tokenCfg.maxBurn > 0.0 {
255 assert(info.amt < tokenCfg.maxBurn, message: "burn amount larger than max burn")
256 }
257 // calculate burnId
258 let concatStr = user.toString().concat(tokStr).concat(info.amt.toString()).concat(info.nonce.toString())
259 let burnId = String.encodeHex(HashAlgorithm.SHA3_256.hash(concatStr.utf8))
260 assert(!self.records.containsKey(burnId), message: "burnId already exists")
261 self.records[burnId] = true
262
263 let capability = self.account.capabilities.storage.issue<&MinterBurnerMap>(self.FTMBMapPath)
264 let mbMap = capability.borrow()!
265 let burnVault <-from.withdraw(amount: info.amt)
266 mbMap.burn(id: tokStr, from: <-burnVault)
267
268 emit Burn(
269 burnId: burnId,
270 burner: user,
271 token: tokStr,
272 amount: info.amt,
273 toChain: info.toChId,
274 toAddr: info.toAddr,
275 nonce: info.nonce
276 )
277 }
278 // large amount mint
279 access(all) fun executeDelayedTransfer(mintId: String) {
280 pre {
281 !self.isPaused: "contract is paused"
282 }
283 DelayedTransfer.executeDelayXfer(mintId)
284 }
285}