Skip to content

Conversation

paolodamico
Copy link
Contributor

@paolodamico paolodamico commented Aug 27, 2025

Changes

  • Enables key rotation by calling swapOwner on the Safe contract to rotate the owner of the Safe.
  • Refactors as_execute_user_op_call_data in the Is4337Encodable as for types of transactions this code will be exactly the same.
  • Introduces an on-chain integration test to swap the Safe owner and a generic mock RPC HTTP client for other integration tests.

⚠️ Breaking Changes

  • Renames transaction_transfer to tx_transfer. Less verbosity, especially as we introduce other types of transactions.

🔜 Changes coming soon

  • Rotating owners in Optimism too

Notes

  • Tests assisted by AI (manually reviewed)

@paolodamico paolodamico marked this pull request as ready for review August 28, 2025 21:34
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

@paolodamico paolodamico requested a review from Copilot August 28, 2025 21:42
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces owner rotation functionality for Safe smart accounts by implementing a new tx_swap_safe_owner method that enables key rotation through the Safe contract's swapOwner function. The implementation includes comprehensive integration tests and refactors existing code for better reusability.

  • Adds Safe owner swap functionality with SafeOwner transaction type and tx_swap_safe_owner method
  • Refactors Is4337Encodable trait to provide shared as_execute_user_op_call_data implementation
  • Renames transaction_transfer to tx_transfer for consistency and brevity

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
bedrock/src/transaction/mod.rs Exports SafeOwner and adds tx_swap_safe_owner method to SafeSmartAccount
bedrock/src/transaction/contracts/safe_owner.rs Implements SafeOwner struct and Is4337Encodable trait for owner swap functionality
bedrock/src/transaction/contracts/mod.rs Adds safe_owner module export
bedrock/src/transaction/contracts/erc20.rs Refactors to use new Is4337Encodable trait methods
bedrock/src/smart_account/transaction_4337.rs Refactors Is4337Encodable trait with shared implementation
bedrock/src/smart_account/nonce.rs Adds SwapOwner transaction type ID
bedrock/tests/test_smart_account_safe_owner_swap.rs Comprehensive integration test for Safe owner swap functionality
bedrock/tests/common.rs Adds AnvilBackedHttpClient mock for testing 4337 operations
bedrock/tests/test_smart_account_transfer.rs Updates method name from transaction_transfer to tx_transfer

sol! {
///Owner Manager Interface for the Safe
///
/// Reference: <https://github.com/safe-global/safe-smart-account/blob/v1.4.1/contracts/base/OwnerManager.sol>
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm assuming there is no difference here between 1.3.0 and 1.4.1 OwnerManager? Just wanted to ask


/// Represents a Safe owner swap transaction for key rotation.
pub struct SafeOwner {
/// The inner call data for the ERC-20 `transferCall` function.
Copy link
Collaborator

Choose a reason for hiding this comment

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

comment is wrong

/// - `new_owner`: The new owner to replace the old owner
#[must_use]
pub fn new(
wallet_address: Address,
Copy link
Collaborator

Choose a reason for hiding this comment

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

lets use safe_address, wallet_address is unclear

},
};

const SENTINEL_ADDRESS: Address =
Copy link
Collaborator

@karankurbur karankurbur Aug 29, 2025

Choose a reason for hiding this comment

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

Can you add a doc here on what this magic address is

) -> Self {
Self {
call_data: IOwnerManager::swapOwnerCall {
prev_owner: SENTINEL_ADDRESS,
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this always the case? I'm not familiar with this logic in the contract

What if a user rotates for the 2nd/3rd time

}
.abi_encode()
.into()
fn target_address(&self) -> Address {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Better to define these fns on the Is4337Encodable instead of duplicating inside each impl?

/// - Will throw an RPC error if the transaction submission fails.
/// - Will throw an error if the global HTTP client has not been initialized.
pub async fn transaction_transfer(
pub async fn tx_transfer(
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: lets remove tx_ prefix? Its already on the safe_account object

/// # Errors
/// - Will throw a parsing error if any of the provided attributes are invalid.
/// - Will throw an RPC error if the transaction submission fails.
pub async fn tx_swap_safe_owner(
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: swap_owner?

/// This is used to allow key rotation. The EOA signer that can act on behalf of the Safe is rotated.
///
/// # Arguments
/// - `old_owner`: The EOA of the old owner (address).
Copy link
Collaborator

Choose a reason for hiding this comment

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

we can fetch this from on-chain instead of having to take in as input

Copy link
Collaborator

Choose a reason for hiding this comment

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

do we need anything to enforce that this only done for world app safes? Ie check that threshold/signers length is 1


// Create SafeSmartAccount instance
let safe_account =
SafeSmartAccount::new(initial_owner_key_hex, &safe_address.to_string())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we make the parameter be Address instead of hex?

* refactors

* improvements

* clarify for tests
prev_owner: SENTINEL_ADDRESS,
old_owner,
new_owner,
}
Copy link

Choose a reason for hiding this comment

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

Bug: Incorrect prev_owner Handling in SafeOwner::new

The SafeOwner::new function hardcodes prev_owner to SENTINEL_ADDRESS (0x1) for the swapOwner call. This is only correct for single-owner Safes or the first owner swap. For multi-owner Safes or subsequent rotations, prev_owner must be the actual preceding owner, causing transactions to fail on-chain.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants