Repository: thehubbleproject/hubble-contracts (ts)

Issue:
One can slash a coordinator if they include 2 or more txs with the same sender
### What's wrong

In the signature check, we recover the signing message with the committed tx data and a nonce.

The nonce is acquired from the state. The disputer provides the state corresponding to the post-state root. The Authenticity contract checks if the state is indeed included in that state root, and subtract 1 from that state's nonce to get the correct signing nonce.

The problem is the 2 post states that the disputer provides will be the same state.

Say, a sender sends 2 transfers with nonce 0 and 1, and those were included in a commitment. The post-state of the sender of that commitment is 2. The disputer provides the same states, and the Authenticity contract computes Merkle inclusion check, then concludes the signing nonce should be 1. The first tx whose nonce is 0 will fail the signature check.

### How can we fix it

In the coordinator implementation, never includes 2 txs with the same sender in a commitment.


Pull request:
Naive implementation to fix double sender check
Fix #382 

- We first reproduce the bug
- Then we see the gas cost increase of the naive approach

### Signature check Gas Cost

### This PR

- Transfer
  - senders all different
    - operation gas cost: 4664005
    - transaction gas cost: 6377081
  - some same senders
    - operation gas cost: 4798157
- Mass Migration
  - senders all different. operation gas cost: 4614020
  - some same senders: operation gas cost: 4550813
- Create2Transfer
  - senders all different
    - operation gas cost: 5406666
    - transaction gas cost: 8009615
  - some same senders
    - operation gas cost: 5438038

#### Master (senders all different)

- Transfer
  - operation gas cost: 4610066
  - transaction gas cost: 6323012
- Mass Migration
  - operation gas cost: 4603308
- Create2Transfer
  - operation gas cost: 5338076
  - transaction gas cost: 7941113





