Skip to content

feat(STBLE-3855): Adding fine grained RBAC for solana contract - #20

Merged
denys-cb merged 20 commits into
coinbase:mainfrom
OliverCai0:oliver/rbac-no-scripts
Aug 19, 2026
Merged

feat(STBLE-3855): Adding fine grained RBAC for solana contract #20
denys-cb merged 20 commits into
coinbase:mainfrom
OliverCai0:oliver/rbac-no-scripts

Conversation

@OliverCai0

@OliverCai0 OliverCai0 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Description

Breaking Change: Replace the legacy two-authority pool model with a four-role model (pause / unpause / treasury / configure), add a withdraw-recipient allowlist, and ship a one-shot migrate_authorities path for existing pools—covered by expanded Anchor tests and a bankrun migration test.

On-chain (programs/stable-swapper)

  • Roles: split ops/pause into Pause (hot), Unpause (cold), Treasury (hot), Configure (cold); each self-rotates only
  • Allowlist: withdraw_liquidity only to token accounts owned by withdraw_recipients (Configure manages the list)
    migrate_authorities: realloc legacy layout (1719) → RBAC layout (2107); co-signed by legacy ops + pause; seeds allowlist; rejects double-migrate / zero keys / wrong owner
  • Granular errors, init guards, pause/unpause split for swaps / withdraws / per-token

Tests

  • stable-swapper.ts: role matrix, allowlist, pauses, swaps under the new model
  • migration.ts (bankrun): legacy account → migrate happy path (+ related checks)

Migration was also verified via localnet and devnet scripts that can be found here in a separate branch.

General flow of verification:

  1. Deploy old version of contract to either localnet/devnet with fresh address.
  2. Create pools and test swap functionality.
  3. Deploy new version of contract and invoke migration on legacy pool accounts
  4. Smoke test new authority controls and swap functionality.
  5. Cleanup

Below are some artifacts produced via a singular run:

