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
allow rekey/verify anywhere in group, implementation updates
  • Loading branch information
joe-p committed Feb 15, 2024
commit d1754b156a2a37842c8e79ab88740837e3a6816a
168 changes: 93 additions & 75 deletions ARCs/arc-0058.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,29 @@ 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",
"name": "arc58_changeAdmin",
"desc": "Attempt to change the admin for this app. Some implementations MAY not support this.",
"args": [
{
"name": "controlledAddress",
"type": "address",
"desc": "The address of the abstracted account. If zeroAddress, then the address of the contract account will be used"
},
{
"name": "admin",
"type": "address",
"desc": "The admin for this app"
"name": "newAdmin",
"type": "account",
"desc": "The new admin"
}
],
"returns": {
"type": "void"
}
},
{
"name": "arc58_getAdmin",
"desc": "Get the admin of this app. This method SHOULD always be used rather than reading directly from statebecause different implementations may have different ways of determining the admin.",
"args": [],
"returns": {
"type": "address"
}
},
{
"name": "arc58_verifyAuthAddr",
"desc": "Verify the abstracted account is rekeyed to this app",
Expand Down Expand Up @@ -113,20 +116,6 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
"type": "void"
}
},
{
"name": "arc58_changeAdmin",
"desc": "Change the admin for this app",
"args": [
{
"name": "newAdmin",
"type": "account",
"desc": "The new admin"
}
],
"returns": {
"type": "void"
}
},
{
"name": "arc58_addPlugin",
"desc": "Add an app to the list of approved plugins",
Expand All @@ -137,7 +126,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
"desc": "The app to add"
},
{
"name": "allowedCaller",
"name": "address",
"type": "address",
"desc": "The address of that's allowed to call the appor the global zero address for all addresses"
},
Expand All @@ -161,7 +150,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
"desc": "The app to remove"
},
{
"name": "allowedCaller",
"name": "address",
"type": "address"
}
],
Expand All @@ -184,7 +173,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f
"desc": "The plugin app"
},
{
"name": "allowedCaller",
"name": "address",
"type": "address"
},
{
Expand Down Expand Up @@ -270,7 +259,7 @@ TODO: Some functional tests are in this repo https://github.com/joe-p/account_ab
```ts
import { Contract } from '@algorandfoundation/tealscript';

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

export class AbstractedAccount extends Contract {
/** Target AVM 10 */
Expand All @@ -284,64 +273,95 @@ export class AbstractedAccount extends Contract {
* The key is the appID + address, the value (referred to as `end`)
* is the timestamp when the permission expires for the address to call the app for your account.
*/

plugins = BoxMap<PluginsKey, uint64>({ prefix: 'p' });

/**
* Plugins that have been given a name for discoverability
*/
namedPlugins = BoxMap<bytes, PluginsKey>({ prefix: 'n' });

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

/**
* Ensure that by the end of the group the abstracted account has control of its address
*/
private verifyRekeyToAbstractedAccount(): void {
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.controlledAddress.value || lastTxn.rekeyTo !== this.getAuthAddr()) {
verifyAppCallTxn(lastTxn, {
applicationID: this.app,
applicationArgs: {
0: method('arc58_verifyAuthAddr()void'),
},
});
let rekeyedBack = false;

for (let i = this.txn.groupIndex; i < this.txnGroup.length; i += 1) {
const txn = this.txnGroup[i];

// The transaction is an explicit rekey back
if (txn.sender === this.address.value && txn.rekeyTo === this.getAuthAddr()) {
rekeyedBack = true;
break;
}

// The transaction is an application call to this app's arc58_verifyAuthAddr method
if (
txn.typeEnum === TransactionType.ApplicationCall &&
txn.applicationID === this.app &&
txn.numAppArgs === 1 &&
txn.applicationArgs[0] === method('arc58_verifyAuthAddr()void')
) {
rekeyedBack = true;
break;
}
}

assert(rekeyedBack);
}

/**
* What the value of this.address.value.authAddr should be when this.address
* is able to be controlled by this app. It will either be this.app.address or zeroAddress
*/
private getAuthAddr(): Address {
return this.controlledAddress.value === this.app.address ? Address.zeroAddress : this.app.address;
return this.address.value === this.app.address ? Address.zeroAddress : this.app.address;
}

/**
* Create an abstracted account application
* Create an abstracted account application.
* This is not part of ARC58 and implementation specific.
*
* @param controlledAddress The address of the abstracted account. If zeroAddress, then the address of the contract account will be used
* @param address 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(controlledAddress: Address, admin: Address): void {
createApplication(address: Address, admin: Address): void {
verifyAppCallTxn(this.txn, {
sender: { includedIn: [controlledAddress, admin] },
sender: { includedIn: [address, admin] },
});

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

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

/**
* Attempt to change the admin for this app. Some implementations MAY not support this.
*
* @param newAdmin The new admin
*/
arc58_changeAdmin(newAdmin: Account): void {
verifyTxn(this.txn, { sender: this.admin.value });
this.admin.value = newAdmin;
}

/**
* Get the admin of this app. This method SHOULD always be used rather than reading directly from state
* because different implementations may have different ways of determining the admin.
*/
arc58_getAdmin(): Address {
return this.admin.value;
}

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

/**
Expand All @@ -354,7 +374,7 @@ export class AbstractedAccount extends Contract {
verifyAppCallTxn(this.txn, { sender: this.admin.value });

sendPayment({
sender: this.controlledAddress.value,
sender: this.address.value,
receiver: addr,
rekeyTo: addr,
note: 'rekeying abstracted account',
Expand All @@ -369,17 +389,17 @@ export class AbstractedAccount extends Contract {
* @param plugin The app to rekey to
*/
arc58_rekeyToPlugin(plugin: Application): void {
const globalKey: PluginsKey = { application: plugin, allowedCaller: globals.zeroAddress };
const globalKey: PluginsKey = { application: plugin, address: 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, allowedCaller: this.txn.sender };
const key: PluginsKey = { application: plugin, address: this.txn.sender };
assert(this.plugins(key).exists && this.plugins(key).value > globals.latestTimestamp);
}

sendPayment({
sender: this.controlledAddress.value,
receiver: this.controlledAddress.value,
sender: this.address.value,
receiver: this.address.value,
rekeyTo: plugin.address,
note: 'rekeying to plugin app',
});
Expand All @@ -396,28 +416,17 @@ export class AbstractedAccount extends Contract {
this.arc58_rekeyToPlugin(this.namedPlugins(name).value.application);
}

/**
* Change the admin for this app
*
* @param newAdmin The new admin
*/
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 allowedCaller The address of that's allowed to call the app
* @param address 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
*/
arc58_addPlugin(app: Application, allowedCaller: Address, end: uint64): void {
arc58_addPlugin(app: Application, address: Address, end: uint64): void {
verifyTxn(this.txn, { sender: this.admin.value });
const key: PluginsKey = { application: app, allowedCaller: allowedCaller };
const key: PluginsKey = { application: app, address: address };
this.plugins(key).value = end;
}

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

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

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

const key: PluginsKey = { application: app, allowedCaller: allowedCaller };
const key: PluginsKey = { application: app, address: address };
this.namedPlugins(name).value = key;
this.plugins(key).value = end;
}
Expand All @@ -466,9 +475,14 @@ https://github.com/joe-p/account_abstraction.git

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

### Diagrams
### Potential Use Cases

These are potential use cases of this ARC. These are NOT part of the ARC itself and should likely be further developed and discussed in a seperate ARC. They soley exist to demonstrate what the usage of this ARC will look like.

#### 0-ALGO Opt-In Onboarding

Using the above reference implementation, 0-ALGO onboarding can be done via the following sequence. It should be noted that a different implementation focused specifically on this use case could be even more efficient.

```mermaid
sequenceDiagram
participant Wallet
Expand All @@ -494,7 +508,7 @@ sequenceDiagram

#### 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
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 using the above reference implmentation.

```mermaid
sequenceDiagram
Expand All @@ -511,6 +525,10 @@ sequenceDiagram
note over Wallet: Address: EXISTING_ADDRESS<br/>Auth Addr: APP_ADDRESS
```

#### Postmortem Authorization

Using this ARC, you could created an abstracted account that changes who the admin is after a certain amount of time has passed without the original admin's interaction. The admin of the account will effectively be controlled by a time-based dead man's switch.

## 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