-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c160a04
commit c912241
Showing
2 changed files
with
209 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
// SPDX-License-Identifier: agpl-3.0 | ||
pragma solidity 0.8.7; | ||
|
||
import "./Identity.sol"; | ||
import "./interfaces/IERC20.sol"; | ||
|
||
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); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters