diff --git a/contracts/contracts/BLSWallet.sol b/contracts/contracts/BLSWallet.sol index d414cafe..b40719cc 100644 --- a/contracts/contracts/BLSWallet.sol +++ b/contracts/contracts/BLSWallet.sol @@ -14,31 +14,75 @@ import "./interfaces/IWallet.sol"; contract BLSWallet is Initializable, IBLSWallet { uint256 public nonce; + bytes32 public recoveryHash; + bytes32 pendingRecoveryHash; + uint256 pendingRecoveryHashTime; + bytes public approvedProxyAdminFunction; + bytes pendingPAFunction; + uint256 pendingPAFunctionTime; // BLS variables uint256[4] public blsPublicKey; + uint256[4] pendingBLSPublicKey; + uint256 pendingBLSPublicKeyTime; address public trustedBLSGateway; + address pendingBLSGateway; + uint256 pendingGatewayTime; + + event PendingRecoveryHashSet( + bytes32 pendingRecoveryHash + ); + event PendingBLSKeySet( + uint256[4] pendingBLSKey + ); + event PendingGatewaySet( + address pendingGateway + ); + event PendingProxyAdminFunctionSet( + bytes pendingProxyAdminFunction + ); + + event RecoveryHashUpdated( + bytes32 oldHash, + bytes32 newHash + ); + event BLSKeySet( + uint256[4] oldBLSKey, + uint256[4] newBLSKey + ); + event GatewayUpdated( + address oldGateway, + address newGateway + ); + event ProxyAdminFunctionApproved( + bytes approvedProxyAdmin + ); function initialize( address blsGateway ) external initializer { nonce = 0; trustedBLSGateway = blsGateway; + pendingGatewayTime = type(uint256).max; + pendingPAFunctionTime = type(uint256).max; } /** */ function latchBLSPublicKey( uint256[4] memory blsKey ) public onlyTrustedGateway { - for (uint256 i=0; i<4; i++) { - require( - blsPublicKey[i] == 0, - "BLSWallet: public key already set" - ); - } + require(isZeroBLSKey(blsPublicKey), "BLSWallet: public key already set"); blsPublicKey = blsKey; } + function isZeroBLSKey(uint256[4] memory blsKey) public pure returns (bool) { + bool isZero = true; + for (uint256 i=0; isZero && i<4; i++) { + isZero = (blsKey[i] == 0); + } + return isZero; + } + receive() external payable {} fallback() external payable {} @@ -49,18 +93,100 @@ contract BLSWallet is Initializable, IBLSWallet return blsPublicKey; } + /** + Wallet can update its recovery hash + */ + function setRecoveryHash(bytes32 hash) public onlyThis { + if (recoveryHash == bytes32(0)) { + recoveryHash = hash; + clearPendingRecoveryHash(); + emit RecoveryHashUpdated(bytes32(0), recoveryHash); + } + else { + pendingRecoveryHash = hash; + pendingRecoveryHashTime = block.timestamp + 604800; // 1 week from now + emit PendingRecoveryHashSet(pendingRecoveryHash); + } + } + + /** + Wallet can update its BLS key + */ + function setBLSPublicKey(uint256[4] memory blsKey) public onlyThis { + require(isZeroBLSKey(blsKey) == false, "BLSWallet: blsKey must be non-zero"); + pendingBLSPublicKey = blsKey; + pendingBLSPublicKeyTime = block.timestamp + 604800; // 1 week from now + emit PendingBLSKeySet(pendingBLSPublicKey); + } + /** Wallet can migrate to a new gateway, eg additional signature support */ - function setTrustedBLSGateway(address blsGateway) public onlyThis { - uint256 size; - // solhint-disable-next-line no-inline-assembly - assembly { size := extcodesize(blsGateway) } - require( - (blsGateway != address(0)) && (size > 0), - "BLSWallet: gateway address param not valid" - ); - trustedBLSGateway = blsGateway; + function setTrustedGateway(address blsGateway) public onlyTrustedGateway { + pendingBLSGateway = blsGateway; + pendingGatewayTime = block.timestamp + 604800; // 1 week from now + emit PendingGatewaySet(pendingBLSGateway); + } + + /** + Prepare wallet with desired implementation contract to upgrade to. + */ + function setProxyAdminFunction(bytes calldata encodedFunction) public onlyTrustedGateway { + pendingPAFunction = encodedFunction; + pendingPAFunctionTime = block.timestamp + 604800; // 1 week from now + emit PendingProxyAdminFunctionSet(pendingPAFunction); + } + + /** + Set results of any pending set operation if their respective timestamp has elapsed. + */ + function setAnyPending() public { + if (block.timestamp > pendingRecoveryHashTime) { + bytes32 previousRecoveryHash = recoveryHash; + recoveryHash = pendingRecoveryHash; + clearPendingRecoveryHash(); + emit RecoveryHashUpdated(previousRecoveryHash, recoveryHash); + } + if (block.timestamp > pendingBLSPublicKeyTime) { + uint256[4] memory previousBLSPublicKey = blsPublicKey; + blsPublicKey = pendingBLSPublicKey; + pendingBLSPublicKeyTime = type(uint256).max; + pendingBLSPublicKey = [0,0,0,0]; + emit BLSKeySet(previousBLSPublicKey, blsPublicKey); + } + if (block.timestamp > pendingGatewayTime) { + address previousGateway = trustedBLSGateway; + trustedBLSGateway = pendingBLSGateway; + pendingGatewayTime = type(uint256).max; + pendingBLSGateway = address(0); + emit GatewayUpdated(previousGateway, trustedBLSGateway); + } + if (block.timestamp > pendingPAFunctionTime) { + approvedProxyAdminFunction = pendingPAFunction; + pendingPAFunctionTime = type(uint256).max; + pendingPAFunction = new bytes(0); + emit ProxyAdminFunctionApproved(approvedProxyAdminFunction); + } + } + + function clearPendingRecoveryHash() internal { + pendingRecoveryHashTime = type(uint256).max; + pendingRecoveryHash = bytes32(0); + } + + function recover( + uint256[4] calldata newBLSKey + ) public onlyTrustedGateway { + // set new bls key + blsPublicKey = newBLSKey; + // clear any pending operations + clearPendingRecoveryHash(); + pendingBLSPublicKeyTime = type(uint256).max; + pendingBLSPublicKey = [0,0,0,0]; + pendingGatewayTime = type(uint256).max; + pendingBLSGateway = address(0); + pendingPAFunctionTime = type(uint256).max; + pendingPAFunction = new bytes(0); } /** @@ -113,6 +239,10 @@ contract BLSWallet is Initializable, IBLSWallet } } + function clearApprovedProxyAdminFunction() public onlyTrustedGateway { + approvedProxyAdminFunction = new bytes(0); + } + /** Consecutive nonce increment, contract can be upgraded for other types */ diff --git a/contracts/contracts/VerificationGateway.sol b/contracts/contracts/VerificationGateway.sol index 1c0fffd8..36299999 100644 --- a/contracts/contracts/VerificationGateway.sol +++ b/contracts/contracts/VerificationGateway.sol @@ -141,18 +141,19 @@ contract VerificationGateway } /** - Calls to proxy admin, exclusively from a wallet. + Calls to proxy admin, exclusively from a wallet. Must be called twice. + Once to set the function in the wallet as pending, then again after the recovery time. @param hash calling wallet's bls public key hash @param encodedFunction the selector and params to call (first encoded param must be calling wallet) */ function walletAdminCall( bytes32 hash, bytes calldata encodedFunction - ) public onlyWallet(hash) returns ( - bytes memory - ) { - // ensure first parameter is the calling wallet - bytes memory encodedAddress = abi.encode(address(walletFromHash(hash))); + ) public onlyWallet(hash) { + IWallet wallet = walletFromHash(hash); + + // ensure first parameter is the calling wallet address + bytes memory encodedAddress = abi.encode(address(wallet)); uint8 selectorOffset = 4; for (uint256 i=0; i<32; i++) { require( @@ -160,9 +161,61 @@ contract VerificationGateway "VG: first param to proxy admin is not calling wallet" ); } - (bool success, bytes memory result) = address(walletProxyAdmin).call(encodedFunction); - require(success, "VG: call to proxy admin failed"); - return result; + + wallet.setAnyPending(); + + // ensure wallet has pre-approved encodedFunction + bytes memory approvedFunction = wallet.approvedProxyAdminFunction(); + bool matchesApproved = (encodedFunction.length == approvedFunction.length); + for (uint i=0; matchesApproved && i 0), + "BLSWallet: gateway address param not valid" + ); + walletFromHash(hash).setTrustedGateway(blsGateway); } /** @@ -189,12 +242,11 @@ contract VerificationGateway // create wallet if not found createNewWallet(bundle.senderPublicKeys[i]); - // construct params for signature verification + // calculate public key hash publicKeyHash = keccak256(abi.encodePacked( bundle.senderPublicKeys[i] )); wallet = walletFromHash(publicKeyHash); - // check nonce then perform action if (bundle.operations[i].nonce == wallet.nonce()) { // request wallet perform operation @@ -221,7 +273,6 @@ contract VerificationGateway ) private { bytes32 publicKeyHash = keccak256(abi.encodePacked(publicKey)); address blsWallet = address(walletFromHash(publicKeyHash)); - // wallet with publicKeyHash doesn't exist at expected create2 address if (blsWallet == address(0)) { blsWallet = address(new TransparentUpgradeableProxy{salt: publicKeyHash}( diff --git a/contracts/contracts/interfaces/IWallet.sol b/contracts/contracts/interfaces/IWallet.sol index 361dbc22..1e441794 100644 --- a/contracts/contracts/interfaces/IWallet.sol +++ b/contracts/contracts/interfaces/IWallet.sol @@ -26,6 +26,18 @@ interface IWallet { bool success, bytes[] memory results ); + + function recoveryHash() external returns (bytes32); + function recover(uint256[4] calldata newBLSKey) external; + + // prepares gateway to be set (after pending timestamp) + function setTrustedGateway(address gateway) external; + // checks any pending variables and sets them if past their timestamp + function setAnyPending() external; + + function setProxyAdminFunction(bytes memory) external; + function approvedProxyAdminFunction() external view returns (bytes memory); + function clearApprovedProxyAdminFunction() external; } /** Interface for bls-specific functions @@ -38,7 +50,5 @@ interface IBLSWallet is IWallet { ) external; function getBLSPublicKey() external view returns (uint256[4] memory); - function setTrustedBLSGateway(address blsGateway) external; - } \ No newline at end of file diff --git a/contracts/shared/helpers/Fixture.ts b/contracts/shared/helpers/Fixture.ts index ca6ca070..dd42afa8 100644 --- a/contracts/shared/helpers/Fixture.ts +++ b/contracts/shared/helpers/Fixture.ts @@ -1,12 +1,19 @@ import "@nomiclabs/hardhat-ethers"; -import { ethers } from "hardhat"; -import { Signer, Contract, ContractFactory, BigNumber } from "ethers"; +import { ethers, network } from "hardhat"; +import { + Signer, + Contract, + ContractFactory, + BigNumber, + BigNumberish, +} from "ethers"; import { Provider } from "@ethersproject/abstract-provider"; import { BlsWalletWrapper, BlsWalletSigner, initBlsWalletSigner, + Bundle, } from "../../clients/src"; import Range from "./Range"; @@ -131,4 +138,66 @@ export default class Fixture { this.lazyBlsWallets.map((lazyWallet) => lazyWallet()), ); } + + bundleFrom( + wallet: BlsWalletWrapper, + contract: Contract, + method: string, + params: any[], + nonce: BigNumberish, + ethValue: BigNumberish = 0, + ): Bundle { + return this.blsWalletSigner.aggregate([ + wallet.sign({ + nonce: nonce, + actions: [ + { + ethValue: ethValue, + contractAddress: contract.address, + encodedFunction: contract.interface.encodeFunctionData( + method, + params, + ), + }, + ], + }), + ]); + } + + async call( + wallet: BlsWalletWrapper, + contract: Contract, + method: string, + params: any[], + nonce: BigNumberish, + ethValue: BigNumberish = 0, + ) { + await ( + await this.verificationGateway.processBundle( + this.bundleFrom(wallet, contract, method, params, nonce, ethValue), + ) + ).wait(); + } + + async callStatic( + wallet: BlsWalletWrapper, + contract: Contract, + method: string, + params: any[], + nonce: BigNumberish, + ethValue: BigNumberish = 0, + ) { + return await this.verificationGateway.callStatic.processBundle( + this.bundleFrom(wallet, contract, method, params, nonce, ethValue), + ); + } + + async advanceTimeBy(seconds: number) { + // Advance time one week + const latestTimestamp = (await ethers.provider.getBlock("latest")) + .timestamp; + await network.provider.send("evm_setNextBlockTimestamp", [ + BigNumber.from(latestTimestamp).add(seconds).toHexString(), + ]); + } } diff --git a/contracts/shared/helpers/callProxyAdmin.ts b/contracts/shared/helpers/callProxyAdmin.ts index 89778d2c..f8af02cd 100644 --- a/contracts/shared/helpers/callProxyAdmin.ts +++ b/contracts/shared/helpers/callProxyAdmin.ts @@ -16,25 +16,6 @@ export async function proxyAdminCall( ).wait(); } -/** Statically call sign a single operation bundle that contains a single - * proxy admin action, and return the bytes result of a staticcall to the - * verification gateway. - */ -export async function proxyAdminCallStatic( - fx: Fixture, - wallet: BlsWalletWrapper, - functionName: string, - functionParams: any[], -): Promise { - const bundleResult = await fx.verificationGateway.callStatic.processBundle( - await proxyAdminBundle(fx, wallet, functionName, functionParams), - ); - return ethers.utils.defaultAbiCoder.decode( - ["bytes"], - bundleResult.results[0][0], // first and only operation/action result - )[0]; -} - /** proxyAdmin function data is a parameter to the walletAdminCall * function of the Verification Gateway. The encoded admin call is * then wrapped in an action, signed into a single operation bundle. diff --git a/contracts/test/recovery-test.ts b/contracts/test/recovery-test.ts new file mode 100644 index 00000000..e9623317 --- /dev/null +++ b/contracts/test/recovery-test.ts @@ -0,0 +1,148 @@ +import { expect } from "chai"; + +import { ethers, network } from "hardhat"; + +import Fixture from "../shared/helpers/Fixture"; +import deployAndRunPrecompileCostEstimator from "../shared/helpers/deployAndRunPrecompileCostEstimator"; +import { defaultDeployerAddress } from "../shared/helpers/deployDeployer"; + +import { BigNumber } from "ethers"; +import { PublicKey } from "../clients/deps/hubble-bls/mcl"; + +describe("Recovery", async function () { + this.beforeAll(async function () { + // deploy the deployer contract for the transient hardhat network + if (network.name === "hardhat") { + // fund deployer wallet address + const fundedSigner = (await ethers.getSigners())[0]; + await ( + await fundedSigner.sendTransaction({ + to: defaultDeployerAddress(), + value: ethers.utils.parseEther("1"), + }) + ).wait(); + + // deploy the precompile contract (via deployer) + console.log("PCE:", await deployAndRunPrecompileCostEstimator()); + } + }); + + const safetyDelaySeconds = 7 * 24 * 60 * 60; + let fx: Fixture; + let wallet1, wallet2, walletAttacker; + let blsWallet; + let recoverySigner; + let hash1, hash2; + let salt; + let recoveryHash; + beforeEach(async function () { + fx = await Fixture.create(); + + wallet1 = await fx.lazyBlsWallets[0](); + wallet2 = await fx.lazyBlsWallets[1](); + walletAttacker = await fx.lazyBlsWallets[2](); + blsWallet = await ethers.getContractAt("BLSWallet", wallet1.address); + recoverySigner = (await ethers.getSigners())[1]; + + hash1 = wallet1.blsWalletSigner.getPublicKeyHash(wallet1.privateKey); + hash2 = wallet2.blsWalletSigner.getPublicKeyHash(wallet2.privateKey); + salt = "0x1234567812345678123456781234567812345678123456781234567812345678"; + recoveryHash = ethers.utils.solidityKeccak256( + ["address", "bytes32", "bytes32"], + [recoverySigner.address, hash1, salt], + ); + }); + + it("should update bls key", async function () { + const newKey: PublicKey = [1, 2, 3, 4].map(BigNumber.from); + const initialKey = await blsWallet.getBLSPublicKey(); + + await fx.call(wallet1, blsWallet, "setBLSPublicKey", [newKey], 1); + + expect(await blsWallet.getBLSPublicKey()).to.eql(initialKey); + + await fx.advanceTimeBy(safetyDelaySeconds + 1); + await (await blsWallet.setAnyPending()).wait(); + + expect(await blsWallet.getBLSPublicKey()).to.eql(newKey); + }); + + it("should set recovery hash", async function () { + // set instantly from 0 value + await fx.call(wallet1, blsWallet, "setRecoveryHash", [recoveryHash], 1); + expect(await blsWallet.recoveryHash()).to.equal(recoveryHash); + + // new value set after delay from non-zero value + salt = "0x" + "AB".repeat(32); + const newRecoveryHash = ethers.utils.solidityKeccak256( + ["address", "bytes32", "bytes32"], + [recoverySigner.address, hash1, salt], + ); + await fx.call(wallet1, blsWallet, "setRecoveryHash", [newRecoveryHash], 2); + expect(await blsWallet.recoveryHash()).to.equal(recoveryHash); + await fx.advanceTimeBy(safetyDelaySeconds + 1); + await (await blsWallet.setAnyPending()).wait(); + expect(await blsWallet.recoveryHash()).to.equal(newRecoveryHash); + }); + + it("should recover before bls key update", async function () { + await fx.call(wallet1, blsWallet, "setRecoveryHash", [recoveryHash], 1); + const attackKey: PublicKey = + await walletAttacker.blsWalletSigner.getPublicKey( + walletAttacker.privateKey, + ); + + // Attacker assumed to have compromised current bls key, and wishes to reset + // the contract's bls key to their own. + await fx.call(wallet1, blsWallet, "setBLSPublicKey", [attackKey], 2); + + await fx.advanceTimeBy(safetyDelaySeconds / 2); // wait half the time + await (await blsWallet.setAnyPending()).wait(); + + const safeKey: PublicKey = await wallet2.blsWalletSigner.getPublicKey( + wallet2.privateKey, + ); + await ( + await fx.verificationGateway + .connect(recoverySigner) + .recoverWallet(hash1, salt, safeKey) + ).wait(); + + // key reset via recovery + expect(await blsWallet.getBLSPublicKey()).to.eql( + safeKey.map(BigNumber.from), + ); + + await fx.advanceTimeBy(safetyDelaySeconds / 2 + 1); // wait remainder the time + + // attacker's key not set after waiting full safety delay + expect(await blsWallet.getBLSPublicKey()).to.eql( + safeKey.map(BigNumber.from), + ); + + let walletFromKey = await fx.verificationGateway.walletFromHash( + wallet1.blsWalletSigner.getPublicKeyHash(wallet1.privateKey), + ); + expect(walletFromKey).to.not.equal(blsWallet.address); + walletFromKey = await fx.verificationGateway.walletFromHash( + walletAttacker.blsWalletSigner.getPublicKeyHash( + walletAttacker.privateKey, + ), + ); + expect(walletFromKey).to.not.equal(blsWallet.address); + walletFromKey = await fx.verificationGateway.walletFromHash( + wallet2.blsWalletSigner.getPublicKeyHash(wallet2.privateKey), + ); + expect(walletFromKey).to.equal(blsWallet.address); + + // verify recovered bls key can successfully call wallet-only function (eg setTrustedGateway) + const res = await fx.callStatic( + wallet2, + fx.verificationGateway, + "setTrustedBLSGateway", + [hash2, fx.verificationGateway.address], + 3, + ); + expect(res.successes[0]).to.equal(true); + }); +}); diff --git a/contracts/test/upgrade-test.ts b/contracts/test/upgrade-test.ts index 6395881f..57a9c7d1 100644 --- a/contracts/test/upgrade-test.ts +++ b/contracts/test/upgrade-test.ts @@ -9,7 +9,6 @@ import { defaultDeployerAddress } from "../shared/helpers/deployDeployer"; import { proxyAdminBundle, proxyAdminCall, - proxyAdminCallStatic, } from "../shared/helpers/callProxyAdmin"; import Create2Fixture from "../shared/helpers/Create2Fixture"; import { BLSOpen } from "../typechain"; @@ -36,29 +35,12 @@ describe("Upgrade", async function () { } }); + const safetyDelaySeconds = 7 * 24 * 60 * 60; let fx: Fixture; beforeEach(async function () { fx = await Fixture.create(); }); - it("should read proxy admin function", async function () { - const wallet = await fx.lazyBlsWallets[0](); - - const resultBytes = await proxyAdminCallStatic( - fx, - wallet, - "getProxyAdmin", - [wallet.address], - ); - const adminAddress = ethers.utils.defaultAbiCoder.decode( - ["address"], - resultBytes, - )[0]; - expect(adminAddress).to.equal( - await fx.verificationGateway.walletProxyAdmin(), - ); - }); - it("should upgrade wallet contract", async function () { const MockWalletUpgraded = await ethers.getContractFactory( "MockWalletUpgraded", @@ -66,6 +48,23 @@ describe("Upgrade", async function () { const mockWalletUpgraded = await MockWalletUpgraded.deploy(); const wallet = await fx.lazyBlsWallets[0](); + + // prepare call + await proxyAdminCall(fx, wallet, "upgrade", [ + wallet.address, + mockWalletUpgraded.address, + ]); + + // Advance time one week + const latestTimestamp = (await ethers.provider.getBlock("latest")) + .timestamp; + await network.provider.send("evm_setNextBlockTimestamp", [ + BigNumber.from(latestTimestamp) + .add(safetyDelaySeconds + 1) + .toHexString(), + ]); + + // make call await proxyAdminCall(fx, wallet, "upgrade", [ wallet.address, mockWalletUpgraded.address, @@ -100,13 +99,28 @@ describe("Upgrade", async function () { // Sign simple address message const addressMessage = solidityPack(["address"], [walletAddress]); const signedAddress = blsSigner.sign(addressMessage); + + const proxyAdmin2Address = await vg2.walletProxyAdmin(); // Get admin action to change proxy const bundle = await proxyAdminBundle(fx, walletOldVg, "changeProxyAdmin", [ walletAddress, - await vg2.walletProxyAdmin(), + proxyAdmin2Address, ]); const changeProxyAction = bundle.operations[0].actions[0]; + // prepare call + await proxyAdminCall(fx, walletOldVg, "changeProxyAdmin", [ + walletAddress, + proxyAdmin2Address, + ]); + + // Advance time one week + await fx.advanceTimeBy(safetyDelaySeconds + 1); + + const hash = walletOldVg.blsWalletSigner.getPublicKeyHash( + walletOldVg.privateKey, + ); + // Atomically perform actions: // 1. register external wallet in vg2 // 2. change proxy admin to that in vg2 @@ -115,7 +129,7 @@ describe("Upgrade", async function () { await fx.verificationGateway.processBundle( fx.blsWalletSigner.aggregate([ walletOldVg.sign({ - nonce: BigNumber.from(1), + nonce: BigNumber.from(2), actions: [ { ethValue: 0, @@ -128,12 +142,12 @@ describe("Upgrade", async function () { changeProxyAction, { ethValue: 0, - contractAddress: walletAddress, - encodedFunction: ( - await ethers.getContractFactory("BLSWallet") - ).interface.encodeFunctionData("setTrustedBLSGateway", [ - vg2.address, - ]), + contractAddress: fx.verificationGateway.address, + encodedFunction: + fx.verificationGateway.interface.encodeFunctionData( + "setTrustedBLSGateway", + [hash, vg2.address], + ), }, ], }), @@ -142,27 +156,34 @@ describe("Upgrade", async function () { ).wait(); // Create required objects for data/contracts for checks - const hash = walletOldVg.blsWalletSigner.getPublicKeyHash( - walletOldVg.privateKey, - ); const proxyAdmin = await ethers.getContractAt( "ProxyAdmin", await vg2.walletProxyAdmin(), ); - const blsWallet = await ethers.getContractAt("BLSWallet", walletAddress); // Direct checks corresponding to each action expect(await vg2.walletFromHash(hash)).to.equal(walletAddress); expect(await proxyAdmin.getProxyAdmin(walletAddress)).to.equal( proxyAdmin.address, ); + + const blsWallet = await ethers.getContractAt("BLSWallet", walletAddress); + // New verification gateway pending + expect(await blsWallet.trustedBLSGateway()).to.equal( + fx.verificationGateway.address, + ); + // Advance time one week + await fx.advanceTimeBy(safetyDelaySeconds + 1); + // set pending + await (await blsWallet.setAnyPending()).wait(); + // Check new verification gateway was set expect(await blsWallet.trustedBLSGateway()).to.equal(vg2.address); // Check new gateway has wallet via static call through new gateway const bundleResult = await vg2.callStatic.processBundle( fx.blsWalletSigner.aggregate([ walletOldVg.sign({ - nonce: BigNumber.from(2), + nonce: BigNumber.from(3), actions: [ { ethValue: 0,