[Deployment] Deploying multisig smart wallet in the Account abstraction multisig tutorial #625
-
EnvironmentTestnet zkSolc Versionlatest zksync-ethers Version6.7.0 Hardhat.config.tsimport { HardhatUserConfig } from "hardhat/config";
import "@matterlabs/hardhat-zksync";
const config: HardhatUserConfig = {
defaultNetwork: "zkSyncSepoliaTestnet",
networks: {
zkSyncSepoliaTestnet: {
url: "https://sepolia.era.zksync.dev",
ethNetwork: "sepolia",
zksync: true,
verifyURL: "https://explorer.sepolia.era.zksync.dev/contract_verification",
},
zkSyncMainnet: {
url: "https://mainnet.era.zksync.io",
ethNetwork: "mainnet",
zksync: true,
verifyURL: "https://zksync2-mainnet-explorer.zksync.io/contract_verification",
},
zkSyncGoerliTestnet: { // deprecated network
url: "https://testnet.era.zksync.dev",
ethNetwork: "goerli",
zksync: true,
verifyURL: "https://zksync2-testnet-explorer.zksync.dev/contract_verification",
},
dockerizedNode: {
url: "http://localhost:3050",
ethNetwork: "http://localhost:8545",
zksync: true,
},
inMemoryNode: {
url: "http://127.0.0.1:8011",
ethNetwork: "localhost", // in-memory node doesn't support eth node; removing this line will cause an error
zksync: true,
},
hardhat: {
zksync: true,
},
},
zksolc: {
version: "latest",
settings: {
// find all available options in the official documentation
// https://era.zksync.io/docs/tools/hardhat/hardhat-zksync-solc.html#configuration
isSystem: true,
},
},
solidity: {
version: "0.8.17",
},
};
export default config; Deployment Script (WITHOUT PRIVATE KEY)import { utils, Wallet, Provider, EIP712Signer, types } from "zksync-ethers";
import * as ethers from "ethers";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import dotenv from "dotenv";
// Load env file
dotenv.config();
const PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY || "";
// Put the address of your AA factory
const AA_FACTORY_ADDRESS = "0x14eE2E811547fC3d0eDFb453f0865267a56d6878";
export default async function (hre: HardhatRuntimeEnvironment) {
const provider = new Provider(hre.network.config.url);
// const provider = new Provider('http://127.0.0.1:8011')
// Private key of the account used to deploy
const wallet = new Wallet(PRIVATE_KEY).connect(provider);
const factoryArtifact = await hre.artifacts.readArtifact("AAFactory");
const aaFactory = new ethers.Contract(
AA_FACTORY_ADDRESS,
factoryArtifact.abi,
wallet,
);
// The two owners of the multisig
const owner1 = Wallet.createRandom();
const owner2 = Wallet.createRandom();
// For the simplicity of the tutorial, we will use zero hash as salt
const salt = ethers.ZeroHash;
console.log(salt, owner1.address, owner2.address);
// deploy account owned by owner1 & owner2
const tx = await aaFactory.deployAccount(
salt,
owner1.address,
owner2.address,
);
await tx.wait();
// Getting the address of the deployed contract account
// Always use the JS utility methods
const abiCoder = new ethers.AbiCoder();
const multisigAddress = utils.create2Address(
AA_FACTORY_ADDRESS,
await aaFactory.aaBytecodeHash(),
salt,
abiCoder.encode(["address", "address"], [owner1.address, owner2.address]),
);
console.log(`Multisig account deployed on address ${multisigAddress}`);
console.log("Sending funds to multisig account");
// Send funds to the multisig account we just deployed
await (
await wallet.sendTransaction({
to: multisigAddress,
// You can increase the amount of ETH sent to the multisig
value: ethers.parseEther("0.0008"),
nonce: await wallet.getNonce(),
})
).wait();
let multisigBalance = await provider.getBalance(multisigAddress);
console.log(`Multisig account balance is ${multisigBalance.toString()}`);
// Transaction to deploy a new account using the multisig we just deployed
let aaTx = await aaFactory.deployAccount.populateTransaction(
salt,
// These are accounts that will own the newly deployed account
Wallet.createRandom().address,
Wallet.createRandom().address,
);
const gasLimit = await provider.estimateGas({
...aaTx,
from: wallet.address,
});
const gasPrice = await provider.getGasPrice();
aaTx = {
...aaTx,
// deploy a new account using the multisig
from: multisigAddress,
gasLimit: gasLimit,
gasPrice: gasPrice,
chainId: (await provider.getNetwork()).chainId,
nonce: await provider.getTransactionCount(multisigAddress),
type: 113,
customData: {
gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
} as types.Eip712Meta,
value: 0n,
};
const signedTxHash = EIP712Signer.getSignedDigest(aaTx);
// Sign the transaction with both owners
const signature = ethers.concat([
ethers.Signature.from(owner1.signingKey.sign(signedTxHash)).serialized,
ethers.Signature.from(owner2.signingKey.sign(signedTxHash)).serialized,
]);
aaTx.customData = {
...aaTx.customData,
customSignature: signature,
};
console.log(
`The multisig's nonce before the first tx is ${await provider.getTransactionCount(
multisigAddress,
)}`,
);
const sentTx = await provider.broadcastTransaction(
types.Transaction.from(aaTx).serialized,
);
console.log(`Transaction sent from multisig with hash ${sentTx.hash}`);
await sentTx.wait();
// Checking that the nonce for the account has increased
console.log(
`The multisig's nonce after the first tx is ${await provider.getTransactionCount(
multisigAddress,
)}`,
);
multisigBalance = await provider.getBalance(multisigAddress);
console.log(`Multisig account balance is now ${multisigBalance.toString()}`);
} Package.json{
"name": "zksync-hardhat-template",
"description": "A template for zkSync smart contracts development with Hardhat",
"private": true,
"author": "Matter Labs",
"license": "MIT",
"repository": "https://github.com/matter-labs/zksync-hardhat-template.git",
"scripts": {
"deploy:factory": "hardhat deploy-zksync --script deploy-factory.ts",
"deploy:multisig": "hardhat deploy-zksync --script deploy-multisig.ts",
"compile": "hardhat compile",
"clean": "hardhat clean",
"test": "hardhat test --network hardhat"
},
"devDependencies": {
"@matterlabs/hardhat-zksync": "^1.0.0",
"@matterlabs/zksync-contracts": "^0.6.1",
"@nomiclabs/hardhat-etherscan": "^3.1.7",
"@openzeppelin/contracts": "^4.6.0",
"@types/chai": "^4.3.4",
"@types/mocha": "^10.0.1",
"chai": "^4.3.7",
"dotenv": "^16.0.3",
"ethers": "^6.9.2",
"hardhat": "^2.12.4",
"mocha": "^10.2.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5",
"zksync-ethers": "^6.7.0"
}
} Contract Code// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IAccount.sol";
import "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
// Used for signature validation
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
// Access zkSync system contracts for nonce validation via NONCE_HOLDER_SYSTEM_CONTRACT
import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";
// to call non-view function of system contracts
import "@matterlabs/zksync-contracts/l2/system-contracts/libraries/SystemContractsCaller.sol";
contract TwoUserMultisig is IAccount, IERC1271 {
// to get transaction hash
using TransactionHelper for Transaction;
// state variables for account owners
address public owner1;
address public owner2;
bytes4 constant EIP1271_SUCCESS_RETURN_VALUE = 0x1626ba7e;
modifier onlyBootloader() {
require(
msg.sender == BOOTLOADER_FORMAL_ADDRESS,
"Only bootloader can call this function"
);
// Continue execution if called from the bootloader.
_;
}
constructor(address _owner1, address _owner2) {
owner1 = _owner1;
owner2 = _owner2;
}
function validateTransaction(
bytes32,
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) external payable override onlyBootloader returns (bytes4 magic) {
return _validateTransaction(_suggestedSignedHash, _transaction);
}
function _validateTransaction(
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) internal returns (bytes4 magic) {
// Incrementing the nonce of the account.
// Note, that reserved[0] by convention is currently equal to the nonce passed in the transaction
SystemContractsCaller.systemCallWithPropagatedRevert(
uint32(gasleft()),
address(NONCE_HOLDER_SYSTEM_CONTRACT),
0,
abi.encodeCall(INonceHolder.incrementMinNonceIfEquals, (_transaction.nonce))
);
bytes32 txHash;
// While the suggested signed hash is usually provided, it is generally
// not recommended to rely on it to be present, since in the future
// there may be tx types with no suggested signed hash.
if (_suggestedSignedHash == bytes32(0)) {
txHash = _transaction.encodeHash();
} else {
txHash = _suggestedSignedHash;
}
// The fact there is enough balance for the account
// should be checked explicitly to prevent user paying for fee for a
// transaction that wouldn't be included on Ethereum.
uint256 totalRequiredBalance = _transaction.totalRequiredBalance();
require(totalRequiredBalance <= address(this).balance, "Not enough balance for fee + value");
if (isValidSignature(txHash, _transaction.signature) == EIP1271_SUCCESS_RETURN_VALUE) {
magic = ACCOUNT_VALIDATION_SUCCESS_MAGIC;
} else {
magic = bytes4(0);
}
}
function executeTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
) external payable override onlyBootloader {
_executeTransaction(_transaction);
}
function _executeTransaction(Transaction calldata _transaction) internal {
address to = address(uint160(_transaction.to));
uint128 value = Utils.safeCastToU128(_transaction.value);
bytes memory data = _transaction.data;
if (to == address(DEPLOYER_SYSTEM_CONTRACT)) {
uint32 gas = Utils.safeCastToU32(gasleft());
// Note, that the deployer contract can only be called
// with a "systemCall" flag.
SystemContractsCaller.systemCallWithPropagatedRevert(gas, to, value, data);
} else {
bool success;
assembly {
success := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0)
}
require(success);
}
}
function executeTransactionFromOutside(Transaction calldata _transaction)
external
payable
{
bytes4 magic = _validateTransaction(bytes32(0), _transaction);
require(magic == ACCOUNT_VALIDATION_SUCCESS_MAGIC, "NOT VALIDATED");
_executeTransaction(_transaction);
}
function isValidSignature(bytes32 _hash, bytes memory _signature)
public
view
override
returns (bytes4 magic)
{
magic = EIP1271_SUCCESS_RETURN_VALUE;
if (_signature.length != 130) {
// Signature is invalid anyway, but we need to proceed with the signature verification as usual
// in order for the fee estimation to work correctly
_signature = new bytes(130);
// Making sure that the signatures look like a valid ECDSA signature and are not rejected rightaway
// while skipping the main verification process.
_signature[64] = bytes1(uint8(27));
_signature[129] = bytes1(uint8(27));
}
(bytes memory signature1, bytes memory signature2) = extractECDSASignature(_signature);
if(!checkValidECDSASignatureFormat(signature1) || !checkValidECDSASignatureFormat(signature2)) {
magic = bytes4(0);
}
address recoveredAddr1 = ECDSA.recover(_hash, signature1);
address recoveredAddr2 = ECDSA.recover(_hash, signature2);
// Note, that we should abstain from using the require here in order to allow for fee estimation to work
if(recoveredAddr1 != owner1 || recoveredAddr2 != owner2) {
magic = bytes4(0);
}
}
// This function verifies that the ECDSA signature is both in correct format and non-malleable
function checkValidECDSASignatureFormat(bytes memory _signature) internal pure returns (bool) {
if(_signature.length != 65) {
return false;
}
uint8 v;
bytes32 r;
bytes32 s;
// Signature loading code
// we jump 32 (0x20) as the first slot of bytes contains the length
// we jump 65 (0x41) per signature
// for v we load 32 bytes ending with v (the first 31 come from s) then apply a mask
assembly {
r := mload(add(_signature, 0x20))
s := mload(add(_signature, 0x40))
v := and(mload(add(_signature, 0x41)), 0xff)
}
if(v != 27 && v != 28) {
return false;
}
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
if(uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return false;
}
return true;
}
function extractECDSASignature(bytes memory _fullSignature) internal pure returns (bytes memory signature1, bytes memory signature2) {
require(_fullSignature.length == 130, "Invalid length");
signature1 = new bytes(65);
signature2 = new bytes(65);
// Copying the first signature. Note, that we need an offset of 0x20
// since it is where the length of the `_fullSignature` is stored
assembly {
let r := mload(add(_fullSignature, 0x20))
let s := mload(add(_fullSignature, 0x40))
let v := and(mload(add(_fullSignature, 0x41)), 0xff)
mstore(add(signature1, 0x20), r)
mstore(add(signature1, 0x40), s)
mstore8(add(signature1, 0x60), v)
}
// Copying the second signature.
assembly {
let r := mload(add(_fullSignature, 0x61))
let s := mload(add(_fullSignature, 0x81))
let v := and(mload(add(_fullSignature, 0x82)), 0xff)
mstore(add(signature2, 0x20), r)
mstore(add(signature2, 0x40), s)
mstore8(add(signature2, 0x60), v)
}
}
function payForTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
) external payable override onlyBootloader {
bool success = _transaction.payToTheBootloader();
require(success, "Failed to pay the fee to the operator");
}
function prepareForPaymaster(
bytes32, // _txHash
bytes32, // _suggestedSignedHash
Transaction calldata _transaction
) external payable override onlyBootloader {
_transaction.processPaymasterInput();
}
fallback() external {
// fallback of default account shouldn't be called by bootloader under no circumstances
assert(msg.sender != BOOTLOADER_FORMAL_ADDRESS);
// If the contract is called directly, behave like an EOA
}
receive() external payable {
// If the contract is called directly, behave like an EOA.
// Note, that is okay if the bootloader sends funds with no calldata as it may be used for refunds/operator payments
}
} Does this work on other EVMs? (If yes, please list at least 1 of them)I didn't try it Description of What Your Contract DoesDeployes an multisig wallet account Repo Link (Optional)No response Additional Detailsi'm following this tutorial: https://code.zksync.io/tutorials/native-aa-multisig |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 2 replies
-
It looks like you've provided a detailed setup for deploying a multisig smart wallet using account abstraction on ZKsync, specifically targeting the ZKsync Sepolia Testnet. Your configuration and deployment scripts appear to be well-structured for this purpose. If you're encountering any specific issues or have questions about the process, please let me know so I can assist you further. If everything is working as expected, then you're on the right track with your current setup and deployment strategy. Your feedback helps us serve you better. If this fully addresses your question, please give it a thumbs up. If it doesn't, please give it a thumbs down. This will inform the community and team so we can improve our support. |
Beta Was this translation helpful? Give feedback.
-
Additional information: |
Beta Was this translation helpful? Give feedback.
-
I have changed the deploy AA factory. |
Beta Was this translation helpful? Give feedback.
I have changed the deploy AA factory.
const factory = await deployer.deploy( factoryArtifact, [bytecodeHash], "create", undefined, [ aaArtifact.bytecode, ], );
This problem does not happen again.