Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Commit

Permalink
Merge pull request #54 from getwax/safe-bls-module
Browse files Browse the repository at this point in the history
Safe BLS module
  • Loading branch information
JohnGuilding authored Aug 29, 2023
2 parents 4c3274c + 7161132 commit 5a98d15
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 1 deletion.
1 change: 1 addition & 0 deletions account-integrations/safe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
"@nomicfoundation/hardhat-toolbox": "^3.0.0",
"@nomicfoundation/hardhat-verify": "^1.0.0",
"@thehubbleproject/bls": "^0.5.1",
"@typechain/ethers-v6": "^0.4.0",
"@typechain/hardhat": "^8.0.0",
"@types/chai": "^4.2.0",
Expand Down
101 changes: 101 additions & 0 deletions account-integrations/safe/src/SafeBlsPlugin.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

import {BaseAccount} from "account-abstraction/contracts/core/BaseAccount.sol";
import {IEntryPoint, UserOperation} from "account-abstraction/contracts/interfaces/IEntryPoint.sol";
import {BLS} from "account-abstraction/contracts/samples/bls/lib/hubble-contracts/contracts/libs/BLS.sol";

interface ISafe {
function enableModule(address module) external;

function execTransactionFromModule(
address to,
uint256 value,
bytes memory data,
uint8 operation
) external returns (bool success);
}

