-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add first steps for the zksync minimal account abstraction impl
- Loading branch information
1 parent
38d303a
commit 0129eb1
Showing
12 changed files
with
694 additions
and
2 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
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
Submodule foundry-devops
added at
df9f90
Submodule foundry-era-contracts
added at
3f99de
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,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" | ||
} | ||
} |
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,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(); | ||
} | ||
} | ||
} | ||
} |
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 @@ | ||
// 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"" | ||
}); | ||
} | ||
} |
Oops, something went wrong.