Patch:
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index cd0a070..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2021 Hubble-Project
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/contracts/Create2Transfer.sol b/contracts/Create2Transfer.sol
index 0532f51..63ea1ab 100644
--- a/contracts/Create2Transfer.sol
+++ b/contracts/Create2Transfer.sol
@@ -11,22 +11,10 @@ contract Create2Transfer {
     using SafeMath for uint256;
 
     function checkSignature(
-        uint256[2] memory signature,
-        Types.SignatureProofWithReceiver memory proof,
-        bytes32 stateRoot,
-        bytes32 accountRoot,
-        bytes32 domain,
-        bytes memory txs
+        Types.AuthCommon memory common,
+        Types.SignatureProofWithReceiver memory proof
     ) public view returns (Types.Result) {
-        return
-            Authenticity.verifyCreate2Transfer(
-                signature,
-                proof,
-                stateRoot,
-                accountRoot,
-                domain,
-                txs
-            );
+        return Authenticity.verifyCreate2Transfer(common, proof);
     }
 
     /**
diff --git a/contracts/MassMigrations.sol b/contracts/MassMigrations.sol
index c215a7c..ca3ebd9 100644
--- a/contracts/MassMigrations.sol
+++ b/contracts/MassMigrations.sol
@@ -13,24 +13,11 @@ contract MassMigration {
     using Tx for bytes;
 
     function checkSignature(
-        uint256[2] memory signature,
+        Types.AuthCommon memory common,
         Types.SignatureProof memory proof,
-        bytes32 stateRoot,
-        bytes32 accountRoot,
-        bytes32 domain,
-        uint256 spokeID,
-        bytes memory txs
+        uint256 spokeID
     ) public view returns (Types.Result) {
-        return
-            Authenticity.verifyMassMigration(
-                signature,
-                proof,
-                stateRoot,
-                accountRoot,
-                domain,
-                spokeID,
-                txs
-            );
+        return Authenticity.verifyMassMigration(common, proof, spokeID);
     }
 
     /**
diff --git a/contracts/Transfer.sol b/contracts/Transfer.sol
index 4050742..b090540 100644
--- a/contracts/Transfer.sol
+++ b/contracts/Transfer.sol
@@ -12,22 +12,10 @@ contract Transfer {
     using Tx for bytes;
 
     function checkSignature(
-        uint256[2] memory signature,
-        Types.SignatureProof memory proof,
-        bytes32 stateRoot,
-        bytes32 accountRoot,
-        bytes32 domain,
-        bytes memory txs
+        Types.AuthCommon memory common,
+        Types.SignatureProof memory proof
     ) public view returns (Types.Result) {
-        return
-            Authenticity.verifyTransfer(
-                signature,
-                proof,
-                stateRoot,
-                accountRoot,
-                domain,
-                txs
-            );
+        return Authenticity.verifyTransfer(common, proof);
     }
 
     /**
diff --git a/contracts/client/FrontendCreate2Transfer.sol b/contracts/client/FrontendCreate2Transfer.sol
index 5e0cc17..93ca268 100644
--- a/contracts/client/FrontendCreate2Transfer.sol
+++ b/contracts/client/FrontendCreate2Transfer.sol
@@ -196,14 +196,14 @@ contract FrontendCreate2Transfer {
         bytes32 domain,
         bytes memory txs
     ) public view returns (Types.Result) {
-        return
-            Authenticity.verifyCreate2Transfer(
-                signature,
-                proof,
-                stateRoot,
-                accountRoot,
-                domain,
-                txs
-            );
+        Types.AuthCommon memory common = Types.AuthCommon({
+            signature: signature,
+            stateRoot: stateRoot,
+            accountRoot: accountRoot,
+            domain: domain,
+            txs: txs
+        });
+
+        return Authenticity.verifyCreate2Transfer(common, proof);
     }
 }
diff --git a/contracts/client/FrontendMassMigration.sol b/contracts/client/FrontendMassMigration.sol
index 6387d0e..295895f 100644
--- a/contracts/client/FrontendMassMigration.sol
+++ b/contracts/client/FrontendMassMigration.sol
@@ -173,15 +173,13 @@ contract FrontendMassMigration {
         uint256 spokeID,
         bytes memory txs
     ) public view returns (Types.Result) {
-        return
-            Authenticity.verifyMassMigration(
-                signature,
-                proof,
-                stateRoot,
-                accountRoot,
-                domain,
-                spokeID,
-                txs
-            );
+        Types.AuthCommon memory common = Types.AuthCommon({
+            signature: signature,
+            stateRoot: stateRoot,
+            accountRoot: accountRoot,
+            domain: domain,
+            txs: txs
+        });
+        return Authenticity.verifyMassMigration(common, proof, spokeID);
     }
 }
diff --git a/contracts/client/FrontendTransfer.sol b/contracts/client/FrontendTransfer.sol
index 3e00f1a..5e68c12 100644
--- a/contracts/client/FrontendTransfer.sol
+++ b/contracts/client/FrontendTransfer.sol
@@ -163,14 +163,13 @@ contract FrontendTransfer {
         bytes32 domain,
         bytes memory txs
     ) public view returns (Types.Result) {
-        return
-            Authenticity.verifyTransfer(
-                signature,
-                proof,
-                stateRoot,
-                accountRoot,
-                domain,
-                txs
-            );
+        Types.AuthCommon memory common = Types.AuthCommon({
+            signature: signature,
+            stateRoot: stateRoot,
+            accountRoot: accountRoot,
+            domain: domain,
+            txs: txs
+        });
+        return Authenticity.verifyTransfer(common, proof);
     }
 }
diff --git a/contracts/libs/Authenticity.sol b/contracts/libs/Authenticity.sol
index 7aa7f62..8601572 100644
--- a/contracts/libs/Authenticity.sol
+++ b/contracts/libs/Authenticity.sol
@@ -15,51 +15,56 @@ library Authenticity {
     using Types for Types.UserState;
 
     function verifyTransfer(
-        uint256[2] memory signature,
-        Types.SignatureProof memory proof,
-        bytes32 stateRoot,
-        bytes32 accountRoot,
-        bytes32 domain,
-        bytes memory txs
+        Types.AuthCommon memory common,
+        Types.SignatureProof memory proof
     ) internal view returns (Types.Result) {
-        uint256 size = txs.transferSize();
+        uint256 size = common.txs.transferSize();
         uint256[2][] memory messages = new uint256[2][](size);
-        for (uint256 i = 0; i < size; i++) {
-            Tx.Transfer memory _tx = txs.transferDecode(i);
+        uint256[] memory senders = new uint256[](size);
+        // We reverse loop the transactions to compute nonce correctly
+        for (uint256 j = 0; j < size; j++) {
+            // i is the index counting down from tail
+            uint256 i = size - j - 1;
+            Tx.Transfer memory _tx = common.txs.transferDecode(i);
             // check state inclusion
             require(
                 MerkleTree.verify(
-                    stateRoot,
+                    common.stateRoot,
                     keccak256(proof.states[i].encode()),
                     _tx.fromIndex,
                     proof.stateWitnesses[i]
                 ),
                 "Authenticity: state inclusion signer"
             );
+            require(proof.states[i].nonce > 0, "Authenticity: zero nonce");
 
             // check pubkey inclusion
             require(
                 MerkleTree.verify(
-                    accountRoot,
+                    common.accountRoot,
                     keccak256(abi.encodePacked(proof.pubkeys[i])),
                     proof.states[i].pubkeyID,
                     proof.pubkeyWitnesses[i]
                 ),
                 "Authenticity: account does not exists"
             );
-
+            uint256 nonce = proof.states[i].nonce - 1;
+            // Add a huge value to avoid collision to the array initial value 0
+            uint256 safeIndex = _tx.fromIndex + 1000000000000000;
+
+            for (uint256 k = 0; k < j; k++) {
+                // Update nonce if the sender is seen before
+                if (senders[k] == safeIndex) nonce--;
+            }
+            senders[j] = safeIndex;
             // construct the message
-            require(proof.states[i].nonce > 0, "Authenticity: zero nonce");
-            bytes memory txMsg = Tx.transferMessageOf(
-                _tx,
-                proof.states[i].nonce - 1
-            );
-            messages[i] = BLS.hashToPoint(domain, txMsg);
+            bytes memory txMsg = Tx.transferMessageOf(_tx, nonce);
+            messages[i] = BLS.hashToPoint(common.domain, txMsg);
         }
         bool callSuccess;
         bool checkSuccess;
         (checkSuccess, callSuccess) = BLS.verifyMultiple(
-            signature,
+            common.signature,
             proof.pubkeys,
             messages
         );
@@ -73,33 +78,32 @@ library Authenticity {
     }
 
     function verifyMassMigration(
-        uint256[2] memory signature,
+        Types.AuthCommon memory common,
         Types.SignatureProof memory proof,
-        bytes32 stateRoot,
-        bytes32 accountRoot,
-        bytes32 domain,
-        uint256 spokeID,
-        bytes memory txs
+        uint256 spokeID
     ) internal view returns (Types.Result) {
-        uint256 size = txs.massMigrationSize();
+        uint256 size = common.txs.massMigrationSize();
         uint256[2][] memory messages = new uint256[2][](size);
-        for (uint256 i = 0; i < size; i++) {
-            Tx.MassMigration memory _tx = txs.massMigrationDecode(i);
+        uint256[] memory senders = new uint256[](size);
+        for (uint256 j = 0; j < size; j++) {
+            uint256 i = size - j - 1;
+            Tx.MassMigration memory _tx = common.txs.massMigrationDecode(i);
             // check state inclusion
             require(
                 MerkleTree.verify(
-                    stateRoot,
+                    common.stateRoot,
                     keccak256(proof.states[i].encode()),
                     _tx.fromIndex,
                     proof.stateWitnesses[i]
                 ),
                 "Authenticity: state inclusion signer"
             );
+            require(proof.states[i].nonce > 0, "Authenticity: zero nonce");
 
             // check pubkey inclusion
             require(
                 MerkleTree.verify(
-                    accountRoot,
+                    common.accountRoot,
                     keccak256(abi.encodePacked(proof.pubkeys[i])),
                     proof.states[i].pubkeyID,
                     proof.pubkeyWitnesses[i]
@@ -107,19 +111,20 @@ library Authenticity {
                 "Authenticity: account does not exists"
             );
 
-            // construct the message
-            require(proof.states[i].nonce > 0, "Authenticity: zero nonce");
-            bytes memory txMsg = Tx.massMigrationMessageOf(
-                _tx,
-                proof.states[i].nonce - 1,
-                spokeID
-            );
-            messages[i] = BLS.hashToPoint(domain, txMsg);
+            uint256 nonce = proof.states[i].nonce - 1;
+            uint256 safeIndex = _tx.fromIndex + 1000000000000000;
+
+            for (uint256 k = 0; k < j; k++) {
+                if (senders[k] == safeIndex) nonce--;
+            }
+            senders[j] = safeIndex;
+            bytes memory txMsg = Tx.massMigrationMessageOf(_tx, nonce, spokeID);
+            messages[i] = BLS.hashToPoint(common.domain, txMsg);
         }
         bool callSuccess;
         bool checkSuccess;
         (checkSuccess, callSuccess) = BLS.verifyMultiple(
-            signature,
+            common.signature,
             proof.pubkeys,
             messages
         );
@@ -134,33 +139,32 @@ library Authenticity {
     }
 
     function verifyCreate2Transfer(
-        uint256[2] memory signature,
-        Types.SignatureProofWithReceiver memory proof,
-        bytes32 stateRoot,
-        bytes32 accountRoot,
-        bytes32 domain,
-        bytes memory txs
+        Types.AuthCommon memory common,
+        Types.SignatureProofWithReceiver memory proof
     ) internal view returns (Types.Result) {
-        uint256 size = txs.create2TransferSize();
+        uint256 size = common.txs.create2TransferSize();
         uint256[2][] memory messages = new uint256[2][](size);
-        for (uint256 i = 0; i < size; i++) {
-            Tx.Create2Transfer memory _tx = txs.create2TransferDecode(i);
+        uint256[] memory senders = new uint256[](size);
+        for (uint256 j = 0; j < size; j++) {
+            uint256 i = size - j - 1;
+            Tx.Create2Transfer memory _tx = common.txs.create2TransferDecode(i);
 
             // check state inclusion
             require(
                 MerkleTree.verify(
-                    stateRoot,
+                    common.stateRoot,
                     keccak256(proof.states[i].encode()),
                     _tx.fromIndex,
                     proof.stateWitnesses[i]
                 ),
                 "Authenticity: state inclusion signer"
             );
+            require(proof.states[i].nonce > 0, "Authenticity: zero nonce");
 
             // check pubkey inclusion
             require(
                 MerkleTree.verify(
-                    accountRoot,
+                    common.accountRoot,
                     keccak256(abi.encodePacked(proof.pubkeysSender[i])),
                     proof.states[i].pubkeyID,
                     proof.pubkeyWitnessesSender[i]
@@ -171,7 +175,7 @@ library Authenticity {
             // check receiver pubkey inclusion at committed accID
             require(
                 MerkleTree.verify(
-                    accountRoot,
+                    common.accountRoot,
                     keccak256(abi.encodePacked(proof.pubkeysReceiver[i])),
                     _tx.toPubkeyID,
                     proof.pubkeyWitnessesReceiver[i]
@@ -180,20 +184,26 @@ library Authenticity {
             );
 
             // construct the message
-            require(proof.states[i].nonce > 0, "Authenticity: zero nonce");
 
+            uint256 nonce = proof.states[i].nonce - 1;
+            uint256 safeIndex = _tx.fromIndex + 1000000000000000;
+
+            for (uint256 k = 0; k < j; k++) {
+                if (senders[k] == safeIndex) nonce--;
+            }
+            senders[j] = safeIndex;
             bytes memory txMsg = Tx.create2TransferMessageOf(
                 _tx,
-                proof.states[i].nonce - 1,
+                nonce,
                 proof.pubkeysReceiver[i]
             );
 
-            messages[i] = BLS.hashToPoint(domain, txMsg);
+            messages[i] = BLS.hashToPoint(common.domain, txMsg);
         }
         bool callSuccess;
         bool checkSuccess;
         (checkSuccess, callSuccess) = BLS.verifyMultiple(
-            signature,
+            common.signature,
             proof.pubkeysSender,
             messages
         );
diff --git a/contracts/libs/Types.sol b/contracts/libs/Types.sol
index dc0727b..c1681bd 100644
--- a/contracts/libs/Types.sol
+++ b/contracts/libs/Types.sol
@@ -21,6 +21,14 @@ library Types {
         bytes32[][] pubkeyWitnessesReceiver;
     }
 
+    struct AuthCommon {
+        uint256[2] signature;
+        bytes32 stateRoot;
+        bytes32 accountRoot;
+        bytes32 domain;
+        bytes txs;
+    }
+
     enum Usage { Genesis, Transfer, MassMigration, Create2Transfer, Deposit }
 
     // Batch represents the batch submitted periodically to the ethereum chain
diff --git a/contracts/rollup/Rollup.sol b/contracts/rollup/Rollup.sol
index 1f1d80d..255da30 100644
--- a/contracts/rollup/Rollup.sol
+++ b/contracts/rollup/Rollup.sol
@@ -341,15 +341,14 @@ contract RollupCore is BatchManager {
             checkInclusion(batches[batchID].commitmentRoot, target),
             "Rollup: Commitment not present in batch"
         );
-
-        Types.Result result = transfer.checkSignature(
-            target.commitment.body.signature,
-            signatureProof,
-            target.commitment.stateRoot,
-            target.commitment.body.accountRoot,
-            appID,
-            target.commitment.body.txs
-        );
+        Types.AuthCommon memory common = Types.AuthCommon({
+            signature: target.commitment.body.signature,
+            stateRoot: target.commitment.stateRoot,
+            accountRoot: target.commitment.body.accountRoot,
+            domain: appID,
+            txs: target.commitment.body.txs
+        });
+        Types.Result result = transfer.checkSignature(common, signatureProof);
 
         if (result != Types.Result.Ok) startRollingBack(batchID);
     }
@@ -363,15 +362,18 @@ contract RollupCore is BatchManager {
             checkInclusion(batches[batchID].commitmentRoot, target),
             "Commitment not present in batch"
         );
+        Types.AuthCommon memory common = Types.AuthCommon({
+            signature: target.commitment.body.signature,
+            stateRoot: target.commitment.stateRoot,
+            accountRoot: target.commitment.body.accountRoot,
+            domain: appID,
+            txs: target.commitment.body.txs
+        });
 
         Types.Result result = massMigration.checkSignature(
-            target.commitment.body.signature,
+            common,
             signatureProof,
-            target.commitment.stateRoot,
-            target.commitment.body.accountRoot,
-            appID,
-            target.commitment.body.spokeID,
-            target.commitment.body.txs
+            target.commitment.body.spokeID
         );
 
         if (result != Types.Result.Ok) startRollingBack(batchID);
@@ -386,14 +388,17 @@ contract RollupCore is BatchManager {
             checkInclusion(batches[batchID].commitmentRoot, target),
             "Rollup: Commitment not present in batch"
         );
+        Types.AuthCommon memory common = Types.AuthCommon({
+            signature: target.commitment.body.signature,
+            stateRoot: target.commitment.stateRoot,
+            accountRoot: target.commitment.body.accountRoot,
+            domain: appID,
+            txs: target.commitment.body.txs
+        });
 
         Types.Result result = create2Transfer.checkSignature(
-            target.commitment.body.signature,
-            signatureProof,
-            target.commitment.stateRoot,
-            target.commitment.body.accountRoot,
-            appID,
-            target.commitment.body.txs
+            common,
+            signatureProof
         );
 
         if (result != Types.Result.Ok) startRollingBack(batchID);
diff --git a/scripts/deploy.ts b/scripts/deploy.ts
index eb0eb31..513371e 100644
--- a/scripts/deploy.ts
+++ b/scripts/deploy.ts
@@ -33,18 +33,12 @@ async function main() {
     parameters.GENESIS_STATE_ROOT =
         argv.root || getDefaultGenesisRoot(parameters);
 
-    const genesisEth1Block = await provider.getBlockNumber();
     const contracts = await deployAll(signer, parameters, true);
     let addresses: { [key: string]: string } = {};
     Object.keys(contracts).map((contract: string) => {
         addresses[contract] = contracts[contract as keyof allContracts].address;
     });
-    const appID = await contracts.rollup.appID();
-    const auxiliary = {
-        domain: appID,
-        genesisEth1Block
-    };
-    const configs = { parameters, addresses, auxiliary };
+    const configs = { parameters, addresses };
     console.log("Writing genesis.json");
     fs.writeFileSync("genesis.json", JSON.stringify(configs, null, 4));
     console.log("Successsfully deployed", configs);
diff --git a/ts/factory.ts b/ts/factory.ts
index 82c7c09..c91c9d2 100644
--- a/ts/factory.ts
+++ b/ts/factory.ts
@@ -174,18 +174,23 @@ export function txTransferFactory(
     const txs: TxTransfer[] = [];
     const signatures = [];
     const senders = [];
+    const seenNonce: { [stateID: number]: number } = {};
     for (let i = 0; i < n; i++) {
         const sender = group.getUser(i % group.size);
         const receiver = group.getUser((i + 5) % group.size);
         const senderState = group.getState(sender);
         const amount = USDT.castBigNumber(senderState.balance.div(10));
         const fee = USDT.castBigNumber(amount.div(10));
+        const nonce = seenNonce[sender.stateID]
+            ? seenNonce[sender.stateID] + 1
+            : senderState.nonce;
+        seenNonce[sender.stateID] = nonce;
         const tx = new TxTransfer(
             sender.stateID,
             receiver.stateID,
             amount,
             fee,
-            senderState.nonce,
+            nonce,
             USDT
         );
         txs.push(tx);
@@ -200,17 +205,22 @@ export function txTransferFactory(
 export function txCreate2TransferFactory(
     registered: Group,
     unregistered: Group
-): { txs: TxCreate2Transfer[]; signature: solG1 } {
+): { txs: TxCreate2Transfer[]; signature: solG1; senders: User[] } {
     const txs: TxCreate2Transfer[] = [];
     const signatures = [];
-    if (registered.size != unregistered.size)
-        throw new Error("This factory supports same number of users only");
-    for (let i = 0; i < registered.size; i++) {
-        const sender = registered.getUser(i);
-        const reciver = unregistered.getUser(i);
+    const senders = [];
+    const seenNonce: { [stateID: number]: number } = {};
+    const n = Math.max(registered.size, unregistered.size);
+    for (let i = 0; i < n; i++) {
+        const sender = registered.getUser(i % registered.size);
+        const reciver = unregistered.getUser(i % unregistered.size);
         const senderState = registered.getState(sender);
         const amount = USDT.castBigNumber(senderState.balance.div(10));
         const fee = USDT.castBigNumber(amount.div(10));
+        const nonce = seenNonce[sender.stateID]
+            ? seenNonce[sender.stateID] + 1
+            : senderState.nonce;
+        seenNonce[sender.stateID] = nonce;
 
         const tx = new TxCreate2Transfer(
             sender.stateID,
@@ -219,14 +229,15 @@ export function txCreate2TransferFactory(
             reciver.pubkeyID,
             amount,
             fee,
-            senderState.nonce,
+            nonce,
             USDT
         );
         txs.push(tx);
         signatures.push(sender.sign(tx));
+        senders.push(sender);
     }
     const signature = aggregate(signatures).sol;
-    return { txs, signature };
+    return { txs, signature, senders };
 }
 
 export function txMassMigrationFactory(
@@ -236,16 +247,22 @@ export function txMassMigrationFactory(
     const txs: TxMassMigration[] = [];
     const signatures = [];
     const senders = [];
+    const seenNonce: { [stateID: number]: number } = {};
     for (const sender of group.userIterator()) {
         const senderState = group.getState(sender);
         const amount = USDT.castBigNumber(senderState.balance.div(10));
         const fee = USDT.castBigNumber(amount.div(10));
+        const nonce = seenNonce[sender.stateID]
+            ? seenNonce[sender.stateID] + 1
+            : senderState.nonce;
+        seenNonce[sender.stateID] = nonce;
+
         const tx = new TxMassMigration(
             sender.stateID,
             amount,
             spokeID,
             fee,
-            senderState.nonce,
+            nonce,
             USDT
         );
         txs.push(tx);
diff --git a/ts/tx.ts b/ts/tx.ts
index 34a700f..2211eb4 100644
--- a/ts/tx.ts
+++ b/ts/tx.ts
@@ -143,6 +143,9 @@ export class TxTransfer implements SignableTx {
         ]);
         return hexlify(concated);
     }
+    public toString(): string {
+        return `<Transfer ${this.fromIndex}->${this.toIndex} $${this.amount}  fee ${this.fee}  nonce ${this.nonce}>`;
+    }
 }
 
 export class TxMassMigration implements SignableTx {
@@ -221,6 +224,9 @@ export class TxMassMigration implements SignableTx {
         ]);
         return hexlify(concated);
     }
+    public toString(): string {
+        return `<Migration ${this.fromIndex}->${this.spokeID} $${this.amount}  fee ${this.fee}  nonce ${this.nonce}>`;
+    }
 }
 
 export class TxCreate2Transfer implements SignableTx {
@@ -332,4 +338,7 @@ export class TxCreate2Transfer implements SignableTx {
         ]);
         return hexlify(concated);
     }
+    public toString(): string {
+        return `<Create2Transfer ${this.fromIndex}->${this.toIndex} (${this.toPubkeyID}) $${this.amount}  fee ${this.fee}  nonce ${this.nonce}>`;
+    }
 }


Test patch:
diff --git a/contracts/test/TestCreate2Transfer.sol b/contracts/test/TestCreate2Transfer.sol
index 44a7a3b..0175417 100644
--- a/contracts/test/TestCreate2Transfer.sol
+++ b/contracts/test/TestCreate2Transfer.sol
@@ -16,14 +16,14 @@ contract TestCreate2Transfer is Create2Transfer {
         bytes memory txs
     ) public returns (uint256, Types.Result) {
         uint256 operationCost = gasleft();
-        Types.Result err = checkSignature(
-            signature,
-            proof,
-            stateRoot,
-            accountRoot,
-            domain,
-            txs
-        );
+        Types.AuthCommon memory common = Types.AuthCommon({
+            signature: signature,
+            stateRoot: stateRoot,
+            accountRoot: accountRoot,
+            domain: domain,
+            txs: txs
+        });
+        Types.Result err = checkSignature(common, proof);
         return (operationCost - gasleft(), err);
     }
 
diff --git a/contracts/test/TestMassMigration.sol b/contracts/test/TestMassMigration.sol
index fff36c8..3e72e9c 100644
--- a/contracts/test/TestMassMigration.sol
+++ b/contracts/test/TestMassMigration.sol
@@ -15,15 +15,14 @@ contract TestMassMigration is MassMigration {
         bytes memory txs
     ) public view returns (uint256 gasCost, Types.Result result) {
         gasCost = gasleft();
-        result = checkSignature(
-            signature,
-            proof,
-            stateRoot,
-            accountRoot,
-            domain,
-            spokeID,
-            txs
-        );
+        Types.AuthCommon memory common = Types.AuthCommon({
+            signature: signature,
+            stateRoot: stateRoot,
+            accountRoot: accountRoot,
+            domain: domain,
+            txs: txs
+        });
+        result = checkSignature(common, proof, spokeID);
         gasCost = gasCost - gasleft();
     }
 
diff --git a/contracts/test/TestTransfer.sol b/contracts/test/TestTransfer.sol
index 3411ab0..6dcb1a7 100644
--- a/contracts/test/TestTransfer.sol
+++ b/contracts/test/TestTransfer.sol
@@ -16,14 +16,14 @@ contract TestTransfer is Transfer {
         bytes memory txs
     ) public returns (uint256, Types.Result) {
         uint256 operationCost = gasleft();
-        Types.Result result = checkSignature(
-            signature,
-            proof,
-            stateRoot,
-            accountRoot,
-            domain,
-            txs
-        );
+        Types.AuthCommon memory common = Types.AuthCommon({
+            signature: signature,
+            stateRoot: stateRoot,
+            accountRoot: accountRoot,
+            domain: domain,
+            txs: txs
+        });
+        Types.Result result = checkSignature(common, proof);
         return (operationCost - gasleft(), result);
     }
 
diff --git a/test/slow/commit.create2transfer.test.ts b/test/slow/commit.create2transfer.test.ts
index caf2ebe..6e9c0e8 100644
--- a/test/slow/commit.create2transfer.test.ts
+++ b/test/slow/commit.create2transfer.test.ts
@@ -131,6 +131,45 @@ describe("Rollup Create2Transfer Commitment", () => {
         const receipt = await checkSigTx.wait();
         console.log("transaction gas cost:", receipt.gasUsed?.toNumber());
     }).timeout(800000);
+    it("transfer commitment: signature check, with 2 more tx from same sender", async function() {
+        const fewSenderGroup = usersWithState.slice(5);
+        const { txs, signature, senders } = txCreate2TransferFactory(
+            fewSenderGroup,
+            usersWithoutState
+        );
+        const pubkeysSender = senders.map(sender => sender.pubkey);
+        const pubkeysReceiver = usersWithoutState.getPubkeys();
+        const pubkeyWitnessesSender = senders.map(sender =>
+            registry.witness(sender.pubkeyID)
+        );
+        const pubkeyWitnessesReceiver = usersWithoutState
+            .getPubkeyIDs()
+            .map(pubkeyID => registry.witness(pubkeyID));
+        stateTree.processCreate2TransferCommit(txs, 0);
+
+        const postProofs = txs.map(tx => stateTree.getState(tx.fromIndex));
+        const proof = {
+            states: postProofs.map(proof => proof.state),
+            stateWitnesses: postProofs.map(proof => proof.witness),
+            pubkeysSender,
+            pubkeyWitnessesSender,
+            pubkeysReceiver: pubkeysReceiver,
+            pubkeyWitnessesReceiver: pubkeyWitnessesReceiver
+        };
+        const {
+            0: gasCost,
+            1: result
+        } = await rollup.callStatic._checkSignature(
+            signature,
+            proof,
+            stateTree.root,
+            registry.root(),
+            DOMAIN,
+            serialize(txs)
+        );
+        assert.equal(result, Result.Ok, `Got ${Result[result]}`);
+        console.log("operation gas cost:", gasCost.toString());
+    }).timeout(400000);
     it("create2trasnfer commitment: processTx", async function() {
         const { txs } = txCreate2TransferFactory(
             usersWithState,
diff --git a/test/slow/commit.massMigration.test.ts b/test/slow/commit.massMigration.test.ts
index 4f30ec6..338857e 100644
--- a/test/slow/commit.massMigration.test.ts
+++ b/test/slow/commit.massMigration.test.ts
@@ -88,6 +88,51 @@ describe("Rollup Mass Migration", () => {
         assert.equal(result, Result.Ok, `Got ${Result[result]}`);
         console.log("operation gas cost:", gasCost.toString());
     }).timeout(400000);
+    it("checks signature: same sender", async function() {
+        const smallGroup = users.slice(4);
+        let manySameSenderGroup = smallGroup;
+        for (let i = 0; i < 7; i++) {
+            manySameSenderGroup = manySameSenderGroup.join(smallGroup);
+        }
+        const { txs, signature, senders } = txMassMigrationFactory(
+            manySameSenderGroup,
+            spokeID
+        );
+        const pubkeys = senders.map(sender => sender.pubkey);
+        const pubkeyWitnesses = senders.map(sender =>
+            registry.witness(sender.pubkeyID)
+        );
+        stateTree.processMassMigrationCommit(txs, 0);
+        const serialized = serialize(txs);
+
+        // Need post stateWitnesses
+        const postProofs = txs.map(tx => stateTree.getState(tx.fromIndex));
+
+        const postStateRoot = stateTree.root;
+        const accountRoot = registry.root();
+
+        const proof = {
+            states: postProofs.map(proof => proof.state),
+            stateWitnesses: postProofs.map(proof => proof.witness),
+            pubkeys,
+            pubkeyWitnesses
+        };
+        const {
+            0: gasCost,
+            1: result
+        } = await rollup.callStatic.testCheckSignature(
+            signature,
+            proof,
+            postStateRoot,
+            accountRoot,
+            DOMAIN,
+            spokeID,
+            serialized
+        );
+        assert.equal(result, Result.Ok, `Got ${Result[result]}`);
+        console.log("operation gas cost:", gasCost.toString());
+    }).timeout(800000);
+
     it("checks state transitions", async function() {
         const { txs } = txMassMigrationFactory(users, spokeID);
         const feeReceiver = 0;
diff --git a/test/slow/commit.transfer.test.ts b/test/slow/commit.transfer.test.ts
index 46609ae..b289ebe 100644
--- a/test/slow/commit.transfer.test.ts
+++ b/test/slow/commit.transfer.test.ts
@@ -105,7 +105,40 @@ describe("Rollup Transfer Commitment", () => {
         const receipt = await tx.wait();
         console.log("transaction gas cost:", receipt.gasUsed?.toNumber());
     }).timeout(400000);
+    it("transfer commitment: signature check, with 2 more tx from same sender", async function() {
+        const fewSenderGroup = users.slice(3);
+        const { txs, signature, senders } = txTransferFactory(
+            fewSenderGroup,
+            COMMIT_SIZE
+        );
+        const pubkeys = senders.map(sender => sender.pubkey);
+        const pubkeyWitnesses = senders.map(sender =>
+            registry.witness(sender.pubkeyID)
+        );
 
+        stateTree.processTransferCommit(txs, 0);
+
+        const postProofs = txs.map(tx => stateTree.getState(tx.fromIndex));
+        const proof = {
+            states: postProofs.map(proof => proof.state),
+            stateWitnesses: postProofs.map(proof => proof.witness),
+            pubkeys,
+            pubkeyWitnesses
+        };
+        const {
+            0: gasCost,
+            1: result
+        } = await rollup.callStatic._checkSignature(
+            signature,
+            proof,
+            stateTree.root,
+            registry.root(),
+            DOMAIN,
+            serialize(txs)
+        );
+        assert.equal(result, Result.Ok, `Got ${Result[result]}`);
+        console.log("operation gas cost:", gasCost.toString());
+    }).timeout(400000);
     it("transfer commitment: processTx", async function() {
         const { txs } = txTransferFactory(users, COMMIT_SIZE);
         for (const tx of txs) {