Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

Audit Fixes #13

Merged
merged 13 commits into from
Dec 9, 2024
Merged
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
34 changes: 27 additions & 7 deletions contracts/AccountFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.17;

import {DEPLOYER_SYSTEM_CONTRACT, IContractDeployer} from '@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol';
import {SystemContractsCaller} from '@matterlabs/zksync-contracts/l2/system-contracts/libraries/SystemContractsCaller.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {Ownable, Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol';

import {Errors} from './libraries/Errors.sol';
import {IClaveRegistry} from './interfaces/IClaveRegistry.sol';
Expand All @@ -12,9 +12,13 @@ import {IClaveRegistry} from './interfaces/IClaveRegistry.sol';
* @title Factory contract to create Clave accounts in zkSync Era
* @author https://getclave.io
*/
contract AccountFactory is Ownable {
// Addresses of the implementation and registry contract
contract AccountFactory is Ownable2Step {
// Address of the account implementation
address public implementationAddress;
// Selector of the account initializer function
bytes4 public initializerSelector;

// Account registry contract address
address public registry;

// Account creation bytecode hash
Expand Down Expand Up @@ -63,12 +67,14 @@ contract AccountFactory is Ownable {
*/
constructor(
address _implementation,
bytes4 _initializerSelector,
address _registry,
bytes32 _proxyBytecodeHash,
address _deployer,
address _owner
) Ownable(_owner) {
implementationAddress = _implementation;
initializerSelector = _initializerSelector;
registry = _registry;
proxyBytecodeHash = _proxyBytecodeHash;
deployer = _deployer;
Expand All @@ -84,8 +90,21 @@ contract AccountFactory is Ownable {
function deployAccount(
bytes32 salt,
bytes memory initializer
) external returns (address accountAddress) {

) external payable returns (address accountAddress) {
// Check that the initializer is not empty
if (initializer.length < 4) {
revert Errors.INVALID_INITIALIZER();
}
// Check that the initializer selector is correct
{
bytes4 selector;
assembly ('memory-safe') {
selector := mload(add(initializer, 0x20))
}
if (selector != initializerSelector) {
revert Errors.INVALID_INITIALIZER();
}
}
// Deploy the implementation contract
(bool success, bytes memory returnData) = SystemContractsCaller.systemCallWithReturndata(
uint32(gasleft()),
Expand Down Expand Up @@ -118,7 +137,7 @@ contract AccountFactory is Ownable {
initializeSuccess := call(
gas(),
accountAddress,
0,
callvalue(),
add(initializer, 0x20),
mload(initializer),
0,
Expand Down Expand Up @@ -161,8 +180,9 @@ contract AccountFactory is Ownable {
* @notice Changes the implementation contract address
* @param newImplementation address - Address of the new implementation contract
*/
function changeImplementation(address newImplementation) external onlyOwner {
function changeImplementation(address newImplementation, bytes4 newInitializerSelector) external onlyOwner {
implementationAddress = newImplementation;
initializerSelector = newInitializerSelector;

emit ImplementationChanged(newImplementation);
}
Expand Down
41 changes: 29 additions & 12 deletions contracts/ClaveImplementation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,15 @@ contract ClaveImplementation is
address initialK1Validator,
bytes[] calldata modules,
Call calldata initCall
) public initializer {
) public payable initializer {
__ERC1271Handler_init();
// check that this account is being deployed by the initial signer or the factory authorized deployer
AccountFactory factory = AccountFactory(msg.sender);
// require a specific salt for the acccount based on the initial k1 owner
address expectedAddress = factory.getAddressForSalt(keccak256(abi.encodePacked(initialK1Owner)));
if (address(this) != expectedAddress) {
revert Errors.INVALID_SALT();
}
address thisDeployer = factory.accountToDeployer(address(this));
if (thisDeployer != factory.deployer()) {
if (initialK1Owner != thisDeployer) {
Expand All @@ -91,8 +96,27 @@ contract ClaveImplementation is
}
}

// Receive function to allow ETHs
// Receive function to allow ETH to be sent to the account
// with no additional calldata
receive() external payable {}

// Fallback function to allow ETH to be sent to the account
// with arbitrary calldata to mirror the behavior of an EOA
fallback() external payable {
// Simulate the behavior of the EOA if it is called via `delegatecall`.
address codeAddress = SystemContractHelper.getCodeAddress();
if (codeAddress != address(this)) {
// If the function was delegate called, behave like an EOA.
assembly {
return(0, 0)
}
}

// fallback of default account shouldn't be called by bootloader under any circumstances
assert(msg.sender != BOOTLOADER_FORMAL_ADDRESS);

// If the contract is called directly, behave like an EOA
}

/**
* @notice Called by the bootloader to validate that an account agrees to process the transaction
Expand Down Expand Up @@ -172,6 +196,7 @@ contract ClaveImplementation is
revert Errors.VALIDATION_HOOK_FAILED();
}

_incrementNonce(transaction.nonce);
_executeTransaction(transaction);
}

Expand Down Expand Up @@ -223,25 +248,17 @@ contract ClaveImplementation is
bytes32 signedHash,
Transaction calldata transaction
) internal returns (bytes4 magicValue) {
if (transaction.signature.length == 65) {
// This is a gas estimation
return bytes4(0);
}

// Extract the signature, validator address and hook data from the transaction.signature
(bytes memory signature, address validator, bytes[] memory hookData) = SignatureDecoder
.decodeSignature(transaction.signature);

// Run validation hooks
bool hookSuccess = runValidationHooks(signedHash, transaction, hookData);

if (!hookSuccess) {
return bytes4(0);
}

// Handle validation
bool valid = _handleValidation(validator, signedHash, signature);

magicValue = valid ? ACCOUNT_VALIDATION_SUCCESS_MAGIC : bytes4(0);
magicValue = (hookSuccess && valid) ? ACCOUNT_VALIDATION_SUCCESS_MAGIC : bytes4(0);
}

function _executeTransaction(
Expand Down
4 changes: 2 additions & 2 deletions contracts/ClaveRegistry.sol
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.17;

import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {Ownable, Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol';

import {Errors} from './libraries/Errors.sol';
import {IClaveRegistry} from './interfaces/IClaveRegistry.sol';

contract ClaveRegistry is Ownable, IClaveRegistry {
contract ClaveRegistry is Ownable2Step, IClaveRegistry {
mapping(address => bool) public isFactory;
// Mapping of Clave accounts
mapping(address => bool) public isClave;
Expand Down
8 changes: 6 additions & 2 deletions contracts/handlers/ValidationHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.17;

import {SignatureDecoder} from '../libraries/SignatureDecoder.sol';
import {BytesLinkedList} from '../libraries/LinkedList.sol';
import {BytesLinkedList, AddressLinkedList} from '../libraries/LinkedList.sol';
import {OwnerManager} from '../managers/OwnerManager.sol';
import {ValidatorManager} from '../managers/ValidatorManager.sol';

Expand All @@ -19,7 +19,11 @@ abstract contract ValidationHandler is OwnerManager, ValidatorManager {
bytes32 signedHash,
bytes memory signature
) internal view returns (bool) {
if (_r1IsValidator(validator)) {
if (validator <= AddressLinkedList.SENTINEL_ADDRESS) {
// address less than or equal to sentinel address can't be used in linked list
// implementation so this scenario is never valid
return false;
} else if (_r1IsValidator(validator)) {
mapping(bytes => bytes) storage owners = OwnerManager._r1OwnersLinkedList();
bytes memory cursor = owners[BytesLinkedList.SENTINEL_BYTES];
while (cursor.length > BytesLinkedList.SENTINEL_LENGTH) {
Expand Down
2 changes: 2 additions & 0 deletions contracts/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ library Errors {

error DEPLOYMENT_FAILED(); // 0x0f02d218
error INITIALIZATION_FAILED(); // 0x5b101091
error INVALID_INITIALIZER();
error INVALID_SALT();

/*//////////////////////////////////////////////////////////////
PAYMASTER
Expand Down
10 changes: 10 additions & 0 deletions contracts/managers/HookManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ abstract contract HookManager is IHookManager, Auth {
uint256 idx = 0;
// Iterate through hooks
while (cursor > AddressLinkedList.SENTINEL_ADDRESS) {
// hookData array is out of bounds for the number of hooks
if (idx >= hookData.length) {
return false;
}

// Call it with corresponding hookData
bool success = _call(
cursor,
Expand Down Expand Up @@ -171,6 +176,11 @@ abstract contract HookManager is IHookManager, Auth {
} else {
_executionHooksLinkedList().remove(hook);
}

// if the hook removal occured during execution of hooks, the hook
// context will not be cleaned up during post execution so we need
// to delete it manually
_deleteContext(hook);

(bool success, ) = hook.excessivelySafeCall(
gasleft(),
Expand Down
11 changes: 1 addition & 10 deletions contracts/managers/OwnerManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,23 +91,14 @@ abstract contract OwnerManager is IOwnerManager, Auth {
function _r1RemoveOwner(bytes calldata pubKey) internal {
_r1OwnersLinkedList().remove(pubKey);

// The wallet should have at least one owner
if (
_k1OwnersLinkedList().isEmpty() && _r1OwnersLinkedList().isEmpty()
) {
revert Errors.EMPTY_OWNERS();
}

emit R1RemoveOwner(pubKey);
}

function _k1RemoveOwner(address addr) internal {
_k1OwnersLinkedList().remove(addr);

// The wallet should have at least one owner
if (
_k1OwnersLinkedList().isEmpty() && _r1OwnersLinkedList().isEmpty()
) {
if (_k1OwnersLinkedList().isEmpty()) {
revert Errors.EMPTY_OWNERS();
}

Expand Down
13 changes: 1 addition & 12 deletions contracts/managers/ValidatorManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -85,25 +85,14 @@ abstract contract ValidatorManager is IValidatorManager, Auth {
function _r1RemoveValidator(address validator) internal {
_r1ValidatorsLinkedList().remove(validator);

// At least one validator must be present
if (
_r1ValidatorsLinkedList().isEmpty() &&
_k1ValidatorsLinkedList().isEmpty()
) {
revert Errors.EMPTY_VALIDATORS();
}

emit R1RemoveValidator(validator);
}

function _k1RemoveValidator(address validator) internal {
_k1ValidatorsLinkedList().remove(validator);

// At least one validator must be present
if (
_r1ValidatorsLinkedList().isEmpty() &&
_k1ValidatorsLinkedList().isEmpty()
) {
if (_k1ValidatorsLinkedList().isEmpty()) {
revert Errors.EMPTY_VALIDATORS();
}

Expand Down
37 changes: 36 additions & 1 deletion test/deployments/deployer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { expect } from 'chai';
import type { ec } from 'elliptic';
import type { BytesLike, HDNodeWallet } from 'ethers';
import { parseEther } from 'ethers';
import { getAddress, hexlify, parseEther, randomBytes } from 'ethers';
import * as hre from 'hardhat';
import type { Contract, Wallet } from 'zksync-ethers';
import { Provider } from 'zksync-ethers';
Expand Down Expand Up @@ -104,5 +104,40 @@ describe('Clave Contracts - Deployer class tests', () => {
expect(await registry.isClave(accountAddress)).to.be.true;
expect(await registry.isClave(factoryAddress)).not.to.be.true;
});

it('should not deploy an account with an invalid salt', async () => {
const salt = randomBytes(32);
await expect(deployer.account(wallet, factory, eoaValidator, { salt: hexlify(salt) }))
.to.be.revertedWithCustomError(factory, "INITIALIZATION_FAILED");
});

it('should not deploy an account with an empty initializer', async () => {
await expect(deployer.account(wallet, factory, eoaValidator, { initializer: '0x' }))
.to.be.revertedWithCustomError(factory, "INVALID_INITIALIZER");
});

it('should not deploy an account with an invalid initializer selector', async () => {
await expect(deployer.account(wallet, factory, eoaValidator, { initializer: '0xabababab' }))
.to.be.revertedWithCustomError(factory, "INVALID_INITIALIZER");
});

it('should deploy an account with a payable initial call', async () => {
const target = getAddress('0x0000000000000000000000000000000000abcdef');
const initialCall = {
target,
allowFailure: false,
value: parseEther('0.9'),
callData: '0x',
};
const otherWallet = getWallet(hre, LOCAL_RICH_WALLETS[1].privateKey);
const newAccount = await deployer.account(otherWallet, factory, eoaValidator, {
initialCall,
callValue: parseEther('1')
});

expect(await provider.getBalance(target)).to.eq(parseEther('0.9'));
expect(await provider.getBalance(await newAccount.getAddress())).to.eq(parseEther('0.1'));

});
});
});
Loading
Loading