Skip to content

ARC-0058: Plugin-Based Account Abstraction #269

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
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
Prev Previous commit
Next Next commit
add sequence diagrams and new implementation
  • Loading branch information
joe-p committed Feb 2, 2024
commit 57d7f2ba84d9661c2adc4913bc9b79ecbb52b4ad
138 changes: 93 additions & 45 deletions ARCs/arc-0058.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ Another common point of friction for end-users in the Algorand ecosystem is ASA
An Abstracted Account App that adheres to this standard **MUST** implement the following methods

```json
"methods": [
"methods": [
{
"name": "createApplication",
"desc": "Create an abstracted account application",
"args": [
{
"name": "address",
"name": "controlledAddress",
"type": "address",
"desc": "The address of the abstracted account. If zeroAddress, then the address of the contract account will be used"
},
Expand All @@ -59,15 +59,15 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
}
},
{
"name": "verifyAuthAddr",
"name": "arc58_verifyAuthAddr",
"desc": "Verify the abstracted account is rekeyed to this app",
"args": [],
"returns": {
"type": "void"
}
},
{
"name": "rekeyTo",
"name": "arc58_rekeyTo",
"desc": "Rekey the abstracted account to another address. Primarily useful for rekeying to an EOA.",
"args": [
{
Expand All @@ -86,7 +86,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
}
},
{
"name": "rekeyToPlugin",
"name": "arc58_rekeyToPlugin",
"desc": "Temporarily rekey to an approved plugin app address",
"args": [
{
Expand All @@ -100,7 +100,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
}
},
{
"name": "rekeyToNamedPlugin",
"name": "arc58_rekeyToNamedPlugin",
"desc": "Temporarily rekey to a named plugin app address",
"args": [
{
Expand All @@ -114,7 +114,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
}
},
{
"name": "changeAdmin",
"name": "arc58_changeAdmin",
"desc": "Change the admin for this app",
"args": [
{
Expand All @@ -128,7 +128,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
}
},
{
"name": "addPlugin",
"name": "arc58_addPlugin",
"desc": "Add an app to the list of approved plugins",
"args": [
{
Expand All @@ -137,7 +137,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
"desc": "The app to add"
},
{
"name": "address",
"name": "allowedCaller",
"type": "address",
"desc": "The address of that's allowed to call the appor the global zero address for all addresses"
},
Expand All @@ -152,7 +152,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
}
},
{
"name": "removePlugin",
"name": "arc58_removePlugin",
"desc": "Remove an app from the list of approved plugins",
"args": [
{
Expand All @@ -161,7 +161,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
"desc": "The app to remove"
},
{
"name": "address",
"name": "allowedCaller",
"type": "address"
}
],
Expand All @@ -170,7 +170,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
}
},
{
"name": "addNamedPlugin",
"name": "arc58_addNamedPlugin",
"desc": "Add a named plugin",
"args": [
{
Expand All @@ -184,7 +184,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
"desc": "The plugin app"
},
{
"name": "address",
"name": "allowedCaller",
"type": "address"
},
{
Expand All @@ -197,7 +197,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
}
},
{
"name": "removeNamedPlugin",
"name": "arc58_removeNamedPlugin",
"desc": "Remove a named plugin",
"args": [
{
Expand Down Expand Up @@ -265,10 +265,12 @@ TODO: Some functional tests are in this repo https://github.com/joe-p/account_ab

## Reference Implementation

### Abstracted App

```ts
import { Contract } from '@algorandfoundation/tealscript';

type PluginsKey = { application: Application; address: Address };
type PluginsKey = { application: Application; allowedCaller: Address };

