Transaction Hash:
Block:
24001737 at Dec-13-2025 05:55:47 AM +UTC
Transaction Fee:
0.000005434317113758 ETH
$0.01
Gas Used:
148,886 Gas / 0.036499853 Gwei
Account State Difference:
| Address | Before | After | State Difference | ||
|---|---|---|---|---|---|
| 0x18677585...03f6Dc8a4 | 1.582420070284507174 Eth | 1.582430070284507174 Eth | 0.00001 | ||
|
0x4838B106...B0BAD5f97
Miner
| (Titan Builder) | 13.50922660178519698 Eth | 13.509226601785494752 Eth | 0.000000000000297772 | |
| 0xCFaFC6Bd...712165AA8 |
0.867717173965665499 Eth
Nonce: 11693
|
0.867711739648551741 Eth
Nonce: 11694
| 0.000005434317113758 | ||
| 0xDA49160b...753e8154F | 0.007780418806993033 Eth | 0.007770418806993033 Eth | 0.00001 |
Execution Trace
AmbireAccount.execute( calls=, signature=0x9F7FB9E49197722DD7C27BBA5C1C71B512F2ECE5AEBB8AD5B3D1FEBFE9ACD6DA132732BC195CBA188EC9F0C530B22447EE8A6679BBDA19B9CF023EEADBCA5B5A1B01 )
AmbireAccount.execute( calls=, signature=0x9F7FB9E49197722DD7C27BBA5C1C71B512F2ECE5AEBB8AD5B3D1FEBFE9ACD6DA132732BC195CBA188EC9F0C530B22447EE8A6679BBDA19B9CF023EEADBCA5B5A1B01 )-
Null: 0x000...001.a92dec43( ) - ETH 0.000001
0x18677585f024a5f93cf17808313168603f6dc8a4.CALL( ) - ETH 0.000001
0x18677585f024a5f93cf17808313168603f6dc8a4.CALL( ) - ETH 0.000001
0x18677585f024a5f93cf17808313168603f6dc8a4.CALL( ) - ETH 0.000001
0x18677585f024a5f93cf17808313168603f6dc8a4.CALL( ) - ETH 0.000001
0x18677585f024a5f93cf17808313168603f6dc8a4.CALL( ) - ETH 0.000001
0x18677585f024a5f93cf17808313168603f6dc8a4.CALL( ) - ETH 0.000001
0x18677585f024a5f93cf17808313168603f6dc8a4.CALL( ) - ETH 0.000001
0x18677585f024a5f93cf17808313168603f6dc8a4.CALL( ) - ETH 0.000001
0x18677585f024a5f93cf17808313168603f6dc8a4.CALL( ) - ETH 0.000001
0x18677585f024a5f93cf17808313168603f6dc8a4.CALL( ) -
Ambire Wallet: Deployer.00000000( )
-
File 1 of 2: AmbireAccount
File 2 of 2: AmbireAccount
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
import './libs/SignatureValidator.sol';
import './ExternalSigValidator.sol';
import './libs/erc4337/PackedUserOperation.sol';
import './libs/erc4337/UserOpHelper.sol';
import './deployless/IAmbireAccount.sol';
/**
* @notice A validator that performs DKIM signature recovery
* @dev All external/public functions (that are not view/pure) use `payable` because AmbireAccount
* is a wallet contract, and any ETH sent to it is not lost, but on the other hand not having `payable`
* makes the Solidity compiler add an extra check for `msg.value`, which in this case is wasted gas
*/
contract AmbireAccount is IAmbireAccount {
\t// @dev We do not have a constructor. This contract cannot be initialized with any valid `privileges` by itself!
\t// The intended use case is to deploy one base implementation contract, and create a minimal proxy for each user wallet, by
\t// using our own code generation to insert SSTOREs to initialize `privileges` (it was previously called IdentityProxyDeploy.js, now src/libs/proxyDeploy/deploy.ts)
\taddress private constant FALLBACK_HANDLER_SLOT = address(0x6969);
\t// @dev This is how we understand if msg.sender is the entry point
\tbytes32 private constant ENTRY_POINT_MARKER = 0x0000000000000000000000000000000000000000000000000000000000007171;
\t// Externally validated signatures
\tuint8 private constant SIGMODE_EXTERNALLY_VALIDATED = 255;
\t// Variables
\tmapping(address => bytes32) public privileges;
\tuint256 public nonce;
\t// Events
\tevent LogPrivilegeChanged(address indexed addr, bytes32 priv);
\tevent LogErr(address indexed to, uint256 value, bytes data, bytes returnData); // only used in tryCatch
\t// This contract can accept ETH without calldata
\treceive() external payable {}
\t/**
\t * @dev To support EIP 721 and EIP 1155, we need to respond to those methods with their own method signature
\t * @return bytes4 onERC721Received function selector
\t */
\tfunction onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
\t\treturn this.onERC721Received.selector;
\t}
\t/**
\t * @dev To support EIP 721 and EIP 1155, we need to respond to those methods with their own method signature
\t * @return bytes4 onERC1155Received function selector
\t */
\tfunction onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) {
\t\treturn this.onERC1155Received.selector;
\t}
\t/**
\t * @dev To support EIP 721 and EIP 1155, we need to respond to those methods with their own method signature
\t * @return bytes4 onERC1155Received function selector
\t */
\tfunction onERC1155BatchReceived(
\t\taddress,
\t\taddress,
\t\tuint256[] calldata,
\t\tuint256[] calldata,
\t\tbytes calldata
\t) external pure returns (bytes4) {
\t\treturn this.onERC1155BatchReceived.selector;
\t}
\t/**
\t * @notice fallback method: currently used to call the fallback handler
\t * which is set by the user and can be changed
\t * @dev this contract can accept ETH with calldata, hence payable
\t */
\tfallback() external payable {
\t\t// We store the fallback handler at this magic slot
\t\taddress fallbackHandler = address(uint160(uint(privileges[FALLBACK_HANDLER_SLOT])));
\t\tif (fallbackHandler == address(0)) return;
\t\tassembly {
\t\t\t// we can use addr 0 because logic is taking full control of the
\t\t\t// execution making sure it returns itself and does not
\t\t\t// rely on any further Solidity code.
\t\t\tcalldatacopy(0, 0, calldatasize())
\t\t\tlet result := delegatecall(gas(), fallbackHandler, 0, calldatasize(), 0, 0)
\t\t\tlet size := returndatasize()
\t\t\treturndatacopy(0, 0, size)
\t\t\tif eq(result, 0) {
\t\t\t\trevert(0, size)
\t\t\t}
\t\t\treturn(0, size)
\t\t}
\t}
\t/**
\t * @notice used to set the privilege of a key (by `addr`)
\t * @dev normal signatures will be considered valid if the
\t * `addr` they are signed with has non-zero (not 0x000..000) privilege set; we can set the privilege to
\t * a hash of the recovery keys and timelock (see `RecoveryInfo`) to enable recovery signatures
\t * @param addr the address to give privs to
\t * @param priv the privs to give
\t */
\tfunction setAddrPrivilege(address addr, bytes32 priv) external payable {
\t\trequire(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
\t\tprivileges[addr] = priv;
\t\temit LogPrivilegeChanged(addr, priv);
\t}
\t/**
\t * @notice Useful when we need to do multiple operations but ignore failures in some of them
\t * @param to address we're sending value to
\t * @param value the amount
\t * @param data callData
\t */
\tfunction tryCatch(address to, uint256 value, bytes calldata data) external payable {
\t\trequire(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
\t\tuint256 gasBefore = gasleft();
\t\t(bool success, bytes memory returnData) = to.call{ value: value, gas: gasBefore }(data);
\t\trequire(gasleft() > gasBefore / 64, 'TRYCATCH_OOG');
\t\tif (!success) emit LogErr(to, value, data, returnData);
\t}
\t/**
\t * @notice same as `tryCatch` but with a gas limit
\t * @param to address we're sending value to
\t * @param value the amount
\t * @param data callData
\t * @param gasLimit how much gas is allowed
\t */
\tfunction tryCatchLimit(address to, uint256 value, bytes calldata data, uint256 gasLimit) external payable {
\t\trequire(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
\t\tuint256 gasBefore = gasleft();
\t\t(bool success, bytes memory returnData) = to.call{ value: value, gas: gasLimit }(data);
\t\trequire(gasleft() > gasBefore / 64, 'TRYCATCH_OOG');
\t\tif (!success) emit LogErr(to, value, data, returnData);
\t}
\t/**
\t * @notice execute: this method is used to execute a single bundle of calls that are signed with a key
\t * that is authorized to execute on this account (in `privileges`)
\t * @dev WARNING: if the signature of this is changed, we have to change AmbireAccountFactory
\t * @param calls the transaction we're executing. They may not execute
\t * if specific cases. One such is when setting a timelock
\t * @param signature the signature for the transactions
\t */
\tfunction execute(Transaction[] calldata calls, bytes calldata signature) public payable {
\t\taddress signerKey;
\t\tuint8 sigMode = uint8(signature[signature.length - 1]);
\t\tuint256 currentNonce = nonce;
\t\t// we increment the nonce here (not using `nonce++` to save some gas)
\t\tnonce = currentNonce + 1;
\t\tif (sigMode == SIGMODE_EXTERNALLY_VALIDATED) {
\t\t\tbool isValidSig;
\t\t\tuint256 timestampValidAfter;
\t\t\t(signerKey, isValidSig, timestampValidAfter) = validateExternalSig(calls, signature);
\t\t\tif (!isValidSig) {
\t\t\t\trequire(block.timestamp >= timestampValidAfter, 'SIGNATURE_VALIDATION_TIMELOCK');
\t\t\t\trevert('SIGNATURE_VALIDATION_FAIL');
\t\t\t}
\t\t} else {
\t\t\tsignerKey = SignatureValidator.recoverAddr(
\t\t\t\tkeccak256(abi.encode(address(this), block.chainid, currentNonce, calls)),
\t\t\t\tsignature,
\t\t\t\ttrue
\t\t\t);
\t\t\trequire(privileges[signerKey] != bytes32(0), 'INSUFFICIENT_PRIVILEGE');
\t\t}
\t\texecuteBatch(calls);
\t\t// The actual anti-bricking mechanism - do not allow a signerKey to drop their own privileges
\t\trequire(privileges[signerKey] != bytes32(0), 'PRIVILEGE_NOT_DOWNGRADED');
\t}
\t/**
\t * @notice allows executing multiple bundles of calls (batch together multiple executes)
\t * @param toExec an array of execute function parameters
\t */
\tfunction executeMultiple(ExecuteArgs[] calldata toExec) external payable {
\t\tfor (uint256 i = 0; i != toExec.length; i++) execute(toExec[i].calls, toExec[i].signature);
\t}
\t/**
\t * @notice Allows executing calls if the caller itself is authorized
\t * @dev no need for nonce management here cause we're not dealing with sigs
\t * @param calls the transaction we're executing
\t */
\tfunction executeBySender(Transaction[] calldata calls) external payable {
\t\trequire(privileges[msg.sender] != bytes32(0), 'INSUFFICIENT_PRIVILEGE');
\t\texecuteBatch(calls);
\t\t// again, anti-bricking
\t\trequire(privileges[msg.sender] != bytes32(0), 'PRIVILEGE_NOT_DOWNGRADED');
\t}
\t/**
\t * @notice allows the contract itself to execute a batch of calls
\t * self-calling is useful in cases like wanting to do multiple things in a tryCatchLimit
\t * @param calls the calls we're executing
\t */
\tfunction executeBySelf(Transaction[] calldata calls) external payable {
\t\trequire(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
\t\texecuteBatch(calls);
\t}
\t/**
\t * @notice allows the contract itself to execute a single calls
\t * self-calling is useful when you want to workaround the executeBatch()
\t * protection of not being able to call address(0)
\t * @param call the call we're executing
\t */
\tfunction executeBySelfSingle(Transaction calldata call) external payable {
\t\trequire(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
\t\texecuteCall(call.to, call.value, call.data);
\t}
\t/**
\t * @notice Execute a batch of transactions
\t * @param calls the transaction we're executing
\t */
\tfunction executeBatch(Transaction[] memory calls) internal {
\t\tuint256 len = calls.length;
\t\tfor (uint256 i = 0; i < len; i++) {
\t\t\tTransaction memory call = calls[i];
\t\t\tif (call.to != address(0)) executeCall(call.to, call.value, call.data);
\t\t}
\t}
\t/**
\t * @notice Execute a signle transaction
\t * @dev we shouldn't use address.call(), cause: https://github.com/ethereum/solidity/issues/2884
\t * @param to the address we're sending to
\t * @param value the amount we're sending
\t * @param data callData
\t */
\tfunction executeCall(address to, uint256 value, bytes memory data) internal {
\t\tassembly {
\t\t\tlet result := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0)
\t\t\tif eq(result, 0) {
\t\t\t\tlet size := returndatasize()
\t\t\t\tlet ptr := mload(0x40)
\t\t\t\treturndatacopy(ptr, 0, size)
\t\t\t\trevert(ptr, size)
\t\t\t}
\t\t}
\t}
\t/**
\t * @notice EIP-1271 implementation
\t * @dev see https://eips.ethereum.org/EIPS/eip-1271
\t * @param hash the signed hash
\t * @param signature the signature for the signed hash
\t * @return bytes4 is it a success or a failure
\t */
\tfunction isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
\t\t(address recovered, bool usedUnprotected) = SignatureValidator.recoverAddrAllowUnprotected(hash, signature, false);
\t\tif (uint256(privileges[recovered]) > (usedUnprotected ? 1 : 0)) {
\t\t\t// bytes4(keccak256("isValidSignature(bytes32,bytes)")
\t\t\treturn 0x1626ba7e;
\t\t} else {
\t\t\treturn 0xffffffff;
\t\t}
\t}
\t/**
\t * @notice EIP-1155 implementation
\t * we pretty much only need to signal that we support the interface for 165, but for 1155 we also need the fallback function
\t * @param interfaceID the interface we're signaling support for
\t * @return bool do we support the interface or not
\t */
\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool) {
\t\tbool supported = interfaceID == 0x01ffc9a7 || // ERC-165 support (i.e. `bytes4(keccak256('supportsInterface(bytes4)'))`).
\t\t\tinterfaceID == 0x150b7a02 || // ERC721TokenReceiver
\t\t\tinterfaceID == 0x4e2312e0 || // ERC-1155 `ERC1155TokenReceiver` support (i.e. `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) ^ bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`).
\t\t\tinterfaceID == 0x0a417632; // used for checking whether the account is v2 or not
\t\tif (supported) return true;
\t\taddress payable fallbackHandler = payable(address(uint160(uint256(privileges[FALLBACK_HANDLER_SLOT]))));
\t\tif (fallbackHandler == address(0)) return false;
\t\treturn AmbireAccount(fallbackHandler).supportsInterface(interfaceID);
\t}
\t//
\t// EIP-4337 implementation
\t//
\t// return value in case of signature failure, with no time-range.
\t// equivalent to packSigTimeRange(true,0,0);
\tuint256 constant internal SIG_VALIDATION_FAILED = 1;
\t// equivalent to packSigTimeRange(false,0,0);
\tuint256 constant internal SIG_VALIDATION_SUCCESS = 0;
\t/**
\t * @notice EIP-4337 implementation
\t * @dev We have an edge case for enabling ERC-4337 in the first if statement.
\t * If the function call is to execute, we do not perform an userOp sig validation.
\t * We require a one time hash nonce commitment from the paymaster for the given
\t * req. We use this to give permissions to the entry point on the fly
\t * and enable ERC-4337
\t * @param op the PackedUserOperation we're executing
\t * @param userOpHash the hash we've committed to
\t * @param missingAccountFunds the funds the account needs to pay
\t * @return uint256 0 for success, 1 for signature failure, and a uint256
\t * packed timestamp for a future valid signature:
\t * address aggregator, uint48 validUntil, uint48 validAfter
\t */
\tfunction validateUserOp(PackedUserOperation calldata op, bytes32 userOpHash, uint256 missingAccountFunds)
\texternal payable returns (uint256)
\t{
\t\t// enable running executeMultiple operation through the entryPoint if
\t\t// a paymaster sponsors it with a commitment one-time nonce.
\t\t// two use cases:
\t\t// 1) enable 4337 on a network by giving privileges to the entryPoint
\t\t// 2) key recovery. If the key is lost, we cannot sign the userOp,
\t\t// so we have to go to `execute` to trigger the recovery logic
\t\t// Why executeMultiple but not execute?
\t\t// executeMultiple allows us to combine recovery + fee payment calls.
\t\t// The fee payment call will be with a signature from the new key
\t\tif (op.callData.length >= 4 && bytes4(op.callData[0:4]) == this.executeMultiple.selector) {
\t\t\t// Require a paymaster, otherwise this mode can be used by anyone to get the user to spend their deposit
\t\t\t// @estimation-no-revert
\t\t\tif (op.signature.length != 0) return SIG_VALIDATION_FAILED;
\t\t\trequire(
\t\t\t\top.paymasterAndData.length >= UserOpHelper.PAYMASTER_DATA_OFFSET &&
\t\t\t\tbytes20(op.paymasterAndData[:UserOpHelper.PAYMASTER_ADDR_OFFSET]) != bytes20(0),
\t\t\t\t'validateUserOp: paymaster required in execute() mode'
\t\t\t);
\t\t\t// hashing in everything except sender (nonces are scoped by sender anyway), nonce, signature
\t\t\tuint256 targetNonce = uint256(keccak256(
\t\t\t\tabi.encode(op.initCode, op.callData, op.accountGasLimits, op.preVerificationGas, op.gasFees, op.paymasterAndData)
\t\t\t)) << 64;
\t\t\t// @estimation-no-revert
\t\t\tif (op.nonce != targetNonce) return SIG_VALIDATION_FAILED;
\t\t\treturn SIG_VALIDATION_SUCCESS;
\t\t}
\t\trequire(privileges[msg.sender] == ENTRY_POINT_MARKER, 'validateUserOp: not from entryPoint');
\t\t// @estimation
\t\t// paying should happen even if signature validation fails
\t\tif (missingAccountFunds > 0) {
\t\t\t// NOTE: MAY pay more than the minimum, to deposit for future transactions
\t\t\t(bool success,) = msg.sender.call{value : missingAccountFunds}('');
\t\t\t// ignore failure (its EntryPoint's job to verify, not account.)
\t\t\t(success);
\t\t}
\t\t// this is replay-safe because userOpHash is retrieved like this: keccak256(abi.encode(userOp.hash(), address(this), block.chainid))
\t\taddress signer = SignatureValidator.recoverAddr(userOpHash, op.signature, true);
\t\tif (privileges[signer] == bytes32(0)) return SIG_VALIDATION_FAILED;
\t\treturn SIG_VALIDATION_SUCCESS;
\t}
\tfunction validateExternalSig(Transaction[] memory calls, bytes calldata signature)
\tinternal returns(address signerKey, bool isValidSig, uint256 timestampValidAfter) {
\t\t(bytes memory sig, ) = SignatureValidator.splitSignature(signature);
\t\t// the address of the validator we're using for this validation
\t\taddress validatorAddr;
\t\t// all the data needed by the validator to execute the validation.
\t\t// In the case of DKIMRecoverySigValidator, this is AccInfo:
\t\t// abi.encode {string emailFrom; string emailTo; string domainName;
\t\t// bytes dkimPubKeyModulus; bytes dkimPubKeyExponent; address secondaryKey;
\t\t// bool acceptUnknownSelectors; uint32 waitUntilAcceptAdded;
\t\t// uint32 waitUntilAcceptRemoved; bool acceptEmptyDKIMSig;
\t\t// bool acceptEmptySecondSig;uint32 onlyOneSigTimelock;}
\t\t// The struct is declared in DKIMRecoverySigValidator
\t\tbytes memory validatorData;
\t\t// the signature data needed by the external validator.
\t\t// In the case of DKIMRecoverySigValidator, this is abi.encode(
\t\t// SignatureMeta memory sigMeta, bytes memory dkimSig, bytes memory secondSig
\t\t// ).
\t\tbytes memory innerSig;
\t\t// the signerKey in this case is an arbitrary value that does
\t\t// not have any specific purpose other than representing
\t\t// the privileges key
\t\t(signerKey, validatorAddr, validatorData, innerSig) = abi.decode(sig, (address, address, bytes, bytes));
\t\trequire(
\t\t\tprivileges[signerKey] == keccak256(abi.encode(validatorAddr, validatorData)),
\t\t\t'EXTERNAL_VALIDATION_NOT_SET'
\t\t);
\t\t// The sig validator itself should throw when a signature isn't validated successfully
\t\t// the return value just indicates whether we want to execute the current calls
\t\t(isValidSig, timestampValidAfter) = ExternalSigValidator(validatorAddr).validateSig(validatorData, innerSig, calls);
\t}
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
import './deployless/IAmbireAccount.sol';
import './libs/Transaction.sol';
/**
* @notice A contract used for deploying AmbireAccount.sol
* @dev We use create2 to get the AmbireAccount address. It's deterministic:
* if the same data is passed to it, the same address will pop out.
*/
contract AmbireFactory {
\tevent LogDeployed(address addr, uint256 salt);
\taddress public immutable allowedToDrain;
\tconstructor(address allowed) {
\t\tallowedToDrain = allowed;
\t}
\t/**
\t * @notice Allows anyone to deploy any contracft with a specific code/salt
\t * @dev This is safe because it's CREATE2 deployment
\t * @param code the code to be deployed
\t * @param salt the salt to shuffle the computed address
\t * @return address the deployed address
\t */
\tfunction deploy(bytes calldata code, uint256 salt) external returns(address) {
\t\treturn deploySafe(code, salt);
\t}
\t
\t/**
\t * @notice Call this when you want to deploy the contract and execute calls
\t * @dev When the relayer needs to act upon an /identity/:addr/submit call, it'll either call execute on the AmbireAccount directly
\t * if it's already deployed, or call `deployAndExecute` if the account is still counterfactual
\t * we can't have deployAndExecuteBySender, because the sender will be the factory
\t * @param code the code to be deployed
\t * @param salt the salt to shuffle the computed address
\t * @param txns the txns the are going to be executed
\t * @param signature the signature for the txns
\t * @return address the deployed address
\t */
\tfunction deployAndExecute(
\t\tbytes calldata code,
\t\tuint256 salt,
\t\tTransaction[] calldata txns,
\t\tbytes calldata signature
\t) external returns (address){
\t\taddress payable addr = payable(deploySafe(code, salt));
\t\tIAmbireAccount(addr).execute(txns, signature);
\t\treturn addr;
\t}
\t
\t/**
\t * @notice Call this when you want to deploy the contract and call executeMultiple
\t * @dev when the relayer needs to act upon an /identity/:addr/submit call,
\t * it'll either call execute on the AmbireAccount directly. If it's already
\t * deployed, or call `deployAndExecuteMultiple` if the account is still
\t * counterfactual but there are multiple accountOps to send
\t * @param code the code to be deployed
\t * @param salt the salt to shuffle the computed address
\t * @param toExec [txns, signature] execute parameters
\t * @return address the deployed address
\t */
\tfunction deployAndExecuteMultiple(
\t\tbytes calldata code,
\t\tuint256 salt,
\t\tIAmbireAccount.ExecuteArgs[] calldata toExec
\t) external returns (address){
\t\taddress payable addr = payable(deploySafe(code, salt));
\t\tIAmbireAccount(addr).executeMultiple(toExec);
\t\treturn addr;
\t}
\t/**
\t * @notice This method can be used to withdraw stuck tokens or airdrops
\t * @dev Only allowedToDrain can do the call
\t * @param to receiver
\t * @param value how much to be sent
\t * @param data if a token has airdropped, code to send it
\t * @param gas maximum gas willing to spend
\t */
\tfunction call(address to, uint256 value, bytes calldata data, uint256 gas) external {
\t\trequire(msg.sender == allowedToDrain, 'ONLY_AUTHORIZED');
\t\t(bool success, bytes memory err) = to.call{ gas: gas, value: value }(data);
\t\trequire(success, string(err));
\t}
\t
\t/**
\t * @dev This is done to mitigate possible frontruns where, for example,
\t * where deploying the same code/salt via deploy() would make a pending
\t * deployAndExecute fail. The way we mitigate that is by checking if the
\t * contract is already deployed and if so, we continue execution
\t * @param code the code to be deployed
\t * @param salt the salt to shuffle the computed address
\t * @return address the deployed address
\t */
\tfunction deploySafe(bytes memory code, uint256 salt) internal returns (address) {
\t\taddress expectedAddr = address(
\t\t\tuint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(code)))))
\t\t);
\t\tuint256 size;
\t\tassembly {
\t\t\tsize := extcodesize(expectedAddr)
\t\t}
\t\t// If there is code at that address, we can assume it's the one we were about to deploy,
\t\t// because of how CREATE2 and keccak256 works
\t\tif (size == 0) {
\t\t\taddress addr;
\t\t\tassembly {
\t\t\t\taddr := create2(0, add(code, 0x20), mload(code), salt)
\t\t\t}
\t\t\trequire(addr != address(0), 'FAILED_DEPLOYING');
\t\t\trequire(addr == expectedAddr, 'FAILED_MATCH');
\t\t\temit LogDeployed(addr, salt);
\t\t}
\t\treturn expectedAddr;
\t}
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
import './deployless/IAmbireAccount.sol';
import './libs/erc4337/IPaymaster.sol';
import './libs/SignatureValidator.sol';
import './libs/erc4337/UserOpHelper.sol';
contract AmbirePaymaster is IPaymaster {
\taddress immutable public relayer;
\tconstructor(address _relayer) {
\t\trelayer = _relayer;
\t}
\t/**
\t * @notice This method can be used to withdraw stuck tokens or airdrops
\t *
\t * @param to The address we're calling
\t * @param value The value in the call
\t * @param\tdata\tthe call data
\t * @param\tgas\tthe call gas
\t */
\tfunction call(address to, uint256 value, bytes calldata data, uint256 gas) external payable {
\t\trequire(msg.sender == relayer, 'call: not relayer');
\t\t(bool success, bytes memory err) = to.call{ gas: gas, value: value }(data);
\t\trequire(success, string(err));
\t}
\t/**
\t * @notice Validate user operations the paymaster has signed
\t * We do not need to send funds to the EntryPoint because we rely on pre-existing deposit.
\t * Requests are chain specific to prevent signature reuse.
\t * @dev We have two use cases for the paymaster:
\t * - normal erc-4337. Everything is per ERC-4337 standard, the nonce is sequential.
\t * - an executeMultiple call. If the calldata is executeMultiple, we've hardcoded
\t * a 0 nonce. That's what's called a one-time hash nonce and its key is actually
\t * the commitment. Check EntryPoint -> NonceManager for more information.
\t *
\t * @param userOp the UserOperation we're executing
\t * @return context context is returned in the postOp and called by the
\t * EntryPoint. But we're not using postOp is context is always emtpy
\t * @return validationData This consists of:
\t * - an aggregator address: address(uint160(validationData)). This is used
\t * when you want an outer contract to determine whether the signature is valid.
\t * In our case, this is always 0 (address 0) for valid signatures and
\t * 1 (address 1) for invalid. This is what the entry point expects and
\t * in those two cases, an outer contract is obviously not called.
\t * - a uint48 validUntil: uint48(validationData >> 160)
\t * A Paymaster signature can be signed at time "x" but delayed intentionally
\t * until time "y" when a fee payment's price has dropped significantly or
\t * some other issue. validUntil sets a time validity for the signature
* - a uint48 validAfter: uint48(validationData >> (48 + 160))
\t * If the signature should be valid only after a period of time,
\t * we tweak the validAfter property.
\t * For more information, check EntryPoint -> _getValidationData()
\t */
\tfunction validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32, uint256)
\t\texternal
\t\tview
\t\treturns (bytes memory context, uint256 validationData)
\t{
\t\t(uint48 validUntil, uint48 validAfter, bytes memory signature) = abi.decode(
\t\t\tuserOp.paymasterAndData[UserOpHelper.PAYMASTER_DATA_OFFSET:],
\t\t\t(uint48, uint48, bytes)
\t\t);
\t\tbytes memory callData = userOp.callData;
\t\tbytes32 hash = keccak256(abi.encode(
\t\t\tblock.chainid,
\t\t\taddress(this),
\t\t\t// entry point
\t\t\tmsg.sender,
\t\t\tvalidUntil,
\t\t\tvalidAfter,
\t\t\t// everything except paymasterAndData and signature
\t\t\tuserOp.sender,
\t\t\t// for the nonce we have an exception case: one-time nonces depend on paymasterAndData, which is generated by the relayer
\t\t\t// we can't have this as part of the sig cuz we create a cyclical dep
\t\t\t// the nonce can only be used once, so one cannot replay the gas payment
\t\t\tcallData.length >= 4 && bytes4(userOp.callData[0:4]) == IAmbireAccount.executeMultiple.selector ? 0 : userOp.nonce,
\t\t\tuserOp.initCode,
\t\t\tcallData,
\t\t\tuserOp.accountGasLimits,
\t\t\tuserOp.preVerificationGas,
\t\t\tuserOp.gasFees
\t\t));
\t\t(address recovered, ) = SignatureValidator.recoverAddrAllowUnprotected(hash, signature, true);
\t\tbool isValidSig = recovered == relayer;
\t\t// see _packValidationData: https://github.com/eth-infinitism/account-abstraction/blob/f2b09e60a92d5b3177c68d9f382912ccac19e8db/contracts/core/Helpers.sol#L73-L80
\t\treturn ("", uint160(isValidSig ? 0 : 1) | (uint256(validUntil) << 160) | (uint256(validAfter) << 208));
\t}
\t/**
\t * @notice No-op, won't be used because we don't return a context
\t * @param mode .
\t * @param context .
\t * @param actualGasCost .
\t */
\tfunction postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) external {
\t\t// No-op, won't be used because we don't return a context
\t}
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
import './libs/Transaction.sol';
/**
* @title ExternalSigValidator
* @notice A way to add custom recovery to AmbireAccount.
* address accountAddr is the Ambire account address
* bytes calldata data is all the data needed by the ExternalSigValidator.
* It could be anything and it's validator specific.
* bytes calldata sig is the signature we're validating. Notice its not
* bytes32 so there could be cases where its not only the signature. It's
* validator specific
* uint256 nonce - the Ambire account nonce
* Transaction[] calldata calls - the txns that are going to be executed
* if the validation is successful
* @dev Not all passed properties necessarily need to be used.
*/
abstract contract ExternalSigValidator {
\tfunction validateSig(
\t\tbytes calldata data,
\t\tbytes calldata sig,
\t\tTransaction[] calldata calls
\t) external virtual returns (bool isValidSignature, uint256 timestampValidAfter);
}// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.7;
import '../libs/Transaction.sol';
interface IAmbireAccount {
\tfunction privileges(address addr) external returns (bytes32);
\tfunction nonce() external returns (uint);
\tstruct RecoveryInfo {
\t\taddress[] keys;
\t\tuint timelock;
\t}
\tstruct ExecuteArgs {
\t\tTransaction[] calls;
\t\tbytes signature;
\t}
\tfunction setAddrPrivilege(address addr, bytes32 priv) external payable;
\tfunction tryCatch(address to, uint value, bytes calldata data) external payable;
\tfunction tryCatchLimit(address to, uint value, bytes calldata data, uint gasLimit) external payable;
\tfunction execute(Transaction[] calldata txns, bytes calldata signature) external payable;
\tfunction executeBySender(Transaction[] calldata txns) external payable;
\tfunction executeBySelf(Transaction[] calldata txns) external payable;
\tfunction executeMultiple(ExecuteArgs[] calldata toExec) external payable;
\t// EIP 1271 implementation
\t// see https://eips.ethereum.org/EIPS/eip-1271
\tfunction isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4);
\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool);
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
library Bytes {
\tfunction trimToSize(bytes memory b, uint256 newLen) internal pure {
\t\trequire(b.length > newLen, 'BytesLib: only shrinking');
\t\tassembly {
\t\t\tmstore(b, newLen)
\t\t}
\t}
\t/***********************************|
\t| Read Bytes Functions |
\t|__________________________________*/
\t/**
\t * @dev Reads a bytes32 value from a position in a byte array.
\t * @param b Byte array containing a bytes32 value.
\t * @param index Index in byte array of bytes32 value.
\t * @return result bytes32 value from byte array.
\t */
\tfunction readBytes32(bytes memory b, uint256 index) internal pure returns (bytes32 result) {
\t\t// Arrays are prefixed by a 256 bit length parameter
\t\tindex += 32;
\t\trequire(b.length >= index, 'BytesLib: length');
\t\t// Read the bytes32 from array memory
\t\tassembly {
\t\t\tresult := mload(add(b, index))
\t\t}
\t\treturn result;
\t}
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
import './Bytes.sol';
interface IERC1271Wallet {
\tfunction isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue);
}
library SignatureValidator {
\tusing Bytes for bytes;
\tenum SignatureMode {
\t\t// the first mode Unprotected is used in combination with EIP-1271 signature verification to do
\t\t// EIP-712 verifications, as well as "Ethereum signed message:" message verifications
\t\t// The caveat with this is that we need to ensure that the signer key used for it isn't reused, or the message body
\t\t// itself contains context about the wallet (such as it's address)
\t\t// We do this, rather than applying the prefix on-chain, because if we do you won't be able to see the message
\t\t// when signing on a hardware wallet (you'll only see the hash) - since `isValidSignature` can only receive the hash -
\t\t// if the prefix is applied on-chain you can never match it - it's hash(prefix+hash(msg)) vs hash(prefix+msg)
\t\t// As for transactions (`execute()`), those can be signed with any of the modes
\t\t// Otherwise, if it's reused, we MUST use `Standard` mode which always wraps the final digest hash, but unfortnately this means
\t\t// you can't preview the full message when signing on a HW wallet
\t\tUnprotected,
\t\tStandard,
\t\tSmartWallet,
\t\tSpoof,
\t\tSchnorr,
\t\tMultisig,
\t\t// WARNING: Signature modes should not be more than 26 as the "v"
\t\t// value for standard ecrecover is 27/28
\t\t// WARNING: must always be last
\t\tLastUnused
\t}
\t// bytes4(keccak256("isValidSignature(bytes32,bytes)"))
\tbytes4 internal constant ERC1271_MAGICVALUE_BYTES32 = 0x1626ba7e;
\t// secp256k1 group order
\tuint256 internal constant Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
\tfunction splitSignature(bytes memory sig) internal pure returns (bytes memory, uint8) {
\t\tuint8 modeRaw;
\t\tunchecked {
\t\t\tmodeRaw = uint8(sig[sig.length - 1]);
\t\t}
\t\tsig.trimToSize(sig.length - 1);
\t\treturn (sig, modeRaw);
\t}
\tfunction recoverAddr(bytes32 hash, bytes memory sig, bool allowSpoofing) internal view returns (address) {
\t\t(address recovered, bool usedUnprotected) = recoverAddrAllowUnprotected(hash, sig, allowSpoofing);
\t\trequire(!usedUnprotected, 'SV_USED_UNBOUND');
\t\treturn recovered;
\t}
\tfunction recoverAddrAllowUnprotected(bytes32 hash, bytes memory sig, bool allowSpoofing) internal view returns (address, bool) {
\t\trequire(sig.length != 0, 'SV_SIGLEN');
\t\tuint8 modeRaw;
\t\tunchecked {
\t\t\tmodeRaw = uint8(sig[sig.length - 1]);
\t\t}
\t\t// Ensure we're in bounds for mode; Solidity does this as well but it will just silently blow up rather than showing a decent error
\t\tif (modeRaw >= uint8(SignatureMode.LastUnused)) {
\t\t\tif (sig.length == 65) modeRaw = uint8(SignatureMode.Unprotected);
\t\t\telse revert('SV_SIGMODE');
\t\t}
\t\tSignatureMode mode = SignatureMode(modeRaw);
\t\t// the address of the key we are gonna be returning
\t\taddress signerKey;
\t\t// wrap in the EIP712 wrapping if it's not unbound
\t\t// multisig gets an exception because each inner sig will have to apply this logic
\t\t// @TODO should spoofing be removed from this?
\t\tbool isUnprotected = mode == SignatureMode.Unprotected || mode == SignatureMode.Multisig;
\t\tif (!isUnprotected) {
\t\t\tbytes32 DOMAIN_SEPARATOR = keccak256(abi.encode(
\t\t\t\tkeccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)'),
\t\t\t\tkeccak256(bytes('Ambire')),
\t\t\t\tkeccak256(bytes('1')),
\t\t\t\tblock.chainid,
\t\t\t\taddress(this),
\t\t\t\tbytes32(0)
\t\t\t));
\t\t\thash = keccak256(abi.encodePacked(
\t\t\t\t'\\x19\\x01',
\t\t\t\tDOMAIN_SEPARATOR,
\t\t\t\tkeccak256(abi.encode(
\t\t\t\t\tkeccak256(bytes('AmbireOperation(address account,bytes32 hash)')),
\t\t\t\t\taddress(this),
\t\t\t\t\thash
\t\t\t\t))
\t\t\t));
\t\t}
\t\t// {r}{s}{v}{mode}
\t\tif (mode == SignatureMode.Unprotected || mode == SignatureMode.Standard) {
\t\t\trequire(sig.length == 65 || sig.length == 66, 'SV_LEN');
\t\t\tbytes32 r = sig.readBytes32(0);
\t\t\tbytes32 s = sig.readBytes32(32);
\t\t\tuint8 v = uint8(sig[64]);
\t\t\tsignerKey = ecrecover(hash, v, r, s);
\t\t// {sig}{verifier}{mode}
\t\t} else if (mode == SignatureMode.Schnorr) {
\t\t\t// Based on https://hackmd.io/@nZ-twauPRISEa6G9zg3XRw/SyjJzSLt9
\t\t\t// You can use this library to produce signatures: https://github.com/borislav-itskov/schnorrkel.js
\t\t\t// px := public key x-coord
\t\t\t// e := schnorr signature challenge
\t\t\t// s := schnorr signature
\t\t\t// parity := public key y-coord parity (27 or 28)
\t\t\t// last uint8 is for the Ambire sig mode - it's ignored
\t\t\tsig.trimToSize(sig.length - 1);
\t\t\t(bytes32 px, bytes32 e, bytes32 s, uint8 parity) = abi.decode(sig, (bytes32, bytes32, bytes32, uint8));
\t\t\t// ecrecover = (m, v, r, s);
\t\t\tbytes32 sp = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
\t\t\tbytes32 ep = bytes32(Q - mulmod(uint256(e), uint256(px), Q));
\t\t\trequire(sp != bytes32(Q));
\t\t\t// the ecrecover precompile implementation checks that the `r` and `s`
\t\t\t// inputs are non-zero (in this case, `px` and `ep`), thus we don't need to
\t\t\t// check if they're zero.
\t\t\taddress R = ecrecover(sp, parity, px, ep);
\t\t\trequire(R != address(0), 'SV_ZERO_SIG');
\t\t\trequire(e == keccak256(abi.encodePacked(R, uint8(parity), px, hash)), 'SV_SCHNORR_FAILED');
\t\t\tsignerKey = address(uint160(uint256(keccak256(abi.encodePacked('SCHNORR', px)))));
\t\t} else if (mode == SignatureMode.Multisig) {
\t\t\tsig.trimToSize(sig.length - 1);
\t\t\tbytes[] memory signatures = abi.decode(sig, (bytes[]));
\t\t\t// since we're in a multisig, we care if any of the inner sigs are unbound
\t\t\tisUnprotected = false;
\t\t\tfor (uint256 i = 0; i != signatures.length; i++) {
\t\t\t\t(address inner, bool isInnerUnprotected) = recoverAddrAllowUnprotected(hash, signatures[i], false);
\t\t\t\tif (isInnerUnprotected) isUnprotected = true;
\t\t\t\tsignerKey = address(
\t\t\t\t\tuint160(uint256(keccak256(abi.encodePacked(signerKey, inner))))
\t\t\t\t);
\t\t\t}
\t\t} else if (mode == SignatureMode.SmartWallet) {
\t\t\t// 32 bytes for the addr, 1 byte for the type = 33
\t\t\trequire(sig.length > 33, 'SV_LEN_WALLET');
\t\t\tuint256 newLen;
\t\t\tunchecked {
\t\t\t\tnewLen = sig.length - 33;
\t\t\t}
\t\t\tIERC1271Wallet wallet = IERC1271Wallet(address(uint160(uint256(sig.readBytes32(newLen)))));
\t\t\tsig.trimToSize(newLen);
\t\t\trequire(ERC1271_MAGICVALUE_BYTES32 == wallet.isValidSignature(hash, sig), 'SV_WALLET_INVALID');
\t\t\tsignerKey = address(wallet);
\t\t// {address}{mode}; the spoof mode is used when simulating calls
\t\t} else if (mode == SignatureMode.Spoof && allowSpoofing) {
\t\t\t// This is safe cause it's specifically intended for spoofing sigs in simulation conditions, where tx.origin can be controlled
\t\t\t// We did not choose 0x00..00 because in future network upgrades tx.origin may be nerfed or there may be edge cases in which
\t\t\t// it is zero, such as native account abstraction
\t\t\t// slither-disable-next-line tx-origin
\t\t\trequire(tx.origin == address(1) || tx.origin == address(6969), 'SV_SPOOF_ORIGIN');
\t\t\trequire(sig.length == 33, 'SV_SPOOF_LEN');
\t\t\tsig.trimToSize(32);
\t\t\t// To simulate the gas usage; check is just to silence unused warning
\t\t\trequire(ecrecover(0, 0, 0, 0) != address(6969));
\t\t\tsignerKey = abi.decode(sig, (address));
\t\t} else {
\t\t\trevert('SV_TYPE');
\t\t}
\t\trequire(signerKey != address(0), 'SV_ZERO_SIG');
\t\treturn (signerKey, isUnprotected);
\t}
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
// Transaction structure
// we handle replay protection separately by requiring (address(this), chainID, nonce) as part of the sig
// @dev a better name for this would be `Call`, but we are keeping `Transaction` for backwards compatibility
struct Transaction {
address to;
uint256 value;
bytes data;
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;
import "./PackedUserOperation.sol";
/**
* the interface exposed by a paymaster contract, who agrees to pay the gas for user's operations.
* a paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction.
*/
interface IPaymaster {
enum PostOpMode {
opSucceeded, // user op succeeded
opReverted, // user op reverted. still has to pay for gas.
postOpReverted //user op succeeded, but caused postOp to revert. Now it's a 2nd call, after user's op was deliberately reverted.
}
/**
* payment validation: check if paymaster agrees to pay.
* Must verify sender is the entryPoint.
* Revert to reject this request.
* Note that bundlers will reject this method if it changes the state, unless the paymaster is trusted (whitelisted)
* The paymaster pre-pays using its deposit, and receive back a refund after the postOp method returns.
* @param userOp the user operation
* @param userOpHash hash of the user's request data.
* @param maxCost the maximum cost of this transaction (based on maximum gas and gas price from userOp)
* @return context value to send to a postOp
* zero length to signify postOp is not required.
* @return validationData signature and time-range of this operation, encoded the same as the return value of validateUserOperation
* <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure,
* otherwise, an address of an "authorizer" contract.
* <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite"
* <6-byte> validAfter - first timestamp this operation is valid
* Note that the validation code cannot use block.timestamp (or block.number) directly.
*/
function validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost)
external returns (bytes memory context, uint256 validationData);
/**
* post-operation handler.
* Must verify sender is the entryPoint
* @param mode enum with the following options:
* opSucceeded - user operation succeeded.
* opReverted - user op reverted. still has to pay for gas.
* postOpReverted - user op succeeded, but caused postOp (in mode=opSucceeded) to revert.
* Now this is the 2nd call, after user's op was deliberately reverted.
* @param context - the context value returned by validatePaymasterUserOp
* @param actualGasCost - actual gas used so far (without this postOp call).
*/
function postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) external;
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
/**
* User Operation struct
* @param sender - The sender account of this request.
* @param nonce - Unique value the sender uses to verify it is not a replay.
* @param initCode - If set, the account contract will be created by this constructor/
* @param callData - The method call to execute on this account.
* @param accountGasLimits - Packed gas limits for validateUserOp and gas limit passed to the callData method call.
* @param preVerificationGas - Gas not calculated by the handleOps method, but added to the gas paid.
* Covers batch overhead.
* @param gasFees - packed gas fields maxPriorityFeePerGas and maxFeePerGas - Same as EIP-1559 gas parameters.
* @param paymasterAndData - If set, this field holds the paymaster address, verification gas limit, postOp gas limit and paymaster-specific extra data
* The paymaster will pay for the transaction instead of the sender.
* @param signature - Sender-verified signature over the entire request, the EntryPoint address and the chain ID.
*/
struct PackedUserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
// callGasLimit + verificationGasLimit
bytes32 accountGasLimits;
uint256 preVerificationGas;
// maxFeePerGas + maxPriorityFeePerGas
bytes32 gasFees;
bytes paymasterAndData;
bytes signature;
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
library UserOpHelper {
\tuint256 public constant PAYMASTER_ADDR_OFFSET = 20;
// 52 = 20 address + 16 paymasterVerificationGasLimit + 16 paymasterPostOpGasLimit
\tuint256 public constant PAYMASTER_DATA_OFFSET = 52;
}
File 2 of 2: AmbireAccount
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
import './libs/SignatureValidator.sol';
import './ExternalSigValidator.sol';
import './libs/erc4337/PackedUserOperation.sol';
import './libs/erc4337/UserOpHelper.sol';
import './deployless/IAmbireAccount.sol';
/**
* @notice A validator that performs DKIM signature recovery
* @dev All external/public functions (that are not view/pure) use `payable` because AmbireAccount
* is a wallet contract, and any ETH sent to it is not lost, but on the other hand not having `payable`
* makes the Solidity compiler add an extra check for `msg.value`, which in this case is wasted gas
*/
contract AmbireAccount is IAmbireAccount {
\t// @dev We do not have a constructor. This contract cannot be initialized with any valid `privileges` by itself!
\t// The intended use case is to deploy one base implementation contract, and create a minimal proxy for each user wallet, by
\t// using our own code generation to insert SSTOREs to initialize `privileges` (it was previously called IdentityProxyDeploy.js, now src/libs/proxyDeploy/deploy.ts)
\taddress private constant FALLBACK_HANDLER_SLOT = address(0x6969);
\t// @dev This is how we understand if msg.sender is the entry point
\tbytes32 private constant ENTRY_POINT_MARKER = 0x0000000000000000000000000000000000000000000000000000000000007171;
\t// Externally validated signatures
\tuint8 private constant SIGMODE_EXTERNALLY_VALIDATED = 255;
\t// Variables
\tmapping(address => bytes32) public privileges;
\tuint256 public nonce;
\t// Events
\tevent LogPrivilegeChanged(address indexed addr, bytes32 priv);
\tevent LogErr(address indexed to, uint256 value, bytes data, bytes returnData); // only used in tryCatch
\t// This contract can accept ETH without calldata
\treceive() external payable {}
\t/**
\t * @dev To support EIP 721 and EIP 1155, we need to respond to those methods with their own method signature
\t * @return bytes4 onERC721Received function selector
\t */
\tfunction onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
\t\treturn this.onERC721Received.selector;
\t}
\t/**
\t * @dev To support EIP 721 and EIP 1155, we need to respond to those methods with their own method signature
\t * @return bytes4 onERC1155Received function selector
\t */
\tfunction onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) {
\t\treturn this.onERC1155Received.selector;
\t}
\t/**
\t * @dev To support EIP 721 and EIP 1155, we need to respond to those methods with their own method signature
\t * @return bytes4 onERC1155Received function selector
\t */
\tfunction onERC1155BatchReceived(
\t\taddress,
\t\taddress,
\t\tuint256[] calldata,
\t\tuint256[] calldata,
\t\tbytes calldata
\t) external pure returns (bytes4) {
\t\treturn this.onERC1155BatchReceived.selector;
\t}
\t/**
\t * @notice fallback method: currently used to call the fallback handler
\t * which is set by the user and can be changed
\t * @dev this contract can accept ETH with calldata, hence payable
\t */
\tfallback() external payable {
\t\t// We store the fallback handler at this magic slot
\t\taddress fallbackHandler = address(uint160(uint(privileges[FALLBACK_HANDLER_SLOT])));
\t\tif (fallbackHandler == address(0)) return;
\t\tassembly {
\t\t\t// we can use addr 0 because logic is taking full control of the
\t\t\t// execution making sure it returns itself and does not
\t\t\t// rely on any further Solidity code.
\t\t\tcalldatacopy(0, 0, calldatasize())
\t\t\tlet result := delegatecall(gas(), fallbackHandler, 0, calldatasize(), 0, 0)
\t\t\tlet size := returndatasize()
\t\t\treturndatacopy(0, 0, size)
\t\t\tif eq(result, 0) {
\t\t\t\trevert(0, size)
\t\t\t}
\t\t\treturn(0, size)
\t\t}
\t}
\t/**
\t * @notice used to set the privilege of a key (by `addr`)
\t * @dev normal signatures will be considered valid if the
\t * `addr` they are signed with has non-zero (not 0x000..000) privilege set; we can set the privilege to
\t * a hash of the recovery keys and timelock (see `RecoveryInfo`) to enable recovery signatures
\t * @param addr the address to give privs to
\t * @param priv the privs to give
\t */
\tfunction setAddrPrivilege(address addr, bytes32 priv) external payable {
\t\trequire(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
\t\tprivileges[addr] = priv;
\t\temit LogPrivilegeChanged(addr, priv);
\t}
\t/**
\t * @notice Useful when we need to do multiple operations but ignore failures in some of them
\t * @param to address we're sending value to
\t * @param value the amount
\t * @param data callData
\t */
\tfunction tryCatch(address to, uint256 value, bytes calldata data) external payable {
\t\trequire(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
\t\tuint256 gasBefore = gasleft();
\t\t(bool success, bytes memory returnData) = to.call{ value: value, gas: gasBefore }(data);
\t\trequire(gasleft() > gasBefore / 64, 'TRYCATCH_OOG');
\t\tif (!success) emit LogErr(to, value, data, returnData);
\t}
\t/**
\t * @notice same as `tryCatch` but with a gas limit
\t * @param to address we're sending value to
\t * @param value the amount
\t * @param data callData
\t * @param gasLimit how much gas is allowed
\t */
\tfunction tryCatchLimit(address to, uint256 value, bytes calldata data, uint256 gasLimit) external payable {
\t\trequire(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
\t\tuint256 gasBefore = gasleft();
\t\t(bool success, bytes memory returnData) = to.call{ value: value, gas: gasLimit }(data);
\t\trequire(gasleft() > gasBefore / 64, 'TRYCATCH_OOG');
\t\tif (!success) emit LogErr(to, value, data, returnData);
\t}
\t/**
\t * @notice execute: this method is used to execute a single bundle of calls that are signed with a key
\t * that is authorized to execute on this account (in `privileges`)
\t * @dev WARNING: if the signature of this is changed, we have to change AmbireAccountFactory
\t * @param calls the transaction we're executing. They may not execute
\t * if specific cases. One such is when setting a timelock
\t * @param signature the signature for the transactions
\t */
\tfunction execute(Transaction[] calldata calls, bytes calldata signature) public payable {
\t\taddress signerKey;
\t\tuint8 sigMode = uint8(signature[signature.length - 1]);
\t\tuint256 currentNonce = nonce;
\t\t// we increment the nonce here (not using `nonce++` to save some gas)
\t\tnonce = currentNonce + 1;
\t\tif (sigMode == SIGMODE_EXTERNALLY_VALIDATED) {
\t\t\tbool isValidSig;
\t\t\tuint256 timestampValidAfter;
\t\t\t(signerKey, isValidSig, timestampValidAfter) = validateExternalSig(calls, signature);
\t\t\tif (!isValidSig) {
\t\t\t\trequire(block.timestamp >= timestampValidAfter, 'SIGNATURE_VALIDATION_TIMELOCK');
\t\t\t\trevert('SIGNATURE_VALIDATION_FAIL');
\t\t\t}
\t\t} else {
\t\t\tsignerKey = SignatureValidator.recoverAddr(
\t\t\t\tkeccak256(abi.encode(address(this), block.chainid, currentNonce, calls)),
\t\t\t\tsignature,
\t\t\t\ttrue
\t\t\t);
\t\t\trequire(privileges[signerKey] != bytes32(0), 'INSUFFICIENT_PRIVILEGE');
\t\t}
\t\texecuteBatch(calls);
\t\t// The actual anti-bricking mechanism - do not allow a signerKey to drop their own privileges
\t\trequire(privileges[signerKey] != bytes32(0), 'PRIVILEGE_NOT_DOWNGRADED');
\t}
\t/**
\t * @notice allows executing multiple bundles of calls (batch together multiple executes)
\t * @param toExec an array of execute function parameters
\t */
\tfunction executeMultiple(ExecuteArgs[] calldata toExec) external payable {
\t\tfor (uint256 i = 0; i != toExec.length; i++) execute(toExec[i].calls, toExec[i].signature);
\t}
\t/**
\t * @notice Allows executing calls if the caller itself is authorized
\t * @dev no need for nonce management here cause we're not dealing with sigs
\t * @param calls the transaction we're executing
\t */
\tfunction executeBySender(Transaction[] calldata calls) external payable {
\t\trequire(privileges[msg.sender] != bytes32(0), 'INSUFFICIENT_PRIVILEGE');
\t\texecuteBatch(calls);
\t\t// again, anti-bricking
\t\trequire(privileges[msg.sender] != bytes32(0), 'PRIVILEGE_NOT_DOWNGRADED');
\t}
\t/**
\t * @notice allows the contract itself to execute a batch of calls
\t * self-calling is useful in cases like wanting to do multiple things in a tryCatchLimit
\t * @param calls the calls we're executing
\t */
\tfunction executeBySelf(Transaction[] calldata calls) external payable {
\t\trequire(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
\t\texecuteBatch(calls);
\t}
\t/**
\t * @notice allows the contract itself to execute a single calls
\t * self-calling is useful when you want to workaround the executeBatch()
\t * protection of not being able to call address(0)
\t * @param call the call we're executing
\t */
\tfunction executeBySelfSingle(Transaction calldata call) external payable {
\t\trequire(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
\t\texecuteCall(call.to, call.value, call.data);
\t}
\t/**
\t * @notice Execute a batch of transactions
\t * @param calls the transaction we're executing
\t */
\tfunction executeBatch(Transaction[] memory calls) internal {
\t\tuint256 len = calls.length;
\t\tfor (uint256 i = 0; i < len; i++) {
\t\t\tTransaction memory call = calls[i];
\t\t\tif (call.to != address(0)) executeCall(call.to, call.value, call.data);
\t\t}
\t}
\t/**
\t * @notice Execute a signle transaction
\t * @dev we shouldn't use address.call(), cause: https://github.com/ethereum/solidity/issues/2884
\t * @param to the address we're sending to
\t * @param value the amount we're sending
\t * @param data callData
\t */
\tfunction executeCall(address to, uint256 value, bytes memory data) internal {
\t\tassembly {
\t\t\tlet result := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0)
\t\t\tif eq(result, 0) {
\t\t\t\tlet size := returndatasize()
\t\t\t\tlet ptr := mload(0x40)
\t\t\t\treturndatacopy(ptr, 0, size)
\t\t\t\trevert(ptr, size)
\t\t\t}
\t\t}
\t}
\t/**
\t * @notice EIP-1271 implementation
\t * @dev see https://eips.ethereum.org/EIPS/eip-1271
\t * @param hash the signed hash
\t * @param signature the signature for the signed hash
\t * @return bytes4 is it a success or a failure
\t */
\tfunction isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
\t\t(address recovered, bool usedUnprotected) = SignatureValidator.recoverAddrAllowUnprotected(hash, signature, false);
\t\tif (uint256(privileges[recovered]) > (usedUnprotected ? 1 : 0)) {
\t\t\t// bytes4(keccak256("isValidSignature(bytes32,bytes)")
\t\t\treturn 0x1626ba7e;
\t\t} else {
\t\t\treturn 0xffffffff;
\t\t}
\t}
\t/**
\t * @notice EIP-1155 implementation
\t * we pretty much only need to signal that we support the interface for 165, but for 1155 we also need the fallback function
\t * @param interfaceID the interface we're signaling support for
\t * @return bool do we support the interface or not
\t */
\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool) {
\t\tbool supported = interfaceID == 0x01ffc9a7 || // ERC-165 support (i.e. `bytes4(keccak256('supportsInterface(bytes4)'))`).
\t\t\tinterfaceID == 0x150b7a02 || // ERC721TokenReceiver
\t\t\tinterfaceID == 0x4e2312e0 || // ERC-1155 `ERC1155TokenReceiver` support (i.e. `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) ^ bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`).
\t\t\tinterfaceID == 0x0a417632; // used for checking whether the account is v2 or not
\t\tif (supported) return true;
\t\taddress payable fallbackHandler = payable(address(uint160(uint256(privileges[FALLBACK_HANDLER_SLOT]))));
\t\tif (fallbackHandler == address(0)) return false;
\t\treturn AmbireAccount(fallbackHandler).supportsInterface(interfaceID);
\t}
\t//
\t// EIP-4337 implementation
\t//
\t// return value in case of signature failure, with no time-range.
\t// equivalent to packSigTimeRange(true,0,0);
\tuint256 constant internal SIG_VALIDATION_FAILED = 1;
\t// equivalent to packSigTimeRange(false,0,0);
\tuint256 constant internal SIG_VALIDATION_SUCCESS = 0;
\t/**
\t * @notice EIP-4337 implementation
\t * @dev We have an edge case for enabling ERC-4337 in the first if statement.
\t * If the function call is to execute, we do not perform an userOp sig validation.
\t * We require a one time hash nonce commitment from the paymaster for the given
\t * req. We use this to give permissions to the entry point on the fly
\t * and enable ERC-4337
\t * @param op the PackedUserOperation we're executing
\t * @param userOpHash the hash we've committed to
\t * @param missingAccountFunds the funds the account needs to pay
\t * @return uint256 0 for success, 1 for signature failure, and a uint256
\t * packed timestamp for a future valid signature:
\t * address aggregator, uint48 validUntil, uint48 validAfter
\t */
\tfunction validateUserOp(PackedUserOperation calldata op, bytes32 userOpHash, uint256 missingAccountFunds)
\texternal payable returns (uint256)
\t{
\t\t// enable running executeMultiple operation through the entryPoint if
\t\t// a paymaster sponsors it with a commitment one-time nonce.
\t\t// two use cases:
\t\t// 1) enable 4337 on a network by giving privileges to the entryPoint
\t\t// 2) key recovery. If the key is lost, we cannot sign the userOp,
\t\t// so we have to go to `execute` to trigger the recovery logic
\t\t// Why executeMultiple but not execute?
\t\t// executeMultiple allows us to combine recovery + fee payment calls.
\t\t// The fee payment call will be with a signature from the new key
\t\tif (op.callData.length >= 4 && bytes4(op.callData[0:4]) == this.executeMultiple.selector) {
\t\t\t// Require a paymaster, otherwise this mode can be used by anyone to get the user to spend their deposit
\t\t\t// @estimation-no-revert
\t\t\tif (op.signature.length != 0) return SIG_VALIDATION_FAILED;
\t\t\trequire(
\t\t\t\top.paymasterAndData.length >= UserOpHelper.PAYMASTER_DATA_OFFSET &&
\t\t\t\tbytes20(op.paymasterAndData[:UserOpHelper.PAYMASTER_ADDR_OFFSET]) != bytes20(0),
\t\t\t\t'validateUserOp: paymaster required in execute() mode'
\t\t\t);
\t\t\t// hashing in everything except sender (nonces are scoped by sender anyway), nonce, signature
\t\t\tuint256 targetNonce = uint256(keccak256(
\t\t\t\tabi.encode(op.initCode, op.callData, op.accountGasLimits, op.preVerificationGas, op.gasFees, op.paymasterAndData)
\t\t\t)) << 64;
\t\t\t// @estimation-no-revert
\t\t\tif (op.nonce != targetNonce) return SIG_VALIDATION_FAILED;
\t\t\treturn SIG_VALIDATION_SUCCESS;
\t\t}
\t\trequire(privileges[msg.sender] == ENTRY_POINT_MARKER, 'validateUserOp: not from entryPoint');
\t\t// @estimation
\t\t// paying should happen even if signature validation fails
\t\tif (missingAccountFunds > 0) {
\t\t\t// NOTE: MAY pay more than the minimum, to deposit for future transactions
\t\t\t(bool success,) = msg.sender.call{value : missingAccountFunds}('');
\t\t\t// ignore failure (its EntryPoint's job to verify, not account.)
\t\t\t(success);
\t\t}
\t\t// this is replay-safe because userOpHash is retrieved like this: keccak256(abi.encode(userOp.hash(), address(this), block.chainid))
\t\taddress signer = SignatureValidator.recoverAddr(userOpHash, op.signature, true);
\t\tif (privileges[signer] == bytes32(0)) return SIG_VALIDATION_FAILED;
\t\treturn SIG_VALIDATION_SUCCESS;
\t}
\tfunction validateExternalSig(Transaction[] memory calls, bytes calldata signature)
\tinternal returns(address signerKey, bool isValidSig, uint256 timestampValidAfter) {
\t\t(bytes memory sig, ) = SignatureValidator.splitSignature(signature);
\t\t// the address of the validator we're using for this validation
\t\taddress validatorAddr;
\t\t// all the data needed by the validator to execute the validation.
\t\t// In the case of DKIMRecoverySigValidator, this is AccInfo:
\t\t// abi.encode {string emailFrom; string emailTo; string domainName;
\t\t// bytes dkimPubKeyModulus; bytes dkimPubKeyExponent; address secondaryKey;
\t\t// bool acceptUnknownSelectors; uint32 waitUntilAcceptAdded;
\t\t// uint32 waitUntilAcceptRemoved; bool acceptEmptyDKIMSig;
\t\t// bool acceptEmptySecondSig;uint32 onlyOneSigTimelock;}
\t\t// The struct is declared in DKIMRecoverySigValidator
\t\tbytes memory validatorData;
\t\t// the signature data needed by the external validator.
\t\t// In the case of DKIMRecoverySigValidator, this is abi.encode(
\t\t// SignatureMeta memory sigMeta, bytes memory dkimSig, bytes memory secondSig
\t\t// ).
\t\tbytes memory innerSig;
\t\t// the signerKey in this case is an arbitrary value that does
\t\t// not have any specific purpose other than representing
\t\t// the privileges key
\t\t(signerKey, validatorAddr, validatorData, innerSig) = abi.decode(sig, (address, address, bytes, bytes));
\t\trequire(
\t\t\tprivileges[signerKey] == keccak256(abi.encode(validatorAddr, validatorData)),
\t\t\t'EXTERNAL_VALIDATION_NOT_SET'
\t\t);
\t\t// The sig validator itself should throw when a signature isn't validated successfully
\t\t// the return value just indicates whether we want to execute the current calls
\t\t(isValidSig, timestampValidAfter) = ExternalSigValidator(validatorAddr).validateSig(validatorData, innerSig, calls);
\t}
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
import './deployless/IAmbireAccount.sol';
import './libs/Transaction.sol';
/**
* @notice A contract used for deploying AmbireAccount.sol
* @dev We use create2 to get the AmbireAccount address. It's deterministic:
* if the same data is passed to it, the same address will pop out.
*/
contract AmbireFactory {
\tevent LogDeployed(address addr, uint256 salt);
\taddress public immutable allowedToDrain;
\tconstructor(address allowed) {
\t\tallowedToDrain = allowed;
\t}
\t/**
\t * @notice Allows anyone to deploy any contracft with a specific code/salt
\t * @dev This is safe because it's CREATE2 deployment
\t * @param code the code to be deployed
\t * @param salt the salt to shuffle the computed address
\t * @return address the deployed address
\t */
\tfunction deploy(bytes calldata code, uint256 salt) external returns(address) {
\t\treturn deploySafe(code, salt);
\t}
\t
\t/**
\t * @notice Call this when you want to deploy the contract and execute calls
\t * @dev When the relayer needs to act upon an /identity/:addr/submit call, it'll either call execute on the AmbireAccount directly
\t * if it's already deployed, or call `deployAndExecute` if the account is still counterfactual
\t * we can't have deployAndExecuteBySender, because the sender will be the factory
\t * @param code the code to be deployed
\t * @param salt the salt to shuffle the computed address
\t * @param txns the txns the are going to be executed
\t * @param signature the signature for the txns
\t * @return address the deployed address
\t */
\tfunction deployAndExecute(
\t\tbytes calldata code,
\t\tuint256 salt,
\t\tTransaction[] calldata txns,
\t\tbytes calldata signature
\t) external returns (address){
\t\taddress payable addr = payable(deploySafe(code, salt));
\t\tIAmbireAccount(addr).execute(txns, signature);
\t\treturn addr;
\t}
\t
\t/**
\t * @notice Call this when you want to deploy the contract and call executeMultiple
\t * @dev when the relayer needs to act upon an /identity/:addr/submit call,
\t * it'll either call execute on the AmbireAccount directly. If it's already
\t * deployed, or call `deployAndExecuteMultiple` if the account is still
\t * counterfactual but there are multiple accountOps to send
\t * @param code the code to be deployed
\t * @param salt the salt to shuffle the computed address
\t * @param toExec [txns, signature] execute parameters
\t * @return address the deployed address
\t */
\tfunction deployAndExecuteMultiple(
\t\tbytes calldata code,
\t\tuint256 salt,
\t\tIAmbireAccount.ExecuteArgs[] calldata toExec
\t) external returns (address){
\t\taddress payable addr = payable(deploySafe(code, salt));
\t\tIAmbireAccount(addr).executeMultiple(toExec);
\t\treturn addr;
\t}
\t/**
\t * @notice This method can be used to withdraw stuck tokens or airdrops
\t * @dev Only allowedToDrain can do the call
\t * @param to receiver
\t * @param value how much to be sent
\t * @param data if a token has airdropped, code to send it
\t * @param gas maximum gas willing to spend
\t */
\tfunction call(address to, uint256 value, bytes calldata data, uint256 gas) external {
\t\trequire(msg.sender == allowedToDrain, 'ONLY_AUTHORIZED');
\t\t(bool success, bytes memory err) = to.call{ gas: gas, value: value }(data);
\t\trequire(success, string(err));
\t}
\t
\t/**
\t * @dev This is done to mitigate possible frontruns where, for example,
\t * where deploying the same code/salt via deploy() would make a pending
\t * deployAndExecute fail. The way we mitigate that is by checking if the
\t * contract is already deployed and if so, we continue execution
\t * @param code the code to be deployed
\t * @param salt the salt to shuffle the computed address
\t * @return address the deployed address
\t */
\tfunction deploySafe(bytes memory code, uint256 salt) internal returns (address) {
\t\taddress expectedAddr = address(
\t\t\tuint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(code)))))
\t\t);
\t\tuint256 size;
\t\tassembly {
\t\t\tsize := extcodesize(expectedAddr)
\t\t}
\t\t// If there is code at that address, we can assume it's the one we were about to deploy,
\t\t// because of how CREATE2 and keccak256 works
\t\tif (size == 0) {
\t\t\taddress addr;
\t\t\tassembly {
\t\t\t\taddr := create2(0, add(code, 0x20), mload(code), salt)
\t\t\t}
\t\t\trequire(addr != address(0), 'FAILED_DEPLOYING');
\t\t\trequire(addr == expectedAddr, 'FAILED_MATCH');
\t\t\temit LogDeployed(addr, salt);
\t\t}
\t\treturn expectedAddr;
\t}
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
import './deployless/IAmbireAccount.sol';
import './libs/erc4337/IPaymaster.sol';
import './libs/SignatureValidator.sol';
import './libs/erc4337/UserOpHelper.sol';
contract AmbirePaymaster is IPaymaster {
\taddress immutable public relayer;
\tconstructor(address _relayer) {
\t\trelayer = _relayer;
\t}
\t/**
\t * @notice This method can be used to withdraw stuck tokens or airdrops
\t *
\t * @param to The address we're calling
\t * @param value The value in the call
\t * @param\tdata\tthe call data
\t * @param\tgas\tthe call gas
\t */
\tfunction call(address to, uint256 value, bytes calldata data, uint256 gas) external payable {
\t\trequire(msg.sender == relayer, 'call: not relayer');
\t\t(bool success, bytes memory err) = to.call{ gas: gas, value: value }(data);
\t\trequire(success, string(err));
\t}
\t/**
\t * @notice Validate user operations the paymaster has signed
\t * We do not need to send funds to the EntryPoint because we rely on pre-existing deposit.
\t * Requests are chain specific to prevent signature reuse.
\t * @dev We have two use cases for the paymaster:
\t * - normal erc-4337. Everything is per ERC-4337 standard, the nonce is sequential.
\t * - an executeMultiple call. If the calldata is executeMultiple, we've hardcoded
\t * a 0 nonce. That's what's called a one-time hash nonce and its key is actually
\t * the commitment. Check EntryPoint -> NonceManager for more information.
\t *
\t * @param userOp the UserOperation we're executing
\t * @return context context is returned in the postOp and called by the
\t * EntryPoint. But we're not using postOp is context is always emtpy
\t * @return validationData This consists of:
\t * - an aggregator address: address(uint160(validationData)). This is used
\t * when you want an outer contract to determine whether the signature is valid.
\t * In our case, this is always 0 (address 0) for valid signatures and
\t * 1 (address 1) for invalid. This is what the entry point expects and
\t * in those two cases, an outer contract is obviously not called.
\t * - a uint48 validUntil: uint48(validationData >> 160)
\t * A Paymaster signature can be signed at time "x" but delayed intentionally
\t * until time "y" when a fee payment's price has dropped significantly or
\t * some other issue. validUntil sets a time validity for the signature
* - a uint48 validAfter: uint48(validationData >> (48 + 160))
\t * If the signature should be valid only after a period of time,
\t * we tweak the validAfter property.
\t * For more information, check EntryPoint -> _getValidationData()
\t */
\tfunction validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32, uint256)
\t\texternal
\t\tview
\t\treturns (bytes memory context, uint256 validationData)
\t{
\t\t(uint48 validUntil, uint48 validAfter, bytes memory signature) = abi.decode(
\t\t\tuserOp.paymasterAndData[UserOpHelper.PAYMASTER_DATA_OFFSET:],
\t\t\t(uint48, uint48, bytes)
\t\t);
\t\tbytes memory callData = userOp.callData;
\t\tbytes32 hash = keccak256(abi.encode(
\t\t\tblock.chainid,
\t\t\taddress(this),
\t\t\t// entry point
\t\t\tmsg.sender,
\t\t\tvalidUntil,
\t\t\tvalidAfter,
\t\t\t// everything except paymasterAndData and signature
\t\t\tuserOp.sender,
\t\t\t// for the nonce we have an exception case: one-time nonces depend on paymasterAndData, which is generated by the relayer
\t\t\t// we can't have this as part of the sig cuz we create a cyclical dep
\t\t\t// the nonce can only be used once, so one cannot replay the gas payment
\t\t\tcallData.length >= 4 && bytes4(userOp.callData[0:4]) == IAmbireAccount.executeMultiple.selector ? 0 : userOp.nonce,
\t\t\tuserOp.initCode,
\t\t\tcallData,
\t\t\tuserOp.accountGasLimits,
\t\t\tuserOp.preVerificationGas,
\t\t\tuserOp.gasFees
\t\t));
\t\t(address recovered, ) = SignatureValidator.recoverAddrAllowUnprotected(hash, signature, true);
\t\tbool isValidSig = recovered == relayer;
\t\t// see _packValidationData: https://github.com/eth-infinitism/account-abstraction/blob/f2b09e60a92d5b3177c68d9f382912ccac19e8db/contracts/core/Helpers.sol#L73-L80
\t\treturn ("", uint160(isValidSig ? 0 : 1) | (uint256(validUntil) << 160) | (uint256(validAfter) << 208));
\t}
\t/**
\t * @notice No-op, won't be used because we don't return a context
\t * @param mode .
\t * @param context .
\t * @param actualGasCost .
\t */
\tfunction postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) external {
\t\t// No-op, won't be used because we don't return a context
\t}
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
import './libs/Transaction.sol';
/**
* @title ExternalSigValidator
* @notice A way to add custom recovery to AmbireAccount.
* address accountAddr is the Ambire account address
* bytes calldata data is all the data needed by the ExternalSigValidator.
* It could be anything and it's validator specific.
* bytes calldata sig is the signature we're validating. Notice its not
* bytes32 so there could be cases where its not only the signature. It's
* validator specific
* uint256 nonce - the Ambire account nonce
* Transaction[] calldata calls - the txns that are going to be executed
* if the validation is successful
* @dev Not all passed properties necessarily need to be used.
*/
abstract contract ExternalSigValidator {
\tfunction validateSig(
\t\tbytes calldata data,
\t\tbytes calldata sig,
\t\tTransaction[] calldata calls
\t) external virtual returns (bool isValidSignature, uint256 timestampValidAfter);
}// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.7;
import '../libs/Transaction.sol';
interface IAmbireAccount {
\tfunction privileges(address addr) external returns (bytes32);
\tfunction nonce() external returns (uint);
\tstruct RecoveryInfo {
\t\taddress[] keys;
\t\tuint timelock;
\t}
\tstruct ExecuteArgs {
\t\tTransaction[] calls;
\t\tbytes signature;
\t}
\tfunction setAddrPrivilege(address addr, bytes32 priv) external payable;
\tfunction tryCatch(address to, uint value, bytes calldata data) external payable;
\tfunction tryCatchLimit(address to, uint value, bytes calldata data, uint gasLimit) external payable;
\tfunction execute(Transaction[] calldata txns, bytes calldata signature) external payable;
\tfunction executeBySender(Transaction[] calldata txns) external payable;
\tfunction executeBySelf(Transaction[] calldata txns) external payable;
\tfunction executeMultiple(ExecuteArgs[] calldata toExec) external payable;
\t// EIP 1271 implementation
\t// see https://eips.ethereum.org/EIPS/eip-1271
\tfunction isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4);
\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool);
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
library Bytes {
\tfunction trimToSize(bytes memory b, uint256 newLen) internal pure {
\t\trequire(b.length > newLen, 'BytesLib: only shrinking');
\t\tassembly {
\t\t\tmstore(b, newLen)
\t\t}
\t}
\t/***********************************|
\t| Read Bytes Functions |
\t|__________________________________*/
\t/**
\t * @dev Reads a bytes32 value from a position in a byte array.
\t * @param b Byte array containing a bytes32 value.
\t * @param index Index in byte array of bytes32 value.
\t * @return result bytes32 value from byte array.
\t */
\tfunction readBytes32(bytes memory b, uint256 index) internal pure returns (bytes32 result) {
\t\t// Arrays are prefixed by a 256 bit length parameter
\t\tindex += 32;
\t\trequire(b.length >= index, 'BytesLib: length');
\t\t// Read the bytes32 from array memory
\t\tassembly {
\t\t\tresult := mload(add(b, index))
\t\t}
\t\treturn result;
\t}
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
import './Bytes.sol';
interface IERC1271Wallet {
\tfunction isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue);
}
library SignatureValidator {
\tusing Bytes for bytes;
\tenum SignatureMode {
\t\t// the first mode Unprotected is used in combination with EIP-1271 signature verification to do
\t\t// EIP-712 verifications, as well as "Ethereum signed message:" message verifications
\t\t// The caveat with this is that we need to ensure that the signer key used for it isn't reused, or the message body
\t\t// itself contains context about the wallet (such as it's address)
\t\t// We do this, rather than applying the prefix on-chain, because if we do you won't be able to see the message
\t\t// when signing on a hardware wallet (you'll only see the hash) - since `isValidSignature` can only receive the hash -
\t\t// if the prefix is applied on-chain you can never match it - it's hash(prefix+hash(msg)) vs hash(prefix+msg)
\t\t// As for transactions (`execute()`), those can be signed with any of the modes
\t\t// Otherwise, if it's reused, we MUST use `Standard` mode which always wraps the final digest hash, but unfortnately this means
\t\t// you can't preview the full message when signing on a HW wallet
\t\tUnprotected,
\t\tStandard,
\t\tSmartWallet,
\t\tSpoof,
\t\tSchnorr,
\t\tMultisig,
\t\t// WARNING: Signature modes should not be more than 26 as the "v"
\t\t// value for standard ecrecover is 27/28
\t\t// WARNING: must always be last
\t\tLastUnused
\t}
\t// bytes4(keccak256("isValidSignature(bytes32,bytes)"))
\tbytes4 internal constant ERC1271_MAGICVALUE_BYTES32 = 0x1626ba7e;
\t// secp256k1 group order
\tuint256 internal constant Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
\tfunction splitSignature(bytes memory sig) internal pure returns (bytes memory, uint8) {
\t\tuint8 modeRaw;
\t\tunchecked {
\t\t\tmodeRaw = uint8(sig[sig.length - 1]);
\t\t}
\t\tsig.trimToSize(sig.length - 1);
\t\treturn (sig, modeRaw);
\t}
\tfunction recoverAddr(bytes32 hash, bytes memory sig, bool allowSpoofing) internal view returns (address) {
\t\t(address recovered, bool usedUnprotected) = recoverAddrAllowUnprotected(hash, sig, allowSpoofing);
\t\trequire(!usedUnprotected, 'SV_USED_UNBOUND');
\t\treturn recovered;
\t}
\tfunction recoverAddrAllowUnprotected(bytes32 hash, bytes memory sig, bool allowSpoofing) internal view returns (address, bool) {
\t\trequire(sig.length != 0, 'SV_SIGLEN');
\t\tuint8 modeRaw;
\t\tunchecked {
\t\t\tmodeRaw = uint8(sig[sig.length - 1]);
\t\t}
\t\t// Ensure we're in bounds for mode; Solidity does this as well but it will just silently blow up rather than showing a decent error
\t\tif (modeRaw >= uint8(SignatureMode.LastUnused)) {
\t\t\tif (sig.length == 65) modeRaw = uint8(SignatureMode.Unprotected);
\t\t\telse revert('SV_SIGMODE');
\t\t}
\t\tSignatureMode mode = SignatureMode(modeRaw);
\t\t// the address of the key we are gonna be returning
\t\taddress signerKey;
\t\t// wrap in the EIP712 wrapping if it's not unbound
\t\t// multisig gets an exception because each inner sig will have to apply this logic
\t\t// @TODO should spoofing be removed from this?
\t\tbool isUnprotected = mode == SignatureMode.Unprotected || mode == SignatureMode.Multisig;
\t\tif (!isUnprotected) {
\t\t\tbytes32 DOMAIN_SEPARATOR = keccak256(abi.encode(
\t\t\t\tkeccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)'),
\t\t\t\tkeccak256(bytes('Ambire')),
\t\t\t\tkeccak256(bytes('1')),
\t\t\t\tblock.chainid,
\t\t\t\taddress(this),
\t\t\t\tbytes32(0)
\t\t\t));
\t\t\thash = keccak256(abi.encodePacked(
\t\t\t\t'\\x19\\x01',
\t\t\t\tDOMAIN_SEPARATOR,
\t\t\t\tkeccak256(abi.encode(
\t\t\t\t\tkeccak256(bytes('AmbireOperation(address account,bytes32 hash)')),
\t\t\t\t\taddress(this),
\t\t\t\t\thash
\t\t\t\t))
\t\t\t));
\t\t}
\t\t// {r}{s}{v}{mode}
\t\tif (mode == SignatureMode.Unprotected || mode == SignatureMode.Standard) {
\t\t\trequire(sig.length == 65 || sig.length == 66, 'SV_LEN');
\t\t\tbytes32 r = sig.readBytes32(0);
\t\t\tbytes32 s = sig.readBytes32(32);
\t\t\tuint8 v = uint8(sig[64]);
\t\t\tsignerKey = ecrecover(hash, v, r, s);
\t\t// {sig}{verifier}{mode}
\t\t} else if (mode == SignatureMode.Schnorr) {
\t\t\t// Based on https://hackmd.io/@nZ-twauPRISEa6G9zg3XRw/SyjJzSLt9
\t\t\t// You can use this library to produce signatures: https://github.com/borislav-itskov/schnorrkel.js
\t\t\t// px := public key x-coord
\t\t\t// e := schnorr signature challenge
\t\t\t// s := schnorr signature
\t\t\t// parity := public key y-coord parity (27 or 28)
\t\t\t// last uint8 is for the Ambire sig mode - it's ignored
\t\t\tsig.trimToSize(sig.length - 1);
\t\t\t(bytes32 px, bytes32 e, bytes32 s, uint8 parity) = abi.decode(sig, (bytes32, bytes32, bytes32, uint8));
\t\t\t// ecrecover = (m, v, r, s);
\t\t\tbytes32 sp = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
\t\t\tbytes32 ep = bytes32(Q - mulmod(uint256(e), uint256(px), Q));
\t\t\trequire(sp != bytes32(Q));
\t\t\t// the ecrecover precompile implementation checks that the `r` and `s`
\t\t\t// inputs are non-zero (in this case, `px` and `ep`), thus we don't need to
\t\t\t// check if they're zero.
\t\t\taddress R = ecrecover(sp, parity, px, ep);
\t\t\trequire(R != address(0), 'SV_ZERO_SIG');
\t\t\trequire(e == keccak256(abi.encodePacked(R, uint8(parity), px, hash)), 'SV_SCHNORR_FAILED');
\t\t\tsignerKey = address(uint160(uint256(keccak256(abi.encodePacked('SCHNORR', px)))));
\t\t} else if (mode == SignatureMode.Multisig) {
\t\t\tsig.trimToSize(sig.length - 1);
\t\t\tbytes[] memory signatures = abi.decode(sig, (bytes[]));
\t\t\t// since we're in a multisig, we care if any of the inner sigs are unbound
\t\t\tisUnprotected = false;
\t\t\tfor (uint256 i = 0; i != signatures.length; i++) {
\t\t\t\t(address inner, bool isInnerUnprotected) = recoverAddrAllowUnprotected(hash, signatures[i], false);
\t\t\t\tif (isInnerUnprotected) isUnprotected = true;
\t\t\t\tsignerKey = address(
\t\t\t\t\tuint160(uint256(keccak256(abi.encodePacked(signerKey, inner))))
\t\t\t\t);
\t\t\t}
\t\t} else if (mode == SignatureMode.SmartWallet) {
\t\t\t// 32 bytes for the addr, 1 byte for the type = 33
\t\t\trequire(sig.length > 33, 'SV_LEN_WALLET');
\t\t\tuint256 newLen;
\t\t\tunchecked {
\t\t\t\tnewLen = sig.length - 33;
\t\t\t}
\t\t\tIERC1271Wallet wallet = IERC1271Wallet(address(uint160(uint256(sig.readBytes32(newLen)))));
\t\t\tsig.trimToSize(newLen);
\t\t\trequire(ERC1271_MAGICVALUE_BYTES32 == wallet.isValidSignature(hash, sig), 'SV_WALLET_INVALID');
\t\t\tsignerKey = address(wallet);
\t\t// {address}{mode}; the spoof mode is used when simulating calls
\t\t} else if (mode == SignatureMode.Spoof && allowSpoofing) {
\t\t\t// This is safe cause it's specifically intended for spoofing sigs in simulation conditions, where tx.origin can be controlled
\t\t\t// We did not choose 0x00..00 because in future network upgrades tx.origin may be nerfed or there may be edge cases in which
\t\t\t// it is zero, such as native account abstraction
\t\t\t// slither-disable-next-line tx-origin
\t\t\trequire(tx.origin == address(1) || tx.origin == address(6969), 'SV_SPOOF_ORIGIN');
\t\t\trequire(sig.length == 33, 'SV_SPOOF_LEN');
\t\t\tsig.trimToSize(32);
\t\t\t// To simulate the gas usage; check is just to silence unused warning
\t\t\trequire(ecrecover(0, 0, 0, 0) != address(6969));
\t\t\tsignerKey = abi.decode(sig, (address));
\t\t} else {
\t\t\trevert('SV_TYPE');
\t\t}
\t\trequire(signerKey != address(0), 'SV_ZERO_SIG');
\t\treturn (signerKey, isUnprotected);
\t}
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
// Transaction structure
// we handle replay protection separately by requiring (address(this), chainID, nonce) as part of the sig
// @dev a better name for this would be `Call`, but we are keeping `Transaction` for backwards compatibility
struct Transaction {
address to;
uint256 value;
bytes data;
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;
import "./PackedUserOperation.sol";
/**
* the interface exposed by a paymaster contract, who agrees to pay the gas for user's operations.
* a paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction.
*/
interface IPaymaster {
enum PostOpMode {
opSucceeded, // user op succeeded
opReverted, // user op reverted. still has to pay for gas.
postOpReverted //user op succeeded, but caused postOp to revert. Now it's a 2nd call, after user's op was deliberately reverted.
}
/**
* payment validation: check if paymaster agrees to pay.
* Must verify sender is the entryPoint.
* Revert to reject this request.
* Note that bundlers will reject this method if it changes the state, unless the paymaster is trusted (whitelisted)
* The paymaster pre-pays using its deposit, and receive back a refund after the postOp method returns.
* @param userOp the user operation
* @param userOpHash hash of the user's request data.
* @param maxCost the maximum cost of this transaction (based on maximum gas and gas price from userOp)
* @return context value to send to a postOp
* zero length to signify postOp is not required.
* @return validationData signature and time-range of this operation, encoded the same as the return value of validateUserOperation
* <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure,
* otherwise, an address of an "authorizer" contract.
* <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite"
* <6-byte> validAfter - first timestamp this operation is valid
* Note that the validation code cannot use block.timestamp (or block.number) directly.
*/
function validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost)
external returns (bytes memory context, uint256 validationData);
/**
* post-operation handler.
* Must verify sender is the entryPoint
* @param mode enum with the following options:
* opSucceeded - user operation succeeded.
* opReverted - user op reverted. still has to pay for gas.
* postOpReverted - user op succeeded, but caused postOp (in mode=opSucceeded) to revert.
* Now this is the 2nd call, after user's op was deliberately reverted.
* @param context - the context value returned by validatePaymasterUserOp
* @param actualGasCost - actual gas used so far (without this postOp call).
*/
function postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) external;
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
/**
* User Operation struct
* @param sender - The sender account of this request.
* @param nonce - Unique value the sender uses to verify it is not a replay.
* @param initCode - If set, the account contract will be created by this constructor/
* @param callData - The method call to execute on this account.
* @param accountGasLimits - Packed gas limits for validateUserOp and gas limit passed to the callData method call.
* @param preVerificationGas - Gas not calculated by the handleOps method, but added to the gas paid.
* Covers batch overhead.
* @param gasFees - packed gas fields maxPriorityFeePerGas and maxFeePerGas - Same as EIP-1559 gas parameters.
* @param paymasterAndData - If set, this field holds the paymaster address, verification gas limit, postOp gas limit and paymaster-specific extra data
* The paymaster will pay for the transaction instead of the sender.
* @param signature - Sender-verified signature over the entire request, the EntryPoint address and the chain ID.
*/
struct PackedUserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
// callGasLimit + verificationGasLimit
bytes32 accountGasLimits;
uint256 preVerificationGas;
// maxFeePerGas + maxPriorityFeePerGas
bytes32 gasFees;
bytes paymasterAndData;
bytes signature;
}
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
library UserOpHelper {
\tuint256 public constant PAYMASTER_ADDR_OFFSET = 20;
// 52 = 20 address + 16 paymasterVerificationGasLimit + 16 paymasterPostOpGasLimit
\tuint256 public constant PAYMASTER_DATA_OFFSET = 52;
}