Root + Impact
Description
Normal Behavior
The secret vault should provide a coherent storage model where users can store and retrieve their secrets using consistent address targeting for both read and write operations.
Issue
The contract has a fundamental read/write address mismatch that makes the storage system completely incoherent. The set_secret
function stores data under the caller's address, while get_secret
always reads from the hardcoded @owner
address. This creates a situation where most users can write data they can never read, and the contract fails to implement any logical storage model.
public entry fun set_secret(caller: &signer, secret: vector<u8>) {
let secret_vault = Vault{secret: string::utf8(secret)};
move_to(caller, secret_vault);
event::emit(SetNewSecret {});
}
#[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 non-owner user who calls set_secret
creates unreachable data
The address mismatch occurs on every write operation by non-owners
Contract deployment immediately creates this inconsistent state
No mechanism exists to resolve the mismatch
Impact:
Complete failure of core contract functionality for non-owner users
Data loss - users lose access to their stored secrets permanently
Business logic breakdown - contract violates basic read/write consistency
Unusable contract - primary use case (storing/retrieving secrets) is broken
Resource waste - users pay gas for operations that create inaccessible data
Proof of Concept
The following test demonstrates how the address mismatch breaks the contract's functionality:
#[test(owner = @0xcc, user_a = @0x111, user_b = @0x222)]
fun test_read_write_address_mismatch(owner: &signer, user_a: &signer, user_b: &signer) acquires Vault {
use aptos_framework::account;
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(user_a));
account::create_account_for_test(signer::address_of(user_b));
let user_a_addr = signer::address_of(user_a);
let user_b_addr = signer::address_of(user_b);
let user_a_secret = b"user_a_private_data";
set_secret(user_a, user_a_secret);
let user_b_secret = b"user_b_private_data";
set_secret(user_b, user_b_secret);
let vault_a = borrow_global<Vault>(user_a_addr);
let vault_b = borrow_global<Vault>(user_b_addr);
assert!(vault_a.secret == string::utf8(user_a_secret), 301);
assert!(vault_b.secret == string::utf8(user_b_secret), 302);
}
#[test(user = @0x333)]
#[expected_failure]
fun test_get_secret_fails_on_mismatch(user: &signer) acquires Vault {
set_secret(user, b"my_secret");
get_secret(signer::address_of(user));
}
Recommended Mitigation
Choose one consistent storage model and implement it properly:
public entry fun set_secret(caller: &signer, secret: vector<u8>) {
+ assert!(signer::address_of(caller) == @owner, NOT_OWNER); // Only owner can write
let secret_vault = Vault{secret: string::utf8(secret)};
- move_to(caller, secret_vault);
+ move_to(@owner, secret_vault); // Always store under @owner
event::emit(SetNewSecret {});
}
#[view]
public fun get_secret(caller: address): String acquires Vault {
assert!(caller == @owner, NOT_OWNER);
let vault = borrow_global<Vault>(@owner); // Consistent with storage
vault.secret
}