fabric-samples/token-erc-20/chaincode-javascript/test/tokenERC20.test.js
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

287 lines
12 KiB
JavaScript

/*
SPDX-License-Identifier: Apache-2.0
*/
'use strict';
const { Context } = require('fabric-contract-api');
const { ChaincodeStub, ClientIdentity } = require('fabric-shim');
const { TokenERC20Contract } = require('..');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const sinon = require('sinon');
const expect = chai.expect;
chai.should();
chai.use(chaiAsPromised);
describe('Chaincode', () => {
let sandbox;
let token;
let ctx;
let mockStub;
let mockClientIdentity;
beforeEach('Sandbox creation', () => {
sandbox = sinon.createSandbox();
token = new TokenERC20Contract('token-erc20');
ctx = sinon.createStubInstance(Context);
mockStub = sinon.createStubInstance(ChaincodeStub);
ctx.stub = mockStub;
mockClientIdentity = sinon.createStubInstance(ClientIdentity);
ctx.clientIdentity = mockClientIdentity;
mockStub.putState.resolves('some state');
mockStub.setEvent.returns('set event');
});
afterEach('Sandbox restoration', () => {
sandbox.restore();
});
describe('#TokenName', () => {
it('should work', async () => {
mockStub.getState.resolves('some state');
const response = await token.TokenName(ctx);
sinon.assert.calledWith(mockStub.getState, 'name');
expect(response).to.equals('some state');
});
});
describe('#Symbol', () => {
it('should work', async () => {
mockStub.getState.resolves('some state');
const response = await token.Symbol(ctx);
sinon.assert.calledWith(mockStub.getState, 'symbol');
expect(response).to.equals('some state');
});
});
describe('#Decimals', () => {
it('should work', async () => {
mockStub.getState.resolves(Buffer.from('2'));
const response = await token.Decimals(ctx);
sinon.assert.calledWith(mockStub.getState, 'decimals');
expect(response).to.equals(2);
});
});
describe('#TotalSupply', () => {
it('should work', async () => {
mockStub.getState.resolves(Buffer.from('10000'));
const response = await token.TotalSupply(ctx);
sinon.assert.calledWith(mockStub.getState, 'totalSupply');
expect(response).to.equals(10000);
});
});
describe('#BalanceOf', () => {
it('should work', async () => {
mockStub.createCompositeKey.returns('balance_Alice');
mockStub.getState.resolves(Buffer.from('1000'));
const response = await token.BalanceOf(ctx, 'Alice');
expect(response).to.equals(1000);
});
});
describe('#_transfer', () => {
it('should fail when the sender and the receipient are the same', async () => {
await expect(token._transfer(ctx, 'Alice', 'Alice', '1000'))
.to.be.rejectedWith(Error, 'cannot transfer to and from same client account');
});
it('should fail when the sender does not have enough token', async () => {
mockStub.createCompositeKey.withArgs('balance', ['Alice']).returns('balance_Alice');
mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('500'));
await expect(token._transfer(ctx, 'Alice', 'Bob', '1000'))
.to.be.rejectedWith(Error, 'client account Alice has insufficient funds.');
});
it('should transfer to a new account when the sender has enough token', async () => {
mockStub.createCompositeKey.withArgs('balance', ['Alice']).returns('balance_Alice');
mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000'));
mockStub.createCompositeKey.withArgs('balance', ['Bob']).returns('balance_Bob');
mockStub.getState.withArgs('balance_Bob').resolves(null);
const response = await token._transfer(ctx, 'Alice', 'Bob', '1000');
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('0'));
sinon.assert.calledWith(mockStub.putState.getCall(1), 'balance_Bob', Buffer.from('1000'));
expect(response).to.equals(true);
});
it('should transfer to the existing account when the sender has enough token', async () => {
mockStub.createCompositeKey.withArgs('balance', ['Alice']).returns('balance_Alice');
mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000'));
mockStub.createCompositeKey.withArgs('balance', ['Bob']).returns('balance_Bob');
mockStub.getState.withArgs('balance_Bob').resolves(Buffer.from('2000'));
const response = await token._transfer(ctx, 'Alice', 'Bob', '1000');
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('0'));
sinon.assert.calledWith(mockStub.putState.getCall(1), 'balance_Bob', Buffer.from('3000'));
expect(response).to.equals(true);
});
});
describe('#Transfer', () => {
it('should work', async () => {
mockClientIdentity.getID.returns('Alice');
sinon.stub(token, '_transfer').returns(true);
const response = await token.Transfer(ctx, 'Bob', '1000');
const event = { from: 'Alice', to: 'Bob', value: 1000 };
sinon.assert.calledWith(mockStub.setEvent, 'Transfer', Buffer.from(JSON.stringify(event)));
expect(response).to.equals(true);
});
});
describe('#TransferFrom', () => {
it('should fail when the spender is not allowed to spend the token', async () => {
mockClientIdentity.getID.returns('Charlie');
mockStub.createCompositeKey.withArgs('allowance', ['Alice', 'Charlie']).returns('allowance_Alice_Charlie');
mockStub.getState.withArgs('allowance_Alice_Charlie').resolves(Buffer.from('0'));
await expect(token.TransferFrom(ctx, 'Alice', 'Bob', '1000'))
.to.be.rejectedWith(Error, 'The spender does not have enough allowance to spend.');
});
it('should transfer when the spender is allowed to spend the token', async () => {
mockClientIdentity.getID.returns('Charlie');
mockStub.createCompositeKey.withArgs('allowance', ['Alice', 'Charlie']).returns('allowance_Alice_Charlie');
mockStub.getState.withArgs('allowance_Alice_Charlie').resolves(Buffer.from('3000'));
sinon.stub(token, '_transfer').returns(true);
const response = await token.TransferFrom(ctx, 'Alice', 'Bob', '1000');
sinon.assert.calledWith(mockStub.putState, 'allowance_Alice_Charlie', Buffer.from('2000'));
const event = { from: 'Alice', to: 'Bob', value: 1000 };
sinon.assert.calledWith(mockStub.setEvent, 'Transfer', Buffer.from(JSON.stringify(event)));
expect(response).to.equals(true);
});
});
describe('#Approve', () => {
it('should work', async () => {
mockClientIdentity.getID.returns('Dave');
mockStub.createCompositeKey.returns('allowance_Dave_Eve');
const response = await token.Approve(ctx, 'Ellen', '1000');
sinon.assert.calledWith(mockStub.putState, 'allowance_Dave_Eve', Buffer.from('1000'));
expect(response).to.equals(true);
});
});
describe('#Allowance', () => {
it('should work', async () => {
mockStub.createCompositeKey.returns('allowance_Dave_Eve');
mockStub.getState.resolves(Buffer.from('1000'));
const response = await token.Allowance(ctx, 'Dave', 'Eve');
expect(response).to.equals(1000);
});
});
describe('#SetOption', () => {
it('should work', async () => {
const response = await token.SetOption(ctx, 'some name', 'some symbol', '2');
sinon.assert.calledWith(mockStub.putState, 'name', Buffer.from('some name'));
sinon.assert.calledWith(mockStub.putState, 'symbol', Buffer.from('some symbol'));
sinon.assert.calledWith(mockStub.putState, 'decimals', Buffer.from('2'));
expect(response).to.equals(true);
});
});
describe('#Mint', () => {
it('should add token to a new account and a new total supply', async () => {
mockClientIdentity.getMSPID.returns('Org1MSP');
mockClientIdentity.getID.returns('Alice');
mockStub.createCompositeKey.returns('balance_Alice');
mockStub.getState.withArgs('balance_Alice').resolves(null);
mockStub.getState.withArgs('totalSupply').resolves(null);
const response = await token.Mint(ctx, '1000');
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('1000'));
sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('1000'));
expect(response).to.equals(true);
});
it('should add token to the existing account and the existing total supply', async () => {
mockClientIdentity.getMSPID.returns('Org1MSP');
mockClientIdentity.getID.returns('Alice');
mockStub.createCompositeKey.returns('balance_Alice');
mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000'));
mockStub.getState.withArgs('totalSupply').resolves(Buffer.from('2000'));
const response = await token.Mint(ctx, '1000');
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('2000'));
sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('3000'));
expect(response).to.equals(true);
});
it('should add token to a new account and the existing total supply', async () => {
mockClientIdentity.getMSPID.returns('Org1MSP');
mockClientIdentity.getID.returns('Alice');
mockStub.createCompositeKey.returns('balance_Alice');
mockStub.getState.withArgs('balance_Alice').resolves(null);
mockStub.getState.withArgs('totalSupply').resolves(Buffer.from('2000'));
const response = await token.Mint(ctx, '1000');
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('1000'));
sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('3000'));
expect(response).to.equals(true);
});
});
describe('#Burn', () => {
it('should work', async () => {
mockClientIdentity.getMSPID.returns('Org1MSP');
mockClientIdentity.getID.returns('Alice');
mockStub.createCompositeKey.returns('balance_Alice');
mockStub.getState.withArgs('balance_Alice').resolves(Buffer.from('1000'));
mockStub.getState.withArgs('totalSupply').resolves(Buffer.from('2000'));
const response = await token.Burn(ctx, '1000');
sinon.assert.calledWith(mockStub.putState.getCall(0), 'balance_Alice', Buffer.from('0'));
sinon.assert.calledWith(mockStub.putState.getCall(1), 'totalSupply', Buffer.from('1000'));
expect(response).to.equals(true);
});
});
describe('#ClientAccountBalance', () => {
it('should work', async () => {
mockClientIdentity.getID.returns('Alice');
mockStub.createCompositeKey.returns('balance_Alice');
mockStub.getState.resolves(Buffer.from('1000'));
const response = await token.ClientAccountBalance(ctx,);
expect(response).to.equals(1000);
});
});
describe('#ClientAccountID', () => {
it('should work', async () => {
mockClientIdentity.getID.returns('x509::{subject DN}::{issuer DN}');
const response = await token.ClientAccountID(ctx);
sinon.assert.calledOnce(mockClientIdentity.getID);
expect(response).to.equals('x509::{subject DN}::{issuer DN}');
});
});
});