{
  "cluster": "devnet",
  "rpcUrl": "https://api.devnet.solana.com",
  "walletPath": "/Users/olivercai/.config/solana/id.json",
  "programId": "76XzMrhT9BNxcRrcYEUWyiGxsXkiefY5QG3sjfy1f8QV",
  "programKeypairPath": "/Users/olivercai/Desktop/coinbase/stable-swapper/solana/.migration-verify/keys/devnet-program-keypair.json",
  "poolPda": "CBTBL4fL5fwpaKT9tWwGXFVht3qezhXb7z94CJgHadNa",
  "legacyCommit": "3f5b5d8",
  "pinsPatched": true,
  "phase": "05-smoke-passed",
  "mintA": "5qmsSLNim4EqFg2LGfYmqSehvDmhDQpAXPVwv719cHAT",
  "mintB": "NjXtMfxPuqY9BQLs1B7DDaxdepmJ93ef7bn6dTLbX5L",
  "mintAKeypairPath": "/Users/olivercai/Desktop/coinbase/stable-swapper/solana/.migration-verify/keys/mint-a.json",
  "mintBKeypairPath": "/Users/olivercai/Desktop/coinbase/stable-swapper/solana/.migration-verify/keys/mint-b.json",
  "vaultTokenAccountA": "GEuSz2hfwBf2u76NVATaBv1cYSgEfwsikBJwNyD7xSsp",
  "vaultTokenAccountB": "9pM233zphwCQcNngLyzFvYfp4nQFzfgbXFbrrU6sW5y8",
  "feeRateBps": 0,
  "preSwapSignature": "5iwzJa3t7rH1W1uVGinXyfCpAfjfccyJKDgxsus7c8gdXYB9RfjBfNiieAb94EhwH63MmnyjyuB3jwEqbRpMXyuC",
  "legacyOpsAuthority": "8oKsNgVaVsh8YBp4tJpRcwbAhQ27mmXH31Q6aeYurFhH",
  "legacyPauseAuthority": "8oKsNgVaVsh8YBp4tJpRcwbAhQ27mmXH31Q6aeYurFhH",
  "feeRecipient": "8oKsNgVaVsh8YBp4tJpRcwbAhQ27mmXH31Q6aeYurFhH",
  "roleKeyPaths": {
    "pause": "/Users/olivercai/Desktop/coinbase/stable-swapper/solana/.migration-verify/keys/role-pause.json",
    "unpause": "/Users/olivercai/Desktop/coinbase/stable-swapper/solana/.migration-verify/keys/role-unpause.json",
    "treasury": "/Users/olivercai/Desktop/coinbase/stable-swapper/solana/.migration-verify/keys/role-treasury.json",
    "configure": "/Users/olivercai/Desktop/coinbase/stable-swapper/solana/.migration-verify/keys/role-configure.json",
    "withdrawRecipient": "/Users/olivercai/Desktop/coinbase/stable-swapper/solana/.migration-verify/keys/role-withdraw-recipient.json",
    "stranger": "/Users/olivercai/Desktop/coinbase/stable-swapper/solana/.migration-verify/keys/role-stranger.json"
  },
  "migrateSignature": "53XWHKqAv7p1y3b9jjsSkRjDNVyjUgSmK6DzPGfP1JZBA9Mr41P5aWZejnL5SaZmCv2zehDaCpyF6MvBsWJXEgMz",
  "smokeSignatures": {
    "pauseSwaps": "DDnemUePs3P1UmCdFyfZ39nKoMTnKLxNptNVyi4cK2QV1y9RZ29MZqAkr9fC8BQ2BysxeQrs6u5RLPTytWyEipx",
    "swapWhilePaused": null,
    "unpauseSwaps": "5ZWGWSpF2F72MMNQUemVXoZPGS1nWnpXbJ7AERDzmak4Q1c86vDF7bgfwL3sf2dF3Dq6jWVRp2fiXUA3vQMHxVyG",
    "pauseWithdraws": "2yjbLcqVo2vRNXoN3SCE7tjGSetuTRA951PVip4X9NqEFeiDn6d94rct8odpCNzxkasoDVXZTzkv66vVMo6s65Xd",
    "withdrawWhilePaused": null,
    "unpauseWithdraws": "5JMDiCo3bmPXRVUjkbA3gAPaNnSX6ZboCj6cdTFAysw2WC6gEs4LowhZKaVNmwk82hgfY2qff8TLp6Kj7JARmV4g",
    "treasuryWithdrawAllowlisted": "2AKyjQjKjTALMUTKD3FzTCgkgBJfKKyX5d39TFgh3SLLj1yG4jfp7rqKG4x6bYfFpdt5vZ8zFRbbCzMbK4JxFSsp",
    "treasuryWithdrawDenyStranger": null,
    "addWithdrawRecipient": "4DwQWKXhNqFJaQSwpT8TaqCJX9NAXe6YDiuZP7uL7rf4LDMNYySHrLh19cLunUPTcdEfxWDTV8MvwApZVLGwcpGB",
    "removeWithdrawRecipient": "4QZTpcKn7wzQ7gNHPxkD3Z2eusB7qaYAn5YkL3jSPnmAf7mSnNw9278XZXb63HTzWyqHCNFfaDgZFaG7Frit8ueh",
    "postMigrateSwap": "4fFCk7NqLAk2JhJkCwKHp36DfHx5DhSRuoWfe7E51gj4xmXXPMzYR3JRR38z1Xh6PANZzpztdb8za6BST26QPeaY"
  }
}

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Dependency update
  • Documentation
  • Refactor / cleanup
  • Other (describe below)

Checklist

  • Tests are included and passing
  • Code is formatted and linted
  • Build succeeds
  • Documentation is updated for any public API changes
  • All commits are signed

@cb-heimdall

cb-heimdall commented Aug 4, 2026

Copy link
Copy Markdown

✅ Heimdall Review Status

