mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 07:25:10 +00:00
Using the ERC-20 sample, you can submit a transfer to and from the same account. Because the code doesn't handle this, it ends up minting new tokens into that account. The correct behaviour is not specified by the ERC-20 specification, although the OpenZeppelin implementation seems to permit it. IMO we should just block it with an error because I can't see a use case for allowing it and it is most likely a user error. Signed-off-by: Simon Stone <sstone1@uk.ibm.com>
483 lines
16 KiB
Go
483 lines
16 KiB
Go
package chaincode
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
|
|
"github.com/hyperledger/fabric-contract-api-go/contractapi"
|
|
)
|
|
|
|
// Define key names for options
|
|
const totalSupplyKey = "totalSupply"
|
|
|
|
// Define objectType names for prefix
|
|
const allowancePrefix = "allowance"
|
|
|
|
// SmartContract provides functions for transferring tokens between accounts
|
|
type SmartContract struct {
|
|
contractapi.Contract
|
|
}
|
|
|
|
// event provides an organized struct for emitting events
|
|
type event struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Value int `json:"value"`
|
|
}
|
|
|
|
// Mint creates new tokens and adds them to minter's account balance
|
|
// This function triggers a Transfer event
|
|
func (s *SmartContract) Mint(ctx contractapi.TransactionContextInterface, amount int) error {
|
|
|
|
// Check minter authorization - this sample assumes Org1 is the central banker with privilege to mint new tokens
|
|
clientMSPID, err := ctx.GetClientIdentity().GetMSPID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get MSPID: %v", err)
|
|
}
|
|
if clientMSPID != "Org1MSP" {
|
|
return fmt.Errorf("client is not authorized to mint new tokens")
|
|
}
|
|
|
|
// Get ID of submitting client identity
|
|
minter, err := ctx.GetClientIdentity().GetID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get client id: %v", err)
|
|
}
|
|
|
|
if amount <= 0 {
|
|
return fmt.Errorf("mint amount must be a positive integer")
|
|
}
|
|
|
|
currentBalanceBytes, err := ctx.GetStub().GetState(minter)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read minter account %s from world state: %v", minter, err)
|
|
}
|
|
|
|
var currentBalance int
|
|
|
|
// If minter current balance doesn't yet exist, we'll create it with a current balance of 0
|
|
if currentBalanceBytes == nil {
|
|
currentBalance = 0
|
|
} else {
|
|
currentBalance, _ = strconv.Atoi(string(currentBalanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer.
|
|
}
|
|
|
|
updatedBalance := currentBalance + amount
|
|
|
|
err = ctx.GetStub().PutState(minter, []byte(strconv.Itoa(updatedBalance)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the totalSupply
|
|
totalSupplyBytes, err := ctx.GetStub().GetState(totalSupplyKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to retrieve total token supply: %v", err)
|
|
}
|
|
|
|
var totalSupply int
|
|
|
|
// If no tokens have been minted, initialize the totalSupply
|
|
if totalSupplyBytes == nil {
|
|
totalSupply = 0
|
|
} else {
|
|
totalSupply, _ = strconv.Atoi(string(totalSupplyBytes)) // Error handling not needed since Itoa() was used when setting the totalSupply, guaranteeing it was an integer.
|
|
}
|
|
|
|
// Add the mint amount to the total supply and update the state
|
|
totalSupply += amount
|
|
err = ctx.GetStub().PutState(totalSupplyKey, []byte(strconv.Itoa(totalSupply)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Emit the Transfer event
|
|
transferEvent := event{"0x0", minter, amount}
|
|
transferEventJSON, err := json.Marshal(transferEvent)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to obtain JSON encoding: %v", err)
|
|
}
|
|
err = ctx.GetStub().SetEvent("Transfer", transferEventJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set event: %v", err)
|
|
}
|
|
|
|
log.Printf("minter account %s balance updated from %d to %d", minter, currentBalance, updatedBalance)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Burn redeems tokens the minter's account balance
|
|
// This function triggers a Transfer event
|
|
func (s *SmartContract) Burn(ctx contractapi.TransactionContextInterface, amount int) error {
|
|
|
|
// Check minter authorization - this sample assumes Org1 is the central banker with privilege to burn new tokens
|
|
clientMSPID, err := ctx.GetClientIdentity().GetMSPID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get MSPID: %v", err)
|
|
}
|
|
if clientMSPID != "Org1MSP" {
|
|
return fmt.Errorf("client is not authorized to mint new tokens")
|
|
}
|
|
|
|
// Get ID of submitting client identity
|
|
minter, err := ctx.GetClientIdentity().GetID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get client id: %v", err)
|
|
}
|
|
|
|
if amount <= 0 {
|
|
return errors.New("burn amount must be a positive integer")
|
|
}
|
|
|
|
currentBalanceBytes, err := ctx.GetStub().GetState(minter)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read minter account %s from world state: %v", minter, err)
|
|
}
|
|
|
|
var currentBalance int
|
|
|
|
// Check if minter current balance exists
|
|
if currentBalanceBytes == nil {
|
|
return errors.New("The balance does not exist")
|
|
}
|
|
|
|
currentBalance, _ = strconv.Atoi(string(currentBalanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer.
|
|
|
|
updatedBalance := currentBalance - amount
|
|
|
|
err = ctx.GetStub().PutState(minter, []byte(strconv.Itoa(updatedBalance)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the totalSupply
|
|
totalSupplyBytes, err := ctx.GetStub().GetState(totalSupplyKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to retrieve total token supply: %v", err)
|
|
}
|
|
|
|
// If no tokens have been minted, throw error
|
|
if totalSupplyBytes == nil {
|
|
return errors.New("totalSupply does not exist")
|
|
}
|
|
|
|
totalSupply, _ := strconv.Atoi(string(totalSupplyBytes)) // Error handling not needed since Itoa() was used when setting the totalSupply, guaranteeing it was an integer.
|
|
|
|
// Subtract the burn amount to the total supply and update the state
|
|
totalSupply -= amount
|
|
err = ctx.GetStub().PutState(totalSupplyKey, []byte(strconv.Itoa(totalSupply)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Emit the Transfer event
|
|
transferEvent := event{minter, "0x0", amount}
|
|
transferEventJSON, err := json.Marshal(transferEvent)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to obtain JSON encoding: %v", err)
|
|
}
|
|
err = ctx.GetStub().SetEvent("Transfer", transferEventJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set event: %v", err)
|
|
}
|
|
|
|
log.Printf("minter account %s balance updated from %d to %d", minter, currentBalance, updatedBalance)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Transfer transfers tokens from client account to recipient account
|
|
// recipient account must be a valid clientID as returned by the ClientID() function
|
|
// This function triggers a Transfer event
|
|
func (s *SmartContract) Transfer(ctx contractapi.TransactionContextInterface, recipient string, amount int) error {
|
|
|
|
// Get ID of submitting client identity
|
|
clientID, err := ctx.GetClientIdentity().GetID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get client id: %v", err)
|
|
}
|
|
|
|
err = transferHelper(ctx, clientID, recipient, amount)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to transfer: %v", err)
|
|
}
|
|
|
|
// Emit the Transfer event
|
|
transferEvent := event{clientID, recipient, amount}
|
|
transferEventJSON, err := json.Marshal(transferEvent)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to obtain JSON encoding: %v", err)
|
|
}
|
|
err = ctx.GetStub().SetEvent("Transfer", transferEventJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set event: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// BalanceOf returns the balance of the given account
|
|
func (s *SmartContract) BalanceOf(ctx contractapi.TransactionContextInterface, account string) (int, error) {
|
|
balanceBytes, err := ctx.GetStub().GetState(account)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to read from world state: %v", err)
|
|
}
|
|
if balanceBytes == nil {
|
|
return 0, fmt.Errorf("the account %s does not exist", account)
|
|
}
|
|
|
|
balance, _ := strconv.Atoi(string(balanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer.
|
|
|
|
return balance, nil
|
|
}
|
|
|
|
// ClientAccountBalance returns the balance of the requesting client's account
|
|
func (s *SmartContract) ClientAccountBalance(ctx contractapi.TransactionContextInterface) (int, error) {
|
|
|
|
// Get ID of submitting client identity
|
|
clientID, err := ctx.GetClientIdentity().GetID()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get client id: %v", err)
|
|
}
|
|
|
|
balanceBytes, err := ctx.GetStub().GetState(clientID)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to read from world state: %v", err)
|
|
}
|
|
if balanceBytes == nil {
|
|
return 0, fmt.Errorf("the account %s does not exist", clientID)
|
|
}
|
|
|
|
balance, _ := strconv.Atoi(string(balanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer.
|
|
|
|
return balance, nil
|
|
}
|
|
|
|
// ClientAccountID returns the id of the requesting client's account
|
|
// In this implementation, the client account ID is the clientId itself
|
|
// Users can use this function to get their own account id, which they can then give to others as the payment address
|
|
func (s *SmartContract) ClientAccountID(ctx contractapi.TransactionContextInterface) (string, error) {
|
|
|
|
// Get ID of submitting client identity
|
|
clientAccountID, err := ctx.GetClientIdentity().GetID()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get client id: %v", err)
|
|
}
|
|
|
|
return clientAccountID, nil
|
|
}
|
|
|
|
// TotalSupply returns the total token supply
|
|
func (s *SmartContract) TotalSupply(ctx contractapi.TransactionContextInterface) (int, error) {
|
|
|
|
// Retrieve total supply of tokens from state of smart contract
|
|
totalSupplyBytes, err := ctx.GetStub().GetState(totalSupplyKey)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to retrieve total token supply: %v", err)
|
|
}
|
|
|
|
var totalSupply int
|
|
|
|
// If no tokens have been minted, return 0
|
|
if totalSupplyBytes == nil {
|
|
totalSupply = 0
|
|
} else {
|
|
totalSupply, _ = strconv.Atoi(string(totalSupplyBytes)) // Error handling not needed since Itoa() was used when setting the totalSupply, guaranteeing it was an integer.
|
|
}
|
|
|
|
log.Printf("TotalSupply: %d tokens", totalSupply)
|
|
|
|
return totalSupply, nil
|
|
}
|
|
|
|
// Approve allows the spender to withdraw from the calling client's token account
|
|
// The spender can withdraw multiple times if necessary, up to the value amount
|
|
// This function triggers an Approval event
|
|
func (s *SmartContract) Approve(ctx contractapi.TransactionContextInterface, spender string, value int) error {
|
|
|
|
// Get ID of submitting client identity
|
|
owner, err := ctx.GetClientIdentity().GetID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get client id: %v", err)
|
|
}
|
|
|
|
// Create allowanceKey
|
|
allowanceKey, err := ctx.GetStub().CreateCompositeKey(allowancePrefix, []string{owner, spender})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create the composite key for prefix %s: %v", allowancePrefix, err)
|
|
}
|
|
|
|
// Update the state of the smart contract by adding the allowanceKey and value
|
|
err = ctx.GetStub().PutState(allowanceKey, []byte(strconv.Itoa(value)))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update state of smart contract for key %s: %v", allowanceKey, err)
|
|
}
|
|
|
|
// Emit the Approval event
|
|
approvalEvent := event{owner, spender, value}
|
|
approvalEventJSON, err := json.Marshal(approvalEvent)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to obtain JSON encoding: %v", err)
|
|
}
|
|
err = ctx.GetStub().SetEvent("Approval", approvalEventJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set event: %v", err)
|
|
}
|
|
|
|
log.Printf("client %s approved a withdrawal allowance of %d for spender %s", owner, value, spender)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Allowance returns the amount still available for the spender to withdraw from the owner
|
|
func (s *SmartContract) Allowance(ctx contractapi.TransactionContextInterface, owner string, spender string) (int, error) {
|
|
|
|
// Create allowanceKey
|
|
allowanceKey, err := ctx.GetStub().CreateCompositeKey(allowancePrefix, []string{owner, spender})
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to create the composite key for prefix %s: %v", allowancePrefix, err)
|
|
}
|
|
|
|
// Read the allowance amount from the world state
|
|
allowanceBytes, err := ctx.GetStub().GetState(allowanceKey)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to read allowance for %s from world state: %v", allowanceKey, err)
|
|
}
|
|
|
|
var allowance int
|
|
|
|
// If no current allowance, set allowance to 0
|
|
if allowanceBytes == nil {
|
|
allowance = 0
|
|
} else {
|
|
allowance, err = strconv.Atoi(string(allowanceBytes)) // Error handling not needed since Itoa() was used when setting the totalSupply, guaranteeing it was an integer.
|
|
}
|
|
|
|
log.Printf("The allowance left for spender %s to withdraw from owner %s: %d", spender, owner, allowance)
|
|
|
|
return allowance, nil
|
|
}
|
|
|
|
// TransferFrom transfers the value amount from the "from" address to the "to" address
|
|
// This function triggers a Transfer event
|
|
func (s *SmartContract) TransferFrom(ctx contractapi.TransactionContextInterface, from string, to string, value int) error {
|
|
|
|
// Get ID of submitting client identity
|
|
spender, err := ctx.GetClientIdentity().GetID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get client id: %v", err)
|
|
}
|
|
|
|
// Create allowanceKey
|
|
allowanceKey, err := ctx.GetStub().CreateCompositeKey(allowancePrefix, []string{from, spender})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create the composite key for prefix %s: %v", allowancePrefix, err)
|
|
}
|
|
|
|
// Retrieve the allowance of the spender
|
|
currentAllowanceBytes, err := ctx.GetStub().GetState(allowanceKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to retrieve the allowance for %s from world state: %v", allowanceKey, err)
|
|
}
|
|
|
|
var currentAllowance int
|
|
currentAllowance, _ = strconv.Atoi(string(currentAllowanceBytes)) // Error handling not needed since Itoa() was used when setting the totalSupply, guaranteeing it was an integer.
|
|
|
|
// Check if transferred value is less than allowance
|
|
if currentAllowance < value {
|
|
return fmt.Errorf("spender does not have enough allowance for transfer")
|
|
}
|
|
|
|
// Initiate the transfer
|
|
err = transferHelper(ctx, from, to, value)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to transfer: %v", err)
|
|
}
|
|
|
|
// Decrease the allowance
|
|
updatedAllowance := currentAllowance - value
|
|
err = ctx.GetStub().PutState(allowanceKey, []byte(strconv.Itoa(updatedAllowance)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Emit the Transfer event
|
|
transferEvent := event{from, to, value}
|
|
transferEventJSON, err := json.Marshal(transferEvent)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to obtain JSON encoding: %v", err)
|
|
}
|
|
err = ctx.GetStub().SetEvent("Transfer", transferEventJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set event: %v", err)
|
|
}
|
|
|
|
log.Printf("spender %s allowance updated from %d to %d", spender, currentAllowance, updatedAllowance)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper Functions
|
|
|
|
// transferHelper is a helper function that transfers tokens from the "from" address to the "to" address
|
|
// Dependant functions include Transfer and TransferFrom
|
|
func transferHelper(ctx contractapi.TransactionContextInterface, from string, to string, value int) error {
|
|
|
|
if from == to {
|
|
return fmt.Errorf("cannot transfer to and from same client account")
|
|
}
|
|
|
|
if value < 0 { // transfer of 0 is allowed in ERC-20, so just validate against negative amounts
|
|
return fmt.Errorf("transfer amount cannot be negative")
|
|
}
|
|
|
|
fromCurrentBalanceBytes, err := ctx.GetStub().GetState(from)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read client account %s from world state: %v", from, err)
|
|
}
|
|
|
|
if fromCurrentBalanceBytes == nil {
|
|
return fmt.Errorf("client account %s has no balance", from)
|
|
}
|
|
|
|
fromCurrentBalance, _ := strconv.Atoi(string(fromCurrentBalanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer.
|
|
|
|
if fromCurrentBalance < value {
|
|
return fmt.Errorf("client account %s has insufficient funds", from)
|
|
}
|
|
|
|
toCurrentBalanceBytes, err := ctx.GetStub().GetState(to)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read recipient account %s from world state: %v", to, err)
|
|
}
|
|
|
|
var toCurrentBalance int
|
|
// If recipient current balance doesn't yet exist, we'll create it with a current balance of 0
|
|
if toCurrentBalanceBytes == nil {
|
|
toCurrentBalance = 0
|
|
} else {
|
|
toCurrentBalance, _ = strconv.Atoi(string(toCurrentBalanceBytes)) // Error handling not needed since Itoa() was used when setting the account balance, guaranteeing it was an integer.
|
|
}
|
|
|
|
fromUpdatedBalance := fromCurrentBalance - value
|
|
toUpdatedBalance := toCurrentBalance + value
|
|
|
|
err = ctx.GetStub().PutState(from, []byte(strconv.Itoa(fromUpdatedBalance)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ctx.GetStub().PutState(to, []byte(strconv.Itoa(toUpdatedBalance)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Printf("client %s balance updated from %d to %d", from, fromCurrentBalance, fromUpdatedBalance)
|
|
log.Printf("recipient %s balance updated from %d to %d", to, toCurrentBalance, toUpdatedBalance)
|
|
|
|
return nil
|
|
}
|