export class AbstractedAccount extends Contract {
/** Target AVM 10 */
Expand All @@ -290,8 +292,8 @@ export class AbstractedAccount extends Contract {
*/
namedPlugins = BoxMap<bytes, PluginsKey>({ prefix: 'n' });

/** The address of the abstracted account */
address = GlobalStateKey<Address>();
/** The address this app controls */
controlledAddress = GlobalStateKey<Address>();

/**
* Ensure that by the end of the group the abstracted account has control of its address
Expand All @@ -300,11 +302,11 @@ export class AbstractedAccount extends Contract {
const lastTxn = this.txnGroup[this.txnGroup.length - 1];

// If the last txn isn't a rekey, then assert that the last txn is a call to verifyAuthAddr
if (lastTxn.sender !== this.address.value || lastTxn.rekeyTo !== this.getAuthAddr()) {
if (lastTxn.sender !== this.controlledAddress.value || lastTxn.rekeyTo !== this.getAuthAddr()) {
verifyAppCallTxn(lastTxn, {
applicationID: this.app,
applicationArgs: {
0: method('verifyAuthAddr()void'),
0: method('arc58_verifyAuthAddr()void'),
},
});
}
Expand All @@ -315,31 +317,31 @@ export class AbstractedAccount extends Contract {
* is able to be controlled by this app. It will either be this.app.address or zeroAddress
*/
private getAuthAddr(): Address {
return this.address.value === this.app.address ? Address.zeroAddress : this.app.address;
return this.controlledAddress.value === this.app.address ? Address.zeroAddress : this.app.address;
}

/**
* Create an abstracted account application
*
* @param address The address of the abstracted account. If zeroAddress, then the address of the contract account will be used
* @param controlledAddress The address of the abstracted account. If zeroAddress, then the address of the contract account will be used
* @param admin The admin for this app
*/
createApplication(address: Address, admin: Address): void {
createApplication(controlledAddress: Address, admin: Address): void {
verifyAppCallTxn(this.txn, {
sender: { includedIn: [address, admin] },
sender: { includedIn: [controlledAddress, admin] },
});

assert(admin !== address);
assert(admin !== controlledAddress);

this.admin.value = admin;
this.address.value = address === Address.zeroAddress ? this.app.address : address;
this.controlledAddress.value = controlledAddress === Address.zeroAddress ? this.app.address : controlledAddress;
}

/**
* Verify the abstracted account is rekeyed to this app
*/
verifyAuthAddr(): void {
assert(this.address.value.authAddr === this.getAuthAddr());
arc58_verifyAuthAddr(): void {
assert(this.controlledAddress.value.authAddr === this.getAuthAddr());
}

/**
Expand All @@ -348,11 +350,11 @@ export class AbstractedAccount extends Contract {
* @param addr The address to rekey to
* @param flash Whether or not this should be a flash rekey. If true, the rekey back to the app address must done in the same txn group as this call
*/
rekeyTo(addr: Address, flash: boolean): void {
arc58_rekeyTo(addr: Address, flash: boolean): void {
verifyAppCallTxn(this.txn, { sender: this.admin.value });

sendPayment({
sender: this.address.value,
sender: this.controlledAddress.value,
receiver: addr,
rekeyTo: addr,
note: 'rekeying abstracted account',
Expand All @@ -366,18 +368,18 @@ export class AbstractedAccount extends Contract {
*
* @param plugin The app to rekey to
*/
rekeyToPlugin(plugin: Application): void {
const globalKey: PluginsKey = { application: plugin, address: globals.zeroAddress };
arc58_rekeyToPlugin(plugin: Application): void {
const globalKey: PluginsKey = { application: plugin, allowedCaller: globals.zeroAddress };

// If this plugin is not approved globally, then it must be approved for this address
if (!this.plugins(globalKey).exists || this.plugins(globalKey).value < globals.latestTimestamp) {
const key: PluginsKey = { application: plugin, address: this.txn.sender };
const key: PluginsKey = { application: plugin, allowedCaller: this.txn.sender };
assert(this.plugins(key).exists && this.plugins(key).value > globals.latestTimestamp);
}

sendPayment({
sender: this.address.value,
receiver: this.address.value,
sender: this.controlledAddress.value,
receiver: this.controlledAddress.value,
rekeyTo: plugin.address,
note: 'rekeying to plugin app',
});
Expand All @@ -390,31 +392,32 @@ export class AbstractedAccount extends Contract {
*
* @param name The name of the plugin to rekey to
*/
rekeyToNamedPlugin(name: string): void {
this.rekeyToPlugin(this.namedPlugins(name).value.application);
arc58_rekeyToNamedPlugin(name: string): void {
this.arc58_rekeyToPlugin(this.namedPlugins(name).value.application);
}

/**
* Change the admin for this app
*
* @param newAdmin The new admin
*/
changeAdmin(newAdmin: Account): void {
arc58_changeAdmin(newAdmin: Account): void {
verifyTxn(this.txn, { sender: this.admin.value });
assert(newAdmin !== this.controlledAddress.value);
this.admin.value = newAdmin;
}

/**
* Add an app to the list of approved plugins
*
* @param app The app to add
* @param address The address of that's allowed to call the app
* @param allowedCaller The address of that's allowed to call the app
* or the global zero address for all addresses
* @param end The timestamp when the permission expires
*/
addPlugin(app: Application, address: Address, end: uint64): void {
arc58_addPlugin(app: Application, allowedCaller: Address, end: uint64): void {
verifyTxn(this.txn, { sender: this.admin.value });
const key: PluginsKey = { application: app, address: address };
const key: PluginsKey = { application: app, allowedCaller: allowedCaller };
this.plugins(key).value = end;
}

Expand All @@ -423,10 +426,10 @@ export class AbstractedAccount extends Contract {
*
* @param app The app to remove
*/
removePlugin(app: Application, address: Address): void {
arc58_removePlugin(app: Application, allowedCaller: Address): void {
verifyTxn(this.txn, { sender: this.admin.value });

const key: PluginsKey = { application: app, address: address };
const key: PluginsKey = { application: app, allowedCaller: allowedCaller };
this.plugins(key).delete();
}

Expand All @@ -436,11 +439,11 @@ export class AbstractedAccount extends Contract {
* @param app The plugin app
* @param name The plugin name
*/
addNamedPlugin(name: string, app: Application, address: Address, end: uint64): void {
arc58_addNamedPlugin(name: string, app: Application, allowedCaller: Address, end: uint64): void {
verifyTxn(this.txn, { sender: this.admin.value });
assert(!this.namedPlugins(name).exists);

const key: PluginsKey = { application: app, address: address };
const key: PluginsKey = { application: app, allowedCaller: allowedCaller };
this.namedPlugins(name).value = key;
this.plugins(key).value = end;
}
Expand All @@ -450,7 +453,7 @@ export class AbstractedAccount extends Contract {
*
* @param name The plugin name
*/
removeNamedPlugin(name: string): void {
arc58_removeNamedPlugin(name: string): void {
verifyTxn(this.txn, { sender: this.admin.value });

const app = this.namedPlugins(name).value;
Expand All @@ -463,6 +466,51 @@ https://github.com/joe-p/account_abstraction.git

TODO: Migrate to ARC repo, but waiting until development has settled.

### Diagrams
#### 0-ALGO Opt-In Onboarding

```mermaid
sequenceDiagram
participant Wallet
participant Dapp
participant Abstracted App
participant OptIn Plugin
Wallet->>Wallet: Create keypair
note over Wallet: The user should not see<br/>nor care about the address
Wallet->>Dapp: Send public key
Dapp->>Abstracted App: createApplication({ admin: Dapp })
Dapp->>Abstracted App: Add opt-in plugin
Dapp->>Abstracted App: changeAdmin({ admin: Alice })
note over Dapp,Abstracted App: There could also be a specific implementation that does<br/>the above three tranasctions upon create
Dapp->>Wallet: Abstracted App ID
note over Wallet: The user sees the<br/> Abstracted App Address
par Opt-In Transaction Group
Dapp->>Abstracted App: arc58_rekeyToPlugin({ plugin: OptIn Plugin })
Dapp->>Abstracted App: Send ASA MBR
Dapp->>OptIn Plugin: optIn(asset)
Dapp->>Abstracted App: arc58_verifyAuthAddr
end
```

#### Transition Existing Address

If a user wants to transition an existing keypair-based account to an abstracted account and use the existing secret key for admin actions, they need to perform the following steps

```mermaid
sequenceDiagram
participant Wallet
participant Abstracted App
note over Wallet: Address: EXISTING_ADDRESS<br/>Auth Addr: EXISTING_ADDRESS
Wallet->>Wallet: Create new keypair
note over Wallet: Address: NEW_ADDRESS<br/>Auth Addr: NEW_ADDRESS
Wallet->>Wallet: Rekey NEW_ADDRESS to EXISTING_ADDRESS
note over Wallet: Address: NEW_ADDRESS<br/>Auth Addr: EXISTING_ADDRESS
Wallet->>Abstracted App: createApplication({ admin: NEW_ADDRESS, controlledAddress: EXISTING_ADDRESS })
note over Abstracted App: Address: APP_ADDRESS<br/>Admin: NEW_ADDRESS<br/>Controlled Address: EXISTING_ADDRESS
Wallet->>Wallet: Rekey EXISTING_ADDRESS to APP_ADDRESS
note over Wallet: Address: EXISTING_ADDRESS<br/>Auth Addr: APP_ADDRESS
```

## Security Considerations
By adding a plugin to an abstracted account, that plugin can get complete control of the account. As such, extreme diligance must be taken by the end-user to ensure they are adding safe and/or trusted plugins. The security assumptions for plugins are very similar to delegated logic signatures, with the exception that plugins can always be revoked.

Expand Down