Secret Vault on Aptos

First Flight #46
Beginner FriendlyWallet
100 EXP
View results
Submission Details
Severity: medium
Valid

Hardcoded Owner Address Creates Deployment Inflexibility and Business Limitations

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); // @> Hardcoded @owner from Move.toml
let vault = borrow_global<Vault>(@owner); // @> Always reads from @owner address
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); // @0xAAA
let deployer2_addr = signer::address_of(deployer2); // @0xBBB
debug::print(&b"Current @owner is:");
debug::print(&@owner); // Shows @0xcc from Move.toml
debug::print(&b"Deployer 1 address:");
debug::print(&deployer1_addr); // Shows @0xaaa
debug::print(&b"Deployer 2 address:");
debug::print(&deployer2_addr); // Shows @0xbbb
if (deployer1_addr != @owner) { // @0xAAA != @0xcc
debug::print(&b"Deployer 1 CANNOT use their own deployment!");
};
if (deployer2_addr != @owner) { // @0xBBB != @0xcc
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;
+ }
Updates

Lead Judging Commences

bube Lead Judge
15 days ago
bube Lead Judge 14 days ago
Submission Judgement Published
Validated
Assigned finding tags:

The protocol doesn't work as intended

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.