0/5
## Implementing External Transaction Execution in Your zkSync AA Contract Welcome to this lesson where we'll enhance our zkSync Minimal Account Abstraction contract. We'll focus on implementing a crucial function, `executeTransactionFromOutside`, which allows an external entity to submit and pay for a transaction signed by the account owner. This involves significant refactoring of our existing code to promote reusability and maintainability, followed by preparations for testing and deployment. ## Understanding `executeTransactionFromOutside` The primary goal of the `executeTransactionFromOutside` function is to enable a scenario where a transaction, signed by the owner of the smart contract account, can be executed by *someone else*. This "someone else" could be an Externally Owned Account (EOA) or another smart contract, and critically, they will be responsible for paying the gas fees for this execution. This mechanism differs from the standard zkSync account abstraction flow, where the account itself (or an associated paymaster) handles fee payment through the `validateTransaction` and `executeTransaction` functions, which interact directly with the zkSync bootloader. The `executeTransactionFromOutside` function, by contrast, bypasses bootloader-specific logic (like `requireFromBootloader` modifiers) and the account abstraction-specific fee payment mechanisms. It offers a more "traditional" approach for a third party to relay and sponsor a transaction. Initially, our function is a simple stub: ```solidity function executeTransactionFromOutside(Transaction memory _transaction) external payable { // Logic to be added } ``` We will populate this function after refactoring common logic. ## Refactoring for Enhanced Code Reusability Upon inspection, the core logic for validating a transaction (checking the nonce, ensuring sufficient balance, and verifying the signature) and executing a transaction (making the intended call) is largely identical across our existing `validateTransaction`, `executeTransaction` functions, and the new `executeTransactionFromOutside` function. To adhere to the DRY (Don't Repeat Yourself) principle and improve our codebase, we will extract this common logic into two internal helper functions: `_validateTransaction` and `_executeTransaction`. ## Creating the `_validateTransaction` Internal Helper The `_validateTransaction` internal function will encapsulate all the necessary steps to validate a transaction before its execution. This logic was previously part of the `validateTransaction` function. **Purpose:** To consolidate transaction validation logic, including nonce management, balance checks, and signature verification. **Implementation:** The logic, previously spanning lines 88-114 in our original contract, is moved into `_validateTransaction`. This includes: 1. Interacting with the `NONCE_HOLDER_SYSTEM_CONTRACT` to increment the minimum nonce for the account, ensuring replay protection. 2. Calculating the `totalRequiredBalance` for the transaction and checking if the account contract possesses sufficient funds. 3. Encoding the transaction hash, converting it to an Ethereum signed message hash, and recovering the signer's address using `ECDSA.recover`. 4. Comparing the recovered signer with the `owner()` of the account. 5. Returning `ACCOUNT_VALIDATION_SUCCESS_MAGIC` (a specific `bytes4` value) if the signature is valid, or `bytes4(0)` otherwise. ```solidity // INTERNAL FUNCTIONS function _validateTransaction(Transaction memory _transaction) internal returns (bytes4 magic) { // Call nonceholder to increment nonce // This system call increments the nonce if the provided _transaction.nonce matches the current one. SystemContractsCaller.systemCallWithPropagatedRevert( uint32(gasleft()), address(NONCE_HOLDER_SYSTEM_CONTRACT), 0, abi.encodeCall(INonceHolder.incrementMinNonceIfEquals, (_transaction.nonce)) ); // Check if the account has enough balance to cover the transaction's cost uint256 totalRequiredBalance = _transaction.totalRequiredBalance(); if (totalRequiredBalance > address(this).balance) { revert ZkMinimalAccount_NotEnoughBalance(); } // Verify the transaction signature bytes32 txHash = _transaction.encodeHash(); bytes32 convertedHash = MessageHashUtils.toEthSignedMessageHash(txHash); address signer = ECDSA.recover(convertedHash, _transaction.signature); bool isValidSigner = signer == owner(); if (isValidSigner) { magic = ACCOUNT_VALIDATION_SUCCESS_MAGIC; } else { magic = bytes4(0); // Indicates invalid signature } return magic; } ``` With this helper in place, our public `validateTransaction` function becomes much leaner. It now primarily delegates the validation logic to `_validateTransaction` and retains its `requireFromBootLoader` modifier, essential for the standard AA flow. ```solidity function validateTransaction( bytes32, /* _txHash */ bytes32, /* _suggestedSignedHash */ Transaction memory _transaction ) external payable requireFromBootLoader returns (bytes4 magic) { return _validateTransaction(_transaction); } ``` ## Crafting the `_executeTransaction` Internal Helper Similarly, the `_executeTransaction` internal function will consolidate the logic for actually dispatching the transaction's intended call (e.g., transferring tokens, interacting with another contract). This logic was previously within the `executeTransaction` function. **Purpose:** To centralize the execution of the transaction's core action. **Implementation:** The logic from lines 121-137 of our original `executeTransaction` function is moved here. This involves: 1. Extracting the target address (`to`), `value`, and `data` from the `_transaction` struct. 2. Checking if the target address is the `DEPLOYER_SYSTEM_CONTRACT`. If so, it uses a system call for deployment. 3. Otherwise, it performs a standard external `call` using assembly for efficiency and control. 4. Reverting with `ZkMinimalAccount_ExecutionFailed()` if the external call is unsuccessful. ```solidity function _executeTransaction(Transaction memory _transaction) internal { address to = address(uint160(_transaction.to)); uint128 value = Utils.safeCastToU128(_transaction.value); bytes memory data = _transaction.data; // Handle calls to the DEPLOYER_SYSTEM_CONTRACT for contract deployments if (to == address(DEPLOYER_SYSTEM_CONTRACT)) { uint32 gas = Utils.safeCastToU32(gasleft()); SystemContractsCaller.systemCallWithPropagatedRevert(gas, to, value, data); } else { // Standard external call bool success; assembly { success := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0) } if (!success) { revert ZkMinimalAccount_ExecutionFailed(); } } } ``` The public `executeTransaction` function is now also simplified, calling `_executeTransaction` and retaining its `requireFromBootLoaderOrOwner` modifier. ```solidity function executeTransaction( bytes32, /* _txHash */ bytes32, /* _suggestedSignedHash */ Transaction memory _transaction ) external payable requireFromBootLoaderOrOwner { _executeTransaction(_transaction); } ``` ## Finalizing `executeTransactionFromOutside` with Refactored Helpers Now that we have our internal helper functions, implementing `executeTransactionFromOutside` is straightforward. It needs to validate the transaction using the owner's signature and then execute it. ```solidity function executeTransactionFromOutside(Transaction memory _transaction) external payable { bytes4 magic = _validateTransaction(_transaction); // IMPORTANT: Always check the result of validation. // If the signature is not valid, or other validation checks fail, // _validateTransaction will return a magic value other than ACCOUNT_VALIDATION_SUCCESS_MAGIC. if (magic != ACCOUNT_VALIDATION_SUCCESS_MAGIC) { revert ZkMinimalAccount_InvalidSignature(); // Or a more generic validation failed error } _executeTransaction(_transaction); } ``` **Important Note:** During the original walkthrough, checking the `magic` return value from `_validateTransaction` was initially overlooked. It is crucial to verify that `_validateTransaction` returns `ACCOUNT_VALIDATION_SUCCESS_MAGIC`. If it doesn't, the transaction is invalid (e.g., due to a bad signature or insufficient funds as per the internal checks), and execution should not proceed. The code above includes this vital check. ## Enabling Ether Reception with the `receive` Function For our smart contract account to function effectively, especially to pay for its own transactions in the standard AA flow or to simply hold Ether, it must be able to receive Ether. This is achieved by implementing the `receive` fallback function: ```solidity receive() external payable {} ``` This simple function allows the contract to accept incoming Ether transfers. ## Compiling Your zkSync Smart Contract With the changes in place, we can compile our contract using Foundry specifically for the zkSync environment: ```bash forge build --zksync ``` Upon successful compilation, you might observe warnings related to the use of `ecrecover`. While `ecrecover` is standard for ECDSA signature verification on Ethereum, zkSync Era offers native account abstraction features. The warning suggests that relying solely on `ecrecover` with an ECDSA private key might not be the most future-proof approach if zkSync introduces support for alternative signature schemes. For our minimal account, this is acceptable for now. **Troubleshooting Compilation Issues:** If you encounter unexpected compilation problems, a common troubleshooting step is to perform a clean recompile. This involves deleting cached build artifacts: ```bash # If facing issues, try cleaning and recompiling: # rm -rf zkout out cache (or delete these folders manually from your project explorer) # forge build --zksync ``` This forces Foundry to recompile everything from scratch, which can resolve issues related to stale cache data, though it may take slightly longer. ## Preparing for Testing and Deployment on zkSync With our contract compiled, the subsequent steps involve rigorous testing and eventual deployment. * **Testing:** We will proceed to write comprehensive tests for this contract. The testing setup will likely mirror patterns used in previous examples, focusing on verifying the behavior of `executeTransactionFromOutside` alongside the existing AA flow functions. * **Deployment and Scripting:** * At the time of this lesson's original recording, Foundry's scripting capabilities for zkSync were not fully mature. * Therefore, for actual deployment to a network and for interacting with the deployed contract (e.g., sending an account-abstracted transaction), Hardhat, along with JavaScript or TypeScript scripts, provides a more robust solution. We will explore these scripts. * The ultimate goal is to send a true account abstraction transaction on the zkSync Sepolia testnet. * A word of caution: Interacting with testnets, especially for experimental features, should be done responsibly. Acquiring zkSync Sepolia testnet ETH can sometimes be challenging, and intensive operations could potentially contribute to network congestion. ## Valuable Resources for zkSync Development As you delve deeper into zkSync development, remember to leverage available resources: * **zkSync Discord:** The official zkSync Discord server features an AI bot. This bot has been trained on all zkSync documentation and can be an invaluable assistant for answering your questions. You can usually find a link to the Discord on the zkSync documentation website. * **Official zkSync Documentation:** The primary source of truth for all things zkSync is `docs.zksync.io`. ## Summary: A More Modular `ZkMinimalAccount.sol` Through this lesson, our `ZkMinimalAccount.sol` contract has evolved significantly. It now boasts a clearer separation of concerns: * External-facing functions (`validateTransaction`, `executeTransaction`, `executeTransactionFromOutside`) catering to different transaction initiation paths. * Internal helper functions (`_validateTransaction`, `_executeTransaction`) encapsulating core validation and execution logic, promoting code reuse and readability. * A `receive` function to enable ETH deposits. This modular structure not only makes the contract easier to understand and maintain but also enhances its flexibility. The `executeTransactionFromOutside` function, in particular, provides a valuable mechanism for users who wish to have their transactions relayed and gas fees paid by a third party, operating alongside the native account abstraction capabilities of zkSync.
A refactoring masterclass for Implementing External Transaction Execution in Your zkSync AA Contract - Discover how to enable third-party transaction relay and gas payment by adding `executeTransactionFromOutside` to your zkSync AA contract. This lesson thoroughly details refactoring common validation and execution logic into internal helper functions for cleaner, more reusable code.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at EigenLayer
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on May 12, 2025
Solidity Developer
Advanced FoundryDuration: 36min
Duration: 3h 06min
Duration: 5h 02min
Duration: 6h 02min
Duration: 2h 47min
Duration: 1h 23min
Duration: 4h 28min
Duration: 1h 19min
Duration: 1h 10min
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at EigenLayer
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on May 12, 2025