Requirement Status More Info
Reviews 2/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@OliverCai0
OliverCai0 force-pushed the oliver/rbac-no-scripts branch 2 times, most recently from 0050951 to 57aab2e Compare August 4, 2026 13:02
Split pause/unpause/treasury/configure roles, add withdraw-recipient
allowlist, and support one-shot migration from the legacy layout with
Anchor and bankrun test coverage.
@OliverCai0
OliverCai0 force-pushed the oliver/rbac-no-scripts branch from 57aab2e to f097b93 Compare August 4, 2026 13:08
OliverCai0 and others added 4 commits August 4, 2026 09:26
Restore CustomStable naming to shrink the RBAC PR diff; no behavioral change.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@OliverCai0 OliverCai0 changed the title feat: Adding RBAC for solana contract feat(STBLE-3855): Adding RBAC for solana contract Aug 4, 2026
@OliverCai0
OliverCai0 marked this pull request as ready for review August 4, 2026 14:06
@OliverCai0 OliverCai0 changed the title feat(STBLE-3855): Adding RBAC for solana contract feat(STBLE-3855): Adding fine grained RBAC for solana contract Aug 4, 2026

@sddioulde sddioulde left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly looks good to me. left some comments

Comment thread solana/programs/stable-swapper/src/lib.rs
Comment thread solana/programs/stable-swapper/src/lib.rs
Comment thread solana/programs/stable-swapper/src/errors.rs
Comment thread solana/programs/stable-swapper/src/lib.rs Outdated
Comment thread solana/programs/stable-swapper/src/lib.rs Outdated
Roles can only be rotated by their current holder, so a role assigned the
default pubkey is unrecoverable. Reject it in initialize, in each rotation
instruction, and in migrate_authorities.
Give the migration's checks their own errors instead of reporting everything
as LegacyDiscriminatorMismatch: wrong owner, wrong account size, and each
legacy co-signer now fail distinctly, which also puts the previously unused
LegacySizeMismatch to work.
Document that the legacy operations authority funds the realloc, and derive
the growth from the layout constants so the byte count cannot go stale.
@OliverCai0
OliverCai0 requested a review from sddioulde August 5, 2026 20:50

@denys-cb denys-cb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall migration and testing is solid, left some comments, also worth reconciling with PPS a little bit, for example there is no whitelist anymore (mentioned in Configure Authority)

// MUST stay named `LiquidityPool`. Renaming it would break re-deserialization of every
// existing pool on devnet/mainnet and break the migration's discriminator check.
#[account]
pub struct LiquidityPool {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a documented runbook order - legacy pools stop deserializing under the new layout, so between upgrade and migrate everything taking Account fails, pause_swaps included.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will provide a runbook async.

Comment thread solana/package.json Outdated
Comment thread solana/.gitignore
**/deploy/*.json
**/*-keypair.json
**/id.json
package-lock.json

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is dropping package-lock intentional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added back

Comment thread solana/Cargo.lock Outdated
Comment thread solana/README.md
Comment thread solana/programs/stable-swapper/src/lib.rs Outdated
Comment thread solana/programs/stable-swapper/src/lib.rs
Comment thread solana/programs/stable-swapper/src/lib.rs Outdated
Ok(())
}

pub fn update_pause_authority(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each role only rotates itself, so a compromised hot key can't be revoked, and a
typo'd rotation is equally unrecoverable. EVM survives both because the admin can
revoke via grantRole/revokeRole.

Do we need same or similar approach here?

For example, we can make configure_authority to rotate any role, with pending/accept on its
own rotation.
Or maybe separate admin role?

@OliverCai0 OliverCai0 Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like to push back on this. As long as the cold key for upgrades isn't compromised we always have a way forwards to reset authorities, and that authority lives outside of the program. I'm not sure if we want to actually want to expose additional role setting logic anywhere at the moment.

Comment thread solana/programs/stable-swapper/src/lib.rs
Comment thread solana/programs/stable-swapper/src/lib.rs
Comment thread solana/README.md Outdated
@OliverCai0
OliverCai0 requested a review from denys-cb August 6, 2026 18:57
sddioulde
sddioulde previously approved these changes Aug 10, 2026
@OliverCai0
OliverCai0 force-pushed the oliver/rbac-no-scripts branch from 90fd84c to 0f9d487 Compare August 10, 2026 13:52
denys-cb
denys-cb previously approved these changes Aug 10, 2026
@OliverCai0
OliverCai0 dismissed stale reviews from denys-cb and sddioulde via 397bfa3 August 11, 2026 16:13
@denys-cb
denys-cb merged commit be94ba7 into coinbase:main Aug 19, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

5 participants