pragma solidity 0.8.7;
// @TODO: Formatting
library LibBytes {
// @TODO: see if we can just set .length =
function trimToSize(bytes memory b, uint 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;
}
}
interface IERC1271Wallet {
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue);
}
library SignatureValidator {
using LibBytes for bytes;
enum SignatureMode {
EIP712,
EthSign,
SmartWallet,
Spoof
}
// bytes4(keccak256("isValidSignature(bytes32,bytes)"))
bytes4 constant internal ERC1271_MAGICVALUE_BYTES32 = 0x1626ba7e;
function recoverAddr(bytes32 hash, bytes memory sig) internal view returns (address) {
return recoverAddrImpl(hash, sig, false);
}
function recoverAddrImpl(bytes32 hash, bytes memory sig, bool allowSpoofing) internal view returns (address) {
require(sig.length >= 1, "SV_SIGLEN");
uint8 modeRaw;
unchecked { modeRaw = uint8(sig[sig.length - 1]); }
SignatureMode mode = SignatureMode(modeRaw);
// {r}{s}{v}{mode}
if (mode == SignatureMode.EIP712 || mode == SignatureMode.EthSign) {
require(sig.length == 66, "SV_LEN");
bytes32 r = sig.readBytes32(0);
bytes32 s = sig.readBytes32(32);
uint8 v = uint8(sig[64]);
// Hesitant about this check: seems like this is something that has no business being checked on-chain
require(v == 27 || v == 28, "SV_INVALID_V");
if (mode == SignatureMode.EthSign) hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
address signer = ecrecover(hash, v, r, s);
require(signer != address(0), "SV_ZERO_SIG");
return signer;
// {sig}{verifier}{mode}
} else if (mode == SignatureMode.SmartWallet) {
// 32 bytes for the addr, 1 byte for the type = 33
require(sig.length > 33, "SV_LEN_WALLET");
uint 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");
return address(wallet);
// {address}{mode}; the spoof mode is used when simulating calls
} else if (mode == SignatureMode.Spoof && allowSpoofing) {
require(tx.origin == address(1), "SV_SPOOF_ORIGIN");
require(sig.length == 33, "SV_SPOOF_LEN");
sig.trimToSize(32);
return abi.decode(sig, (address));
} else revert("SV_SIGMODE");
}
}
contract Identity {
mapping (address => bytes32) public privileges;
// The next allowed nonce
uint public nonce;
// Events
event LogPrivilegeChanged(address indexed addr, bytes32 priv);
event LogErr(address indexed to, uint value, bytes data, bytes returnData); // only used in tryCatch
// Transaction structure
// we handle replay protection separately by requiring (address(this), chainID, nonce) as part of the sig
struct Transaction {
address to;
uint value;
bytes data;
}
constructor(address[] memory addrs) {
uint len = addrs.length;
for (uint i=0; i<len; i++) {
// @TODO should we allow setting to any arb value here?
privileges[addrs[i]] = bytes32(uint(1));
emit LogPrivilegeChanged(addrs[i], bytes32(uint(1)));
}
}
// This contract can accept ETH without calldata
receive() external payable {}
// This contract can accept ETH with calldata
// However, to support EIP 721 and EIP 1155, we need to respond to those methods with their own method signature
fallback() external payable {
bytes4 method = msg.sig;
if (
method == 0x150b7a02 // bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))
|| method == 0xf23a6e61 // bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))
|| method == 0xbc197c81 // bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))
) {
// Copy back the method
// solhint-disable-next-line no-inline-assembly
assembly {
calldatacopy(0, 0, 0x04)
return (0, 0x20)
}
}
}
function setAddrPrivilege(address addr, bytes32 priv)
external
{
require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL');
// Anti-bricking measure: if the privileges slot is used for special data (not 0x01),
// don't allow to set it to true
if (uint(privileges[addr]) > 1) require(priv != bytes32(uint(1)), 'UNSETTING_SPECIAL_DATA');
privileges[addr] = priv;
emit LogPrivilegeChanged(addr, priv);
}
function tipMiner(uint amount)
external
{
require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL');
// See https://docs.flashbots.net/flashbots-auction/searchers/advanced/coinbase-payment/#managing-payments-to-coinbaseaddress-when-it-is-a-contract
// generally this contract is reentrancy proof cause of the nonce
executeCall(block.coinbase, amount, new bytes(0));
}
function tryCatch(address to, uint value, bytes calldata data)
external
{
require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL');
(bool success, bytes memory returnData) = to.call{value: value, gas: gasleft()}(data);
if (!success) emit LogErr(to, value, data, returnData);
}
// WARNING: if the signature of this is changed, we have to change IdentityFactory
function execute(Transaction[] calldata txns, bytes calldata signature)
external
{
require(txns.length > 0, 'MUST_PASS_TX');
uint currentNonce = nonce;
// NOTE: abi.encode is safer than abi.encodePacked in terms of collision safety
bytes32 hash = keccak256(abi.encode(address(this), block.chainid, currentNonce, txns));
// We have to increment before execution cause it protects from reentrancies
nonce = currentNonce + 1;
address signer = SignatureValidator.recoverAddrImpl(hash, signature, true);
require(privileges[signer] != bytes32(0), 'INSUFFICIENT_PRIVILEGE');
uint len = txns.length;
for (uint i=0; i<len; i++) {
Transaction memory txn = txns[i];
executeCall(txn.to, txn.value, txn.data);
}
// The actual anti-bricking mechanism - do not allow a signer to drop their own priviledges
require(privileges[signer] != bytes32(0), 'PRIVILEGE_NOT_DOWNGRADED');
}
// no need for nonce management here cause we're not dealing with sigs
function executeBySender(Transaction[] calldata txns) external {
require(txns.length > 0, 'MUST_PASS_TX');
require(privileges[msg.sender] != bytes32(0), 'INSUFFICIENT_PRIVILEGE');
uint len = txns.length;
for (uint i=0; i<len; i++) {
Transaction memory txn = txns[i];
executeCall(txn.to, txn.value, txn.data);
}
// again, anti-bricking
require(privileges[msg.sender] != bytes32(0), 'PRIVILEGE_NOT_DOWNGRADED');
}
function executeBySelf(Transaction[] calldata txns) external {
require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL');
require(txns.length > 0, 'MUST_PASS_TX');
uint len = txns.length;
for (uint i=0; i<len; i++) {
Transaction memory txn = txns[i];
executeCall(txn.to, txn.value, txn.data);
}
}
// we shouldn't use address.call(), cause: https://github.com/ethereum/solidity/issues/2884
// copied from https://github.com/uport-project/uport-identity/blob/develop/contracts/Proxy.sol
// there's also
// https://github.com/gnosis/MultiSigWallet/commit/e1b25e8632ca28e9e9e09c81bd20bf33fdb405ce
// https://github.com/austintgriffith/bouncer-proxy/blob/master/BouncerProxy/BouncerProxy.sol
// https://github.com/gnosis/safe-contracts/blob/7e2eeb3328bb2ae85c36bc11ea6afc14baeb663c/contracts/base/Executor.sol
function executeCall(address to, uint256 value, bytes memory data)
internal
{
assembly {
let result := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0)
switch result case 0 {
let size := returndatasize()
let ptr := mload(0x40)
returndatacopy(ptr, 0, size)
revert(ptr, size)
}
default {}
}
// A single call consumes around 477 more gas with the pure solidity version, for whatever reason
// WARNING: do not use this, it corrupts the returnData string (returns it in a slightly different format)
//(bool success, bytes memory returnData) = to.call{value: value, gas: gasleft()}(data);
//if (!success) revert(string(data));
}
// EIP 1271 implementation
// see https://eips.ethereum.org/EIPS/eip-1271
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
if (privileges[SignatureValidator.recoverAddr(hash, signature)] != bytes32(0)) {
// bytes4(keccak256("isValidSignature(bytes32,bytes)")
return 0x1626ba7e;
} else {
return 0xffffffff;
}
}
// 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
function supportsInterface(bytes4 interfaceID) external pure returns (bool) {
return
interfaceID == 0x01ffc9a7 || // ERC-165 support (i.e. `bytes4(keccak256('supportsInterface(bytes4)'))`).
interfaceID == 0x4e2312e0; // ERC-1155 `ERC1155TokenReceiver` support (i.e. `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) ^ bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`).
}
}
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
contract QuickAccManager {
// Note: nonces are scoped by identity rather than by accHash - the reason for this is that there's no reason to scope them by accHash,
// we merely need them for replay protection
mapping (address => uint) public nonces;
mapping (bytes32 => uint) public scheduled;
bytes4 constant CANCEL_PREFIX = 0xc47c3100;
// Events
// we only need those for timelocked stuff so we can show scheduled txns to the user; the oens that get executed immediately do not need logs
event LogScheduled(bytes32 indexed txnHash, bytes32 indexed accHash, address indexed signer, uint nonce, uint time, Identity.Transaction[] txns);
event LogCancelled(bytes32 indexed txnHash, bytes32 indexed accHash, address indexed signer, uint time);
event LogExecScheduled(bytes32 indexed txnHash, bytes32 indexed accHash, uint time);
// EIP 2612
/// @notice Chain Id at this contract's deployment.
uint256 internal immutable DOMAIN_SEPARATOR_CHAIN_ID;
/// @notice EIP-712 typehash for this contract's domain at deployment.
bytes32 internal immutable _DOMAIN_SEPARATOR;
constructor() {
DOMAIN_SEPARATOR_CHAIN_ID = block.chainid;
_DOMAIN_SEPARATOR = calculateDomainSeparator();
}
struct QuickAccount {
uint timelock;
address one;
address two;
// We decided to not allow certain options here such as ability to skip the second sig for send(), but leaving this a struct rather than a tuple
// for clarity and to ensure it's future proof
}
struct DualSig {
bool isBothSigned;
bytes one;
bytes two;
}
// NOTE: a single accHash can control multiple identities, as long as those identities set it's hash in privileges[address(this)]
// this is by design
// isBothSigned is hashed in so that we don't allow signatures from two-sig txns to be reused for single sig txns,
// ...potentially frontrunning a normal two-sig transaction and making it wait
// WARNING: if the signature of this is changed, we have to change IdentityFactory
function send(Identity identity, QuickAccount calldata acc, DualSig calldata sigs, Identity.Transaction[] calldata txns) external {
bytes32 accHash = keccak256(abi.encode(acc));
require(identity.privileges(address(this)) == accHash, 'WRONG_ACC_OR_NO_PRIV');
uint initialNonce = nonces[address(identity)]++;
// Security: we must also hash in the hash of the QuickAccount, otherwise the sig of one key can be reused across multiple accs
bytes32 hash = keccak256(abi.encode(
address(this),
block.chainid,
address(identity),
accHash,
initialNonce,
txns,
sigs.isBothSigned
));
if (sigs.isBothSigned) {
require(acc.one == SignatureValidator.recoverAddr(hash, sigs.one), 'SIG_ONE');
require(acc.two == SignatureValidator.recoverAddr(hash, sigs.two), 'SIG_TWO');
identity.executeBySender(txns);
} else {
address signer = SignatureValidator.recoverAddr(hash, sigs.one);
require(acc.one == signer || acc.two == signer, 'SIG');
// no need to check whether `scheduled[hash]` is already set here cause of the incrementing nonce
scheduled[hash] = block.timestamp + acc.timelock;
emit LogScheduled(hash, accHash, signer, initialNonce, block.timestamp, txns);
}
}
function cancel(Identity identity, QuickAccount calldata acc, uint nonce, bytes calldata sig, Identity.Transaction[] calldata txns) external {
bytes32 accHash = keccak256(abi.encode(acc));
require(identity.privileges(address(this)) == accHash, 'WRONG_ACC_OR_NO_PRIV');
bytes32 hash = keccak256(abi.encode(CANCEL_PREFIX, address(this), block.chainid, address(identity), accHash, nonce, txns, false));
address signer = SignatureValidator.recoverAddr(hash, sig);
require(signer == acc.one || signer == acc.two, 'INVALID_SIGNATURE');
// @NOTE: should we allow cancelling even when it's matured? probably not, otherwise there's a minor grief
// opportunity: someone wants to cancel post-maturity, and you front them with execScheduled
bytes32 hashTx = keccak256(abi.encode(address(this), block.chainid, accHash, nonce, txns, false));
uint scheduledTime = scheduled[hashTx];
require(scheduledTime != 0 && block.timestamp < scheduledTime, 'TOO_LATE');
delete scheduled[hashTx];
emit LogCancelled(hashTx, accHash, signer, block.timestamp);
}
function execScheduled(Identity identity, bytes32 accHash, uint nonce, Identity.Transaction[] calldata txns) external {
require(identity.privileges(address(this)) == accHash, 'WRONG_ACC_OR_NO_PRIV');
bytes32 hash = keccak256(abi.encode(address(this), block.chainid, address(identity), accHash, nonce, txns, false));
uint scheduledTime = scheduled[hash];
require(scheduledTime != 0 && block.timestamp >= scheduledTime, 'NOT_TIME');
delete scheduled[hash];
identity.executeBySender(txns);
emit LogExecScheduled(hash, accHash, block.timestamp);
}
// EIP 1271 implementation
// see https://eips.ethereum.org/EIPS/eip-1271
// NOTE: this method is not intended to be called from off-chain eth_calls; technically it's not a clean EIP 1271
// ...implementation, because EIP1271 assumes every smart wallet implements that method, while this contract is not a smart wallet
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
(uint timelock, bytes memory sig1, bytes memory sig2) = abi.decode(signature, (uint, bytes, bytes));
bytes32 accHash = keccak256(abi.encode(QuickAccount({
timelock: timelock,
one: SignatureValidator.recoverAddr(hash, sig1),
two: SignatureValidator.recoverAddr(hash, sig2)
})));
if (Identity(payable(address(msg.sender))).privileges(address(this)) == accHash) {
// bytes4(keccak256("isValidSignature(bytes32,bytes)")
return 0x1626ba7e;
} else {
return 0xffffffff;
}
}
// EIP 712 methods
// all of the following are 2/2 only
struct DualSigAlwaysBoth {
bytes one;
bytes two;
}
function calculateDomainSeparator() internal view returns (bytes32) {
return keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
// @TODO: maybe we should use a more user friendly name here?
keccak256(bytes('QuickAccManager')),
keccak256(bytes('1')),
block.chainid,
address(this)
)
);
}
/// @notice EIP-712 typehash for this contract's domain.
function DOMAIN_SEPARATOR() public view returns (bytes32) {
return block.chainid == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : calculateDomainSeparator();
}
bytes32 private constant TRANSFER_TYPEHASH = keccak256('Transfer(address tokenAddr,address to,uint256 value,uint256 fee,address identity,uint256 nonce)');
struct Transfer { address token; address to; uint amount; uint fee; }
// WARNING: if the signature of this is changed, we have to change IdentityFactory
function sendTransfer(Identity identity, QuickAccount calldata acc, DualSigAlwaysBoth calldata sigs, Transfer calldata t) external {
require(identity.privileges(address(this)) == keccak256(abi.encode(acc)), 'WRONG_ACC_OR_NO_PRIV');
bytes32 hash = keccak256(abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR(),
keccak256(abi.encode(TRANSFER_TYPEHASH, t.token, t.to, t.amount, t.fee, address(identity), nonces[address(identity)]++))
));
require(acc.one == SignatureValidator.recoverAddr(hash, sigs.one), 'SIG_ONE');
require(acc.two == SignatureValidator.recoverAddr(hash, sigs.two), 'SIG_TWO');
Identity.Transaction[] memory txns = new Identity.Transaction[](2);
txns[0].to = t.token;
txns[0].data = abi.encodeWithSelector(IERC20.transfer.selector, t.to, t.amount);
txns[1].to = t.token;
txns[1].data = abi.encodeWithSelector(IERC20.transfer.selector, msg.sender, t.fee);
identity.executeBySender(txns);
}
// Reference for arrays: https://github.com/sportx-bet/smart-contracts/blob/e36965a0c4748bf73ae15ed3cab5660c9cf722e1/contracts/impl/trading/EIP712FillHasher.sol
// and https://eips.ethereum.org/EIPS/eip-712
// and for signTypedData_v4: https://gist.github.com/danfinlay/750ce1e165a75e1c3387ec38cf452b71
struct Txn { string description; address to; uint value; bytes data; }
bytes32 private constant TXNS_TYPEHASH = keccak256('Txn(string description,address to,uint256 value,bytes data)');
bytes32 private constant BUNDLE_TYPEHASH = keccak256('Bundle(address identity,uint256 nonce,Txn[] transactions)');
// WARNING: if the signature of this is changed, we have to change IdentityFactory
function sendTxns(Identity identity, QuickAccount calldata acc, DualSigAlwaysBoth calldata sigs, Txn[] calldata txns) external {
require(identity.privileges(address(this)) == keccak256(abi.encode(acc)), 'WRONG_ACC_OR_NO_PRIV');
// hashing + prepping args
bytes32[] memory txnBytes = new bytes32[](txns.length);
Identity.Transaction[] memory identityTxns = new Identity.Transaction[](txns.length);
uint txnLen = txns.length;
for (uint256 i = 0; i < txnLen; i++) {
txnBytes[i] = keccak256(abi.encode(TXNS_TYPEHASH, txns[i].description, txns[i].to, txns[i].value, txns[i].data));
identityTxns[i].to = txns[i].to;
identityTxns[i].value = txns[i].value;
identityTxns[i].data = txns[i].data;
}
bytes32 txnsHash = keccak256(abi.encodePacked(txnBytes));
bytes32 hash = keccak256(abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR(),
keccak256(abi.encode(BUNDLE_TYPEHASH, address(identity), nonces[address(identity)]++, txnsHash))
));
require(acc.one == SignatureValidator.recoverAddr(hash, sigs.one), 'SIG_ONE');
require(acc.two == SignatureValidator.recoverAddr(hash, sigs.two), 'SIG_TWO');
identity.executeBySender(identityTxns);
}
}
{
"compilationTarget": {
"QuickAccManager.sol": "QuickAccManager"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"txnHash","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"accHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"signer","type":"address"},{"indexed":false,"internalType":"uint256","name":"time","type":"uint256"}],"name":"LogCancelled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"txnHash","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"accHash","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"time","type":"uint256"}],"name":"LogExecScheduled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"txnHash","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"accHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"signer","type":"address"},{"indexed":false,"internalType":"uint256","name":"nonce","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"time","type":"uint256"},{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"indexed":false,"internalType":"struct Identity.Transaction[]","name":"txns","type":"tuple[]"}],"name":"LogScheduled","type":"event"},{"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract Identity","name":"identity","type":"address"},{"components":[{"internalType":"uint256","name":"timelock","type":"uint256"},{"internalType":"address","name":"one","type":"address"},{"internalType":"address","name":"two","type":"address"}],"internalType":"struct QuickAccManager.QuickAccount","name":"acc","type":"tuple"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"sig","type":"bytes"},{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Identity.Transaction[]","name":"txns","type":"tuple[]"}],"name":"cancel","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract Identity","name":"identity","type":"address"},{"internalType":"bytes32","name":"accHash","type":"bytes32"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Identity.Transaction[]","name":"txns","type":"tuple[]"}],"name":"execScheduled","outputs":[],"stateMutability":"nonpayable","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":[{"internalType":"address","name":"","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"scheduled","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract Identity","name":"identity","type":"address"},{"components":[{"internalType":"uint256","name":"timelock","type":"uint256"},{"internalType":"address","name":"one","type":"address"},{"internalType":"address","name":"two","type":"address"}],"internalType":"struct QuickAccManager.QuickAccount","name":"acc","type":"tuple"},{"components":[{"internalType":"bool","name":"isBothSigned","type":"bool"},{"internalType":"bytes","name":"one","type":"bytes"},{"internalType":"bytes","name":"two","type":"bytes"}],"internalType":"struct QuickAccManager.DualSig","name":"sigs","type":"tuple"},{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Identity.Transaction[]","name":"txns","type":"tuple[]"}],"name":"send","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract Identity","name":"identity","type":"address"},{"components":[{"internalType":"uint256","name":"timelock","type":"uint256"},{"internalType":"address","name":"one","type":"address"},{"internalType":"address","name":"two","type":"address"}],"internalType":"struct QuickAccManager.QuickAccount","name":"acc","type":"tuple"},{"components":[{"internalType":"bytes","name":"one","type":"bytes"},{"internalType":"bytes","name":"two","type":"bytes"}],"internalType":"struct QuickAccManager.DualSigAlwaysBoth","name":"sigs","type":"tuple"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"fee","type":"uint256"}],"internalType":"struct QuickAccManager.Transfer","name":"t","type":"tuple"}],"name":"sendTransfer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract Identity","name":"identity","type":"address"},{"components":[{"internalType":"uint256","name":"timelock","type":"uint256"},{"internalType":"address","name":"one","type":"address"},{"internalType":"address","name":"two","type":"address"}],"internalType":"struct QuickAccManager.QuickAccount","name":"acc","type":"tuple"},{"components":[{"internalType":"bytes","name":"one","type":"bytes"},{"internalType":"bytes","name":"two","type":"bytes"}],"internalType":"struct QuickAccManager.DualSigAlwaysBoth","name":"sigs","type":"tuple"},{"components":[{"internalType":"string","name":"description","type":"string"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct QuickAccManager.Txn[]","name":"txns","type":"tuple[]"}],"name":"sendTxns","outputs":[],"stateMutability":"nonpayable","type":"function"}]