Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/contract/.gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[submodule "packages/contract/lib/forge-std"]
path = packages/contract/lib/forge-std
url = https://github.com/foundry-rs/forge-std
branch = v1.4.0
[submodule "packages/contract/lib/openzeppelin-contracts"]
path = packages/contract/lib/openzeppelin-contracts
url = https://github.com/Openzeppelin/openzeppelin-contracts
branch = v4.8.1
[submodule "packages/contract/lib/account-abstraction"]
path = packages/contract/lib/account-abstraction
url = https://github.com/eth-infinitism/account-abstraction
branch = ver0.6.0
[submodule "packages/contract/lib/openzeppelin-contracts-upgradeable"]
path = packages/contract/lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
[submodule "packages/contract/lib/p256-verifier"]
path = packages/contract/lib/p256-verifier
url = https://github.com/daimo-eth/p256-verifier

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

142 changes: 142 additions & 0 deletions packages/contract/broadcast/DeployVerifier.s.sol/1/run-latest.json

Large diffs are not rendered by default.

144 changes: 144 additions & 0 deletions packages/contract/broadcast/DeployVerifier.s.sol/10/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

127 changes: 63 additions & 64 deletions packages/contract/broadcast/DeployVerifier.s.sol/8453/run-latest.json

Large diffs are not rendered by default.

107 changes: 53 additions & 54 deletions packages/contract/broadcast/DeployVerifier.s.sol/84532/run-latest.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/contract/lib/account-abstraction
2 changes: 1 addition & 1 deletion packages/contract/script/DeployAccountFactory.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ contract DeployScript is Script {
0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
);

