Root + Impact
Description
Normal Behavior
Read-only access functions should return references to data or explicitly indicate when copying occurs, allowing developers to understand performance implications and memory usage patterns.
Issue
The get_secret
function implicitly copies the entire secret string on every call due to Move's automatic copying of Copy
types from borrowed references. This creates unexpected performance overhead, multiple copies of sensitive data in memory, and violates the principle of least surprise for developers expecting reference-based access.
#[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 call to get_secret
triggers implicit copying
Pattern occurs on every read operation
Developers are unaware of the copying behavior
Performance impact scales with secret size and call frequency
Impact:
Performance degradation: Unnecessary string copying on every read
Memory waste: Multiple copies of secrets in memory simultaneously
Security concern: Secrets persist longer in memory due to copies
Developer surprise: Unexpected copying behavior from apparent read-only operation
Scalability issues: Performance degrades with larger secrets or frequent access
Proof of Concept
The following tests demonstrate the implicit copying behavior and its implications:
#[test(owner = @0xcc)]
fun test_illegal_return_from_borrowed_resource(owner: &signer) acquires Vault {
account::create_account_for_test(signer::address_of(owner));
let secret_data = b"test_secret_for_borrowing";
set_secret(owner, secret_data);
let owner_addr = signer::address_of(owner);
let retrieved_secret = get_secret(owner_addr);
assert!(retrieved_secret == string::utf8(secret_data), 700);
let vault = borrow_global<Vault>(owner_addr);
assert!(vault.secret == retrieved_secret, 701);
}
#[test(owner = @0xcc)]
fun test_borrowing_rules_violation_detailed(owner: &signer) acquires Vault {
account::create_account_for_test(signer::address_of(owner));
set_secret(owner, b"test_data");
let vault_ref = borrow_global<Vault>(@owner);
}
#[test(owner = @0xcc)]
fun test_correct_return_implementation(owner: &signer) acquires Vault {
account::create_account_for_test(signer::address_of(owner));
set_secret(owner, b"test_secret");
let owner_addr = signer::address_of(owner);
let vault = borrow_global<Vault>(owner_addr);
let secret_copy = vault.secret;
}
Recommended Mitigation
Verification Pattern (Best Security)
- #[view]
- public fun get_secret(caller: address): String acquires Vault {
+ public entry fun verify_secret(caller: &signer, claimed_secret: vector<u8>): bool acquires Vault {
+ assert!(signer::address_of(caller) == @owner, NOT_OWNER);
let vault = borrow_global<Vault>(@owner);
- vault.secret
+ vault.secret == string::utf8(claimed_secret) // No copying, just comparison
}