forked from dharma-eng/dharma-smart-wallet
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
use Simple Multisigs in place of Gnosis Safe
- Loading branch information
Showing
9 changed files
with
855 additions
and
1,540 deletions.
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
68 changes: 68 additions & 0 deletions
68
contracts/helpers/SmartWalletFactoryV1UserSigningKeyUpdater.sol
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,68 @@ | ||
pragma solidity 0.5.11; // optimization runs: 200, evm version: petersburg | ||
|
||
import "../../interfaces/DharmaSmartWalletFactoryV1Interface.sol"; | ||
import "../../interfaces/DharmaSmartWalletImplementationV0Interface.sol"; | ||
|
||
|
||
/** | ||
* A contract to update the user signing key on a smart wallet deployed or | ||
* counterfactually determined by a DharmaSmartWalletFactoryV1 contract, using | ||
* an existing smart wallet or deploying the smart wallet if necessary. Note | ||
* that this helper only supports V1 smart wallet factories - future versions | ||
* have similar functionality included directly, which improves reliability and | ||
* efficiency. | ||
*/ | ||
contract SmartWalletFactoryV1UserSigningKeyUpdater { | ||
function updateUserSigningKey( | ||
address smartWallet, | ||
DharmaSmartWalletFactoryV1Interface smartWalletFactory, | ||
address currentUserSigningKey, | ||
address newUserSigningKey, | ||
uint256 minimumActionGas, | ||
bytes calldata userSignature, | ||
bytes calldata dharmaSignature | ||
) external returns ( | ||
DharmaSmartWalletImplementationV0Interface wallet | ||
) { | ||
// Deploy a new smart wallet if needed. Factory emits a corresponding event. | ||
wallet = _deployNewSmartWalletIfNeeded( | ||
smartWalletFactory, currentUserSigningKey, smartWallet | ||
); | ||
|
||
// Set new user signing key. Smart wallet emits a corresponding event. | ||
wallet.setUserSigningKey( | ||
newUserSigningKey, minimumActionGas, userSignature, dharmaSignature | ||
); | ||
} | ||
|
||
function _deployNewSmartWalletIfNeeded( | ||
DharmaSmartWalletFactoryV1Interface smartWalletFactory, | ||
address userSigningKey, | ||
address expectedSmartWallet | ||
) internal returns ( | ||
DharmaSmartWalletImplementationV0Interface smartWallet | ||
) { | ||
// Only deploy if a smart wallet doesn't already exist at expected address. | ||
uint256 size; | ||
assembly { size := extcodesize(expectedSmartWallet) } | ||
if (size == 0) { | ||
// Deploy the smart wallet. | ||
smartWallet = DharmaSmartWalletImplementationV0Interface( | ||
smartWalletFactory.newSmartWallet(userSigningKey) | ||
); | ||
} else { | ||
// Reuse the supplied smart wallet address. Note that this helper does | ||
// not perform an extcodehash check, meaning that a contract that is | ||
// not actually a smart wallet may be supplied instead. | ||
smartWallet = DharmaSmartWalletImplementationV0Interface( | ||
expectedSmartWallet | ||
); | ||
|
||
// Ensure that the smart wallet in question has the right key. | ||
require( | ||
smartWallet.getUserSigningKey() == userSigningKey, | ||
"Existing user signing key differs from supplied current signing key." | ||
); | ||
} | ||
} | ||
} |
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 |
---|---|---|
@@ -1,21 +1,194 @@ | ||
pragma solidity 0.5.11; | ||
|
||
import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol"; | ||
|
||
|
||
/** | ||
* @title DharmaAccountRecoveryMultisig | ||
* @notice This contract is an example of a multisig that will control account | ||
* recovery on the initial implentation of Dharma smart wallets. | ||
* @author 0age (derived from Christian Lundkvist's Simple Multisig) | ||
* @notice This contract is a multisig that will control account recovery on the | ||
* Dharma Smart Wallet, based on Christian Lundkvist's Simple Multisig (found at | ||
* https://github.com/christianlundkvist/simple-multisig). The Account Recovery | ||
* Manager is hard-coded as the only allowable call destination, and any changes | ||
* in ownership or signature threshold will require deploying a new multisig and | ||
* transferring ownership of the account recovery manager. | ||
*/ | ||
contract DharmaAccountRecoveryMultisig is GnosisSafe { | ||
constructor(address[] memory owners, uint256 threshold) public { | ||
domainSeparator = keccak256( | ||
abi.encode( | ||
DOMAIN_SEPARATOR_TYPEHASH, | ||
address(this) | ||
) | ||
contract DharmaAccountRecoveryMultisig { | ||
// The nonce is the only mutable state, and is incremented on every call. | ||
uint256 private _nonce; | ||
|
||
// Maintain a mapping and a convenience array of owners. | ||
mapping(address => bool) private _isOwner; | ||
address[] private _owners; | ||
|
||
// The Account Recovery Manager is the only account the multisig can call. | ||
address private constant _DESTINATION = address( | ||
0x00000000003709EDEA9182789f1153E59CFe849e | ||
); | ||
|
||
// The threshold is an exact number of valid signatures that must be supplied. | ||
uint256 private constant _THRESHOLD = 2; | ||
|
||
// Note: Owners must be strictly increasing in order to prevent duplicates. | ||
constructor(address[] memory owners) public { | ||
require(owners.length <= 10, "Cannot have more than 10 owners."); | ||
require(_THRESHOLD <= owners.length, "Threshold cannot exceed total owners."); | ||
|
||
address lastAddress = address(0); | ||
for (uint256 i = 0; i < owners.length; i++) { | ||
require( | ||
owners[i] > lastAddress, "Owner addresses must be strictly increasing." | ||
); | ||
_isOwner[owners[i]] = true; | ||
lastAddress = owners[i]; | ||
} | ||
_owners = owners; | ||
} | ||
|
||
function getNextHash( | ||
bytes calldata data, | ||
address executor, | ||
uint256 gasLimit | ||
) external view returns (bytes32 hash) { | ||
hash = _getHash(data, executor, gasLimit, _nonce); | ||
} | ||
|
||
function getHash( | ||
bytes calldata data, | ||
address executor, | ||
uint256 gasLimit, | ||
uint256 nonce | ||
) external view returns (bytes32 hash) { | ||
hash = _getHash(data, executor, gasLimit, nonce); | ||
} | ||
|
||
function getNonce() external view returns (uint256 nonce) { | ||
nonce = _nonce; | ||
} | ||
|
||
function getOwners() external view returns (address[] memory owners) { | ||
owners = _owners; | ||
} | ||
|
||
function isOwner(address account) external view returns (bool owner) { | ||
owner = _isOwner[account]; | ||
} | ||
|
||
function getThreshold() external pure returns (uint256 threshold) { | ||
threshold = _THRESHOLD; | ||
} | ||
|
||
function getDestination() external pure returns (address destination) { | ||
destination = _DESTINATION; | ||
} | ||
|
||
// Note: addresses recovered from signatures must be strictly increasing. | ||
function execute( | ||
bytes calldata data, | ||
address executor, | ||
uint256 gasLimit, | ||
bytes calldata signatures | ||
) external returns (bool success, bytes memory returnData) { | ||
require( | ||
executor == msg.sender || executor == address(0), | ||
"Must call from the executor account if one is specified." | ||
); | ||
|
||
// Derive the message hash and wrap in the eth signed messsage hash. | ||
bytes32 hash = _toEthSignedMessageHash( | ||
_getHash(data, executor, gasLimit, _nonce) | ||
); | ||
|
||
// Recover each signer from the provided signatures. | ||
address[] memory signers = _recoverGroup(hash, signatures); | ||
|
||
require(signers.length == _THRESHOLD, "Total signers must equal threshold."); | ||
|
||
// Verify that each signatory is an owner and is strictly increasing. | ||
address lastAddress = address(0); // cannot have address(0) as an owner | ||
for (uint256 i = 0; i < signers.length; i++) { | ||
require( | ||
_isOwner[signers[i]], "Signature does not correspond to an owner." | ||
); | ||
require( | ||
signers[i] > lastAddress, "Signer addresses must be strictly increasing." | ||
); | ||
lastAddress = signers[i]; | ||
} | ||
|
||
// Increment the nonce and execute the transaction. | ||
_nonce++; | ||
(success, returnData) = _DESTINATION.call.gas(gasLimit)(data); | ||
} | ||
|
||
function _getHash( | ||
bytes memory data, | ||
address executor, | ||
uint256 gasLimit, | ||
uint256 nonce | ||
) internal view returns (bytes32 hash) { | ||
// Note: this is the data used to create a personal signed message hash. | ||
hash = keccak256( | ||
abi.encodePacked(address(this), nonce, executor, gasLimit, data) | ||
); | ||
setupSafe(owners, threshold, address(0), ""); | ||
} | ||
|
||
/** | ||
* @dev Returns each address that signed a hashed message (`hash`) from a | ||
* collection of `signatures`. | ||
* | ||
* The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: | ||
* this function rejects them by requiring the `s` value to be in the lower | ||
* half order, and the `v` value to be either 27 or 28. | ||
* | ||
* NOTE: This call _does not revert_ if a signature is invalid, or if the | ||
* signer is otherwise unable to be retrieved. In those scenarios, the zero | ||
* address is returned for that signature. | ||
* | ||
* IMPORTANT: `hash` _must_ be the result of a hash operation for the | ||
* verification to be secure: it is possible to craft signatures that recover | ||
* to arbitrary addresses for non-hashed data. | ||
*/ | ||
function _recoverGroup( | ||
bytes32 hash, | ||
bytes memory signatures | ||
) internal pure returns (address[] memory signers) { | ||
// Ensure that the signatures length is a multiple of 65. | ||
if (signatures.length % 65 != 0) { | ||
return new address[](0); | ||
} | ||
|
||
// Create an appropriately-sized array of addresses for each signer. | ||
signers = new address[](signatures.length / 65); | ||
|
||
// Get each signature location and divide into r, s and v variables. | ||
bytes32 signatureLocation; | ||
bytes32 r; | ||
bytes32 s; | ||
uint8 v; | ||
|
||
for (uint256 i = 0; i < signers.length; i++) { | ||
assembly { | ||
signatureLocation := add(signatures, mul(i, 65)) | ||
r := mload(add(signatureLocation, 32)) | ||
s := mload(add(signatureLocation, 64)) | ||
v := byte(0, mload(add(signatureLocation, 96))) | ||
} | ||
|
||
// EIP-2 still allows signature malleability for ecrecover(). Remove | ||
// this possibility and make the signature unique. | ||
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { | ||
continue; | ||
} | ||
|
||
if (v != 27 && v != 28) { | ||
continue; | ||
} | ||
|
||
// If signature is valid & not malleable, add signer address. | ||
signers[i] = ecrecover(hash, v, r, s); | ||
} | ||
} | ||
|
||
function _toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { | ||
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); | ||
} | ||
} |
Oops, something went wrong.