There are 3 major three smart contracts from our end which controls state management, wallet creation and transaction execution.
This is the Singleton contract which gets deployed on every chain. These contracts inherit SAFE contracts version 1.4 for their inherit security in execution and module selection. The singleton contract is made compatible with the current ERC 4337 architecture by including functions required for the communication with EntryPoint.
function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external returns (uint256 validationData) {
_requireFromEntryPoint();
validationData = _validateSignature(userOp, userOpHash);
require(userOp.nonce < type(uint64).max, 'account: nonsequential nonce');
_payPrefund(missingAccountFunds);
}
/**
* ensure the request comes from the known entrypoint.
*/
function _requireFromEntryPoint() internal view virtual {
require(msg.sender == entryPoint, 'account: not from EntryPoint');
}
/// implement template method of BaseAccount
function _validateSignature(
UserOperation calldata userOp,
bytes32 userOpHash
) internal virtual returns (uint256 validationData) {
bytes32 hash = userOpHash.toEthSignedMessageHash();
if (owner != hash.recover(userOp.signature))
return SIG_VALIDATION_FAILED;
return 0;
}
/// @dev Allows the entrypoint to execute a transaction without any further confirmations.
/// @param to Destination address of module transaction.
/// @param value Ether value of module transaction.
/// @param data Data payload of module transaction.
/// @param operation Operation type of module transaction.
function execTransactionFromEntrypoint(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation
) public {
// Only Entrypoint is allowed.
require(msg.sender == entryPoint, 'account: not from EntryPoint');
// Execute transaction without further confirmations.
_executeAndRevert(to, value, data, operation);
}
/// @dev Allows the entrypoint to execute a batch transactions without any further confirmations.
/// @param to Destination addresses of transactions.
/// @param value Ether values of transactions.
/// @param data Data payloads of transactions.
/// @param operation Operation types of transactions.
function execBatchTransactionFromEntrypoint(
address[] calldata to,
uint256[] calldata value,
bytes[] memory data,
Enum.Operation operation
) public {
// Only Entrypoint is allowed.
require(msg.sender == entryPoint, 'account: not from EntryPoint');
// Execute transaction without further confirmations.
require(to.length == data.length, "wrong array lengths");
for(uint256 i=0; i < to.length; i++) {
_executeAndRevert(to[i], value[i], data[i], operation);
}
}
There is also a function which acts as a payload and is executed when a new smart contract wallet is initialized. This function sets the initial storage of the contract with proper user specific values.
/// @dev Setup function sets initial storage of contract.
/// @param _owner List of Safe owners.
/// @param _threshold Number of required confirmations for a Safe transaction.
/// @param to Contract address for optional delegate call.
/// @param data Data payload for optional delegate call.
/// @param fallbackHandler Handler for fallback calls to this contract
/// @param paymentToken Token that should be used for the payment (0 is ETH)
/// @param payment Value that should be paid
/// @param paymentReceiver Address that should receive the payment (or 0 if tx.origin)
/// @param _entryPoint Address for the trusted EIP4337 entrypoint
function setupWithEntrypoint(
address _owner,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver,
address _entryPoint
) external {
entryPoint = _entryPoint;
owner = _owner;
address[] memory owners = new address[](1);
owners[0] = _owner;
_executeAndRevert(
address(this),
0,
abi.encodeCall(
Safe.setup,
(
owners,
_threshold,
to,
data,
fallbackHandler,
paymentToken,
payment,
paymentReceiver
)
),
Enum.Operation.DelegateCall
);
}
We use a Proxy pattern to deploy the smart contract wallet for every user. The proxy factory contract uses the address of the Singleton contract to deploy the proxies contract based on Proxy pattern of SAFE contracts. The proxy points to the singleton and any call to the SCW goes through this proxy. Every proxy maintains their own state of singleton.
There are three funcitons to deploy a proxy contract and all deploy the contract with different with different parameters. We use CREATE2 to deploy contracts in a deterministic address manner.
/// @dev Allows to create a new proxy contract and execute a message call to the new proxy within one transaction.
/// @param _singleton Address of singleton contract. Must be deployed at the time of execution.
/// @param initializer Payload for a message call to be sent to a new proxy contract.
/// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.
function createProxyWithNonce(
address _singleton,
bytes memory initializer,
uint256 saltNonce
) public returns (BananaAccountProxy proxy) {
// If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating it
bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));
proxy = deployProxy(_singleton, initializer, salt);
emit ProxyCreation(proxy, _singleton);
}
/// @dev Allows to create a new proxy contract that should exist only on 1 network (e.g. specific governance or admin accounts)
/// by including the chain id in the create2 salt. Such proxies cannot be created on other networks by replaying the transaction.
/// @param _singleton Address of singleton contract. Must be deployed at the time of execution.
/// @param initializer Payload for a message call to be sent to a new proxy contract.
/// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.
function createChainSpecificProxyWithNonce(
address _singleton,
bytes memory initializer,
uint256 saltNonce
) public returns (BananaAccountProxy proxy) {
// If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating it
bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce, getChainId()));
proxy = deployProxy(_singleton, initializer, salt);
emit ProxyCreation(proxy, _singleton);
}
/// @dev Allows to create a new proxy contract, execute a message call to the new proxy and call a specified callback within one transaction
/// @param _singleton Address of singleton contract. Must be deployed at the time of execution.
/// @param initializer Payload for a message call to be sent to a new proxy contract.
/// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.
/// @param callback Callback that will be invoked after the new proxy contract has been successfully deployed and initialized.
function createProxyWithCallback(
address _singleton,
bytes memory initializer,
uint256 saltNonce,
IProxyCreationCallback callback
) public returns (BananaAccountProxy proxy) {
uint256 saltNonceWithCallback = uint256(keccak256(abi.encodePacked(saltNonce, callback)));
proxy = createProxyWithNonce(_singleton, initializer, saltNonceWithCallback);
if (address(callback) != address(0))
callback.proxyCreated(proxy, _singleton, initializer, saltNonce);
}
This is the library used for the verification of secp256R1 curve’s signature. The verification is an ECDSA signature verification for R1 curve and it first converts the public X and public Y co-ordinates into a Jacobian format. The code performs various scalar multiplications and is inspired from Obvious wallets implementation.