Smart Contract

PegBridge

A.08dd120226ec2213.PegBridge

Deployed

1d ago
Feb 28, 2026, 02:31:18 AM UTC

Dependents

0 imports
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}