| Transaction Hash |
|
Block
|
From
|
To
|
|||||
|---|---|---|---|---|---|---|---|---|---|
Latest 1 internal transaction
| Parent Transaction Hash | Block | From | To | |||
|---|---|---|---|---|---|---|
| 41637314 | 161 days ago | Contract Creation | 0 XDAI |
Loading...
Loading
Minimal Proxy Contract for 0x0f2aa7bcda3d9d210df69a394b6965cb2566c828
Contract Name:
AmbireAccount
Compiler Version
v0.8.19+commit.7dd6d404
Contract Source Code (Solidity Standard Json-Input format)
// 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 {
// @dev We do not have a constructor. This contract cannot be initialized with any valid `privileges` by itself!
// The intended use case is to deploy one base implementation contract, and create a minimal proxy for each user wallet, by
// using our own code generation to insert SSTOREs to initialize `privileges` (it was previously called IdentityProxyDeploy.js, now src/libs/proxyDeploy/deploy.ts)
address private constant FALLBACK_HANDLER_SLOT = address(0x6969);
// @dev This is how we understand if msg.sender is the entry point
bytes32 private constant ENTRY_POINT_MARKER = 0x0000000000000000000000000000000000000000000000000000000000007171;
// Externally validated signatures
uint8 private constant SIGMODE_EXTERNALLY_VALIDATED = 255;
// Variables
mapping(address => bytes32) public privileges;
uint256 public nonce;
// Events
event LogPrivilegeChanged(address indexed addr, bytes32 priv);
event LogErr(address indexed to, uint256 value, bytes data, bytes returnData); // only used in tryCatch
// This contract can accept ETH without calldata
receive() external payable {}
/**
* @dev To support EIP 721 and EIP 1155, we need to respond to those methods with their own method signature
* @return bytes4 onERC721Received function selector
*/
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
return this.onERC721Received.selector;
}
/**
* @dev To support EIP 721 and EIP 1155, we need to respond to those methods with their own method signature
* @return bytes4 onERC1155Received function selector
*/
function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) {
return this.onERC1155Received.selector;
}
/**
* @dev To support EIP 721 and EIP 1155, we need to respond to those methods with their own method signature
* @return bytes4 onERC1155Received function selector
*/
function onERC1155BatchReceived(
address,
address,
uint256[] calldata,
uint256[] calldata,
bytes calldata
) external pure returns (bytes4) {
return this.onERC1155BatchReceived.selector;
}
/**
* @notice fallback method: currently used to call the fallback handler
* which is set by the user and can be changed
* @dev this contract can accept ETH with calldata, hence payable
*/
fallback() external payable {
// We store the fallback handler at this magic slot
address fallbackHandler = address(uint160(uint(privileges[FALLBACK_HANDLER_SLOT])));
if (fallbackHandler == address(0)) return;
assembly {
// we can use addr 0 because logic is taking full control of the
// execution making sure it returns itself and does not
// rely on any further Solidity code.
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), fallbackHandler, 0, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(0, 0, size)
if eq(result, 0) {
revert(0, size)
}
return(0, size)
}
}
/**
* @notice used to set the privilege of a key (by `addr`)
* @dev normal signatures will be considered valid if the
* `addr` they are signed with has non-zero (not 0x000..000) privilege set; we can set the privilege to
* a hash of the recovery keys and timelock (see `RecoveryInfo`) to enable recovery signatures
* @param addr the address to give privs to
* @param priv the privs to give
*/
function setAddrPrivilege(address addr, bytes32 priv) external payable {
require(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
privileges[addr] = priv;
emit LogPrivilegeChanged(addr, priv);
}
/**
* @notice Useful when we need to do multiple operations but ignore failures in some of them
* @param to address we're sending value to
* @param value the amount
* @param data callData
*/
function tryCatch(address to, uint256 value, bytes calldata data) external payable {
require(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
uint256 gasBefore = gasleft();
(bool success, bytes memory returnData) = to.call{ value: value, gas: gasBefore }(data);
require(gasleft() > gasBefore / 64, 'TRYCATCH_OOG');
if (!success) emit LogErr(to, value, data, returnData);
}
/**
* @notice same as `tryCatch` but with a gas limit
* @param to address we're sending value to
* @param value the amount
* @param data callData
* @param gasLimit how much gas is allowed
*/
function tryCatchLimit(address to, uint256 value, bytes calldata data, uint256 gasLimit) external payable {
require(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
uint256 gasBefore = gasleft();
(bool success, bytes memory returnData) = to.call{ value: value, gas: gasLimit }(data);
require(gasleft() > gasBefore / 64, 'TRYCATCH_OOG');
if (!success) emit LogErr(to, value, data, returnData);
}
/**
* @notice execute: this method is used to execute a single bundle of calls that are signed with a key
* that is authorized to execute on this account (in `privileges`)
* @dev WARNING: if the signature of this is changed, we have to change AmbireAccountFactory
* @param calls the transaction we're executing. They may not execute
* if specific cases. One such is when setting a timelock
* @param signature the signature for the transactions
*/
function execute(Transaction[] calldata calls, bytes calldata signature) public payable {
address signerKey;
uint8 sigMode = uint8(signature[signature.length - 1]);
uint256 currentNonce = nonce;
// we increment the nonce here (not using `nonce++` to save some gas)
nonce = currentNonce + 1;
if (sigMode == SIGMODE_EXTERNALLY_VALIDATED) {
bool isValidSig;
uint256 timestampValidAfter;
(signerKey, isValidSig, timestampValidAfter) = validateExternalSig(calls, signature);
if (!isValidSig) {
require(block.timestamp >= timestampValidAfter, 'SIGNATURE_VALIDATION_TIMELOCK');
revert('SIGNATURE_VALIDATION_FAIL');
}
} else {
signerKey = SignatureValidator.recoverAddr(
keccak256(abi.encode(address(this), block.chainid, currentNonce, calls)),
signature,
true
);
require(privileges[signerKey] != bytes32(0), 'INSUFFICIENT_PRIVILEGE');
}
executeBatch(calls);
// The actual anti-bricking mechanism - do not allow a signerKey to drop their own privileges
require(privileges[signerKey] != bytes32(0), 'PRIVILEGE_NOT_DOWNGRADED');
}
/**
* @notice allows executing multiple bundles of calls (batch together multiple executes)
* @param toExec an array of execute function parameters
*/
function executeMultiple(ExecuteArgs[] calldata toExec) external payable {
for (uint256 i = 0; i != toExec.length; i++) execute(toExec[i].calls, toExec[i].signature);
}
/**
* @notice Allows executing calls if the caller itself is authorized
* @dev no need for nonce management here cause we're not dealing with sigs
* @param calls the transaction we're executing
*/
function executeBySender(Transaction[] calldata calls) external payable {
require(privileges[msg.sender] != bytes32(0), 'INSUFFICIENT_PRIVILEGE');
executeBatch(calls);
// again, anti-bricking
require(privileges[msg.sender] != bytes32(0), 'PRIVILEGE_NOT_DOWNGRADED');
}
/**
* @notice allows the contract itself to execute a batch of calls
* self-calling is useful in cases like wanting to do multiple things in a tryCatchLimit
* @param calls the calls we're executing
*/
function executeBySelf(Transaction[] calldata calls) external payable {
require(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
executeBatch(calls);
}
/**
* @notice allows the contract itself to execute a single calls
* self-calling is useful when you want to workaround the executeBatch()
* protection of not being able to call address(0)
* @param call the call we're executing
*/
function executeBySelfSingle(Transaction calldata call) external payable {
require(msg.sender == address(this), 'ONLY_ACCOUNT_CAN_CALL');
executeCall(call.to, call.value, call.data);
}
/**
* @notice Execute a batch of transactions
* @param calls the transaction we're executing
*/
function executeBatch(Transaction[] memory calls) internal {
uint256 len = calls.length;
for (uint256 i = 0; i < len; i++) {
Transaction memory call = calls[i];
if (call.to != address(0)) executeCall(call.to, call.value, call.data);
}
}
/**
* @notice Execute a signle transaction
* @dev we shouldn't use address.call(), cause: https://github.com/ethereum/solidity/issues/2884
* @param to the address we're sending to
* @param value the amount we're sending
* @param data callData
*/
function executeCall(address to, uint256 value, bytes memory data) internal {
assembly {
let result := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0)
if eq(result, 0) {
let size := returndatasize()
let ptr := mload(0x40)
returndatacopy(ptr, 0, size)
revert(ptr, size)
}
}
}
/**
* @notice EIP-1271 implementation
* @dev see https://eips.ethereum.org/EIPS/eip-1271
* @param hash the signed hash
* @param signature the signature for the signed hash
* @return bytes4 is it a success or a failure
*/
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
(address recovered, bool usedUnprotected) = SignatureValidator.recoverAddrAllowUnprotected(hash, signature, false);
if (uint256(privileges[recovered]) > (usedUnprotected ? 1 : 0)) {
// bytes4(keccak256("isValidSignature(bytes32,bytes)")
return 0x1626ba7e;
} else {
return 0xffffffff;
}
}
/**
* @notice EIP-1155 implementation
* we pretty much only need to signal that we support the interface for 165, but for 1155 we also need the fallback function
* @param interfaceID the interface we're signaling support for
* @return bool do we support the interface or not
*/
function supportsInterface(bytes4 interfaceID) external view returns (bool) {
bool supported = interfaceID == 0x01ffc9a7 || // ERC-165 support (i.e. `bytes4(keccak256('supportsInterface(bytes4)'))`).
interfaceID == 0x150b7a02 || // ERC721TokenReceiver
interfaceID == 0x4e2312e0 || // ERC-1155 `ERC1155TokenReceiver` support (i.e. `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) ^ bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`).
interfaceID == 0x0a417632; // used for checking whether the account is v2 or not
if (supported) return true;
address payable fallbackHandler = payable(address(uint160(uint256(privileges[FALLBACK_HANDLER_SLOT]))));
if (fallbackHandler == address(0)) return false;
return AmbireAccount(fallbackHandler).supportsInterface(interfaceID);
}
//
// EIP-4337 implementation
//
// return value in case of signature failure, with no time-range.
// equivalent to packSigTimeRange(true,0,0);
uint256 constant internal SIG_VALIDATION_FAILED = 1;
// equivalent to packSigTimeRange(false,0,0);
uint256 constant internal SIG_VALIDATION_SUCCESS = 0;
/**
* @notice EIP-4337 implementation
* @dev We have an edge case for enabling ERC-4337 in the first if statement.
* If the function call is to execute, we do not perform an userOp sig validation.
* We require a one time hash nonce commitment from the paymaster for the given
* req. We use this to give permissions to the entry point on the fly
* and enable ERC-4337
* @param op the PackedUserOperation we're executing
* @param userOpHash the hash we've committed to
* @param missingAccountFunds the funds the account needs to pay
* @return uint256 0 for success, 1 for signature failure, and a uint256
* packed timestamp for a future valid signature:
* address aggregator, uint48 validUntil, uint48 validAfter
*/
function validateUserOp(PackedUserOperation calldata op, bytes32 userOpHash, uint256 missingAccountFunds)
external payable returns (uint256)
{
// enable running executeMultiple operation through the entryPoint if
// a paymaster sponsors it with a commitment one-time nonce.
// two use cases:
// 1) enable 4337 on a network by giving privileges to the entryPoint
// 2) key recovery. If the key is lost, we cannot sign the userOp,
// so we have to go to `execute` to trigger the recovery logic
// Why executeMultiple but not execute?
// executeMultiple allows us to combine recovery + fee payment calls.
// The fee payment call will be with a signature from the new key
if (op.callData.length >= 4 && bytes4(op.callData[0:4]) == this.executeMultiple.selector) {
// Require a paymaster, otherwise this mode can be used by anyone to get the user to spend their deposit
// @estimation-no-revert
if (op.signature.length != 0) return SIG_VALIDATION_FAILED;
require(
op.paymasterAndData.length >= UserOpHelper.PAYMASTER_DATA_OFFSET &&
bytes20(op.paymasterAndData[:UserOpHelper.PAYMASTER_ADDR_OFFSET]) != bytes20(0),
'validateUserOp: paymaster required in execute() mode'
);
// hashing in everything except sender (nonces are scoped by sender anyway), nonce, signature
uint256 targetNonce = uint256(keccak256(
abi.encode(op.initCode, op.callData, op.accountGasLimits, op.preVerificationGas, op.gasFees, op.paymasterAndData)
)) << 64;
// @estimation-no-revert
if (op.nonce != targetNonce) return SIG_VALIDATION_FAILED;
return SIG_VALIDATION_SUCCESS;
}
require(privileges[msg.sender] == ENTRY_POINT_MARKER, 'validateUserOp: not from entryPoint');
// @estimation
// paying should happen even if signature validation fails
if (missingAccountFunds > 0) {
// NOTE: MAY pay more than the minimum, to deposit for future transactions
(bool success,) = msg.sender.call{value : missingAccountFunds}('');
// ignore failure (its EntryPoint's job to verify, not account.)
(success);
}
// this is replay-safe because userOpHash is retrieved like this: keccak256(abi.encode(userOp.hash(), address(this), block.chainid))
address signer = SignatureValidator.recoverAddr(userOpHash, op.signature, true);
if (privileges[signer] == bytes32(0)) return SIG_VALIDATION_FAILED;
return SIG_VALIDATION_SUCCESS;
}
function validateExternalSig(Transaction[] memory calls, bytes calldata signature)
internal returns(address signerKey, bool isValidSig, uint256 timestampValidAfter) {
(bytes memory sig, ) = SignatureValidator.splitSignature(signature);
// the address of the validator we're using for this validation
address validatorAddr;
// all the data needed by the validator to execute the validation.
// In the case of DKIMRecoverySigValidator, this is AccInfo:
// abi.encode {string emailFrom; string emailTo; string domainName;
// bytes dkimPubKeyModulus; bytes dkimPubKeyExponent; address secondaryKey;
// bool acceptUnknownSelectors; uint32 waitUntilAcceptAdded;
// uint32 waitUntilAcceptRemoved; bool acceptEmptyDKIMSig;
// bool acceptEmptySecondSig;uint32 onlyOneSigTimelock;}
// The struct is declared in DKIMRecoverySigValidator
bytes memory validatorData;
// the signature data needed by the external validator.
// In the case of DKIMRecoverySigValidator, this is abi.encode(
// SignatureMeta memory sigMeta, bytes memory dkimSig, bytes memory secondSig
// ).
bytes memory innerSig;
// the signerKey in this case is an arbitrary value that does
// not have any specific purpose other than representing
// the privileges key
(signerKey, validatorAddr, validatorData, innerSig) = abi.decode(sig, (address, address, bytes, bytes));
require(
privileges[signerKey] == keccak256(abi.encode(validatorAddr, validatorData)),
'EXTERNAL_VALIDATION_NOT_SET'
);
// The sig validator itself should throw when a signature isn't validated successfully
// the return value just indicates whether we want to execute the current calls
(isValidSig, timestampValidAfter) = ExternalSigValidator(validatorAddr).validateSig(validatorData, innerSig, calls);
}
}// 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 {
event LogDeployed(address addr, uint256 salt);
address public immutable allowedToDrain;
constructor(address allowed) {
allowedToDrain = allowed;
}
/**
* @notice Allows anyone to deploy any contracft with a specific code/salt
* @dev This is safe because it's CREATE2 deployment
* @param code the code to be deployed
* @param salt the salt to shuffle the computed address
* @return address the deployed address
*/
function deploy(bytes calldata code, uint256 salt) external returns(address) {
return deploySafe(code, salt);
}
/**
* @notice Call this when you want to deploy the contract and execute calls
* @dev When the relayer needs to act upon an /identity/:addr/submit call, it'll either call execute on the AmbireAccount directly
* if it's already deployed, or call `deployAndExecute` if the account is still counterfactual
* we can't have deployAndExecuteBySender, because the sender will be the factory
* @param code the code to be deployed
* @param salt the salt to shuffle the computed address
* @param txns the txns the are going to be executed
* @param signature the signature for the txns
* @return address the deployed address
*/
function deployAndExecute(
bytes calldata code,
uint256 salt,
Transaction[] calldata txns,
bytes calldata signature
) external returns (address){
address payable addr = payable(deploySafe(code, salt));
IAmbireAccount(addr).execute(txns, signature);
return addr;
}
/**
* @notice Call this when you want to deploy the contract and call executeMultiple
* @dev when the relayer needs to act upon an /identity/:addr/submit call,
* it'll either call execute on the AmbireAccount directly. If it's already
* deployed, or call `deployAndExecuteMultiple` if the account is still
* counterfactual but there are multiple accountOps to send
* @param code the code to be deployed
* @param salt the salt to shuffle the computed address
* @param toExec [txns, signature] execute parameters
* @return address the deployed address
*/
function deployAndExecuteMultiple(
bytes calldata code,
uint256 salt,
IAmbireAccount.ExecuteArgs[] calldata toExec
) external returns (address){
address payable addr = payable(deploySafe(code, salt));
IAmbireAccount(addr).executeMultiple(toExec);
return addr;
}
/**
* @notice This method can be used to withdraw stuck tokens or airdrops
* @dev Only allowedToDrain can do the call
* @param to receiver
* @param value how much to be sent
* @param data if a token has airdropped, code to send it
* @param gas maximum gas willing to spend
*/
function call(address to, uint256 value, bytes calldata data, uint256 gas) external {
require(msg.sender == allowedToDrain, 'ONLY_AUTHORIZED');
(bool success, bytes memory err) = to.call{ gas: gas, value: value }(data);
require(success, string(err));
}
/**
* @dev This is done to mitigate possible frontruns where, for example,
* where deploying the same code/salt via deploy() would make a pending
* deployAndExecute fail. The way we mitigate that is by checking if the
* contract is already deployed and if so, we continue execution
* @param code the code to be deployed
* @param salt the salt to shuffle the computed address
* @return address the deployed address
*/
function deploySafe(bytes memory code, uint256 salt) internal returns (address) {
address expectedAddr = address(
uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(code)))))
);
uint256 size;
assembly {
size := extcodesize(expectedAddr)
}
// If there is code at that address, we can assume it's the one we were about to deploy,
// because of how CREATE2 and keccak256 works
if (size == 0) {
address addr;
assembly {
addr := create2(0, add(code, 0x20), mload(code), salt)
}
require(addr != address(0), 'FAILED_DEPLOYING');
require(addr == expectedAddr, 'FAILED_MATCH');
emit LogDeployed(addr, salt);
}
return expectedAddr;
}
}// 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 {
address immutable public relayer;
constructor(address _relayer) {
relayer = _relayer;
}
/**
* @notice This method can be used to withdraw stuck tokens or airdrops
*
* @param to The address we're calling
* @param value The value in the call
* @param data the call data
* @param gas the call gas
*/
function call(address to, uint256 value, bytes calldata data, uint256 gas) external payable {
require(msg.sender == relayer, 'call: not relayer');
(bool success, bytes memory err) = to.call{ gas: gas, value: value }(data);
require(success, string(err));
}
/**
* @notice Validate user operations the paymaster has signed
* We do not need to send funds to the EntryPoint because we rely on pre-existing deposit.
* Requests are chain specific to prevent signature reuse.
* @dev We have two use cases for the paymaster:
* - normal erc-4337. Everything is per ERC-4337 standard, the nonce is sequential.
* - an executeMultiple call. If the calldata is executeMultiple, we've hardcoded
* a 0 nonce. That's what's called a one-time hash nonce and its key is actually
* the commitment. Check EntryPoint -> NonceManager for more information.
*
* @param userOp the UserOperation we're executing
* @return context context is returned in the postOp and called by the
* EntryPoint. But we're not using postOp is context is always emtpy
* @return validationData This consists of:
* - an aggregator address: address(uint160(validationData)). This is used
* when you want an outer contract to determine whether the signature is valid.
* In our case, this is always 0 (address 0) for valid signatures and
* 1 (address 1) for invalid. This is what the entry point expects and
* in those two cases, an outer contract is obviously not called.
* - a uint48 validUntil: uint48(validationData >> 160)
* A Paymaster signature can be signed at time "x" but delayed intentionally
* until time "y" when a fee payment's price has dropped significantly or
* some other issue. validUntil sets a time validity for the signature
* - a uint48 validAfter: uint48(validationData >> (48 + 160))
* If the signature should be valid only after a period of time,
* we tweak the validAfter property.
* For more information, check EntryPoint -> _getValidationData()
*/
function validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32, uint256)
external
view
returns (bytes memory context, uint256 validationData)
{
(uint48 validUntil, uint48 validAfter, bytes memory signature) = abi.decode(
userOp.paymasterAndData[UserOpHelper.PAYMASTER_DATA_OFFSET:],
(uint48, uint48, bytes)
);
bytes memory callData = userOp.callData;
bytes32 hash = keccak256(abi.encode(
block.chainid,
address(this),
// entry point
msg.sender,
validUntil,
validAfter,
// everything except paymasterAndData and signature
userOp.sender,
// for the nonce we have an exception case: one-time nonces depend on paymasterAndData, which is generated by the relayer
// we can't have this as part of the sig cuz we create a cyclical dep
// the nonce can only be used once, so one cannot replay the gas payment
callData.length >= 4 && bytes4(userOp.callData[0:4]) == IAmbireAccount.executeMultiple.selector ? 0 : userOp.nonce,
userOp.initCode,
callData,
userOp.accountGasLimits,
userOp.preVerificationGas,
userOp.gasFees
));
(address recovered, ) = SignatureValidator.recoverAddrAllowUnprotected(hash, signature, true);
bool isValidSig = recovered == relayer;
// see _packValidationData: https://github.com/eth-infinitism/account-abstraction/blob/f2b09e60a92d5b3177c68d9f382912ccac19e8db/contracts/core/Helpers.sol#L73-L80
return ("", uint160(isValidSig ? 0 : 1) | (uint256(validUntil) << 160) | (uint256(validAfter) << 208));
}
/**
* @notice No-op, won't be used because we don't return a context
* @param mode .
* @param context .
* @param actualGasCost .
*/
function postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) external {
// No-op, won't be used because we don't return a context
}
}// 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 {
function validateSig(
bytes calldata data,
bytes calldata sig,
Transaction[] calldata calls
) external virtual returns (bool isValidSignature, uint256 timestampValidAfter);
}// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.8.7;
import '../libs/Transaction.sol';
interface IAmbireAccount {
function privileges(address addr) external returns (bytes32);
function nonce() external returns (uint);
struct RecoveryInfo {
address[] keys;
uint timelock;
}
struct ExecuteArgs {
Transaction[] calls;
bytes signature;
}
function setAddrPrivilege(address addr, bytes32 priv) external payable;
function tryCatch(address to, uint value, bytes calldata data) external payable;
function tryCatchLimit(address to, uint value, bytes calldata data, uint gasLimit) external payable;
function execute(Transaction[] calldata txns, bytes calldata signature) external payable;
function executeBySender(Transaction[] calldata txns) external payable;
function executeBySelf(Transaction[] calldata txns) external payable;
function executeMultiple(ExecuteArgs[] calldata toExec) external payable;
// EIP 1271 implementation
// see https://eips.ethereum.org/EIPS/eip-1271
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4);
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
library Bytes {
function trimToSize(bytes memory b, uint256 newLen) internal pure {
require(b.length > newLen, 'BytesLib: only shrinking');
assembly {
mstore(b, newLen)
}
}
/***********************************|
| Read Bytes Functions |
|__________________________________*/
/**
* @dev Reads a bytes32 value from a position in a byte array.
* @param b Byte array containing a bytes32 value.
* @param index Index in byte array of bytes32 value.
* @return result bytes32 value from byte array.
*/
function readBytes32(bytes memory b, uint256 index) internal pure returns (bytes32 result) {
// Arrays are prefixed by a 256 bit length parameter
index += 32;
require(b.length >= index, 'BytesLib: length');
// Read the bytes32 from array memory
assembly {
result := mload(add(b, index))
}
return result;
}
}// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.19;
import './Bytes.sol';
interface IERC1271Wallet {
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue);
}
library SignatureValidator {
using Bytes for bytes;
enum SignatureMode {
// the first mode Unprotected is used in combination with EIP-1271 signature verification to do
// EIP-712 verifications, as well as "Ethereum signed message:" message verifications
// The caveat with this is that we need to ensure that the signer key used for it isn't reused, or the message body
// itself contains context about the wallet (such as it's address)
// We do this, rather than applying the prefix on-chain, because if we do you won't be able to see the message
// when signing on a hardware wallet (you'll only see the hash) - since `isValidSignature` can only receive the hash -
// if the prefix is applied on-chain you can never match it - it's hash(prefix+hash(msg)) vs hash(prefix+msg)
// As for transactions (`execute()`), those can be signed with any of the modes
// Otherwise, if it's reused, we MUST use `Standard` mode which always wraps the final digest hash, but unfortnately this means
// you can't preview the full message when signing on a HW wallet
Unprotected,
Standard,
SmartWallet,
Spoof,
Schnorr,
Multisig,
// WARNING: Signature modes should not be more than 26 as the "v"
// value for standard ecrecover is 27/28
// WARNING: must always be last
LastUnused
}
// bytes4(keccak256("isValidSignature(bytes32,bytes)"))
bytes4 internal constant ERC1271_MAGICVALUE_BYTES32 = 0x1626ba7e;
// secp256k1 group order
uint256 internal constant Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
function splitSignature(bytes memory sig) internal pure returns (bytes memory, uint8) {
uint8 modeRaw;
unchecked {
modeRaw = uint8(sig[sig.length - 1]);
}
sig.trimToSize(sig.length - 1);
return (sig, modeRaw);
}
function recoverAddr(bytes32 hash, bytes memory sig, bool allowSpoofing) internal view returns (address) {
(address recovered, bool usedUnprotected) = recoverAddrAllowUnprotected(hash, sig, allowSpoofing);
require(!usedUnprotected, 'SV_USED_UNBOUND');
return recovered;
}
function recoverAddrAllowUnprotected(bytes32 hash, bytes memory sig, bool allowSpoofing) internal view returns (address, bool) {
require(sig.length != 0, 'SV_SIGLEN');
uint8 modeRaw;
unchecked {
modeRaw = uint8(sig[sig.length - 1]);
}
// 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
if (modeRaw >= uint8(SignatureMode.LastUnused)) {
if (sig.length == 65) modeRaw = uint8(SignatureMode.Unprotected);
else revert('SV_SIGMODE');
}
SignatureMode mode = SignatureMode(modeRaw);
// the address of the key we are gonna be returning
address signerKey;
// wrap in the EIP712 wrapping if it's not unbound
// multisig gets an exception because each inner sig will have to apply this logic
// @TODO should spoofing be removed from this?
bool isUnprotected = mode == SignatureMode.Unprotected || mode == SignatureMode.Multisig;
if (!isUnprotected) {
bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)'),
keccak256(bytes('Ambire')),
keccak256(bytes('1')),
block.chainid,
address(this),
bytes32(0)
));
hash = keccak256(abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(
keccak256(bytes('AmbireOperation(address account,bytes32 hash)')),
address(this),
hash
))
));
}
// {r}{s}{v}{mode}
if (mode == SignatureMode.Unprotected || mode == SignatureMode.Standard) {
require(sig.length == 65 || sig.length == 66, 'SV_LEN');
bytes32 r = sig.readBytes32(0);
bytes32 s = sig.readBytes32(32);
uint8 v = uint8(sig[64]);
signerKey = ecrecover(hash, v, r, s);
// {sig}{verifier}{mode}
} else if (mode == SignatureMode.Schnorr) {
// Based on https://hackmd.io/@nZ-twauPRISEa6G9zg3XRw/SyjJzSLt9
// You can use this library to produce signatures: https://github.com/borislav-itskov/schnorrkel.js
// px := public key x-coord
// e := schnorr signature challenge
// s := schnorr signature
// parity := public key y-coord parity (27 or 28)
// last uint8 is for the Ambire sig mode - it's ignored
sig.trimToSize(sig.length - 1);
(bytes32 px, bytes32 e, bytes32 s, uint8 parity) = abi.decode(sig, (bytes32, bytes32, bytes32, uint8));
// ecrecover = (m, v, r, s);
bytes32 sp = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
bytes32 ep = bytes32(Q - mulmod(uint256(e), uint256(px), Q));
require(sp != bytes32(Q));
// the ecrecover precompile implementation checks that the `r` and `s`
// inputs are non-zero (in this case, `px` and `ep`), thus we don't need to
// check if they're zero.
address R = ecrecover(sp, parity, px, ep);
require(R != address(0), 'SV_ZERO_SIG');
require(e == keccak256(abi.encodePacked(R, uint8(parity), px, hash)), 'SV_SCHNORR_FAILED');
signerKey = address(uint160(uint256(keccak256(abi.encodePacked('SCHNORR', px)))));
} else if (mode == SignatureMode.Multisig) {
sig.trimToSize(sig.length - 1);
bytes[] memory signatures = abi.decode(sig, (bytes[]));
// since we're in a multisig, we care if any of the inner sigs are unbound
isUnprotected = false;
for (uint256 i = 0; i != signatures.length; i++) {
(address inner, bool isInnerUnprotected) = recoverAddrAllowUnprotected(hash, signatures[i], false);
if (isInnerUnprotected) isUnprotected = true;
signerKey = address(
uint160(uint256(keccak256(abi.encodePacked(signerKey, inner))))
);
}
} else if (mode == SignatureMode.SmartWallet) {
// 32 bytes for the addr, 1 byte for the type = 33
require(sig.length > 33, 'SV_LEN_WALLET');
uint256 newLen;
unchecked {
newLen = sig.length - 33;
}
IERC1271Wallet wallet = IERC1271Wallet(address(uint160(uint256(sig.readBytes32(newLen)))));
sig.trimToSize(newLen);
require(ERC1271_MAGICVALUE_BYTES32 == wallet.isValidSignature(hash, sig), 'SV_WALLET_INVALID');
signerKey = address(wallet);
// {address}{mode}; the spoof mode is used when simulating calls
} else if (mode == SignatureMode.Spoof && allowSpoofing) {
// This is safe cause it's specifically intended for spoofing sigs in simulation conditions, where tx.origin can be controlled
// We did not choose 0x00..00 because in future network upgrades tx.origin may be nerfed or there may be edge cases in which
// it is zero, such as native account abstraction
// slither-disable-next-line tx-origin
require(tx.origin == address(1) || tx.origin == address(6969), 'SV_SPOOF_ORIGIN');
require(sig.length == 33, 'SV_SPOOF_LEN');
sig.trimToSize(32);
// To simulate the gas usage; check is just to silence unused warning
require(ecrecover(0, 0, 0, 0) != address(6969));
signerKey = abi.decode(sig, (address));
} else {
revert('SV_TYPE');
}
require(signerKey != address(0), 'SV_ZERO_SIG');
return (signerKey, isUnprotected);
}
}// 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 {
uint256 public constant PAYMASTER_ADDR_OFFSET = 20;
// 52 = 20 address + 16 paymasterVerificationGasLimit + 16 paymasterPostOpGasLimit
uint256 public constant PAYMASTER_DATA_OFFSET = 52;
}{
"remappings": [
"forge-std/=lib/forge-std/src/"
],
"optimizer": {
"enabled": true,
"runs": 1000
},
"metadata": {
"useLiteralContent": false,
"bytecodeHash": "ipfs",
"appendCBOR": true
},
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"abi"
]
}
},
"evmVersion": "paris",
"viaIR": true,
"libraries": {}
}Contract ABI
API[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"returnData","type":"bytes"}],"name":"LogErr","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":false,"internalType":"bytes32","name":"priv","type":"bytes32"}],"name":"LogPrivilegeChanged","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Transaction[]","name":"calls","type":"tuple[]"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"execute","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Transaction[]","name":"calls","type":"tuple[]"}],"name":"executeBySelf","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Transaction","name":"call","type":"tuple"}],"name":"executeBySelfSingle","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Transaction[]","name":"calls","type":"tuple[]"}],"name":"executeBySender","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Transaction[]","name":"calls","type":"tuple[]"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct IAmbireAccount.ExecuteArgs[]","name":"toExec","type":"tuple[]"}],"name":"executeMultiple","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"isValidSignature","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155BatchReceived","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"privileges","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"bytes32","name":"priv","type":"bytes32"}],"name":"setAddrPrivilege","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceID","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"tryCatch","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"gasLimit","type":"uint256"}],"name":"tryCatchLimit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"bytes32","name":"accountGasLimits","type":"bytes32"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"bytes32","name":"gasFees","type":"bytes32"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct PackedUserOperation","name":"op","type":"tuple"},{"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"internalType":"uint256","name":"missingAccountFunds","type":"uint256"}],"name":"validateUserOp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]Loading...
Loading
Loading...
Loading
Loading...
Loading
Net Worth in USD
$634.25
Net Worth in XDAI
Token Allocations
CRV
96.57%
ETH
2.97%
AURA
0.46%
Multichain Portfolio | 35 Chains
Loading...
Loading
Loading...
Loading
Loading...
Loading
[ Download: CSV Export ]
A contract address hosts a smart contract, which is a set of code stored on the blockchain that runs when predetermined conditions are met. Learn more about addresses in our Knowledge Base.