fabric-samples/token-erc-20/chaincode-go/chaincode/token_contract.go
Simon Stone 365172ddb3
Stop ERC-20 transfer bug when to/from accounts are the same (#425)
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>
2021-02-24 15:07:06 -05:00

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
}