Skip to content

Commit

Permalink
add first steps for the zksync minimal account abstraction impl
Browse files Browse the repository at this point in the history
  • Loading branch information
BravoNatalie committed Jul 19, 2024
1 parent 38d303a commit 0129eb1
Show file tree
Hide file tree
Showing 12 changed files with 694 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
[submodule "lib/foundry-era-contracts"]
path = lib/foundry-era-contracts
url = https://github.com/Cyfrin/foundry-era-contracts
[submodule "lib/foundry-devops"]
path = lib/foundry-devops
url = https://github.com/Cyfrin/foundry-devops
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@

1. Create a basic AA on Eth
2. Create a basic AA on zksync
3. deploy, and send a userOp tx

PS.: Foundry solidity scripts for zksync does not work 100%, therefore the `ts-scripts` was created.

## TODO
- [ ] add paymaster logic
- [ ] add spend threshold
- [ ] sign the tx with github/google session key
7 changes: 6 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ src = "src"
out = "out"
libs = ["lib"]
remappings = ["@openzeppelin/contracts=lib/openzeppelin-contracts/contracts"]

is-system = true
via-ir = true
fs_permissions = [
{ access = "read", path = "./broadcast" },
{ access = "read", path = "./reports" },
]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions lib/foundry-devops
Submodule foundry-devops added at df9f90
1 change: 1 addition & 0 deletions lib/foundry-era-contracts
Submodule foundry-era-contracts added at 3f99de
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"dependencies": {
"@types/fs-extra": "^11.0.4",
"dotenv": "^16.4.5",
"ethers": "6",
"fs-extra": "^11.2.0",
"typescript": "^5.4.5",
"zksync-ethers": "^6.8.0"
},
"scripts": {
"encryptKey": "ts-node ts-scripts/EncryptKey.ts",
"deploy": "ts-node ts-scripts/DeployZkMinimal.ts",
"sendTx": "ts-node ts-scripts/SendAATx.ts",
"compile": "forge build --zksync"
}
}
164 changes: 164 additions & 0 deletions src/zksync/zkMinimalAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-Licence-Identifier: MIT
pragma solidity 0.8.24;

// zkSync Era Imports
import {
IAccount,
ACCOUNT_VALIDATION_SUCCESS_MAGIC
} from "lib/foundry-era-contracts/src/system-contracts/contracts/interfaces/IAccount.sol";
import {
Transaction,
MemoryTransactionHelper
} from "lib/foundry-era-contracts/src/system-contracts/contracts/libraries/MemoryTransactionHelper.sol";
import {SystemContractsCaller} from
"lib/foundry-era-contracts/src/system-contracts/contracts/libraries/SystemContractsCaller.sol";
import {
NONCE_HOLDER_SYSTEM_CONTRACT,
BOOTLOADER_FORMAL_ADDRESS,
DEPLOYER_SYSTEM_CONTRACT
} from "lib/foundry-era-contracts/src/system-contracts/contracts/Constants.sol";
import {INonceHolder} from "lib/foundry-era-contracts/src/system-contracts/contracts/interfaces/INonceHolder.sol";
import {Utils} from "lib/foundry-era-contracts/src/system-contracts/contracts/libraries/Utils.sol";

// OZ Imports
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

