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

Safe Recovery Plugin #96

Merged
merged 15 commits into from
Sep 20, 2023
Merged
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
2 changes: 2 additions & 0 deletions account-integrations/safe/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Safe plugins

Please note, these plugins are in a pre-alpha state and are not ready for production use. In their current state, the plugins are meant for testing and experimentation.

# Getting Started

1. `cd account-integrations/safe`
Expand Down
4 changes: 2 additions & 2 deletions account-integrations/safe/src/SafeBlsPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ contract SafeBlsPlugin is BaseAccount {
return IEntryPoint(_entryPoint);
}

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

Expand Down Expand Up @@ -100,7 +100,7 @@ contract SafeBlsPlugin is BaseAccount {
* This function prevents using a “key” different from the first “zero” key.
* @param nonce to validate
*/
function _validateNonce(uint256 nonce) internal view override {
function _validateNonce(uint256 nonce) internal pure override {
if (nonce >= type(uint64).max) {
revert NONCE_NOT_SEQUENTIAL();
}
Expand Down
16 changes: 12 additions & 4 deletions account-integrations/safe/src/SafeECDSAPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ interface ISafe {
contract SafeECDSAPlugin is BaseAccount {
using ECDSA for bytes32;

address public immutable myAddress;
address private immutable _owner;
address public immutable myAddress; // Module address
address private _owner; // Key address
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it may be good to deploy this separately from the Safe so that we can store the safe address here to validate that transactions are coming from the right Safe.

Then we can initialize a variable like

address private immutable safeAddress;

address private immutable _entryPoint;

address internal constant _SENTINEL_MODULES = address(0x1);

error NONCE_NOT_SEQUENTIAL();
event OWNER_UPDATED(address indexed oldOwner, address indexed newOwner);

constructor(address entryPointAddress, address ownerAddress) {
myAddress = address(this);
Expand Down Expand Up @@ -71,10 +72,17 @@ contract SafeECDSAPlugin is BaseAccount {
return _owner;
}

function updateOwner(address newOwner) public {
// TODO: Check if msg.sender is the Safe. We can't do that now because the Safe
// and the plugin are deployed at the same time. So the plugin doesn't know the Safe address.
emit OWNER_UPDATED(_owner, newOwner);
_owner = newOwner;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Lets make sure the readme is clear with its state of "pre-alpha" and "not for production use" etc.


function _validateSignature(
UserOperation calldata userOp,
bytes32 userOpHash
) internal override returns (uint256 validationData) {
) internal view override returns (uint256 validationData) {
bytes32 hash = userOpHash.toEthSignedMessageHash();
if (_owner != hash.recover(userOp.signature))
return SIG_VALIDATION_FAILED;
Expand All @@ -86,7 +94,7 @@ contract SafeECDSAPlugin is BaseAccount {
* This function prevents using a “key” different from the first “zero” key.
* @param nonce to validate
*/
function _validateNonce(uint256 nonce) internal view override {
function _validateNonce(uint256 nonce) internal pure override {
if (nonce >= type(uint64).max) {
revert NONCE_NOT_SEQUENTIAL();
}
Expand Down
56 changes: 56 additions & 0 deletions account-integrations/safe/src/SafeECDSARecoveryPlugin.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Enum {
enum Operation {
Call,
DelegateCall
}
}

interface ISafe {
/// @dev Allows a Module to execute a Safe transaction without any further confirmations.
/// @param to Destination address of module transaction.
/// @param value Ether value of module transaction.
/// @param data Data payload of module transaction.
/// @param operation Operation type of module transaction.
function execTransactionFromModule(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation
) external returns (bool success);
}

contract SafeECDSARecoveryPlugin {
address private immutable storedEOA;
address public storedSafe;

error SENDER_NOT_STORED_EOA(address sender);
error ATTEMPTING_RESET_ON_WRONG_SAFE(address attemptedSafe);

constructor(address _safe, address _eoa) {
storedSafe = _safe;
storedEOA = _eoa;
}

modifier onlyStoredEOA {
if (msg.sender != storedEOA) {
revert SENDER_NOT_STORED_EOA(msg.sender);
}
_;
}

function resetEcdsaAddress(
address safe,
address ecdsaPluginAddress,
address newValidatingEcdsaAddress
) external onlyStoredEOA {
if (safe != storedSafe) {
revert ATTEMPTING_RESET_ON_WRONG_SAFE(safe);
}

bytes memory data = abi.encodeWithSignature("updateOwner(address)", newValidatingEcdsaAddress);
ISafe(safe).execTransactionFromModule(ecdsaPluginAddress, 0, data, Enum.Operation.Call);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Good start, will have to update and add to this, linking other verifications beyond onlyStoredEOA

}
136 changes: 136 additions & 0 deletions account-integrations/safe/test/hardhat/SafeECDSARecoveryPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { ethers } from "hardhat";
import { expect } from "chai";
import { HDNodeWallet, Provider } from "ethers";
import { setupTests, sendTx } from "./setupTests";

import { executeContractCallWithSigners } from "./utils/execution";

import { EntryPoint } from "../../typechain-types/lib/account-abstraction/contracts/core/EntryPoint";
import { SafeECDSAPlugin } from "../../typechain-types/src/SafeECDSAPlugin.sol/SafeECDSAPlugin";

const MNEMONIC = "test test test test test test test test test test test junk";

describe("SafeECDSARecoveryPlugin", () => {
let provider: Provider;
let safeSigner: HDNodeWallet;
let entryPoint: EntryPoint;
let safeECDSAPlugin: SafeECDSAPlugin;
let safeCounterfactualAddress: string;

before(async () => {
const {
safeOwner,
entryPointContract,
safeEcdsaPluginContract,
counterfactualAddress,
} = await setupTests();

provider = ethers.provider;
safeSigner = safeOwner;
entryPoint = entryPointContract;
safeECDSAPlugin = safeEcdsaPluginContract;
safeCounterfactualAddress = counterfactualAddress;
});

it("Should enable a recovery plugin on a safe.", async () => {
const [, , recoverySigner] = await ethers.getSigners();

const recoveryPlugin = await (
await ethers.getContractFactory("SafeECDSARecoveryPlugin")
).deploy(safeCounterfactualAddress, recoverySigner.address);
const recoveryPluginAddress = await recoveryPlugin.getAddress();

// Enable recovery plugin on safe

const deployedSafe = await ethers.getContractAt(
"Safe",
safeCounterfactualAddress,
);
const isModuleEnabledBefore = await deployedSafe.isModuleEnabled(
recoveryPluginAddress,
);

await executeContractCallWithSigners(
deployedSafe,
deployedSafe,
"enableModule",
[recoveryPluginAddress],
// @ts-expect-error safeSigner doesn't have all properties for some reason
[safeSigner],
);

const isModuleEnabledAfter = await deployedSafe.isModuleEnabled(
recoveryPluginAddress,
);

expect(isModuleEnabledBefore).to.equal(false);
expect(isModuleEnabledAfter).to.equal(true);
});

it("Should use recovery plugin to reset signing key and then send tx with new key.", async () => {
// Setup recovery plugin

const [, , , recoverySigner] = await ethers.getSigners();

const recoveryPlugin = await (
await ethers.getContractFactory("SafeECDSARecoveryPlugin")
).deploy(safeCounterfactualAddress, recoverySigner.address);
const recoveryPluginAddress = await recoveryPlugin.getAddress();

const deployedSafe = await ethers.getContractAt(
"Safe",
safeCounterfactualAddress,
);

// Enable recovery plugin

await executeContractCallWithSigners(
deployedSafe,
deployedSafe,
"enableModule",
[recoveryPluginAddress],
// @ts-expect-error safeSigner doesn't have all properties for some reason
[safeSigner],
);

const isModuleEnabled = await deployedSafe.isModuleEnabled(
recoveryPluginAddress,
);
expect(isModuleEnabled).to.equal(true);

// Reset ecdsa address

const ecdsaPluginAddress = await safeECDSAPlugin.getAddress();
const newEcdsaPluginSigner = ethers.Wallet.createRandom().connect(provider);

const recoveryPluginSinger = recoveryPlugin.connect(recoverySigner);

await recoveryPluginSinger.resetEcdsaAddress(
await deployedSafe.getAddress(),
ecdsaPluginAddress,
newEcdsaPluginSigner.address,
);

// Send tx with new key

const recipientAddress = ethers.Wallet.createRandom().address;
const transferAmount = ethers.parseEther("1");
const userOpCallData = safeECDSAPlugin.interface.encodeFunctionData(
"execTransaction",
[recipientAddress, transferAmount, "0x00"],
);
const recipientBalanceBefore = await provider.getBalance(recipientAddress);
await sendTx(
newEcdsaPluginSigner,
entryPoint,
safeCounterfactualAddress,
"0x1",
"0x",
userOpCallData,
);

const recipientBalanceAfter = await provider.getBalance(recipientAddress);
const expectedRecipientBalance = recipientBalanceBefore + transferAmount;
expect(recipientBalanceAfter).to.equal(expectedRecipientBalance);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ describe("SafeWebAuthnPlugin", () => {
const getPublicKeyAndSignature = () => {
const publicKey: [BigNumberish, BigNumberish] = [
BigInt(
"114874632398302156264159990279427641021947882640101801130664833947273521181002"
"114874632398302156264159990279427641021947882640101801130664833947273521181002",
),
BigInt(
"32136952818958550240756825111900051564117520891182470183735244184006536587423"
"32136952818958550240756825111900051564117520891182470183735244184006536587423",
),
];

Expand All @@ -80,10 +80,10 @@ describe("SafeWebAuthnPlugin", () => {
const clientChallengeDataOffset = 36;
const signature: [BigNumberish, BigNumberish] = [
BigInt(
"45847212378479006099766816358861726414873720355505495069909394794949093093607"
"45847212378479006099766816358861726414873720355505495069909394794949093093607",
),
BigInt(
"55835259151215769394881684156457977412783812617123006733908193526332337539398"
"55835259151215769394881684156457977412783812617123006733908193526332337539398",
),
];

Expand Down
Loading