contract SafeBlsPlugin is BaseAccount {
// TODO: Use EIP 712 for domain separation
bytes32 public constant BLS_DOMAIN = keccak256("eip4337.bls.domain");
address public immutable myAddress;
uint256[4] private _blsPublicKey;
address private immutable _entryPoint;

address internal constant _SENTINEL_MODULES = address(0x1);

constructor(address entryPointAddress, uint256[4] memory blsPublicKey) {
myAddress = address(this);
_blsPublicKey = blsPublicKey;
_entryPoint = entryPointAddress;
}

function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external override returns (uint256 validationData) {
address payable safeAddress = payable(userOp.sender);
ISafe senderSafe = ISafe(safeAddress);

if (missingAccountFunds != 0) {
senderSafe.execTransactionFromModule(
_entryPoint,
missingAccountFunds,
"",
0
);
}

validationData = _validateSignature(userOp, userOpHash);
}

function execTransaction(
address to,
uint256 value,
bytes calldata data
) external payable {
address payable safeAddress = payable(msg.sender);
ISafe safe = ISafe(safeAddress);
require(
safe.execTransactionFromModule(to, value, data, 0),
"tx failed"
);
}

function enableMyself() public {
ISafe(address(this)).enableModule(myAddress);
}

function entryPoint() public view override returns (IEntryPoint) {
return IEntryPoint(_entryPoint);
}

function owner() public pure returns (uint256[4] memory _blsPublicKey) {
return _blsPublicKey;
}

function _validateSignature(
UserOperation calldata userOp,
bytes32 userOpHash
) internal view override returns (uint256 validationData) {
require(userOp.signature.length == 64, "VG: Sig bytes length must be 64");

uint256[2] memory decodedSignature = abi.decode(userOp.signature, (uint256[2]));

bytes memory hashBytes = abi.encodePacked(userOpHash);
uint256[2] memory message = BLS.hashToPoint(
BLS_DOMAIN,
hashBytes
);
(bool verified, bool callSuccess) = BLS.verifySingle(decodedSignature, _blsPublicKey, message);

if (verified && callSuccess) {
return 0;
}
// TODO: check if wallet recovered
return SIG_VALIDATION_FAILED;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ import {MultiSendCallOnly} from "safe-contracts/contracts/libraries/MultiSendCal
import {SignMessageLib} from "safe-contracts/contracts/libraries/SignMessageLib.sol";
import {SafeL2} from "safe-contracts/contracts/SafeL2.sol";
import {Safe} from "safe-contracts/contracts/Safe.sol";
import {EntryPoint} from "account-abstraction/contracts/core/EntryPoint.sol";
197 changes: 197 additions & 0 deletions account-integrations/safe/test/hardhat/SafeBlsPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { ethers } from "hardhat";
import { expect } from "chai";
import { AddressZero } from "@ethersproject/constants";
import { utils } from "ethers-v5";
import { UserOperationStruct } from "@account-abstraction/contracts";
import { calculateProxyAddress } from "../../utils/calculateProxyAddress";
import { signer as hubbleBlsSigner } from '@thehubbleproject/bls';
import { getUserOpHash } from "@account-abstraction/utils";

import { SafeProxyFactory } from "../../typechain-types/lib/safe-contracts/contracts/proxies/SafeProxyFactory";
import { Safe } from "../../typechain-types/lib/safe-contracts/contracts/Safe";
import { EntryPoint } from "../../typechain-types/lib/account-abstraction/contracts/core/EntryPoint";

const BLS_PRIVATE_KEY = '0xdbe3d601b1b25c42c50015a87855fdce00ea9b3a7e33c92d31c69aeb70708e08';
const MNEMONIC = "test test test test test test test test test test test junk";

let safeProxyFactory: SafeProxyFactory;
let safe: Safe;
let entryPoint: EntryPoint;

describe("SafeBlsPlugin", () => {
const setupTests = async () => {
safeProxyFactory = await (
await ethers.getContractFactory("SafeProxyFactory")
).deploy();
safe = await (
await ethers.getContractFactory("Safe")
).deploy();
entryPoint = await (
await ethers.getContractFactory("EntryPoint")
).deploy();

const provider = ethers.provider;
const userWallet = ethers.Wallet.fromPhrase(MNEMONIC).connect(
provider
);

return {
provider,
userWallet,
entryPoint,
safe,
safeProxyFactory,
};
};

/**
* This test verifies a ERC4337 transaction succeeds when sent via a plugin
* The user operation deploys a Safe with the ERC4337 plugin and a handler
* and executes a transaction, thus verifying two things:
* 1. Deployment of the Safe with the ERC4337 plugin and handler is possible
* 2. Executing a transaction is possible
*/
it("should pass the ERC4337 validation", async () => {
const {
provider,
userWallet,
entryPoint,
safe,
safeProxyFactory,
} = await setupTests();

const domain = utils.arrayify(utils.keccak256(Buffer.from('eip4337.bls.domain')));
const signerFactory = await hubbleBlsSigner.BlsSignerFactory.new();
const blsSigner = signerFactory.getSigner(domain, BLS_PRIVATE_KEY);

const ENTRYPOINT_ADDRESS = await entryPoint.getAddress();

const safeBlsPluginFactory = (
await ethers.getContractFactory("SafeBlsPlugin")
).connect(userWallet);
const safeBlsPlugin = await safeBlsPluginFactory.deploy(
ENTRYPOINT_ADDRESS,
blsSigner.pubkey,
{ gasLimit: 30_000_000 }
);

const feeData = await provider.getFeeData();
if (!feeData.maxFeePerGas || !feeData.maxPriorityFeePerGas) {
throw new Error(
"maxFeePerGas or maxPriorityFeePerGas is null or undefined"
);
}

const maxFeePerGas = "0x" + feeData.maxFeePerGas.toString();
const maxPriorityFeePerGas = "0x" + feeData.maxPriorityFeePerGas.toString();

const safeBlsPluginAddress = await safeBlsPlugin.getAddress();
const singletonAddress = await safe.getAddress();
const factoryAddress = await safeProxyFactory.getAddress();

const moduleInitializer = safeBlsPlugin.interface.encodeFunctionData(
"enableMyself",
[]
);
const encodedInitializer = safe.interface.encodeFunctionData("setup", [
[userWallet.address],
1,
safeBlsPluginAddress,
moduleInitializer,
safeBlsPluginAddress,
AddressZero,
0,
AddressZero,
]);

const deployedAddress = await calculateProxyAddress(
safeProxyFactory as any,
singletonAddress,
encodedInitializer,
73
);

// The initCode contains 20 bytes of the factory address and the rest is the calldata to be forwarded
const initCode = ethers.concat([
factoryAddress,
safeProxyFactory.interface.encodeFunctionData("createProxyWithNonce", [
singletonAddress,
encodedInitializer,
73,
]),
]);

const signer = new ethers.Wallet(
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
);
const recipientAddress = signer.address;
const transferAmount = ethers.parseEther("1");

const userOpCallData = safeBlsPlugin.interface.encodeFunctionData(
"execTransaction",
[recipientAddress, transferAmount, "0x00"]
);

// Native tokens for the pre-fund 💸
await userWallet.sendTransaction({
to: deployedAddress,
value: ethers.parseEther("100"),
});

const unsignedUserOperation: UserOperationStruct = {
sender: deployedAddress,
nonce: "0x0",
initCode,
callData: userOpCallData,
verificationGasLimit: 1e6,
callGasLimit: 1e6,
preVerificationGas: 1e6,
maxFeePerGas,
maxPriorityFeePerGas,
paymasterAndData: "0x",
signature: "",
};

const resolvedUserOp = await ethers.resolveProperties(unsignedUserOperation);
const userOpHash = getUserOpHash(
resolvedUserOp,
ENTRYPOINT_ADDRESS,
Number((await provider.getNetwork()).chainId)
);

// Create BLS signature of the userOpHash
const userOpSignature = await blsSigner.sign(userOpHash);

const userOperation = {
...unsignedUserOperation,
signature: utils.solidityPack(["uint256", "uint256"], userOpSignature),
};

// Uncomment to get a detailed debug message
// const DEBUG_MESSAGE = `
// Using entry point: ${ENTRYPOINT_ADDRESS}
// Deployed Safe address: ${deployedAddress}
// Module/Handler address: ${safeBlsPluginAddress}
// User operation:
// ${JSON.stringify(userOperation, null, 2)}
// `;
// console.log(DEBUG_MESSAGE);

const recipientBalanceBefore = await provider.getBalance(recipientAddress);

try {
const rcpt = await entryPoint
.handleOps(
[userOperation],
ENTRYPOINT_ADDRESS
)
} catch (e) {
console.log('EntryPoint handleOps error=', e);
}

const recipientBalanceAfter = await provider.getBalance(recipientAddress);
const expectedRecipientBalance = recipientBalanceBefore + transferAmount;

expect(recipientBalanceAfter).to.equal(expectedRecipientBalance);
});
});
22 changes: 21 additions & 1 deletion account-integrations/safe/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,14 @@
dependencies:
antlr4ts "^0.5.0-alpha.4"

"@thehubbleproject/bls@^0.5.1":
version "0.5.1"
resolved "https://registry.yarnpkg.com/@thehubbleproject/bls/-/bls-0.5.1.tgz#6b0565f56fc9c8896dcf3c8f0e2214b69a06167f"
integrity sha512-g5zeMZ8js/yg6MjFoC+pt0eqfCL2jC46yLY1LbKNriyqftB1tE3jpG/FMMDIW3x9/yRg/AgUb8Nluqj15tQs+A==
dependencies:
ethers "^5.5.3"
mcl-wasm "^1.0.0"

"@tsconfig/node10@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
Expand Down Expand Up @@ -941,6 +949,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==

"@types/node@^20.2.5":
version "20.5.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.6.tgz#5e9aaa86be03a09decafd61b128d6cec64a5fe40"
integrity sha512-Gi5wRGPbbyOTX+4Y2iULQ27oUPrefaB0PxGQJnfyWN3kvEDGM3mIB5M/gQLmitZf7A9FmLeaqxD3L1CXpm3VKQ==

"@types/node@^8.0.0":
version "8.10.66"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.66.tgz#dd035d409df322acc83dff62a602f12a5783bbb3"
Expand Down Expand Up @@ -2095,7 +2108,7 @@ ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.4:
ethereum-cryptography "^0.1.3"
rlp "^2.2.4"

"ethers-v5@npm:[email protected]", ethers@^5.7.0, ethers@^5.7.1:
"ethers-v5@npm:[email protected]", ethers@^5.5.3, ethers@^5.7.0, ethers@^5.7.1:
version "5.7.2"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e"
integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==
Expand Down Expand Up @@ -3321,6 +3334,13 @@ mcl-wasm@^0.7.1:
resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-0.7.9.tgz#c1588ce90042a8700c3b60e40efb339fc07ab87f"
integrity sha512-iJIUcQWA88IJB/5L15GnJVnSQJmf/YaxxV6zRavv83HILHaJQb6y0iFyDMdDO0gN8X37tdxmAOrH/P8B6RB8sQ==

mcl-wasm@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-1.3.0.tgz#10cd569c8448366a636c312af9207b2ed3f83674"
integrity sha512-7nChbr2sBEk9tSp7h8LGJp+bm2UfRzSZCV9LUD00ZPQCynT2W5dPBQcX27Nd++f3zMEl1nrgsT9OdHgxRdu7jw==
dependencies:
"@types/node" "^20.2.5"

md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
Expand Down

0 comments on commit 5a98d15

Please sign in to comment.