Root + Impact
Description
Normal Behavior
Smart contracts should implement flexible ownership models where the deployer becomes the initial owner, ownership can be transferred between addresses, and the contract works immediately upon deployment without requiring specific hardcoded addresses.
Issue
The Secret Vault contract uses a hardcoded @owner
address from Move.toml for all ownership checks, creating severe deployment and business inflexibility. This design prevents most deployers from using their own deployments and eliminates any possibility of ownership transfer or dynamic ownership management.
#[view]
public fun get_secret(caller: address):String acquires Vault{
assert!(caller == @owner,NOT_OWNER);
let vault = borrow_global<Vault>(@owner);
vault.secret
}
Risk
Likelihood:
Every deployment suffers from this issue
99% of deployers will have addresses different from @owner
No workaround exists without recompilation
Affects all real-world deployment scenarios
Impact:
Deployment unusability: Most deployments are immediately unusable
Business inflexibility: Cannot transfer or sell contracts
Poor developer experience: Requires understanding Move.toml configuration
Vendor lock-in: Tied to specific hardcoded address
No succession planning: Cannot handle ownership changes
Economic waste: Gas spent on unusable deployments
Proof of Concept
The following tests demonstrate the hardcoded owner vulnerability:
#[test(deployer1 = @0xAAA, deployer2 = @0xBBB)]
fun test_hardcoded_owner_inflexibility(deployer1: &signer, deployer2: &signer) {
let deployer1_addr = signer::address_of(deployer1);
let deployer2_addr = signer::address_of(deployer2);
debug::print(&b"Current @owner is:");
debug::print(&@owner);
debug::print(&b"Deployer 1 address:");
debug::print(&deployer1_addr);
debug::print(&b"Deployer 2 address:");
debug::print(&deployer2_addr);
if (deployer1_addr != @owner) {
debug::print(&b"Deployer 1 CANNOT use their own deployment!");
};
if (deployer2_addr != @owner) {
debug::print(&b"Deployer 2 CANNOT use their own deployment!");
};
}
Recommended Mitigation
Implement dynamic ownership with init_module
:
+ struct ContractInfo has key {
+ owner: address,
+ }
+ fun init_module(deployer: &signer) {
+ move_to(deployer, ContractInfo {
+ owner: signer::address_of(deployer)
+ });
+ }
public fun get_secret(caller: address): String acquires Vault, ContractInfo {
- assert!(caller == @owner, NOT_OWNER);
+ let contract_info = borrow_global<ContractInfo>(@secret_vault);
+ assert!(caller == contract_info.owner, NOT_OWNER);
- let vault = borrow_global<Vault>(@owner);
+ let vault = borrow_global<Vault>(contract_info.owner);
vault.secret
}
+ public entry fun transfer_ownership(
+ current_owner: &signer,
+ new_owner: address
+ ) acquires ContractInfo {
+ let contract_info = borrow_global_mut<ContractInfo>(@secret_vault);
+ assert!(signer::address_of(current_owner) == contract_info.owner, NOT_OWNER);
+ contract_info.owner = new_owner;
+ }