Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trampoline demo - two owner account #21

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Trampoline is a chrome extension boilerplate code to showcase your own Smart Contract Wallets with React 18 and Webpack 5 support.

> [!NOTE]
> This branch is the implementation of an account which has two owners.
> See [TwoOwnerAccount Contract](https://github.com/eth-infinitism/trampoline/blob/webauthn/contracts/WebauthnAccount.sol](https://github.com/eth-infinitism/trampoline/blob/trampoline-demo-two-owner/contracts/TwoOwnerAccount.sol)) for it's implementation.


## Installation and Running

### Steps:
Expand Down
66 changes: 66 additions & 0 deletions contracts/TwoOwnerAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

import '@account-abstraction/contracts/samples/SimpleAccount.sol';

/**
* Minimal BLS-based account that uses an aggregated signature.
* The account must maintain its own BLS public key, and expose its trusted signature aggregator.
* Note that unlike the "standard" SimpleAccount, this account can't be called directly
* (normal SimpleAccount uses its "signer" address as both the ecrecover signer, and as a legitimate
* Ethereum sender address. Obviously, a BLS public key is not a valid Ethereum sender address.)
*/
contract TwoOwnerAccount is SimpleAccount {
using ECDSA for bytes32;
address public ownerOne;
address public ownerTwo;

// The constructor is used only for the "implementation" and only sets immutable values.
// Mutable value slots for proxy accounts are set by the 'initialize' function.
constructor(IEntryPoint anEntryPoint) SimpleAccount(anEntryPoint) {}

/**
* The initializer for the BLSAccount instance.
* @param _ownerOne public key from a BLS keypair
* @param _ownerTwo public key from a BLS keypair
*/
function initialize(
address _ownerOne,
address _ownerTwo
) public virtual initializer {
super._initialize(address(0));
ownerOne = _ownerOne;
ownerTwo = _ownerTwo;
}

function _validateSignature(
UserOperation calldata userOp,
bytes32 userOpHash
) internal view override returns (uint256 validationData) {
(userOp, userOpHash);

bytes32 hash = userOpHash.toEthSignedMessageHash();

(bytes memory signatureOne, bytes memory signatureTwo) = abi.decode(
userOp.signature,
(bytes, bytes)
);

address recoveryOne = hash.recover(signatureOne);
address recoveryTwo = hash.recover(signatureTwo);

bool ownerOneCheck = ownerOne == recoveryOne;
bool ownerTwoCheck = ownerTwo == recoveryTwo;

if (ownerOneCheck && ownerTwoCheck) return 0;

return SIG_VALIDATION_FAILED;
}

function encodeSignature(
bytes memory signatureOne,
bytes memory signatureTwo
) public pure returns (bytes memory) {
return (abi.encode(signatureOne, signatureTwo));
}
}
70 changes: 70 additions & 0 deletions contracts/TwoOwnerAccountFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

import '@openzeppelin/contracts/utils/Create2.sol';
import '@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol';

import './TwoOwnerAccount.sol';

contract TwoOwnerAccountFactory {
TwoOwnerAccount public immutable accountImplementation;

constructor(IEntryPoint _entryPoint) {
accountImplementation = new TwoOwnerAccount(_entryPoint);
}

/**
* create an account, and return its address.
* returns the address even if the account is already deployed.
* Note that during UserOperation execution, this method is called only if the account is not deployed.
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
*/
function createAccount(
address _ownerOne,
address _ownerTwo,
uint256 salt
) public returns (TwoOwnerAccount ret) {
address addr = getAddress(_ownerOne, _ownerTwo, salt);
uint256 codeSize = addr.code.length;
if (codeSize > 0) {
return TwoOwnerAccount(payable(addr));
}
ret = TwoOwnerAccount(
payable(
new ERC1967Proxy{salt: bytes32(salt)}(
address(accountImplementation),
abi.encodeCall(
TwoOwnerAccount.initialize,
(_ownerOne, _ownerTwo)
)
)
)
);
}

/**
* calculate the counterfactual address of this account as it would be returned by createAccount()
*/
function getAddress(
address _ownerOne,
address _ownerTwo,
uint256 salt
) public view returns (address) {
return
Create2.computeAddress(
bytes32(salt),
keccak256(
abi.encodePacked(
type(ERC1967Proxy).creationCode,
abi.encode(
address(accountImplementation),
abi.encodeCall(
TwoOwnerAccount.initialize,
(_ownerOne, _ownerTwo)
)
)
)
)
);
}
}
6 changes: 4 additions & 2 deletions deploy/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { DeployFunction } from 'hardhat-deploy/types';
import config from '../src/exconfig';
import fs from 'fs';

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const accounts = await hre.getUnnamedAccounts();
await hre.deployments.deploy('Greeter', {
await hre.deployments.deploy('TwoOwnerAccountFactory', {
from: accounts[0],
deterministicDeployment: true,
args: ['Test'],
args: [config.network.entryPointAddress],
log: true,
});
};
Expand Down
78 changes: 57 additions & 21 deletions src/pages/Account/account-api/account-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ import {
UserOperationStruct,
} from '@account-abstraction/contracts';
import { arrayify, hexConcat } from 'ethers/lib/utils';

import Config from '../../../exconfig';
import { AccountApiParamsType, AccountApiType } from './types';
import { MessageSigningRequest } from '../../Background/redux-slices/signing';
import { TransactionDetailsForUserOp } from '@account-abstraction/sdk/dist/src/TransactionDetailsForUserOp';
import config from '../../../exconfig';
import {
TwoOwnerAccount,
TwoOwnerAccountFactory,
TwoOwnerAccountFactory__factory,
TwoOwnerAccount__factory,
} from './typechain-types';

const FACTORY_ADDRESS =
config.factory_address || '0x6C583EE7f3a80cB53dDc4789B0Af1aaFf90e55F3';
Config.factory_address || '0x6c0ec05Ad55C8B8427119ce50b6087E7B0C9c23e';

/**
* An implementation of the BaseAccountAPI using the SimpleAccount contract.
Expand All @@ -23,40 +28,51 @@ const FACTORY_ADDRESS =
* - nonce method is "nonce()"
* - execute method is "execFromEntryPoint()"
*/
class SimpleAccountAPI extends AccountApiType {
class TwoOwnerAccountAPI extends AccountApiType {
name: string;
factoryAddress?: string;
owner: Wallet;
ownerOne: Wallet;
ownerTwo: string;
index: number;

/**
* our account contract.
* should support the "execFromEntryPoint" and "nonce" methods
*/
accountContract?: SimpleAccount;
accountContract?: TwoOwnerAccount;

factory?: SimpleAccountFactory;
factory?: TwoOwnerAccountFactory;

constructor(params: AccountApiParamsType<{}, { privateKey: string }>) {
constructor(
params: AccountApiParamsType<
{ address: string },
{ privateKey: string; ownerTwo: string }
>
) {
super(params);
this.factoryAddress = FACTORY_ADDRESS;

this.owner = params.deserializeState?.privateKey
this.ownerOne = params.deserializeState?.privateKey
? new ethers.Wallet(params.deserializeState?.privateKey)
: ethers.Wallet.createRandom();

this.ownerTwo = params.deserializeState?.ownerTwo
? params.deserializeState?.ownerTwo
: params.context?.address || '';
this.index = 0;
this.name = 'SimpleAccountAPI';
}

serialize = async (): Promise<{ privateKey: string }> => {
serialize = async (): Promise<{ privateKey: string; ownerTwo: string }> => {
return {
privateKey: this.owner.privateKey,
privateKey: this.ownerOne.privateKey,
ownerTwo: this.ownerTwo,
};
};

async _getAccountContract(): Promise<SimpleAccount> {
async _getAccountContract(): Promise<TwoOwnerAccount> {
if (this.accountContract == null) {
this.accountContract = SimpleAccount__factory.connect(
this.accountContract = TwoOwnerAccount__factory.connect(
await this.getAccountAddress(),
this.provider
);
Expand All @@ -71,7 +87,7 @@ class SimpleAccountAPI extends AccountApiType {
async getAccountInitCode(): Promise<string> {
if (this.factory == null) {
if (this.factoryAddress != null && this.factoryAddress !== '') {
this.factory = SimpleAccountFactory__factory.connect(
this.factory = TwoOwnerAccountFactory__factory.connect(
this.factoryAddress,
this.provider
);
Expand All @@ -82,7 +98,8 @@ class SimpleAccountAPI extends AccountApiType {
return hexConcat([
this.factory.address,
this.factory.interface.encodeFunctionData('createAccount', [
await this.owner.getAddress(),
await this.ownerOne.getAddress(),
this.ownerTwo,
this.index,
]),
]);
Expand Down Expand Up @@ -115,26 +132,45 @@ class SimpleAccountAPI extends AccountApiType {
]);
}

async signUserOpHash(userOpHash: string): Promise<string> {
return await this.owner.signMessage(arrayify(userOpHash));
async signOwnerOne(userOpHash: string): Promise<string> {
return await this.ownerOne.signMessage(arrayify(userOpHash));
}

signMessage = async (
context: any,
request?: MessageSigningRequest
): Promise<string> => {
return this.owner.signMessage(request?.rawSigningData || '');
return this.ownerOne.signMessage(request?.rawSigningData || '');
};

signUserOpWithContext = async (
userOp: UserOperationStruct,
context: any
context: { signedMessage: string }
): Promise<UserOperationStruct> => {
return {
...userOp,
signature: await this.signUserOpHash(await this.getUserOpHash(userOp)),
signature: ethers.utils.defaultAbiCoder.encode(
['bytes', 'bytes'],
[
await this.signOwnerOne(await this.getUserOpHash(userOp)),
context.signedMessage,
]
),
};
};

async createUnsignedUserOp(
info: TransactionDetailsForUserOp
): Promise<UserOperationStruct> {
const userOp = await super.createUnsignedUserOp(info);
await userOp.preVerificationGas;
userOp.preVerificationGas = Number(await userOp.preVerificationGas) * 2.5;
return userOp;
}

getUserOpHashToSign = async (userOp: UserOperationStruct) => {
return this.getUserOpHash(userOp);
};
}

export default SimpleAccountAPI;
export default TwoOwnerAccountAPI;
Loading