DaimoAccountFactory factory = new DaimoAccountFactory{salt: 0}(
DaimoAccountFactory factory = new DaimoAccountFactory{salt: keccak256("splits.damioaccountfactory.v0.1")}(
entryPoint,
verifierProxy
);
Expand Down
8 changes: 7 additions & 1 deletion packages/contract/script/DeployTestAccount.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ contract DeployScript is Script {

DaimoAccount.Call[] memory calls = new DaimoAccount.Call[](0);

factory.createAccount(0, key, calls, 0);
uint8[] memory slots = new uint8[](1);
slots[0] = 0;

bytes32[2][] memory initKeys = new bytes32[2][](1);
initKeys[0] = key;

factory.createAccount(slots, initKeys, 1, calls, 0);

vm.stopBroadcast();
}
Expand Down
4 changes: 2 additions & 2 deletions packages/contract/script/DeployVerifier.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ contract DeployVerifierScript is Script {
vm.startBroadcast();

// Use CREATE2
address verifier = address(new DaimoVerifier{salt: 0}());
address verifier = address(new DaimoVerifier{salt: keccak256("splits.damioverifier.v0.1")}());
address initOwner = 0x8603fb56E2B6DeaF02F3e247110CEc6f4Cbb7C8F; // Daimo Ledger
new DaimoVerifierProxy{salt: 0}(
new DaimoVerifierProxy{salt: keccak256("splits.damioverifierproxy.v0.1")}(
address(verifier), // implementation
abi.encodeWithSelector(DaimoVerifier.init.selector, initOwner)
);
Expand Down
88 changes: 71 additions & 17 deletions packages/contract/src/DaimoAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import "./DaimoVerifier.sol";
/**
* Daimo ERC-4337 contract account.
*
* Implements a 1-of-n multisig with P256 keys. Supports key rotation.
* Implements a m-of-n multisig with P256 keys. Supports key rotation.
*/
contract DaimoAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
struct Call {
Expand All @@ -36,6 +36,9 @@ contract DaimoAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
/// Maximum number of signing keys
uint8 public immutable maxKeys = 20;

/// Required number of keys for valid signature
uint8 public signatureThreshold;

// Return value in case of signature failure, with no time-range.
// Equivalent to _packValidationData(true,0,0)
uint256 private constant _SIG_VALIDATION_FAILED = 1;
Expand All @@ -57,6 +60,9 @@ contract DaimoAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
bytes32[2] key
);

/// Emitted after setting a new threshold.
event ThresholdSet(uint8 threshold);

modifier onlySelf() {
require(msg.sender == address(this), "only self");
_;
Expand All @@ -79,23 +85,37 @@ contract DaimoAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
}

/// Initialize proxy contract storage.
/// @param slot the empty slot to use. Settable in case we need to use slot to signal key type.
/// @param key the initial signing key. All future calls and key rotations must be signed.
/// @param slots the empty slots to use. Settable in case we need to use slot to signal key type.
/// @param initKeys the initial signing keys. All future calls and key rotations must be signed.
/// @param threshold the signing threshold required for future requests
/// @param initCalls contract calls to execute during initialization.
function initialize(
uint8 slot,
bytes32[2] calldata key,
uint8[] calldata slots,
bytes32[2][] calldata initKeys,
uint8 threshold,
Call[] calldata initCalls
) public virtual initializer {
keys[slot] = key;
numActiveKeys = 1;
uint256 slotsLength = slots.length;
require(slotsLength == initKeys.length, "slots length and init keys length must match");
require(threshold > 0, "threshold must be at least 1");
require(threshold <= slotsLength, "threshold cannot be greater than number of signing keys");

for (uint256 i = 0; i < slotsLength;) {
keys[slots[i]] = initKeys[i];
emit SigningKeyAdded(this, slots[i], initKeys[i]);
unchecked {
i++;
}
}

numActiveKeys = uint8(slotsLength);
signatureThreshold = threshold;

for (uint256 i = 0; i < initCalls.length; i++) {
_call(initCalls[i].dest, initCalls[i].value, initCalls[i].data);
}

emit AccountInitialized(entryPoint);
emit SigningKeyAdded(this, slot, key);
}

/// Execute multiple transactions atomically.
Expand Down Expand Up @@ -151,7 +171,7 @@ contract DaimoAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
// for client-side usage
function signatureStruct(Signature memory sig) public {}

// Signature structure: [uint8 keySlot, uint8 signatureType, bytes signature]
// Signature structure: [uint8 numSignatures, uint8 keySlot, uint8 signatureType, bytes signature]
// - keySlot: 0-255
// - signature: abi.encode form of Signature struct
/// Validate any Daimo account signature, whether for a userop or ERC1271 user sig.
Expand All @@ -161,14 +181,36 @@ contract DaimoAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
) private view returns (bool) {
if (signature.length < 1) return false;

// First bit identifies the keySlot
uint8 keySlot = uint8(signature[0]);
// First bit identifies the number of signatures
uint8 numSignatures = uint8(signature[0]);
if (numSignatures < signatureThreshold) return false;

// TODO: this requires all signatures be valid. What
// if 2 sigs are required, 3 get passed in, and 1 is
// invalid. Should op succeed?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah wouldn't worry about it for now but probably for w/e we launch with we'd use a counter

uint256 offset = 1;
for (uint256 i = 0; i < numSignatures;) {
uint8 keySlot = uint8(signature[offset]);
Copy link

@wminshew wminshew Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[reposting from gh so it doesn't get lost] need to add check to make sure keys aren't re-used

pseudo code that ~works gas-efficiently since we don't have access to maps in memory:

        uint256 usedKeys;
        loop i = each used keyslot {
          uint256 bit = 1 << i;
          uint256 flipped = usedKeys ^= bit;
          if (flipped & bit == 0) revert Invalid();
        }

// TODO: is this even needed? can signatures vary in length?
Copy link

@wminshew wminshew Apr 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i assume but don't know that different kinds of signatures might have different lengths. all passkey sigs have the same length i think and i think maybe same as EOAs (since technically it's the same curve with different params..?) idk we'll figure out later

// TODO: best way to do this?
uint16 signatureLength = (uint16(uint8(signature[offset + 1])) << 8) + uint16(uint8(signature[offset + 2]));
Copy link

@wminshew wminshew Apr 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think you can do uint16(signature[offset+1:offset+3])


// If the keySlot is empty, this is an invalid key
uint256 x = uint256(keys[keySlot][0]);
uint256 y = uint256(keys[keySlot][1]);

bool isValid = verifier.verifySignature(message, signature[offset + 3:offset + 3 + signatureLength], x, y);
if (!isValid) {
return false;
}

// If the keySlot is empty, this is an invalid key
uint256 x = uint256(keys[keySlot][0]);
uint256 y = uint256(keys[keySlot][1]);
offset += 1 + 2 + signatureLength;
unchecked {
i++;
}
}

return verifier.verifySignature(message, signature, x, y);
return true;
}

/// ERC1271: validate a user signature, verifying a valid Daimo account
Expand Down Expand Up @@ -196,10 +238,13 @@ contract DaimoAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
// UserOp signature structure:
// - uint8 version
//
// v1: 1+6+1+(unknown) bytes
// v1: 1+6+1+ (unknown) bytes
// - uint48 validUntil
// - uint8 numSignatures
// - uint8 keySlot
// - uint16 signatureLength
// - bytes (type Signature) signature
// - ...

// In all cases, we'll be checking a signature & returning a result.
bytes memory messageToVerify;
Expand All @@ -214,7 +259,7 @@ contract DaimoAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
if (sigLength < 7) return _SIG_VALIDATION_FAILED;
uint48 validUntil = uint48(bytes6(userOp.signature[1:7]));

signature = userOp.signature[7:]; // keySlot, signature
signature = userOp.signature[7:]; // numSignatures, keySlot1, sigLength1, signature1, etc...
messageToVerify = abi.encodePacked(version, validUntil, userOpHash);
returnIfValid.validUntil = validUntil;
} else {
Expand Down Expand Up @@ -295,9 +340,18 @@ contract DaimoAccount is IAccount, UUPSUpgradeable, Initializable, IERC1271 {
function removeSigningKey(uint8 slot) public onlySelf {
require(keys[slot][0] != bytes32(0), "key does not exist");
require(numActiveKeys > 1, "cannot remove only signing key");
require(signatureThreshold < numActiveKeys, "must decrease threshold before removing a signing key");
bytes32[2] memory currentKey = keys[slot];
keys[slot] = [bytes32(0), bytes32(0)];
numActiveKeys--;
emit SigningKeyRemoved(this, slot, currentKey);
}

/// Set the signing threshold
function setThreshold(uint8 threshold) public onlySelf {
require(threshold > 0, "threshold must be at least 1");
require(threshold <= numActiveKeys, "threshold cannot be greater than max keys");
signatureThreshold = threshold;
emit ThresholdSet(threshold);
}
}
16 changes: 9 additions & 7 deletions packages/contract/src/DaimoAccountFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ contract DaimoAccountFactory {
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation.
*/
function createAccount(
uint8 keySlot,
bytes32[2] memory key,
uint8[] memory keySlots,
bytes32[2][] memory keys,
uint8 threshold,
DaimoAccount.Call[] calldata initCalls,
uint256 salt
) public payable returns (DaimoAccount ret) {
address addr = getAddress(keySlot, key, initCalls, salt);
address addr = getAddress(keySlots, keys, threshold, initCalls, salt);

// Prefund the account with msg.value
if (msg.value > 0) {
Expand All @@ -54,7 +55,7 @@ contract DaimoAccountFactory {
address(accountImplementation),
abi.encodeCall(
DaimoAccount.initialize,
(keySlot, key, initCalls)
(keySlots, keys, threshold, initCalls)
)
)
)
Expand All @@ -65,8 +66,9 @@ contract DaimoAccountFactory {
* Calculate the counterfactual address of this account as it would be returned by createAccount()
*/
function getAddress(
uint8 keySlot,
bytes32[2] memory key,
uint8[] memory keySlots,
bytes32[2][] memory keys,
uint8 threshold,
DaimoAccount.Call[] calldata initCalls,
uint256 salt
) public view returns (address) {
Expand All @@ -80,7 +82,7 @@ contract DaimoAccountFactory {
address(accountImplementation),
abi.encodeCall(
DaimoAccount.initialize,
(keySlot, key, initCalls)
(keySlots, keys, threshold, initCalls)
)
)
)
Expand Down
2 changes: 1 addition & 1 deletion packages/contract/src/DaimoVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ contract DaimoVerifier is OwnableUpgradeable, UUPSUpgradeable {
uint256 x,
uint256 y
) public view returns (bool) {
Signature memory sig = abi.decode(signature[1:], (Signature));
Signature memory sig = abi.decode(signature, (Signature));

return
WebAuthn.verifySignature({
Expand Down
40 changes: 34 additions & 6 deletions packages/contract/test/AccountFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,60 @@ contract AccountFactoryTest is Test {
EntryPoint public entryPoint;
DaimoAccountFactory public factory;
DaimoVerifier public verifier;
uint8[] slots;
bytes32[2][] initKeys;
bytes32[2] key1;

function setUp() public {
entryPoint = new EntryPoint();
verifier = new DaimoVerifier();
factory = new DaimoAccountFactory(entryPoint, verifier);

slots = new uint8[](1);
slots[0] = 0;
initKeys = new bytes32[2][](1);
key1 = [bytes32(0), bytes32(0)];
initKeys[0] = key1;
}

function testDeploy() public {
// invalid signing key, irrelevant here
bytes32[2] memory key1 = [bytes32(0), bytes32(0)];

// deploy account
DaimoAccount.Call[] memory calls = new DaimoAccount.Call[](0);
DaimoAccount acc = factory.createAccount{value: 0}(0, key1, calls, 42);
DaimoAccount acc = factory.createAccount{value: 0}(slots, initKeys, 1, calls, 42);
console.log("new account address:", address(acc));
assertEq(acc.numActiveKeys(), uint8(1));

// deploy again = just returns the existing address
// prefund still goes thru to the entrypoint contract
assertEq(entryPoint.getDepositInfo(address(acc)).deposit, 0);
DaimoAccount acc2 = factory.createAccount{value: 9}(0, key1, calls, 42);
DaimoAccount acc2 = factory.createAccount{value: 9}(slots, initKeys, 1, calls, 42);
assertEq(address(acc), address(acc2));
assertEq(entryPoint.getDepositInfo(address(acc)).deposit, 9);

// get the counterfactual address, should be same
address counterfactual = factory.getAddress(0, key1, calls, 42);
address counterfactual = factory.getAddress(slots, initKeys, 1, calls, 42);
assertEq(address(acc), counterfactual);
}

function testMultisigDeploy() public {
DaimoAccount.Call[] memory calls = new DaimoAccount.Call[](0);

uint8[] memory multisigSlots = new uint8[](3);
multisigSlots[0] = 0;
multisigSlots[1] = 1;
multisigSlots[2] = 2;

bytes32[2][] memory multisigKeys = new bytes32[2][](3);
bytes32[2] memory multisigKey0 = [bytes32(0), bytes32(0)];
bytes32[2] memory multisigKey1 = [bytes32(0), bytes32(0)];
bytes32[2] memory multisigKey2 = [bytes32(0), bytes32(0)];
multisigKeys[0] = multisigKey0;
multisigKeys[1] = multisigKey1;
multisigKeys[2] = multisigKey2;

DaimoAccount acc = factory.createAccount{value: 0}(multisigSlots, multisigKeys, 2, calls, 42);
console.log("new account address:", address(acc));
assertEq(acc.numActiveKeys(), uint8(3));
assertEq(acc.signatureThreshold(), uint8(2));
}
}
Loading