github.com/s7techlab/cckit@v0.10.5/examples/erc20/README.md (about) 1 # How to create ERC20 token on Hyperledger Fabric 2 3 As well as Ethereum blockchain, Hyperledger Fabric platform (HLF) can be used for token creation, implemented as 4 smart contract (chaincode in HLF terminology), that holds user balances. Unlike Ethereum, HLF chaincodes can't work 5 with user addresses as a holder key, thus we will use combination of Membership Service Provider (MSP) Identifier 6 and user certificate identifier. Below is an simple example of how to create a token as Golang chaincode on the 7 Hyperledger Fabric platform using CCKit chaincode library. 8 9 ## What is ERC20 token standard 10 11 The [ERC20 token standard](https://github.com/ethereum/eips/issues/20) came about as an attempt to standardize token 12 smart contracts in Ethereum, it describes the functions and events that an Ethereum token contract has to implement. 13 Most of the major tokens on the Ethereum blockchain are ERC20-compliant. ERC-20 has many benefits, including unifying 14 token wallets and ability for exchanges to list more tokens by providing nothing more than the address of the token’s contract. 15 16 ```solidity 17 // ---------------------------------------------------------------------------- 18 // ERC Token Standard #20 Interface 19 // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md 20 // ---------------------------------------------------------------------------- 21 contract ERC20Interface { 22 function totalSupply() public constant returns (uint); 23 function balanceOf(address tokenOwner) public constant returns (uint balance); 24 function allowance(address tokenOwner, address spender) public constant returns (uint remaining); 25 function transfer(address to, uint tokens) public returns (bool success); 26 function approve(address spender, uint tokens) public returns (bool success); 27 function transferFrom(address from, address to, uint tokens) public returns (bool success); 28 29 event Transfer(address indexed from, address indexed to, uint tokens); 30 event Approval(address indexed tokenOwner, address indexed spender, uint tokens); 31 } 32 ``` 33 34 ## ERC20 implementation basics 35 36 Essentially, an Ethereum token contract is a smart contract that holds a map of account addresses and their balances. 37 The balance is a value that is defined by the contract creator - in can be fungible physical objects, another monetary value. 38 The unit of this balance is commonly called a token. 39 40 ERC20 functions do: 41 42 * `balanceOf` : returns the token balance of an owner identifier (account address in case of Ethereum) 43 44 * `transfer` : transfers an amount to an owner identifier of our choosing 45 46 * `approve` : sets an amount of tokens a specified owner identifier is allowed to spend on our behalf 47 48 * `allowance` : check how much an owner identifier is allowed to spend on our behalf 49 50 * `transferFrom` : specify an owner identifier to transfer from if we are allowed by that owner identifier to spend some tokens. 51 52 53 ## Owner identifier in Hyperledger Fabric 54 55 In the Hyperledger Fabric network, all actors have an identity known to other participants. The default [Membership Service 56 Provider](https://hyperledger-fabric.readthedocs.io/en/release-1.3/msp.html) implementation uses X.509 certificates as identities, adopting a traditional Public Key Infrastructure (PKI) 57 hierarchical model. 58 59 Using information about creator of a proposal and asset ownership the chaincode should be able implement chaincode-level 60 access control mechanisms checking is actor can initiate transactions that update the asset. The corresponding chaincode 61 logic has to be able to store this "ownership" information associated with the asset and evaluate it with respect to the 62 proposal creator. 63 64 In HLF network as unique owner identifier (token balance holder) we can use combination of MSP Identifier and user 65 identity identifier. Identity identifier - is concatenation of `Subject` and `Issuer` parts of X.509 certificate. 66 This ID is guaranteed to be unique within the MSP. 67 68 ```go 69 func (c *clientIdentityImpl) GetID() (string, error) { 70 // The leading "x509::" distinquishes this as an X509 certificate, and 71 // the subject and issuer DNs uniquely identify the X509 certificate. 72 // The resulting ID will remain the same if the certificate is renewed. 73 id := fmt.Sprintf("x509::%s::%s", getDN(&c.cert.Subject), getDN(&c.cert.Issuer)) 74 return base64.StdEncoding.EncodeToString([]byte(id)), nil 75 } 76 ```` 77 [Client identity chaincode library](https://github.com/hyperledger/fabric/tree/master/core/chaincode/lib/cid) 78 allows to write chaincode which makes access control decisions based on the identity of the client 79 (i.e. the invoker of the chaincode). 80 81 In particular, you may make access control decisions based on either or both of the following associated with the client: 82 83 * the client identity's MSP (Membership Service Provider) ID 84 * an attribute associated with the client identity 85 86 CCkit contains [identity](https://github.com/s7techlab/cckit/tree/master/identity) package with structures and functions 87 can that be used for implementing access control in chaincode. 88 89 ## Getting started with example 90 91 In our example we use CCKit router for managing smart contract functions. Before you begin, be sure to get `CCkit`: 92 93 `git clone git@github.com:s7techlab/cckit.git` 94 95 and get dependencies using `go mod` command: 96 97 `go mod vendor` 98 99 ERC20 example is located in [examples/erc20](https://github.com/s7techlab/cckit/tree/master/examples/erc20) directory. 100 101 102 103 ## Defining token smart contract functions 104 105 First, we need to define chaincode functions. In our example we use [router](https://github.com/s7techlab/cckit/tree/master/router) 106 package from CCkit, that allows us to define chaincode methods and their parameters in consistent way. 107 108 At first we define `init` function (smart contract constructor) with arguments `symbol`, `name` and `totalSupply`. 109 After that we define chaincode methods, implementing ERC20 interface, adopted to HLF owner identifiers 110 (pair of MSP Id and certificate ID). All querying method are prefixed with `query`, all writing to state methods are prefixed with 111 `invoke`. 112 113 As a result we use [default chaincode](https://github.com/s7techlab/cckit/blob/master/router/chaincode.go) structure, 114 that delegates `Init` and `Invoke` handling to router. 115 116 ```go 117 func NewErc20FixedSupply() *router.Chaincode { 118 119 r := router.New(`erc20fixedSupply`).Use(p.StrictKnown). 120 // Chaincode init function, initiates token smart contract with token symbol, name and totalSupply 121 Init(invokeInitFixedSupply, p.String(`symbol`), p.String(`name`), p.Int(`totalSupply`)). 122 // Get token symbol 123 Query(`symbol`, querySymbol). 124 // Get token name 125 Query(`name`, queryName). 126 // Get the total token supply 127 Query(`totalSupply`, queryTotalSupply). 128 // get account balance 129 Query(`balanceOf`, queryBalanceOf, p.String(`mspId`), p.String(`certId`)). 130 //Send value amount of tokens 131 Invoke(`transfer`, invokeTransfer, p.String(`toMspId`), p.String(`toCertId`), p.Int(`amount`)). 132 // Allow spender to withdraw from your account, multiple times, up to the _value amount. 133 // If this function is called again it overwrites the current allowance with _valu 134 Invoke(`approve`, invokeApprove, p.String(`spenderMspId`), p.String(`spenderCertId`), p.Int(`amount`)). 135 // Returns the amount which _spender is still allowed to withdraw from _owner] 136 Invoke(`allowance`, queryAllowance, p.String(`ownerMspId`), p.String(`ownerCertId`), 137 p.String(`spenderMspId`), p.String(`spenderCertId`)). 138 // Send amount of tokens from owner account to another 139 Invoke(`transferFrom`, invokeTransferFrom, p.String(`fromMspId`), p.String(`fromCertId`), 140 p.String(`toMspId`), p.String(`toCertId`), p.Int(`amount`)) 141 142 return router.NewChaincode(r) 143 } 144 ```` 145 146 ## Chaincode initialization (constructor) 147 148 Chaincode `init` function (token constructor) performs the following actions: 149 150 * puts to chaincode state information about chaincode owner, using 151 [owner](https://github.com/s7techlab/cckit/tree/master/extensions/owner) extension from CCkit 152 * puts to chaincode state token configuration - token symbol, name and total supply 153 * sets chaincode owner balance with total supply 154 155 ```go 156 const SymbolKey = `symbol` 157 const NameKey = `name` 158 const TotalSupplyKey = `totalSupply` 159 160 161 func invokeInitFixedSupply(c router.Context) (interface{}, error) { 162 ownerIdentity, err := owner.SetFromCreator(c) 163 if err != nil { 164 return nil, errors.Wrap(err, `set chaincode owner`) 165 } 166 167 // save token configuration in state 168 if err := c.State().Insert(SymbolKey, c.ArgString(`symbol`)); err != nil { 169 return nil, err 170 } 171 172 if err := c.State().Insert(NameKey, c.ArgString(`name`)); err != nil { 173 return nil, err 174 } 175 176 if err := c.State().Insert(TotalSupplyKey, c.ArgInt(`totalSupply`)); err != nil { 177 return nil, err 178 } 179 180 // set token owner initial balance 181 if err := setBalance(c, ownerIdentity.GetMSPID(), ownerIdentity.GetID(), c.ArgInt(`totalSupply`)); err != nil { 182 return nil, errors.Wrap(err, `set owner initial balance`) 183 } 184 185 return ownerIdentity, nil 186 } 187 ``` 188 189 190 ## Defining events structure types 191 192 We use [Id](https://github.com/s7techlab/cckit/blob/master/identity/entry.go) structure from 193 [identity](https://github.com/s7techlab/cckit/tree/master/identity) package: 194 ```go 195 // Id structure defines short id representation 196 type Id struct { 197 MSP string 198 Cert string 199 } 200 ``` 201 202 And define structures for `Transfer` and `Approve` event: 203 204 ```go 205 type ( 206 Transfer struct { 207 From identity.Id 208 To identity.Id 209 Amount int 210 } 211 212 Approve struct { 213 From identity.Id 214 Spender identity.Id 215 Amount int 216 } 217 ) 218 ``` 219 220 ## Implementing token smart contract functions 221 222 Querying function is quite simple - it's just read value from chaincode state: 223 224 ```go 225 const SymbolKey = `symbol` 226 227 func querySymbol(c r.Context) (interface{}, error) { 228 return c.State().Get(SymbolKey) 229 } 230 ``` 231 232 Some of changing state functions are more complicated. For example in function `invokeTransfer` we do: 233 234 * receive function invoker certificate (via tx `GetCreator()` function) 235 * check transfer destination 236 * get current invoker (payer) balance 237 * check balance to transfer `amount` of tokens 238 * get recipient balance 239 * update payer and recipient balances in chaincode state 240 241 ```go 242 func invokeTransfer(c r.Context) (interface{}, error) { 243 // transfer target 244 toMspId := c.ArgString(`toMspId`) 245 toCertId := c.ArgString(`toCertId`) 246 247 //transfer amount 248 amount := c.ArgInt(`amount`) 249 250 // get informartion about tx creator 251 invoker, err := identity.FromStub(c.Stub()) 252 if err != nil { 253 return nil, err 254 } 255 256 // Disallow to transfer token to same account 257 if invoker.GetMSPID() == toMspId && invoker.GetID() == toCertId { 258 return nil, ErrForbiddenToTransferToSameAccount 259 } 260 261 // get information about invoker balance from state 262 invokerBalance, err := getBalance(c, invoker.GetMSPID(), invoker.GetID()) 263 if err != nil { 264 return nil, err 265 } 266 267 // Check the funds sufficiency 268 if invokerBalance-amount < 0 { 269 return nil, ErrNotEnoughFunds 270 } 271 272 // Get information about recipient balance from state 273 recipientBalance, err := getBalance(c, toMspId, toCertId) 274 if err != nil { 275 return nil, err 276 } 277 278 // Update payer and recipient balance 279 setBalance(c, invoker.GetMSPID(), invoker.GetID(), invokerBalance-amount) 280 setBalance(c, toMspId, toCertId, recipientBalance+amount) 281 282 // Trigger event with name "transfer" and payload - serialized to json Transfer structure 283 c.SetEvent(`transfer`, &Transfer{ 284 From: identity.Id{ 285 MSP: invoker.GetMSPID(), 286 Cert: invoker.GetID(), 287 }, 288 To: identity.Id{ 289 MSP: toMspId, 290 Cert: toCertId, 291 }, 292 Amount: amount, 293 }) 294 295 // return current invoker balance 296 return invokerBalance - amount, nil 297 } 298 299 // setBalance puts balance value to state 300 func setBalance(c r.Context, mspId, certId string, balance int) error { 301 return c.State().Put(balanceKey(mspId, certId), balance) 302 } 303 304 // balanceKey creates composite key for store balance value in state 305 func balanceKey(ownerMspId, ownerCertId string) []string { 306 return []string{BalancePrefix, ownerMspId, ownerCertId} 307 } 308 ``` 309 310 311 ## Testing 312 313 Also, we can fast test our chaincode via CCkit [MockStub](https://github.com/s7techlab/cckit/tree/master/testing). 314 315 To start testing we init chaincode via MockStub with test parameters: 316 317 ```go 318 var _ = Describe(`ERC-20`, func() { 319 320 const TokenSymbol = `HLF` 321 const TokenName = `HLFCoin` 322 const TotalSupply = 10000 323 const Decimals = 3 324 325 //Create chaincode mock 326 erc20fs := testcc.NewMockStub(`erc20`, NewErc20FixedSupply()) 327 328 // load actor certificates 329 actors, err := identity.ActorsFromPemFile(`SOME_MSP`, map[string]string{ 330 `token_owner`: `s7techlab.pem`, 331 `account_holder1`: `victor-nosov.pem`, 332 //`accoubt_holder2`: `victor-nosov.pem` 333 }, examplecert.Content) 334 if err != nil { 335 panic(err) 336 } 337 338 BeforeSuite(func() { 339 // init token haincode 340 expectcc.ResponseOk(erc20fs.From(actors[`token_owner`]).Init(TokenSymbol, TokenName, TotalSupply, Decimals)) 341 }) 342 ``` 343 344 After we can check all token operations: 345 346 ```go 347 Describe("ERC-20 transfer", func() { 348 It("Disallow to transfer token to same account", func() { 349 expectcc.ResponseError( 350 erc20fs.From(actors[`token_owner`]).Invoke( 351 `transfer`, actors[`token_owner`].GetMSPID(), actors[`token_owner`].GetID(), 100), 352 ErrForbiddenToTransferToSameAccount) 353 }) 354 355 It("Disallow token holder with zero balance to transfer tokens", func() { 356 expectcc.ResponseError( 357 erc20fs.From(actors[`account_holder1`]).Invoke( 358 `transfer`, actors[`token_owner`].GetMSPID(), actors[`token_owner`].GetID(), 100), 359 ErrNotEnoughFunds) 360 }) 361 362 It("Allow token holder with non zero balance to transfer tokens", func() { 363 expectcc.PayloadInt( 364 erc20fs.From(actors[`token_owner`]).Invoke( 365 `transfer`, actors[`account_holder1`].GetMSPID(), actors[`account_holder1`].GetID(), 100), 366 TotalSupply-100) 367 368 expectcc.PayloadInt( 369 erc20fs.Query( 370 `balanceOf`, actors[`token_owner`].GetMSPID(), actors[`token_owner`].GetID()), TotalSupply-100) 371 372 expectcc.PayloadInt( 373 erc20fs.Query( 374 `balanceOf`, actors[`account_holder1`].GetMSPID(), actors[`account_holder1`].GetID()), 100) 375 }) 376 }) 377 ```