Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions contracts/admin/tests/upgrade_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ fn test_e2e_v1_to_v2_admin_upgrade_and_rbac_lifecycle() {
assert!(client.has_role(&Role::SuperAdmin, &admin));
assert!(client.has_role(&Role::Admin, &admin));

// Verify the admin can still be retrieved after migration
assert!(client.has_admin());
assert_eq!(client.get_role_admin(&Role::Admin), admin);

// 5. Verify post-upgrade RBAC enforcement and role-gated actions
// Admin (holding SuperAdmin/Admin) grants Minter role to user_a and Pauser role to user_b
client.grant_role(&admin, &Role::Minter, &user_a);
Expand All @@ -155,6 +159,23 @@ fn test_e2e_v1_to_v2_admin_upgrade_and_rbac_lifecycle() {
// Assert unauthorized user cannot grant roles post-upgrade
let post_upgrade_unauth = client.try_grant_role(&user_a, &Role::Pauser, &user_a);
assert!(post_upgrade_unauth.is_err());

// 6. Verify that the admin can still grant SuperAdmin to other addresses
let super_admin = Address::generate(&env);
client.grant_role(&admin, &Role::SuperAdmin, &super_admin);
assert!(client.has_role(&Role::SuperAdmin, &super_admin));

// 7. Verify that the new SuperAdmin can also grant roles
let new_minter = Address::generate(&env);
client.grant_role(&super_admin, &Role::Minter, &new_minter);
assert!(client.has_role(&Role::Minter, &new_minter));
assert!(!client.has_role(&Role::Minter, &user_b));

// 8. Verify that revoking roles works correctly post-migration
client.revoke_role(&admin, &Role::Minter, &user_a);
assert!(!client.has_role(&Role::Minter, &user_a));
// Pauser role should be unaffected
assert!(client.has_role(&Role::Pauser, &user_b));
}

/// Negative case: upgrading with an unauthorized caller must fail.
Expand Down Expand Up @@ -206,6 +227,15 @@ fn test_migrate_admin_idempotency() {

// Verify SuperAdmin status remains valid and uncorrupted
assert!(client.has_role(&Role::SuperAdmin, &admin));

// Verify original admin entry is still intact
assert!(client.has_admin());
assert_eq!(client.get_role_admin(&Role::Admin), admin);

// Verify that the admin can still perform RBAC operations after multiple migrations
let user = Address::generate(&env);
client.grant_role(&admin, &Role::Minter, &user);
assert!(client.has_role(&Role::Minter, &user));
}

/// Boundary case: verify no stale permissions allow ungranted roles post-upgrade.
Expand Down Expand Up @@ -238,6 +268,37 @@ fn test_double_vote_reverts() {

let contract_id = env.register(AdminContract, ());
let client = AdminContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let minter = Address::generate(&env);
let pauser = Address::generate(&env);
let unauthorized = Address::generate(&env);

// Initialize with admin
let init_result = client.try_init_storage(&admin);
assert!(init_result.is_ok());

// Migrate to RBAC
client.migrate_admin();

// Admin has SuperAdmin role
assert!(client.has_role(&Role::SuperAdmin, &admin));
assert!(client.has_role(&Role::Admin, &admin));

// Grant Minter and Pauser roles
client.grant_role(&admin, &Role::Minter, &minter);
client.grant_role(&admin, &Role::Pauser, &pauser);

// Verify RBAC enforcement:
// - Minter can pass require_minter
client.require_minter(&minter);
// - Pauser can pass require_pauser
client.require_pauser(&pauser);
// - Unauthorized user cannot pass require_minter
let unauth_result = client.try_require_minter(&unauthorized);
assert!(unauth_result.is_err());
// - Unauthorized user cannot pass require_pauser
let unauth_pauser_result = client.try_require_pauser(&unauthorized);
assert!(unauth_pauser_result.is_err());

let admin = Address::generate(&env);
let member = Address::generate(&env);
Expand Down
51 changes: 50 additions & 1 deletion docs/UPGRADE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ If you have an existing contract that was deployed before the `SuperAdmin`
role was introduced, use `migrate_admin` to enable `SuperAdmin`-based guards
without resetting state.

### Option 1: CLI

```bash
stellar contract invoke \
--id <CONTRACT_ID> \
Expand All @@ -263,8 +265,55 @@ stellar contract invoke \
migrate_admin
```

### Option 2: TypeScript SDK

```typescript
import { bcForgeClient } from '@bc-forge/sdk';
import { Keypair } from '@stellar/stellar-sdk';

const client = new bcForgeClient({
rpcUrl: 'https://soroban-testnet.stellar.org',
networkPassphrase: 'Test SDF Network ; September 2015',
contractId: '<CONTRACT_ID>',
});

const adminKeypair = Keypair.fromSecret(process.env.ADMIN_SECRET!);
const result = await client.migrateAdmin(adminKeypair);
console.log('Migration TX:', result.hash);
```

### Option 3: Standalone migration script

A standalone migration script is available at `migrations/rbac-migration.ts`.
It provides a complete migration workflow with verification:

```bash
# Dry-run (simulate without submitting)
npx ts-node migrations/rbac-migration.ts \
--rpc-url https://soroban-testnet.stellar.org \
--network-passphrase "Test SDF Network ; September 2015" \
--contract-id <CONTRACT_ID> \
--admin-secret <ADMIN_SECRET> \
--dry-run

# Execute migration
npx ts-node migrations/rbac-migration.ts \
--rpc-url https://soroban-testnet.stellar.org \
--network-passphrase "Test SDF Network ; September 2015" \
--contract-id <CONTRACT_ID> \
--admin-secret <ADMIN_SECRET>
```

The script performs the following steps:
1. Verifies the contract has an admin set
2. Checks if migration is already complete (idempotent)
3. Executes the migration transaction
4. Verifies the admin now has the SuperAdmin role

### Storage migration process

This is a one-shot, idempotent operation:
- Reads the current admin from instance storage.
- Reads the current admin from instance storage (`AdminKey::Admin`).
- Creates a persistent `SuperAdmin(admin)` entry.
- Safe to call multiple times (no-op on subsequent calls).

Expand Down
Loading
Loading