-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
docs: add test for multisig tutorial (#76)
- updates & adds a test for the multisig tutorial - adds a code import plugin & replaces hard-coded code snippets with direct imports - minor small fixes
1 parent
72ad022
commit b46e863
Showing
28 changed files
with
8,053 additions
and
854 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
WALLET_PRIVATE_KEY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
lerna-debug.log* | ||
.vscode | ||
|
||
# hardhat artifacts | ||
artifacts | ||
cache | ||
|
||
# zksync artifacts | ||
artifacts-zk | ||
cache-zk | ||
deployments-zk/ | ||
|
||
# Diagnostic reports (https://nodejs.org/api/report.html) | ||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | ||
|
||
# Runtime data | ||
pids | ||
*.pid | ||
*.seed | ||
*.pid.lock | ||
|
||
# Directory for instrumented libs generated by jscoverage/JSCover | ||
lib-cov | ||
|
||
# Coverage directory used by tools like istanbul | ||
coverage | ||
*.lcov | ||
|
||
# nyc test coverage | ||
.nyc_output | ||
|
||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | ||
.grunt | ||
|
||
# Bower dependency directory (https://bower.io/) | ||
bower_components | ||
|
||
# node-waf configuration | ||
.lock-wscript | ||
|
||
# Compiled binary addons (https://nodejs.org/api/addons.html) | ||
build/Release | ||
|
||
# Dependency directories | ||
node_modules/ | ||
jspm_packages/ | ||
|
||
# TypeScript v1 declaration files | ||
typings/ | ||
|
||
# TypeScript cache | ||
*.tsbuildinfo | ||
|
||
# Optional npm cache directory | ||
.npm | ||
|
||
# Optional eslint cache | ||
.eslintcache | ||
|
||
# Microbundle cache | ||
.rpt2_cache/ | ||
.rts2_cache_cjs/ | ||
.rts2_cache_es/ | ||
.rts2_cache_umd/ | ||
|
||
# Optional REPL history | ||
.node_repl_history | ||
|
||
# Output of 'npm pack' | ||
*.tgz | ||
|
||
# Yarn Integrity file | ||
.yarn-integrity | ||
|
||
# dotenv environment variables file | ||
.env | ||
.env.test | ||
|
||
# parcel-bundler cache (https://parceljs.org/) | ||
.cache | ||
|
||
# Next.js build output | ||
.next | ||
|
||
# Nuxt.js build / generate output | ||
.nuxt | ||
dist | ||
|
||
# Gatsby files | ||
.cache/ | ||
# Comment in the public line in if your project uses Gatsby and *not* Next.js | ||
# https://nextjs.org/blog/next-9-1#public-directory-support | ||
# public | ||
|
||
# vuepress build output | ||
.vuepress/dist | ||
|
||
# Serverless directories | ||
.serverless/ | ||
|
||
# FuseBox cache | ||
.fusebox/ | ||
|
||
# DynamoDB Local files | ||
.dynamodb/ | ||
|
||
# TernJS port file | ||
.tern-port |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
legacy-peer-deps=true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.17; | ||
|
||
import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol"; | ||
import "@matterlabs/zksync-contracts/l2/system-contracts/libraries/SystemContractsCaller.sol"; | ||
|
||
contract AAFactory { | ||
bytes32 public aaBytecodeHash; | ||
|
||
constructor(bytes32 _aaBytecodeHash) { | ||
aaBytecodeHash = _aaBytecodeHash; | ||
} | ||
|
||
function deployAccount( | ||
bytes32 salt, | ||
address owner1, | ||
address owner2 | ||
) external returns (address accountAddress) { | ||
(bool success, bytes memory returnData) = SystemContractsCaller | ||
.systemCallWithReturndata( | ||
uint32(gasleft()), | ||
address(DEPLOYER_SYSTEM_CONTRACT), | ||
uint128(0), | ||
abi.encodeCall( | ||
DEPLOYER_SYSTEM_CONTRACT.create2Account, | ||
(salt, aaBytecodeHash, abi.encode(owner1, owner2), IContractDeployer.AccountAbstractionVersion.Version1) | ||
) | ||
); | ||
require(success, "Deployment failed"); | ||
|
||
(accountAddress) = abi.decode(returnData, (address)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
// 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"; | ||
|
||
// ANCHOR: import-ECDSA | ||
// Used for signature validation | ||
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | ||
// ANCHOR_END: import-ECDSA | ||
|
||
// ANCHOR: import-constants | ||
// Access ZKsync system contracts for nonce validation via NONCE_HOLDER_SYSTEM_CONTRACT | ||
import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol"; | ||
// ANCHOR_END: import-constants | ||
// 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; | ||
|
||
// ANCHOR: account-owner-vars | ||
// state variables for account owners | ||
address public owner1; | ||
address public owner2; | ||
// ANCHOR_END: account-owner-vars | ||
|
||
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. | ||
_; | ||
} | ||
|
||
// ANCHOR: constructor | ||
constructor(address _owner1, address _owner2) { | ||
owner1 = _owner1; | ||
owner2 = _owner2; | ||
} | ||
// ANCHOR_END: constructor | ||
|
||
function validateTransaction( | ||
bytes32, | ||
bytes32 _suggestedSignedHash, | ||
Transaction calldata _transaction | ||
) external payable override onlyBootloader returns (bytes4 magic) { | ||
return _validateTransaction(_suggestedSignedHash, _transaction); | ||
} | ||
|
||
// ANCHOR: _validateTransaction | ||
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); | ||
} | ||
} | ||
// ANCHOR_END: _validateTransaction | ||
|
||
function executeTransaction( | ||
bytes32, | ||
bytes32, | ||
Transaction calldata _transaction | ||
) external payable override onlyBootloader { | ||
_executeTransaction(_transaction); | ||
} | ||
|
||
// ANCHOR: _executeTransaction | ||
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 "enableEraVMExtensions" 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); | ||
} | ||
} | ||
// ANCHOR_END: _executeTransaction | ||
|
||
function executeTransactionFromOutside(Transaction calldata _transaction) | ||
external | ||
payable | ||
{ | ||
bytes4 magic = _validateTransaction(bytes32(0), _transaction); | ||
require(magic == ACCOUNT_VALIDATION_SUCCESS_MAGIC, "NOT VALIDATED"); | ||
_executeTransaction(_transaction); | ||
} | ||
|
||
// ANCHOR: isValidSignature | ||
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); | ||
} | ||
} | ||
// ANCHOR_END: isValidSignature | ||
|
||
// 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) | ||
} | ||
} | ||
|
||
// ANCHOR: payForTransaction | ||
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"); | ||
} | ||
// ANCHOR_END: payForTransaction | ||
|
||
// ANCHOR: prepareForPaymaster | ||
function prepareForPaymaster( | ||
bytes32, // _txHash | ||
bytes32, // _suggestedSignedHash | ||
Transaction calldata _transaction | ||
) external payable override onlyBootloader { | ||
_transaction.processPaymasterInput(); | ||
} | ||
// ANCHOR_END: prepareForPaymaster | ||
|
||
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { utils, Wallet } from 'zksync-ethers'; | ||
import type { HardhatRuntimeEnvironment } from 'hardhat/types'; | ||
import { Deployer } from '@matterlabs/hardhat-zksync'; | ||
import dotenv from 'dotenv'; | ||
dotenv.config(); | ||
|
||
export default async function (hre: HardhatRuntimeEnvironment) { | ||
// Private key of the account used to deploy | ||
const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!); | ||
const deployer = new Deployer(hre, wallet); | ||
const factoryArtifact = await deployer.loadArtifact('AAFactory'); | ||
const aaArtifact = await deployer.loadArtifact('TwoUserMultisig'); | ||
|
||
// Getting the bytecodeHash of the account | ||
const bytecodeHash = utils.hashBytecode(aaArtifact.bytecode); | ||
|
||
const factory = await deployer.deploy( | ||
factoryArtifact, | ||
[bytecodeHash], | ||
undefined, | ||
undefined, // Override transaction section | ||
[ | ||
// Since the factory requires the code of the multisig to be available, | ||
// we need to pass it here as well. | ||
aaArtifact.bytecode, | ||
] | ||
); | ||
|
||
const factoryAddress = await factory.getAddress(); | ||
|
||
console.log(`AA factory address: ${factoryAddress}`); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
// ANCHOR: min-script | ||
import { utils, Wallet, Provider, EIP712Signer, types } from 'zksync-ethers'; | ||
import * as ethers from 'ethers'; | ||
import type { HardhatRuntimeEnvironment } from 'hardhat/types'; | ||
import dotenv from 'dotenv'; | ||
dotenv.config(); | ||
|
||
// Put the address of your AA factory | ||
const AA_FACTORY_ADDRESS = '<FACTORY_ADDRESS>'; //sepolia | ||
|
||
export default async function (hre: HardhatRuntimeEnvironment) { | ||
// @ts-expect-error target network in config file | ||
const provider = new Provider(hre.network.config.url); | ||
// Private key of the account used to deploy | ||
const wallet = new Wallet(process.env.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; | ||
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}`); | ||
// ANCHOR_END: min-script | ||
|
||
// ANCHOR: send-funds | ||
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.008'), | ||
nonce: await wallet.getNonce(), | ||
}) | ||
).wait(); | ||
|
||
let multisigBalance = await provider.getBalance(multisigAddress); | ||
|
||
console.log(`Multisig account balance is ${multisigBalance.toString()}`); | ||
// ANCHOR_END: send-funds | ||
|
||
// ANCHOR: create-deploy-tx | ||
// 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 | ||
); | ||
// ANCHOR_END: create-deploy-tx | ||
|
||
// ANCHOR: tx-gas | ||
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: BigInt(0), | ||
}; | ||
// ANCHOR_END: tx-gas | ||
|
||
// ANCHOR: sign-tx | ||
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, | ||
}; | ||
// ANCHOR_END: sign-tx | ||
|
||
// ANCHOR: broadcast-tx | ||
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()}`); | ||
// ANCHOR_END: broadcast-tx | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import type { 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', | ||
}, | ||
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: { | ||
enableEraVMExtensions: true, | ||
// find all available options in the official documentation | ||
// https://docs.zksync.io/build/tooling/hardhat/hardhat-zksync-solc#configuration | ||
}, | ||
}, | ||
solidity: { | ||
version: '0.8.17', | ||
}, | ||
}; | ||
|
||
export default config; |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
{ | ||
"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:f": "hardhat deploy-zksync --script deploy-factory.ts", | ||
"deploy:m": "hardhat deploy-zksync --script deploy-multisig.ts", | ||
"interact": "hardhat deploy-zksync --script interact.ts", | ||
"compile": "hardhat compile", | ||
"clean": "hardhat clean", | ||
"test": "hardhat test --network hardhat" | ||
}, | ||
"devDependencies": { | ||
"@matterlabs/hardhat-zksync": "1.1.0", | ||
"@matterlabs/zksync-contracts": "^0.6.1", | ||
"@openzeppelin/contracts": "4.9.5", | ||
"@types/chai": "^4.3.16", | ||
"@types/mocha": "^10.0.7", | ||
"chai": "^4.5.0", | ||
"dotenv": "^16.4.5", | ||
"ethers": "^6.13.2", | ||
"hardhat": "^2.22.7", | ||
"mocha": "^10.7.0", | ||
"ts-node": "^10.9.2", | ||
"typescript": "^5.5.4", | ||
"zksync-ethers": "^6.11.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
// 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"; | ||
|
||
|
||
contract TwoUserMultisig is IAccount, IERC1271 { | ||
// to get transaction hash | ||
using TransactionHelper for Transaction; | ||
|
||
|
||
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. | ||
_; | ||
} | ||
|
||
|
||
function validateTransaction( | ||
bytes32, | ||
bytes32 _suggestedSignedHash, | ||
Transaction calldata _transaction | ||
) external payable override onlyBootloader returns (bytes4 magic) { | ||
magic = _validateTransaction(_suggestedSignedHash, _transaction); | ||
} | ||
|
||
function _validateTransaction( | ||
bytes32 _suggestedSignedHash, | ||
Transaction calldata _transaction | ||
) internal returns (bytes4 magic) { | ||
// TO BE IMPLEMENTED | ||
} | ||
|
||
function executeTransaction( | ||
bytes32, | ||
bytes32, | ||
Transaction calldata _transaction | ||
) external payable override onlyBootloader { | ||
_executeTransaction(_transaction); | ||
} | ||
|
||
function _executeTransaction(Transaction calldata _transaction) internal { | ||
// TO BE IMPLEMENTED | ||
} | ||
|
||
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) | ||
{ | ||
// TO BE IMPLEMENTED | ||
} | ||
|
||
function payForTransaction( | ||
bytes32, | ||
bytes32, | ||
Transaction calldata _transaction | ||
) external payable override onlyBootloader { | ||
// TO BE IMPLEMENTED | ||
} | ||
|
||
function prepareForPaymaster( | ||
bytes32, // _txHash | ||
bytes32, // _suggestedSignedHash | ||
Transaction calldata _transaction | ||
) external payable override onlyBootloader { | ||
// TO BE IMPLEMENTED | ||
} | ||
|
||
// 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) | ||
} | ||
} | ||
|
||
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "es5", | ||
"module": "commonjs", | ||
"strict": true, | ||
"esModuleInterop": true, | ||
"moduleResolution": "node", | ||
"forceConsistentCasingInFileNames": true, | ||
"outDir": "dist", | ||
"resolveJsonModule": true | ||
}, | ||
"include": ["./hardhat.config.ts", "./scripts", "./deploy", "./test", "typechain/**/*"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { EOL } from 'os'; | ||
import { join } from 'path'; | ||
import { readFileSync } from 'fs'; | ||
|
||
// files cache | ||
const files = new Map<string, string>(); | ||
|
||
export default defineNitroPlugin((nitroApp) => { | ||
nitroApp.hooks.hook('content:file:beforeParse', (file) => { | ||
if (file.body.includes(':code-import{filePath')) { | ||
const newBody = handleCodeImport(file.body); | ||
file.body = newBody; | ||
} | ||
}); | ||
}); | ||
|
||
function handleCodeImport(body: string) { | ||
const lines = body.split(EOL); | ||
let inCodeBlock = false; | ||
let codeBlockIndent = ''; | ||
|
||
for (let i = 0; i < lines.length; i++) { | ||
const trimmedLine = lines[i].trim(); | ||
|
||
if (trimmedLine.startsWith('```')) { | ||
inCodeBlock = !inCodeBlock; | ||
if (inCodeBlock) { | ||
const matches = lines[i].match(/^\s*/); | ||
codeBlockIndent = matches ? matches[0] : ''; | ||
} | ||
} | ||
|
||
if (inCodeBlock && trimmedLine.includes(':code-import{filePath')) { | ||
const filepath = trimmedLine.split('"')[1]; | ||
let newCode = getCodeFromFilepath(filepath); | ||
|
||
newCode = newCode | ||
.split(EOL) | ||
.map((line) => codeBlockIndent + line) | ||
.join(EOL); | ||
|
||
lines[i] = newCode; | ||
} | ||
} | ||
|
||
return lines.join(EOL); | ||
} | ||
|
||
function getCodeFromFilepath(filepath: string) { | ||
const splitPath = filepath.split(':'); | ||
const cleanPath = splitPath[0]; | ||
const cache = files.get(cleanPath); | ||
let code; | ||
if (cache) { | ||
code = cache; | ||
} else { | ||
const fullPath = join(process.cwd(), 'code', cleanPath); | ||
code = readFileSync(fullPath, 'utf8'); | ||
files.set(filepath, code); | ||
} | ||
const exampleComment = splitPath[1] || null; | ||
if (exampleComment) { | ||
code = extractCommentBlock(code, exampleComment); | ||
} | ||
// remove any other ANCHOR tags | ||
const lines = code.split(EOL); | ||
const trimmedLines = lines.filter((line) => !line.trimStart().startsWith('// ANCHOR')); | ||
return trimmedLines.join(EOL); | ||
} | ||
|
||
function extractCommentBlock(content: string, comment: string | null) { | ||
const commentTypes = ['<!--', '{/*', '//', '/*']; | ||
|
||
const lines = content.split(EOL); | ||
if (!comment) { | ||
return content; | ||
} | ||
|
||
let lineStart = 1; | ||
let lineEnd = 1; | ||
let foundStart = false; | ||
|
||
for (let i = 0; i < lines.length; i++) { | ||
const trimmed = lines[i].replace(/\s/g, ''); | ||
const start = commentTypes.some((type) => trimmed === `${type}ANCHOR:${comment}`); | ||
if (start === true && !foundStart) { | ||
lineStart = i + 1; | ||
foundStart = true; | ||
} else { | ||
const end = commentTypes.some((type) => trimmed === `${type}ANCHOR_END:${comment}`); | ||
if (end === true) lineEnd = i; | ||
} | ||
} | ||
const newLines = lines.slice(lineStart, lineEnd); | ||
const linesContent = newLines.join(EOL); | ||
return linesContent; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import type { IStepConfig } from '../utils/types'; | ||
|
||
export const steps: IStepConfig = { | ||
'initialize-project': { | ||
action: 'runCommand', | ||
prompts: 'Private key of the wallet: |❯ npm: ', | ||
}, | ||
'wait-for-init': { | ||
action: 'wait', | ||
timeout: 15000, | ||
}, | ||
'move-into-project': { | ||
action: 'runCommand', | ||
}, | ||
'delete-templates': { | ||
action: 'runCommand', | ||
commandFolder: 'tests-output/custom-aa-tutorial', | ||
}, | ||
'install-deps': { | ||
action: 'runCommand', | ||
commandFolder: 'tests-output/custom-aa-tutorial', | ||
}, | ||
'hardhat-config': { | ||
action: 'writeToFile', | ||
filepath: 'tests-output/custom-aa-tutorial/hardhat.config.ts', | ||
}, | ||
'use-local-node': { | ||
action: 'modifyFile', | ||
filepath: 'tests-output/custom-aa-tutorial/hardhat.config.ts', | ||
useSetData: " defaultNetwork: 'inMemoryNode',", | ||
atLine: 6, | ||
removeLines: [6], | ||
}, | ||
'start-local-node': { | ||
action: 'runCommand', | ||
commandFolder: 'tests-output/custom-aa-tutorial', | ||
useSetCommand: "bun pm2 start 'era_test_node fork sepolia-testnet' --name era-test-node", | ||
}, | ||
'make-multisig-contract': { | ||
action: 'runCommand', | ||
commandFolder: 'tests-output/custom-aa-tutorial', | ||
}, | ||
'multisig-contract-code': { | ||
action: 'writeToFile', | ||
filepath: 'tests-output/custom-aa-tutorial/contracts/TwoUserMultisig.sol', | ||
addSpacesAfter: false, | ||
}, | ||
'make-factory-contract': { | ||
action: 'runCommand', | ||
commandFolder: 'tests-output/custom-aa-tutorial', | ||
}, | ||
'factory-contract-code': { | ||
action: 'writeToFile', | ||
filepath: 'tests-output/custom-aa-tutorial/contracts/AAFactory.sol', | ||
}, | ||
'make-deploy-script': { | ||
action: 'runCommand', | ||
commandFolder: 'tests-output/custom-aa-tutorial', | ||
}, | ||
'deploy-script-code': { | ||
action: 'writeToFile', | ||
filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-factory.ts', | ||
}, | ||
'env-pk': { | ||
action: 'modifyFile', | ||
filepath: 'tests-output/custom-aa-tutorial/.env', | ||
atLine: 1, | ||
removeLines: [1], | ||
useSetData: 'WALLET_PRIVATE_KEY=0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110', | ||
}, | ||
'compile-and-deploy-factory': { | ||
action: 'runCommand', | ||
commandFolder: 'tests-output/custom-aa-tutorial', | ||
checkForOutput: 'AA factory address:', | ||
saveOutput: 'tests-output/custom-aa-tutorial/deployed-factory-address.txt', | ||
}, | ||
'create-deploy-multisig': { | ||
action: 'runCommand', | ||
commandFolder: 'tests-output/custom-aa-tutorial', | ||
}, | ||
'deploy-multisig-code': { | ||
action: 'writeToFile', | ||
filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', | ||
}, | ||
'deposit-funds': { | ||
action: 'modifyFile', | ||
filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', | ||
atLine: 41, | ||
addSpacesBefore: 1, | ||
}, | ||
'create-deploy-tx': { | ||
action: 'modifyFile', | ||
filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', | ||
atLine: 56, | ||
addSpacesBefore: 1, | ||
}, | ||
'modify-deploy-tx': { | ||
action: 'modifyFile', | ||
filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', | ||
atLine: 64, | ||
addSpacesBefore: 1, | ||
}, | ||
'sign-deploy-tx': { | ||
action: 'modifyFile', | ||
filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', | ||
atLine: 82, | ||
addSpacesBefore: 1, | ||
}, | ||
'send-deploy-tx': { | ||
action: 'modifyFile', | ||
filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', | ||
atLine: 95, | ||
addSpacesBefore: 1, | ||
}, | ||
'final-deploy-script': { | ||
action: 'compareToFile', | ||
filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', | ||
}, | ||
'get-deployed-account-address': { | ||
action: 'extractDataToEnv', | ||
dataFilepath: 'tests-output/custom-aa-tutorial/deployed-factory-address.txt', | ||
regex: /0x[a-fA-F0-9]{40}/, | ||
variableName: 'AA_FACTORY_ADDRESS', | ||
envFilepath: 'tests-output/custom-aa-tutorial/.env', | ||
}, | ||
'deploy-multisig-account': { | ||
action: 'modifyFile', | ||
filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', | ||
atLine: 8, | ||
removeLines: [8], | ||
useSetData: 'const AA_FACTORY_ADDRESS = process.env.AA_FACTORY_ADDRESS || "";', | ||
}, | ||
'run-deploy-multisig': { | ||
action: 'runCommand', | ||
commandFolder: 'tests-output/custom-aa-tutorial', | ||
checkForOutput: 'Multisig account balance is now', | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { test } from '@playwright/test'; | ||
import { setupAndRunTest } from './utils/runTest'; | ||
|
||
test('Native AA Multisig', async ({ page, context }) => { | ||
await setupAndRunTest(page, context, 'custom-aa-tutorial', ['/native-aa-multisig'], 'native-aa-multisig'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
{ | ||
// https://nuxt.com/docs/guide/concepts/typescript | ||
"extends": "./.nuxt/tsconfig.json" | ||
"extends": "./.nuxt/tsconfig.json", | ||
"exclude": ["code"] | ||
} |