This repository has been archived by the owner on Sep 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #54 from getwax/safe-bls-module
Safe BLS module
- Loading branch information
Showing
5 changed files
with
321 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
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,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; | ||
} | ||
} |
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
197 changes: 197 additions & 0 deletions
197
account-integrations/safe/test/hardhat/SafeBlsPlugin.test.ts
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,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); | ||
}); | ||
}); |
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 |
---|---|---|
|
@@ -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" | ||
|
@@ -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" | ||
|
@@ -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== | ||
|
@@ -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" | ||
|