Skip to content
Open
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
7 changes: 5 additions & 2 deletions Anchor.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
[toolchain]
Copy link
Contributor

Choose a reason for hiding this comment

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

this section seems emtpy


[features]
seeds = true
resolution = true
skip-lint = false

[programs.localnet]
program_authority_escrow = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "Localnet"
cluster = "localnet"
wallet = "/home/gbescos/.config/solana/id.json"

[scripts]
Expand Down
59 changes: 58 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 32 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,43 @@
# program-authority-escrow

A minimalistic, stateless program to safely transfer a solana program from one upgrade authority to another one.
A stateless Solana program for safe program authority transfers.

The way it works :
- The current authority uses Propose to transfer the authority of any program to a PDA of the escrow seeded by (current_authority, new_authority)
- Once the authority has been transferred two outcomes are possible :
- If the current authority calls Revert, the PDA will give the authority back to the current authority
- If the new authority calls Accept, the PDA will give the authority to the new authority
The current authority calls `propose` to transfer authority to an escrow PDA seeded by `(current_authority, new_authority)`. From there:
- The current authority can call `revert` to reclaim authority
- The new authority can call `accept` to complete the transfer

Basically, this program enforces that the new authority has signed before they accept the authority.
This makes errors where we mistakenly transfer the authority to a key that we don't own reversible.
This ensures the new authority has signed before accepting, making accidental transfers to wrong keys reversible.

Build with `cargo build-sbf` and test with `cargo test-sbf`.

## Scripts

TypeScript scripts are provided to interact with the on-chain program. All scripts support:
- File-based keypairs or Ledger hardware wallets
- Squads v3 multisig proposals via `--multisig`
Copy link
Contributor

Choose a reason for hiding this comment

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

the transfer and revert scripts don't really support squads. To support squads you need to let currentAuthority be a multisigAuthority


Install dependencies with `yarn install`. See `scripts/helpers.ts` for documentation on CLI arguments.

### Propose

Transfer program authority to the escrow. The current authority proposes the transfer:

## Testing
To run tests:
```shell
cargo test-sbf
yarn propose --keypair <path|ledger> --program <program_address> --authority <new_authority>
```

## Building
To build:
### Accept

Accept a proposed authority transfer. The new authority accepts:

```shell
yarn accept --keypair <path|ledger> --program <program_address> --authority <previous_authority>
```

### Revert

Revert a proposed transfer before it's accepted. The current authority reverts:

```shell
cargo build-sbf
yarn revert --keypair <path|ledger> --program <program_address> --authority <new_authority>
```
Artifacts will be placed at: `target/deploy/*.so`
43 changes: 26 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
{
"scripts": {
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
},
"dependencies": {
"@project-serum/anchor": "^0.26.0"
},
"devDependencies": {
"chai": "^4.3.4",
"mocha": "^9.0.3",
"ts-mocha": "^10.0.0",
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"typescript": "^4.3.5",
"prettier": "^2.6.2"
}
"scripts": {
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check",
"propose": "ts-node scripts/propose.ts",
"revert": "ts-node scripts/revert.ts",
"accept": "ts-node scripts/accept.ts"
},
"dependencies": {
"@coral-xyz/anchor": "^0.30.0",
"@ledgerhq/hw-transport": "^6.31.13",
"@ledgerhq/hw-transport-node-hid": "^6.29.14",
"@sqds/sdk": "^2.0.4",
"@types/minimist": "^1.2.5",
"minimist": "^1.2.8",
"ts-node": "^10.9.2"
},
"devDependencies": {
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"chai": "^4.3.4",
"mocha": "^9.0.3",
"prettier": "^2.6.2",
"ts-mocha": "^10.0.0",
"typescript": "^4.3.5"
}
}
2 changes: 1 addition & 1 deletion programs/program-authority-escrow/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ default = []
custom-heap = []
custom-panic = []
anchor-debug = []
idl-build = ["anchor-lang/idl-build"]

[dependencies]
anchor-lang.workspace = true
Expand All @@ -29,4 +30,3 @@ solana-program-test.workspace = true
solana-sdk.workspace = true
tokio.workspace = true
bincode.workspace = true

2 changes: 1 addition & 1 deletion programs/program-authority-timelock/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ default = []
custom-heap = []
custom-panic = []
anchor-debug = []
idl-build = ["anchor-lang/idl-build"]

[dependencies]
anchor-lang.workspace = true
Expand All @@ -29,4 +30,3 @@ solana-program-test.workspace = true
solana-sdk.workspace = true
tokio.workspace = true
bincode.workspace = true

77 changes: 77 additions & 0 deletions scripts/accept.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { PublicKey } from "@solana/web3.js";
import { getAuthorityPDA, DEFAULT_MULTISIG_PROGRAM_ID } from "@sqds/sdk";
import BN from "bn.js";
import {
parseArgs,
createWallet,
initEscrowProgram,
parseMultisig,
executeOrPropose,
} from "./helpers";

const BPF_UPGRADABLE_LOADER = new PublicKey(
"BPFLoaderUpgradeab1e11111111111111111111111"
);

async function main() {
const args = parseArgs();
const wallet = await createWallet(args);
const { connection, program, programId } = initEscrowProgram(args, wallet);
const multisigAddress = parseMultisig(args);

const programToTransfer = new PublicKey(args.program);
const currentAuthority = new PublicKey(args.authority);

const programData = PublicKey.findProgramAddressSync(
[programToTransfer.toBuffer()],
BPF_UPGRADABLE_LOADER
)[0];

// In multisig mode, the vault becomes the new authority
let newAuthority: PublicKey;
if (multisigAddress) {
[newAuthority] = getAuthorityPDA(
multisigAddress,
new BN(1),
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you're supposed to use the authority index that's specified in the multisig account (you can get it by calling getMultisig)

DEFAULT_MULTISIG_PROGRAM_ID
);
} else {
newAuthority = wallet.publicKey;
}

const escrowAuthority = PublicKey.findProgramAddressSync(
[currentAuthority.toBuffer(), newAuthority.toBuffer()],
programId
)[0];

console.log("Accepting program authority transfer...");
console.log(` Wallet: ${wallet.publicKey.toBase58()}`);
console.log(` Program: ${programToTransfer.toBase58()}`);
console.log(` Current Authority: ${currentAuthority.toBase58()}`);
console.log(` New Authority: ${newAuthority.toBase58()}`);
console.log(` Escrow Authority: ${escrowAuthority.toBase58()}`);

if (multisigAddress) {
console.log(` Multisig: ${multisigAddress.toBase58()}`);
}

const instruction = await program.methods
.accept()
.accounts({
currentAuthority: currentAuthority,
newAuthority: newAuthority,
programAccount: programToTransfer,
})
.accountsPartial({
escrowAuthority: escrowAuthority,
programData: programData,
})
.instruction();

await executeOrPropose(connection, wallet, multisigAddress, instruction);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
41 changes: 0 additions & 41 deletions scripts/commit.ts

This file was deleted.

Loading
Loading