Skip to content

EN-33 Add role management, ownership transfer, and contract upgradeability#2

Open
marwen-abid wants to merge 1 commit intodevelopfrom
feat/EN-33-edit-owner-and-pauser
Open

EN-33 Add role management, ownership transfer, and contract upgradeability#2
marwen-abid wants to merge 1 commit intodevelopfrom
feat/EN-33-edit-owner-and-pauser

Conversation

@marwen-abid
Copy link
Copy Markdown

  • Add get_owner() and get_pauser() query functions to expose the two top-level roles that were previously only readable internally.
  • Add set_owner() for single-signer ownership transfer, gated by current owner auth. Not blocked by pause state.
  • Add set_pauser() with dual-auth: callable by the current owner or the current pauser. When the owner calls it, the pause guard is skipped — this is the critical recovery path for a compromised pauser key that has frozen the contract. When the pauser calls it, require_not_paused is enforced.
  • Add upgrade() on the factory (owner-gated) to replace WASM bytecode in-place via Soroban's native update_current_contract_wasm.
  • Add upgrade() on the issuer contract (factory-gated) and upgrade_issuer() on the factory so the owner can upgrade deployed issuer contracts through the factory.
  • New audit events: OwnerUpdated, PauserUpdated, ContractUpgraded, IssuerUpgraded.

Copy link
Copy Markdown

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 explicit role management (owner/pauser), controlled role rotation, and upgradeability for the factory and deployed issuer contracts, along with new audit events to improve observability of administrative actions.

Changes:

  • Added get_owner / get_pauser queries plus set_owner and dual-authority set_pauser rotation logic on the factory.
  • Added upgrade entrypoints: upgrade (factory self-upgrade), upgrade_issuer (factory-triggered issuer upgrades), and upgrade on the issuer (factory-gated).
  • Added new audit events (OwnerUpdated, PauserUpdated, ContractUpgraded, IssuerUpgraded) and expanded test coverage for role transitions and upgrade authorization.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
contracts/issuer/src/lib.rs Adds factory-gated in-place WASM upgrade entrypoint for issuer contracts.
contracts/factory/src/lib.rs Adds role query/rotation APIs and factory + issuer upgrade entrypoints; emits new audit events.
contracts/factory/src/events.rs Defines new contract events for role updates and upgrades.
contracts/factory/src/test.rs Adds tests for new role APIs and upgrade authorization/success paths.
contracts/factory/Makefile Adjusts the expected location of the built factory WASM artifact.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +587 to +595
let issuer = IssuerClient::new(&env, &issuer_contract_address);
issuer.upgrade(&new_wasm_hash);

IssuerUpgraded {
issuer_id,
token,
new_wasm_hash,
}
.publish(&env);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

upgrade_issuer emits an IssuerUpgraded audit event, but current tests only cover auth and success/failure paths and do not verify the event payload. Consider adding an event assertion to ensure issuer_id, token, and new_wasm_hash are logged correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +519 to +537
/// # Arguments
/// * `env` - Contract environment.
/// * `caller` - Address invoking the operation (must be the current owner or pauser).
/// * `new_pauser` - Address to receive the pauser role.
///
/// # Authorization
/// Requires authorization from `caller`, who must be the current owner or pauser.
pub fn set_pauser(env: Env, caller: Address, new_pauser: Address) {
let current_owner = owner(&env);
let current_pauser = pauser(&env);

if caller == current_owner {
caller.require_auth();
} else if caller == current_pauser {
require_not_paused(&env);
caller.require_auth();
} else {
panic_with_error!(&env, FactoryError::NotAuthorized);
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

set_pauser requires the caller to pass a caller argument, and the function’s behavior (pause guard bypass vs. enforced) depends on that parameter rather than the actual invoker. This is easy for clients to misuse and allows a transaction signed by the owner to bypass the pause guard even if the invoker is the pauser (or vice versa). Consider deriving the caller from the environment (e.g., the immediate invoker) and branching based on whether that invoker equals the stored owner or pauser, then requiring auth for that same invoker.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We can split it into two functions, or remove caller and check auth for both roles.

build:
stellar contract build
@ls -l target/wasm32v1-none/release/*.wasm
@ls -l ../target/wasm32v1-none/release/factory.wasm
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The Makefile now checks for the built WASM at ../target/.../factory.wasm, but other build-time references in this crate (e.g., contract WASM imports in tests) are relative to a target directory under contracts/factory. Unless the build pipeline has been switched to a workspace-level target dir, this path change can make make build report a missing artifact even when the build succeeded. Align the ls path with the actual target dir used by your build/test tooling.

Suggested change
@ls -l ../target/wasm32v1-none/release/factory.wasm
@ls -l target/wasm32v1-none/release/factory.wasm

Copilot uses AI. Check for mistakes.
Comment on lines +556 to +563
pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) {
owner(&env).require_auth();

env.deployer()
.update_current_contract_wasm(new_wasm_hash.clone());

ContractUpgraded { new_wasm_hash }.publish(&env);
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

upgrade emits a ContractUpgraded audit event, but there is no test asserting that the event is emitted with the expected WASM hash. Adding an event assertion (similar to the OwnerUpdated / PauserUpdated tests) would help prevent regressions in audit logging.

Copilot uses AI. Check for mistakes.
Comment on lines +500 to +511
pub fn set_owner(env: Env, new_owner: Address) {
let current_owner = owner(&env);
current_owner.require_auth();

storage::set_owner(&env, &new_owner);

OwnerUpdated {
old_owner: current_owner,
new_owner,
}
.publish(&env);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

set_owner only requires auth from the current owner. If an incorrect new_owner is passed, ownership is irrevocably lost.

Bridge's own Solana contract (update_admin) mitigates this by requiring both the current and new admin to sign the same transaction. The equivalent here would be adding new_owner.require_auth()

Just something to consider.

Comment on lines +84 to +89
let factory: Address = env
.storage()
.instance()
.get(&DataKey::Factory)
.expect("factory not set");
factory.require_auth();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Worth extracting into a helper function?

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.

3 participants