Smart Contract

cBridge

A.08dd120226ec2213.cBridge

Valid From

86,177,924

Deployed

3d ago
Feb 24, 2026, 11:53:21 PM UTC

Dependents

10 imports
1import Pb from 0x08dd120226ec2213
2
3// only need to support signer verify for now
4access(all) contract cBridge {
5
6    access(all) event SignersUpdated(
7      signers: {String: UInt256}
8    )
9
10    // path for admin resource
11    access(all) let AdminPath: StoragePath
12    // unique chainid required by cbridge system
13    access(all) let chainID: UInt64
14    access(contract) let domainPrefix: [UInt8]
15    // save map from pubkey to power, only visible in this contract to extra safety
16    access(contract) var signers: {String: UInt256}
17    access(all) var totalPower: UInt256
18    // block height when signers are last updated by sigs
19    // flow block.timestamp is UFix64 so hard to use
20    access(all) var signerUpdateBlock: UInt64
21
22    // at first we require pubkey to be also present in SignerSig, so verify can just do map lookup in signers
23    // but this make other components in the system to be aware of pubkeys which is hard to do
24    // so without pubkey, we just loop over all signers and call verify, it's less efficient but won't be an issue
25    // as there won't be many signers. we keep struct for future flexibility but pubkey field is NOT needed.
26    access(all) struct SignerSig {
27        access(all) let sig: [UInt8]
28        init(sig: [UInt8]) {
29            self.sig = sig
30        }
31    }
32
33    // Admin is a special authorization resource that 
34    // allows the owner to perform important functions to modify the contract state
35    access(all) resource BridgeAdmin {
36        access(all) fun resetSigners(signers: {String: UInt256}) {
37            cBridge.signers = signers
38            cBridge.totalPower = 0
39            for power in signers.values {
40                cBridge.totalPower = cBridge.totalPower + power
41            }
42            emit SignersUpdated(
43              signers: signers
44            )
45        }
46        access(all) fun createBridgeAdmin(): @BridgeAdmin {
47            return <-create BridgeAdmin()
48        }
49    }
50
51    // getter for signers/power as self.signers is not accessible outside contract
52    access(all) view fun getSigners(): {String: UInt256} {
53        return self.signers
54    }
55
56    // return true if sigs have more than 2/3 total power
57    access(all) fun verify(data:[UInt8], sigs: [SignerSig]): Bool {
58        var signedPower: UInt256 = 0
59        var quorum: UInt256 = (self.totalPower*2)/3 + 1
60        // go over all known signers, for each signer, try to find which sig matches
61        for pubkey in self.signers.keys {
62            let pk = PublicKey(
63                    publicKey: pubkey.decodeHex(),
64                    signatureAlgorithm: SignatureAlgorithm.ECDSA_secp256k1
65            )
66            // for this pk, try to find the matched sig
67            // flow doc says `for index, element in array` but it's only in master https://github.com/onflow/cadence/issues/1230
68            var idx = -1
69            for ssIdx, ss in sigs {
70                if pk.verify(
71                    signature: ss.sig,
72                    signedData: data,
73                    domainSeparationTag: "FLOW-V0.0-user",
74                    hashAlgorithm: HashAlgorithm.SHA3_256
75                ) {
76                    signedPower = signedPower + self.signers[pubkey]!
77                    if signedPower >= quorum {
78                        return true
79                    }
80                    // delete ss from sigs
81                    idx = ssIdx
82                    // now we break the sig loop to check next signer as we only allow one sig per signer
83                    break
84                }
85            }
86            if idx >=0 {
87                sigs.remove(at: idx)
88            }
89        }
90        return false
91    }
92
93    init(chID:UInt64) {
94      self.chainID = chID
95      // domainPrefix is chainID big endianbytes followed by "A.xxxxxx.cBridge".utf8, xxxx is this contract account
96      self.domainPrefix = chID.toBigEndianBytes().concat(self.getType().identifier.utf8)
97  
98      self.signers = {}
99      self.totalPower = 0
100
101      self.signerUpdateBlock = 0
102
103      self.AdminPath = /storage/cBridgeAdmin
104      self.account.storage.save<@BridgeAdmin>(<- create BridgeAdmin(), to: self.AdminPath)
105    }
106
107    // below are struct and func needed to update cBridge.signers by cosigned msg
108    // blockheight is like nonce to avoid replay attack
109    /* proto definition
110    message PbSignerPowerMap {
111        uint64 blockheight = 1; // block when this pb was proposed by sgn, must < currentblock
112        repeated SignerPower = 2;
113    }
114    message SignerPower {
115        bytes pubkey = 1; // raw bytes of pubkey, 64 bytes
116        bytes power = 2; // big.Int.Bytes
117    }
118    */
119    access(all) struct PbSignerPowerMap {
120        access(all) var blockheight: UInt64
121        access(all) let signerMap: {String: UInt256}
122        access(all) var totalPower: UInt256
123        // raw is serialized PbSignerPowerMap
124        init(_ raw: [UInt8]) {
125            self.blockheight = 0
126            self.signerMap = {}
127            self.totalPower = 0
128            // now decode pbraw and assign
129            let buf = Pb.Buffer(raw: raw)
130            while buf.hasMore() {
131              let tagType = buf.decKey()
132              switch Int(tagType.tag) {
133                case 1:
134                  assert(tagType.wt==Pb.WireType.Varint, message: "wrong wire type")
135                  self.blockheight = buf.decVarint()
136                case 2:
137                  assert(tagType.wt==Pb.WireType.LengthDelim, message: "wrong wire type")
138                  // now decode SignerPower msg
139                  // pegbridge.pb.go, as the proto code show, repeated field has a key outside
140                  let tag0 = buf.decKey()
141                  let tag1 = buf.decKey()
142                  assert(tag1.tag==1, message: "tag not 1: ".concat(tag1.tag.toString()))
143                  assert(tag1.wt==Pb.WireType.LengthDelim, message: "wrong wire type")
144                  let pbhex = Pb.toString(buf.decBytes())
145                  let tag2 = buf.decKey()
146                  assert(tag2.tag==2, message: "tag not 2: ".concat(tag2.tag.toString()))
147                  assert(tag2.wt==Pb.WireType.LengthDelim, message: "wrong wire type")
148                  let power = Pb.toUInt256(buf.decBytes())
149                  // add to map
150                  self.signerMap[pbhex] = power
151                  self.totalPower = self.totalPower + power
152                default:
153                  assert(false, message: "unsupported tag")
154              }
155            }
156        }
157    }
158    // new signers can be updated if cosigned by existing signers w/ 2/3 power
159    // pbmsg is serialized PbSignerPowerMap
160    access(all) fun updateSigners(pbmsg:[UInt8], sigs: [SignerSig]) {
161      let domain = self.domainPrefix.concat(".UpdateSigners".utf8)
162      assert(self.verify(data: domain.concat(pbmsg), sigs: sigs), message: "verify sigs failed")
163      let newSigners = PbSignerPowerMap(pbmsg)
164      assert(newSigners.totalPower > 0, message: "total power must > 0")
165      assert(newSigners.blockheight > self.signerUpdateBlock, message: "new signer timestamp must be larger")
166      assert(newSigners.blockheight <= getCurrentBlock().height, message: "new signer timestamp must <= current")
167      // update signerUpdateBlock and signers
168      self.signerUpdateBlock = getCurrentBlock().height
169      self.totalPower = newSigners.totalPower
170      self.signers = newSigners.signerMap
171
172      emit SignersUpdated(
173        signers: newSigners.signerMap
174      )
175    }
176}
177