Secret Vault

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

Critical Read/Write Address Mismatch in Storage Operations

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); // @> Stores under caller's address
event::emit(SetNewSecret {});
}
#[view]
public fun get_secret(caller: address): String acquires Vault {
assert!(caller == @owner, NOT_OWNER);
let vault = borrow_global<Vault>(@owner); // @> Always reads from @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;
// Setup different user addresses
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); // @0x111
let user_b_addr = signer::address_of(user_b); // @0x222
// VULNERABILITY: Non-owners can write data they can't read
let user_a_secret = b"user_a_private_data";
set_secret(user_a, user_a_secret); // Stores under @0x111
let user_b_secret = b"user_b_private_data";
set_secret(user_b, user_b_secret); // Stores under @0x222
// Prove data exists under their addresses
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);
// VULNERABILITY: get_secret cannot access their data
// get_secret only reads from @owner (@0xcc), not from user addresses
// Users have no way to retrieve their stored secrets
}
#[test(user = @0x333)]
#[expected_failure] // Demonstrates get_secret failure
fun test_get_secret_fails_on_mismatch(user: &signer) acquires Vault {
// User stores data under @0x333
set_secret(user, b"my_secret");
// get_secret tries to read from @owner, fails because no data exists there
get_secret(signer::address_of(user)); // Fails with NOT_OWNER or RESOURCE_NOT_FOUND
}

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
}
Updates

Lead Judging Commences

bube Lead Judge
13 days ago
bube Lead Judge 11 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.