/**
* Lifecycle of a type 113 (0x71) transaction
* msg.sender is the bootloader system contract
*
* Phase 1 Validation (light node)
* 1. The user sends the transaction to the "zkSync API client" (sort of a "light node")
* 2. The zkSync API client checks to see the the nonce is unique by querying the NonceHolder system contract
* 3. The zkSync API client calls validateTransaction, which MUST update the nonce
* 4. The zkSync API client checks the nonce is updated
* 5. The zkSync API client calls payForTransaction, or prepareForPaymaster & validateAndPayForPaymasterTransaction
* 6. The zkSync API client verifies that the bootloader gets paid
*
* Phase 2 Execution (main node)
* 7. The zkSync API client passes the validated transaction to the main node / sequencer (as of today, they are the same)
* 8. The main node calls executeTransaction
* 9. If a paymaster was used, the postTransaction is called
*/
contract ZkMinimalAccount is IAccount, Ownable {
using MemoryTransactionHelper for Transaction;

//////////////////// ERRORS ////////////////////////
error ZkMinimalAccount__NotEnoughBalance();
error ZkMinimalAccount__NotFromBootLoader();
error ZkMinimalAccount__ExecutionFailed();
error ZkMinimalAccount__NotFromBootLoaderOrOwner();
error ZkMinimalAccount__FailedToPay();
error ZkMinimalAccount__InvalidSignature();


//////////////////// MODIFIERS ////////////////////////
modifier requireFromBootLoader() {
if (msg.sender != BOOTLOADER_FORMAL_ADDRESS) {
revert ZkMinimalAccount__NotFromBootLoader();
}
_;
}

modifier requireFromBootLoaderOrOwner() {
if (msg.sender != BOOTLOADER_FORMAL_ADDRESS && msg.sender != owner()) {
revert ZkMinimalAccount__NotFromBootLoaderOrOwner();
}
_;
}


//////////////////// EXTERNAL FUNCTIONS ////////////////////////
constructor() Ownable(msg.sender) {}

receive() external payable {}

/**
* @notice Since this version does not have a paymaster, we need to check if there's enough balance in our account
*/
function validateTransaction(bytes32 /*_txHash*/, bytes32 /*_suggestedSignedHash*/, Transaction memory _transaction) external payable requireFromBootLoader returns (bytes4 magic){
return _validateTransaction(_transaction);
}

/**
* @notice only the bootloader can call this function. The paymaster is going to pay for the gas.
*/
function executeTransaction(bytes32 /*_txHash*/, bytes32 /*_suggestedSignedHash*/, Transaction memory _transaction) external requireFromBootLoaderOrOwner payable {
return _executeTransaction(_transaction);
}

/**
* @notice we can sign a transaction and allow anyone to call this function. Who is calling is going to pay for the gas.
*/
function executeTransactionFromOutside(Transaction memory _transaction) external payable {
bytes4 magic = _validateTransaction(_transaction);
if (magic != ACCOUNT_VALIDATION_SUCCESS_MAGIC) {
revert ZkMinimalAccount__InvalidSignature();
}
_executeTransaction(_transaction);
}

function payForTransaction(bytes32 /*_txHash*/, bytes32 /*_suggestedSignedHash*/, Transaction memory _transaction) external payable {
bool success = _transaction.payToTheBootloader();
if (!success) {
revert ZkMinimalAccount__FailedToPay();
}
}

function prepareForPaymaster(bytes32 _txHash, bytes32 _possibleSignedHash, Transaction memory _transaction) external payable{

}

//////////////////// INTERNAL FUNCTIONS ////////////////////////

function _validateTransaction(Transaction memory _transaction) internal returns (bytes4 magic){
// zksync system call simulation, this is used to call the NonceHolder system contract
SystemContractsCaller.systemCallWithPropagatedRevert(
uint32(gasleft()),
address(NONCE_HOLDER_SYSTEM_CONTRACT),
0,
abi.encodeCall(INonceHolder.incrementMinNonceIfEquals, (_transaction.nonce))
);

// check the fee to pay
uint256 totalRequiredBalance = _transaction.totalRequiredBalance();
if(totalRequiredBalance > address(this).balance){
// TODO: add logic for paymaster
revert ZkMinimalAccount__NotEnoughBalance();
}

// check the signature
bytes32 txHash = _transaction.encodeHash();
address signer = ECDSA.recover(txHash, _transaction.signature);

bool isValidSigner = signer == owner();
if (isValidSigner) {
magic = ACCOUNT_VALIDATION_SUCCESS_MAGIC;
} else {
magic = bytes4(0);
}
return magic;
}

function _executeTransaction(Transaction memory _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());
SystemContractsCaller.systemCallWithPropagatedRevert(gas, to, value, data);
}else{
bool success;
assembly {
// (success,) = payable(msg.sender).call{value:missingAccountFunds, gas: type(uint256).max}("");
success := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0)
}
if(!success){
revert ZkMinimalAccount__ExecutionFailed();
}
}
}
}
121 changes: 121 additions & 0 deletions test/zksync/ZkMinimalAccount.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {Test, console2} from "forge-std/Test.sol";
import {ZkMinimalAccount} from "src/zksync/ZkMinimalAccount.sol";
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";

