diff --git a/.github/workflows/storage-collision-check.yml b/.github/workflows/storage-collision-check.yml new file mode 100644 index 0000000..84f40ff --- /dev/null +++ b/.github/workflows/storage-collision-check.yml @@ -0,0 +1,35 @@ +name: Storage Key Collision Check + +on: + push: + branches: [ main, issue-150 ] + pull_request: + branches: [ main ] + +jobs: + check-storage-collision: + name: Check Storage Key Collision & Namespacing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Cache Cargo + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + contracts/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Run Storage Key Collision Check + run: | + chmod +x scripts/check-storage-keys.sh + ./scripts/check-storage-keys.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3238ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Dependencies & Build Outputs +node_modules/ +target/ +contracts/target/ +dist/ +build/ +.next/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs & Temporary files +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +*.swp +*.swo + +# IDE & Editor files +.vscode/ +.idea/ diff --git a/Makefile b/Makefile index 4502130..f3c00d6 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: up down build logs ps clean staging +.PHONY: up down build logs ps clean staging check-storage-keys ## Start the full local stack up: @@ -27,3 +27,8 @@ ps: ## Start staging stack staging: docker compose -f docker-compose.yml -f docker-compose.staging.yml up --build + +## Check for storage key collisions across contracts +check-storage-keys: + ./scripts/check-storage-keys.sh + diff --git a/README.md b/README.md index eb2c0c5..bf20910 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,11 @@ Handles: - Yield verification - Fund distribution +### Storage Key Namespacing & Collision Detection +- All contracts use tuple namespacing `(Symbol, DataKey)` with unique `CONTRACT_PREFIX` identifiers +- Collision detection script: `./scripts/check-storage-keys.sh` (or `make check-storage-keys`) +- Documentation: [docs/storage-namespacing.md](docs/storage-namespacing.md) + --- ## Project Structure diff --git a/contracts/access-control/test_snapshots/test/test_access_control_storage.1.json b/contracts/access-control/test_snapshots/test/test_access_control_storage.1.json new file mode 100644 index 0000000..98b2f35 --- /dev/null +++ b/contracts/access-control/test_snapshots/test/test_access_control_storage.1.json @@ -0,0 +1,198 @@ +{ + "generators": { + "address": 3, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "access_control" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "access_control" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "access_control" + }, + { + "vec": [ + { + "symbol": "Role" + }, + { + "symbol": "operator" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "access_control" + }, + { + "vec": [ + { + "symbol": "Role" + }, + { + "symbol": "operator" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/financing-pool/src/lib.rs b/contracts/financing-pool/src/lib.rs index 309c5b7..b7b65c5 100644 --- a/contracts/financing-pool/src/lib.rs +++ b/contracts/financing-pool/src/lib.rs @@ -609,4 +609,4 @@ mod test; #[cfg(test)] mod reentrancy_tests; #[cfg(test)] -mod upgrade_tests; \ No newline at end of file +mod upgrade_tests; diff --git a/contracts/financing-pool/test_snapshots/test/test_financing_pool_storage.1.json b/contracts/financing-pool/test_snapshots/test/test_financing_pool_storage.1.json new file mode 100644 index 0000000..0ba1ee0 --- /dev/null +++ b/contracts/financing-pool/test_snapshots/test/test_financing_pool_storage.1.json @@ -0,0 +1,251 @@ +{ + "generators": { + "address": 3, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "InvestorBalance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "InvestorBalance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u128": { + "hi": 0, + "lo": 10000 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "TotalLiquidity" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "TotalLiquidity" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u128": { + "hi": 0, + "lo": 10000 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/integration-tests/Cargo.toml b/contracts/integration-tests/Cargo.toml new file mode 100644 index 0000000..d72cc6a --- /dev/null +++ b/contracts/integration-tests/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "integration-tests" +version = "0.1.0" +edition = "2021" + +[dependencies] +soroban-sdk = { workspace = true } +access-control = { path = "../access-control" } +invoice = { path = "../invoice" } +financing-pool = { path = "../financing-pool" } +settlement = { path = "../settlement" } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/integration-tests/src/lib.rs b/contracts/integration-tests/src/lib.rs new file mode 100644 index 0000000..a0e14b1 --- /dev/null +++ b/contracts/integration-tests/src/lib.rs @@ -0,0 +1,2 @@ +#![no_std] +// Integration tests workspace package diff --git a/contracts/integration-tests/test_snapshots/test_shared_env_multi_contract_storage_isolation.1.json b/contracts/integration-tests/test_snapshots/test_shared_env_multi_contract_storage_isolation.1.json new file mode 100644 index 0000000..b508a73 --- /dev/null +++ b/contracts/integration-tests/test_snapshots/test_shared_env_multi_contract_storage_isolation.1.json @@ -0,0 +1,751 @@ +{ + "generators": { + "address": 6, + "nonce": 0 + }, + "auth": [ + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "access_control" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "access_control" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "InvestorBalance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "InvestorBalance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u128": { + "hi": 0, + "lo": 500000 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "TotalLiquidity" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "financing_pool" + }, + { + "vec": [ + { + "symbol": "TotalLiquidity" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u128": { + "hi": 0, + "lo": 500000 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "Invoice" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "Invoice" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u128": { + "hi": 0, + "lo": 150000 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "InvoiceCount" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "InvoiceCount" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u64": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "InvoiceStatus" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "InvoiceStatus" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "EscrowBalance" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "EscrowBalance" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u128": { + "hi": 0, + "lo": 150000 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "Settlement" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "Settlement" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u128": { + "hi": 0, + "lo": 150000 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "SettlementCount" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "SettlementCount" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u64": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/integration-tests/tests/storage_key_collision_test.rs b/contracts/integration-tests/tests/storage_key_collision_test.rs new file mode 100644 index 0000000..680a0b2 --- /dev/null +++ b/contracts/integration-tests/tests/storage_key_collision_test.rs @@ -0,0 +1,145 @@ +use soroban_sdk::{ + testutils::Address as _, + Address, Env, IntoVal, Symbol, Val, +}; + +use access_control::{AccessControlContract, AccessControlDataKey}; +use financing_pool::{FinancingPoolContract, FinancingPoolDataKey}; +use invoice::{InvoiceContract, InvoiceDataKey}; +use settlement::{SettlementContract, SettlementDataKey}; + +#[test] +fn test_no_raw_storage_key_overlap_across_contracts() { + let env = Env::default(); + let dummy_addr = Address::generate(&env); + let dummy_sym = Symbol::new(&env, "TEST"); + + // Enumerate every DataKey variant across access-control, invoice, financing-pool, settlement + let ac_keys = [ + AccessControlDataKey::Admin.namespaced_key(&env), + AccessControlDataKey::Role(dummy_sym.clone(), dummy_addr.clone()).namespaced_key(&env), + AccessControlDataKey::RoleAdmin(dummy_sym.clone()).namespaced_key(&env), + AccessControlDataKey::Paused.namespaced_key(&env), + ]; + + let inv_keys = [ + InvoiceDataKey::Admin.namespaced_key(&env), + InvoiceDataKey::Invoice(1).namespaced_key(&env), + InvoiceDataKey::InvoiceCount.namespaced_key(&env), + InvoiceDataKey::FeeConfig.namespaced_key(&env), + InvoiceDataKey::InvoiceStatus(1).namespaced_key(&env), + ]; + + let pool_keys = [ + FinancingPoolDataKey::Admin.namespaced_key(&env), + FinancingPoolDataKey::Pool(1).namespaced_key(&env), + FinancingPoolDataKey::PoolCount.namespaced_key(&env), + FinancingPoolDataKey::InvestorBalance(dummy_addr.clone()).namespaced_key(&env), + FinancingPoolDataKey::TotalLiquidity.namespaced_key(&env), + ]; + + let set_keys = [ + SettlementDataKey::Admin.namespaced_key(&env), + SettlementDataKey::Settlement(1).namespaced_key(&env), + SettlementDataKey::SettlementCount.namespaced_key(&env), + SettlementDataKey::EscrowBalance.namespaced_key(&env), + ]; + + // Collect all serialized Val payloads into a vector + let mut all_payloads: std::vec::Vec = std::vec::Vec::new(); + + for key in ac_keys.iter() { + let val: Val = key.into_val(&env); + all_payloads.push(val.get_payload()); + } + for key in inv_keys.iter() { + let val: Val = key.into_val(&env); + all_payloads.push(val.get_payload()); + } + for key in pool_keys.iter() { + let val: Val = key.into_val(&env); + all_payloads.push(val.get_payload()); + } + for key in set_keys.iter() { + let val: Val = key.into_val(&env); + all_payloads.push(val.get_payload()); + } + + // Assert that every single key across all 4 contracts produces a unique payload representation + for i in 0..all_payloads.len() { + for j in (i + 1)..all_payloads.len() { + assert!( + all_payloads[i] != all_payloads[j], + "Collision detected between key index {} and {}! (payload: {})", + i, + j, + all_payloads[i] + ); + } + } +} + +#[test] +fn test_shared_env_multi_contract_storage_isolation() { + let env = Env::default(); + + let shared_id = Address::generate(&env); + + let ac_admin = Address::generate(&env); + let inv_admin = Address::generate(&env); + let pool_admin = Address::generate(&env); + let set_admin = Address::generate(&env); + + // Execute contract operations on the EXACT SAME instance address context + env.register_at(&shared_id, AccessControlContract, ()); + + env.as_contract(&shared_id, || { + AccessControlContract::init(env.clone(), ac_admin.clone()); + InvoiceContract::init(env.clone(), inv_admin.clone()); + FinancingPoolContract::init(env.clone(), pool_admin.clone()); + SettlementContract::init(env.clone(), set_admin.clone()); + + // Verify all four contracts maintain their independent Admin state without clobbering each other + assert_eq!( + AccessControlContract::get_admin(env.clone()), + Some(ac_admin.clone()), + "AccessControl admin was clobbered!" + ); + assert_eq!( + InvoiceContract::get_admin(env.clone()), + Some(inv_admin.clone()), + "Invoice admin was clobbered!" + ); + assert_eq!( + FinancingPoolContract::get_admin(env.clone()), + Some(pool_admin.clone()), + "FinancingPool admin was clobbered!" + ); + assert_eq!( + SettlementContract::get_admin(env.clone()), + Some(set_admin.clone()), + "Settlement admin was clobbered!" + ); + + // Execute actions across contracts in shared instance storage + let inv_id = InvoiceContract::create_invoice(env.clone(), 150_000); + assert_eq!(inv_id, 1); + + let investor = Address::generate(&env); + FinancingPoolContract::deposit(env.clone(), investor.clone(), 500_000); + + let set_id = SettlementContract::record_settlement(env.clone(), inv_id, 150_000); + assert_eq!(set_id, 1); + + // Verify invoice count and settlement count do not overlap or clobber each other + assert_eq!(InvoiceContract::get_invoice(env.clone(), 1), Some(150_000)); + assert_eq!( + FinancingPoolContract::get_investor_balance(env.clone(), investor), + 500_000 + ); + assert_eq!( + SettlementContract::get_settlement(env.clone(), 1), + Some(150_000) + ); + }); +} diff --git a/contracts/invoice/test_snapshots/test/test_invoice_storage.1.json b/contracts/invoice/test_snapshots/test/test_invoice_storage.1.json new file mode 100644 index 0000000..e518fc2 --- /dev/null +++ b/contracts/invoice/test_snapshots/test/test_invoice_storage.1.json @@ -0,0 +1,306 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "Invoice" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "Invoice" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u128": { + "hi": 0, + "lo": 5000 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "InvoiceCount" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "InvoiceCount" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u64": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "InvoiceStatus" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "invoice" + }, + { + "vec": [ + { + "symbol": "InvoiceStatus" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/settlement/test_snapshots/test/test_settlement_storage.1.json b/contracts/settlement/test_snapshots/test/test_settlement_storage.1.json new file mode 100644 index 0000000..9d18f4d --- /dev/null +++ b/contracts/settlement/test_snapshots/test/test_settlement_storage.1.json @@ -0,0 +1,303 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "Admin" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "EscrowBalance" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "EscrowBalance" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u128": { + "hi": 0, + "lo": 7500 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "Settlement" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "Settlement" + }, + { + "u64": 1 + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u128": { + "hi": 0, + "lo": 7500 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "SettlementCount" + } + ] + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "settlement" + }, + { + "vec": [ + { + "symbol": "SettlementCount" + } + ] + } + ] + }, + "durability": "persistent", + "val": { + "u64": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/docs/storage-namespacing.md b/docs/storage-namespacing.md new file mode 100644 index 0000000..6e1bfb1 --- /dev/null +++ b/docs/storage-namespacing.md @@ -0,0 +1,51 @@ +# Contract Storage Namespacing Strategy + +## Overview + +In Soroban smart contracts, state data stored in persistent or instance storage is keyed by values of type `Val`. When multiple contracts interact within a shared environment or module structure, storage keys must be strictly namespaced to prevent key collisions and state corruption. + +InvoiceFi enforces a mandatory **Contract Storage Namespacing Strategy** across all core contracts (`access-control`, `invoice`, `financing-pool`, and `settlement`). + +--- + +## Strategy Details + +Each contract defines a unique `CONTRACT_PREFIX` constant and wraps its `DataKey` enum inside a tuple key format `(Symbol, DataKey)`. + +### 1. Prefix Definitions + +| Contract | Constant Name | Value | +| --- | --- | --- | +| Access Control | `CONTRACT_PREFIX` | `"access_control"` | +| Invoice | `CONTRACT_PREFIX` | `"invoice"` | +| Financing Pool | `CONTRACT_PREFIX` | `"financing_pool"` | +| Settlement | `CONTRACT_PREFIX` | `"settlement"` | + +### 2. Tuple Namespacing Pattern + +Every `DataKey` implementation includes a helper method: + +```rust +impl DataKey { + pub fn namespaced_key(&self, env: &Env) -> (Symbol, DataKey) { + (Symbol::new(env, CONTRACT_PREFIX), self.clone()) + } +} +``` + +When reading from or writing to storage, all contracts pass the namespaced key tuple instead of the raw `DataKey` variant: + +```rust +let key = InvoiceDataKey::Invoice(id).namespaced_key(&env); +env.storage().persistent().set(&key, &amount); +``` + +--- + +## Automated Collision Detection + +To guarantee that no collisions occur across contract boundaries: + +1. **Integration Test Suite**: `contracts/integration-tests/tests/storage_key_collision_test.rs` validates payload uniqueness across all key variants and tests shared instance storage isolation. +2. **Automated Verification Script**: `./scripts/check-storage-keys.sh` (or `make check-storage-keys`) verifies unique `CONTRACT_PREFIX` declarations and runs the Rust test suite. +3. **Continuous Integration**: Checked automatically on push and pull requests via `.github/workflows/storage-collision-check.yml`.