Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use static salt for deploy task #228

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

# Start era test node (alpha.34)
- name: Anvil ZKsync Action
uses: dutterbutter/era-test-node-action@298eea49e29e226baa04195b1d0fce7042439259
# Start era test node
- name: Pre-Anvil ZKsync Action
uses: dutterbutter/era-test-node-action@66121581304ee2d4f0c1c2de46a3d08f1c2f5264

- name: Setup pnpm
uses: pnpm/action-setup@v4
Expand Down
22 changes: 8 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ZKsync SSO
# ZKsync SSO Clave Contracts

[![License](https://img.shields.io/badge/license-GPL3-blue)](LICENSE)
[![CI](https://github.com/matter-labs/zksync-account-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/matter-labs/zksync-account-sdk/actions/workflows/ci.yml)
Expand All @@ -12,19 +12,13 @@ Forked from [Clave](https://github.com/getclave/clave-contracts).

<!-- prettier-ignore -->
> [!CAUTION]
> ZKsync SSO is under active development and is not yet feature
> complete. Use it to improve your development applications and tooling. Please
> do not use it in production environments.

- 🧩 Modular smart accounts based on
[ERC-7579](https://eips.ethereum.org/EIPS/eip-7579#modules)
- 🔑 Passkey authentication (no seed phrases)
- ⏰ Sessions w/ easy configuration and management
- 💰 Integrated paymaster support
- ❤️‍🩹 Account recovery _(Coming Soon)_
- 💻 Simple SDKs : JavaScript, iOS/Android _(Coming Soon)_
- 🤝 Open-source authentication server
- 🎓 Examples to get started quickly
> The factory and module interfaces are not yet stable! Any modules created
> against the IModuleValidator interface will likely need to be updated in the
> final version. The code is currently under audit and the latest may contain
> security vulnerabilities.

See the [ZKsync SSO](https://github.com/matter-labs/zksync-sso) project for a
complete developer solution, this project is just the smart contract components.

## Local Development

Expand Down
2 changes: 1 addition & 1 deletion scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function deploy(name: string, deployer: Wallet, proxy: boolean, args?: any
console.log("Deploying", name, "contract...");
let implContract;
if (name == FACTORY_NAME) {
implContract = await deployFactory(deployer, args![0]);
implContract = await deployFactory(deployer, args![0], ethersStaticSalt);
} else {
implContract = await create2(name, deployer, ethersStaticSalt, args);
}
Expand Down
11 changes: 9 additions & 2 deletions scripts/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { ethers } from "ethers";
// TODO: add support for constructor args
task("upgrade", "Upgrades ZKsync SSO contracts")
.addParam("proxy", "address of the proxy to upgrade")
.addParam("beaconAddress", "address of the beacon proxy for the upgrade")
.addPositionalParam("artifactName", "name of the artifact to upgrade to")
.setAction(async (cmd, hre) => {
const { LOCAL_RICH_WALLETS, getProvider, create2, ethersStaticSalt } = require("../test/utils");
const { LOCAL_RICH_WALLETS, getProvider, deployFactory, create2, ethersStaticSalt } = require("../test/utils");

let privateKey: string;
if (hre.network.name == "inMemoryNode" || hre.network.name == "dockerizedNode") {
Expand All @@ -20,9 +21,15 @@ task("upgrade", "Upgrades ZKsync SSO contracts")
}

const wallet = new Wallet(privateKey, getProvider());
let newImpl;

console.log("Deploying new implementation of", cmd.artifactName, "contract...");
const newImpl = await create2(cmd.artifactName, wallet, ethersStaticSalt, []);
if (cmd.artifactName == "AAFactory") {
if (!cmd.beaconAddress) throw "Deploying the AAFactory requires a Beacon Address '--beacon-address <value>'";
newImpl = await deployFactory(wallet, cmd.beaconAddress);
} else {
newImpl = await create2(cmd.artifactName, wallet, ethersStaticSalt, []);
}
console.log("New implementation deployed at:", await newImpl.getAddress());

console.log("Upgrading proxy at", cmd.proxy, "to new implementation...");
Expand Down
8 changes: 6 additions & 2 deletions src/AAFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ contract AAFactory {
event AccountCreated(address indexed accountAddress, string uniqueAccountId);

/// @dev The bytecode hash of the beacon proxy, used for deploying proxy accounts.
bytes32 private immutable beaconProxyBytecodeHash;
address private immutable beacon;
bytes32 public immutable beaconProxyBytecodeHash;
address public immutable beacon;

/// @notice A mapping from unique account IDs to their corresponding deployed account addresses.
mapping(string => address) public accountMappings;
Expand All @@ -31,6 +31,10 @@ contract AAFactory {
beacon = _beacon;
}

function getEncodedBeacon() external view returns (bytes memory) {
return abi.encode(beacon);
}

/// @notice Deploys a new SSO account as a beacon proxy with the specified parameters.
/// @dev Uses `create2` to deploy a proxy account, allowing for deterministic addresses based on the provided salt.
/// @param _salt The salt used for the `create2` deployment to make the address deterministic.
Expand Down
33 changes: 28 additions & 5 deletions test/BasicTest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert, expect } from "chai";
import { parseEther, randomBytes } from "ethers";
import { ethers, parseEther, randomBytes } from "ethers";
import { Wallet, ZeroAddress } from "ethers";
import { it } from "mocha";
import { SmartAccount, utils } from "zksync-ethers";
Expand Down Expand Up @@ -36,16 +36,40 @@ describe("Basic tests", function () {
const aaFactoryContract = await fixtures.getAaFactory();
assert(aaFactoryContract != null, "No AA Factory deployed");

const factoryAddress = await aaFactoryContract.getAddress();
expect(factoryAddress, "the factory address").to.equal(await fixtures.getAaFactoryAddress(), "factory address match");

const bytecodeHash = await aaFactoryContract.beaconProxyBytecodeHash();
const deployedAccountContract = await fixtures.getAccountProxyContract();
const deployedAccountContractCode = await deployedAccountContract.getDeployedCode();
assert(deployedAccountContractCode != null, "No account code deployed");
const ssoBeaconBytecodeHash = ethers.hexlify(utils.hashBytecode(deployedAccountContractCode));
expect(bytecodeHash, "deployed account bytecode hash").to.equal(ssoBeaconBytecodeHash, "deployed account code doesn't match");

const args = await aaFactoryContract.getEncodedBeacon();
const deployedBeaconAddress = new ethers.AbiCoder().encode(["address"], [await fixtures.getBeaconAddress()]);
expect(args, "the beacon address").to.equal(deployedBeaconAddress, "the deployment beacon");

const randomSalt = randomBytes(32);
const standardCreate2Address = utils.create2Address(factoryAddress, bytecodeHash, randomSalt, args) ;

const preDeployAccountCode = await fixtures.wallet.provider.getCode(standardCreate2Address);
expect(preDeployAccountCode , "expected deploy location").to.equal("0x", "nothing deployed here (yet)");

const deployTx = await aaFactoryContract.deployProxySsoAccount(
randomBytes(32),
"id",
randomSalt,
"id" + randomBytes(32).toString(),
[],
[fixtures.wallet.address],
);
const deployTxReceipt = await deployTx.wait();
proxyAccountAddress = deployTxReceipt!.contractAddress!;

const postDeployAccountCode = await fixtures.wallet.provider.getCode(standardCreate2Address);
expect(postDeployAccountCode, "expected deploy location").to.not.equal("0x", "deployment didn't match create2!");

expect(proxyAccountAddress, "the proxy account location via logs").to.not.equal(ZeroAddress, "be a valid address");
expect(proxyAccountAddress, "the proxy account location").to.equal(standardCreate2Address, "be what create2 returns");

const account = SsoAccount__factory.connect(proxyAccountAddress, provider);
assert(await account.k1IsOwner(fixtures.wallet.address));
Expand All @@ -71,8 +95,7 @@ describe("Basic tests", function () {
value,
gasLimit: 300_000n,
};
// TODO: fix gas estimation
// aaTx.gasLimit = await provider.estimateGas(aaTx);
aaTx.gasLimit = await provider.estimateGas(aaTx);

const signedTransaction = await smartAccount.signTransaction(aaTx);
assert(signedTransaction != null, "valid transaction to sign");
Expand Down
60 changes: 34 additions & 26 deletions test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import "@matterlabs/hardhat-zksync-node/dist/type-extensions";
import "@matterlabs/hardhat-zksync-verify/dist/src/type-extensions";

import dotenv from "dotenv";
import { ethers, parseEther } from "ethers";
import { ethers, parseEther, randomBytes } from "ethers";
import { readFileSync } from "fs";
import { promises } from "fs";
import * as hre from "hardhat";
import { ContractFactory, Provider, utils, Wallet } from "zksync-ethers";
import { base64UrlToUint8Array, getPublicKeyBytesFromPasskeySignature, unwrapEC2Signature } from "zksync-sso/utils";

import { AAFactory, ERC20, ExampleAuthServerPaymaster, SessionKeyValidator, SsoAccount, WebAuthValidator, SsoBeacon } from "../typechain-types";
import { AAFactory, ERC20, ExampleAuthServerPaymaster, SessionKeyValidator, SsoAccount, WebAuthValidator, SsoBeacon, AccountProxy__factory, AccountProxy } from "../typechain-types";
import { AAFactory__factory, ERC20__factory, ExampleAuthServerPaymaster__factory, SessionKeyValidator__factory, SsoAccount__factory, WebAuthValidator__factory, SsoBeacon__factory } from "../typechain-types";

export const ethersStaticSalt = new Uint8Array([
Expand All @@ -27,7 +27,7 @@ export class ContractFixtures {
async getAaFactory() {
const beaconAddress = await this.getBeaconAddress();
if (!this._aaFactory) {
this._aaFactory = await deployFactory(this.wallet, beaconAddress);
this._aaFactory = await deployFactory(this.wallet, beaconAddress, ethersStaticSalt);
}
return this._aaFactory;
}
Expand Down Expand Up @@ -78,16 +78,25 @@ export class ContractFixtures {
}

private _accountImplContract: SsoAccount;
async getAccountImplContract() {
async getAccountImplContract(salt?: ethers.BytesLike) {
if (!this._accountImplContract) {
const contract = await create2("SsoAccount", this.wallet, ethersStaticSalt);
const contract = await create2("SsoAccount", this.wallet, salt ?? ethersStaticSalt);
this._accountImplContract = SsoAccount__factory.connect(await contract.getAddress(), this.wallet);
}
return this._accountImplContract;
}

async getAccountImplAddress() {
return (await this.getAccountImplContract()).getAddress();
private _accountProxyContract: AccountProxy;
async getAccountProxyContract() {
if (!this._accountProxyContract) {
const contract = await create2("AccountProxy", this.wallet, ethersStaticSalt, [await this.getBeaconAddress()]);
this._accountProxyContract = AccountProxy__factory.connect(await contract.getAddress(), this.wallet);
}
return this._accountProxyContract;
}

async getAccountImplAddress(salt?: ethers.BytesLike) {
return (await this.getAccountImplContract(salt)).getAddress();
}

async deployERC20(mintTo: string): Promise<ERC20> {
Expand Down Expand Up @@ -145,16 +154,25 @@ export const getProviderL1 = () => {
return provider;
};

export async function deployFactory(wallet: Wallet, beaconAddress: string): Promise<AAFactory> {
export async function deployFactory(wallet: Wallet, beaconAddress: string, salt?: ethers.BytesLike): Promise<AAFactory> {
const factoryArtifact = JSON.parse(await promises.readFile("artifacts-zk/src/AAFactory.sol/AAFactory.json", "utf8"));
const proxyAaArtifact = JSON.parse(await promises.readFile("artifacts-zk/src/AccountProxy.sol/AccountProxy.json", "utf8"));

const deployer = new ContractFactory(factoryArtifact.abi, factoryArtifact.bytecode, wallet);
const deployer = new ContractFactory(factoryArtifact.abi, factoryArtifact.bytecode, wallet, "create2");
const bytecodeHash = utils.hashBytecode(proxyAaArtifact.bytecode);
const factoryBytecodeHash = utils.hashBytecode(factoryArtifact.bytecode);
const factorySalt = ethers.hexlify(salt ?? randomBytes(32));
const constructorArgs = deployer.interface.encodeDeploy([bytecodeHash, beaconAddress]);
const standardCreate2Address = utils.create2Address(wallet.address, factoryBytecodeHash, factorySalt, constructorArgs);
const accountCode = await wallet.provider.getCode(standardCreate2Address);
if (accountCode != "0x") {
logInfo(`Factory already exists at ${standardCreate2Address}`);
return AAFactory__factory.connect(standardCreate2Address, wallet);
}
const factory = await deployer.deploy(
bytecodeHash,
beaconAddress,
{ customData: { factoryDeps: [proxyAaArtifact.bytecode] } },
{ customData: { salt: factorySalt, factoryDeps: [proxyAaArtifact.bytecode] } },
);
const factoryAddress = await factory.getAddress();

Expand Down Expand Up @@ -219,16 +237,6 @@ export const create2 = async (contractName: string, wallet: Wallet, salt: ethers
const accountCode = await wallet.provider.getCode(standardCreate2Address);
if (accountCode != "0x") {
logInfo(`Contract ${contractName} already exists!`);
// if (hre.network.config.verifyURL) {
// logInfo(`Requesting contract verification...`);
// await verifyContract({
// address: standardCreate2Address,
// contract: `${contractArtifact.sourceName}:${contractName}`,
// constructorArguments: constructorArgs,
// bytecode: accountCode,
// });
// }

return new ethers.Contract(standardCreate2Address, contractArtifact.abi, wallet);
}

Expand Down Expand Up @@ -274,13 +282,13 @@ const masterWallet = ethers.Wallet.fromPhrase("stuff slice staff easily soup par
export const LOCAL_RICH_WALLETS = [
hre.network.name == "dockerizedNode"
? {
address: masterWallet.address,
privateKey: masterWallet.privateKey,
}
address: masterWallet.address,
privateKey: masterWallet.privateKey,
}
: {
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
},
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
},
{
address: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049",
privateKey: "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110",
Expand Down