// Era Imports
import {
Transaction,
MemoryTransactionHelper
} from "lib/foundry-era-contracts/src/system-contracts/contracts/libraries/MemoryTransactionHelper.sol";
import {BOOTLOADER_FORMAL_ADDRESS} from "lib/foundry-era-contracts/src/system-contracts/contracts/Constants.sol";
import {ACCOUNT_VALIDATION_SUCCESS_MAGIC} from
"lib/foundry-era-contracts/src/system-contracts/contracts/interfaces/IAccount.sol";

// OZ Imports
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

// Foundry Devops
import {ZkSyncChainChecker} from "lib/foundry-devops/src/ZkSyncChainChecker.sol";

contract ZkMinimalAccountTest is Test, ZkSyncChainChecker {
using MessageHashUtils for bytes32;

ZkMinimalAccount minimalAccount;
ERC20Mock usdc;
bytes4 constant EIP1271_SUCCESS_RETURN_VALUE = 0x1626ba7e;

uint256 constant AMOUNT = 1e18;
bytes32 constant EMPTY_BYTES32 = bytes32(0);
address constant ANVIL_DEFAULT_ACCOUNT = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
uint256 constant ANVIL_DEFAULT_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;

function setUp() public {
minimalAccount = new ZkMinimalAccount();
minimalAccount.transferOwnership(ANVIL_DEFAULT_ACCOUNT);
usdc = new ERC20Mock();
vm.deal(address(minimalAccount), AMOUNT);
}

// forge test --mt testZkOwnerCanExecuteCommands
function testZkOwnerCanExecuteCommands() public {
// Arrange
address dest = address(usdc);
uint256 value = 0;
bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT);

Transaction memory transaction =
_createUnsignedTransaction(minimalAccount.owner(), 113, dest, value, functionData);

// Act
vm.prank(minimalAccount.owner());
minimalAccount.executeTransaction(EMPTY_BYTES32, EMPTY_BYTES32, transaction);

// Assert
assertEq(usdc.balanceOf(address(minimalAccount)), AMOUNT);
}

// forge test --mt testZkValidateTransaction --zksync --system-mode=true
function testZkValidateTransaction() public onlyZkSync {
// Arrange
address dest = address(usdc);
uint256 value = 0;
bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT);
Transaction memory transaction =
_createUnsignedTransaction(minimalAccount.owner(), 113, dest, value, functionData);
transaction = _signTransaction(transaction);

// Act
vm.prank(BOOTLOADER_FORMAL_ADDRESS);
bytes4 magic = minimalAccount.validateTransaction(EMPTY_BYTES32, EMPTY_BYTES32, transaction);

// Assert
assertEq(magic, ACCOUNT_VALIDATION_SUCCESS_MAGIC);
}

/*//////////////////////////////////////////////////////////////
HELPERS
//////////////////////////////////////////////////////////////*/
function _signTransaction(Transaction memory transaction) internal view returns (Transaction memory) {
bytes32 unsignedTransactionHash = MemoryTransactionHelper.encodeHash(transaction);

uint8 v;
bytes32 r;
bytes32 s;
(v, r, s) = vm.sign(ANVIL_DEFAULT_KEY, unsignedTransactionHash);
Transaction memory signedTransaction = transaction;
signedTransaction.signature = abi.encodePacked(r, s, v);
return signedTransaction;
}

function _createUnsignedTransaction(
address from,
uint8 transactionType,
address to,
uint256 value,
bytes memory data
) internal view returns (Transaction memory) {
uint256 nonce = vm.getNonce(address(minimalAccount)); // At this moment, this does not work 100% with zksync
bytes32[] memory factoryDeps = new bytes32[](0);
return Transaction({
txType: transactionType, // type 113 (0x71).
from: uint256(uint160(from)),
to: uint256(uint160(to)),
gasLimit: 16777216,
gasPerPubdataByteLimit: 16777216,
maxFeePerGas: 16777216,
maxPriorityFeePerGas: 16777216,
paymaster: 0,
nonce: nonce,
value: value,
reserved: [uint256(0), uint256(0), uint256(0), uint256(0)],
data: data,
signature: hex"",
factoryDeps: factoryDeps,
paymasterInput: hex"",
reservedDynamic: hex""
});
}
}
Loading

0 comments on commit 0129eb1

Please sign in to comment.