From 309499628cc03ffc63cf5875a1637fe4761f6a9b Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Mon, 8 Jan 2024 19:15:12 -0500 Subject: [PATCH 01/16] plugin-based account abstraction --- ARCs/arc-draft_plugin_abstraction.md | 177 +++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 ARCs/arc-draft_plugin_abstraction.md diff --git a/ARCs/arc-draft_plugin_abstraction.md b/ARCs/arc-draft_plugin_abstraction.md new file mode 100644 index 000000000..3b65deb3f --- /dev/null +++ b/ARCs/arc-draft_plugin_abstraction.md @@ -0,0 +1,177 @@ +--- +arc: +title: Plugin-Based Account Abstraction +description: An extendable standard for account abstraction using stateful applciations +author: Joe Polny (@joe-p), Kyle Breeding aka krby.algo (@kylebeee) +discussions-to: +status: Draft +type: Standards Track +category: ARC +created: 2024-01-08 +requires: 4 +--- + +## Abstract +This ARC proposes a standard for using stateful applications and rekey transactions to enable account abstraction on Algorand. The abstracted account is controlled by a single stateful application which is the auth address of the abstracted account. Other applications can be used as plugin to provide additional functionality to the abstraction account. + +## Motivation +Manually signing transactions for every dApp interaction can be rather fatiguing for the end-user, which results in a frustrating UX. In some cases, it makes specfific app designs that require a lot of transactions borderline impossible without delegation or an escrow account. + +Another common point of friction for end-users in the Algorand ecosystem is ASA opt-in transactions. This is a paticularly high point of friction for onboarding new accounts since they must be funded and then initiate a transaction. This standard can be used to allow mass creation of non-custodial accounts and trigger opt-ins on their behalf. + +## Specification + +### Definitions +**Abstracted Account** - An account that has functionality beyond a typical keypair-based account. + +**Abstracted Account App** - The stateuful application used to control the abstracted account. This app's address is the `auth-addr` of the abstracted account. + +**Plugin** - An additional application that adds functionality to the **Abstracted Account App** (and thus the **Abstracted Account**). + +**Admin** - An account, sepererate from the **Abstracted Account**, that controls the **Abstracted Account App**. In patiular, this app can initiate rekeys, add plugins, and transfer admin. + + +### ARC4 Methods + +An abstracted app account that adheres to this standard **MUST** implement the following methods + +```json + "methods": [ + { + "name": "createApplication", + "desc": "Create an abstracted account application", + "args": [ + { + "name": "address", + "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" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "verifyAppAuthAddr", + "desc": "Verify the abstracted account is rekeyed to this app", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "rekeyTo", + "desc": "Rekey the abstracted account to another address. Primarily useful for rekeying to an EOA.", + "args": [ + { + "name": "addr", + "type": "address", + "desc": "The address to rekey to" + }, + { + "name": "flash", + "type": "bool", + "desc": "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" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "rekeyToPlugin", + "desc": "Temporarily rekey to an approved plugin app address", + "args": [ + { + "name": "plugin", + "type": "application", + "desc": "The app to rekey to" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "changeAdmin", + "desc": "Change the admin for this app", + "args": [ + { + "name": "newAdmin", + "type": "account", + "desc": "The new admin" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "addPlugin", + "desc": "Add an app to the list of approved plugins", + "args": [ + { + "name": "app", + "type": "application", + "desc": "The app to add" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "removePlugin", + "desc": "Remove an app from the list of approved plugins", + "args": [ + { + "name": "app", + "type": "application", + "desc": "The app to remove" + } + ], + "returns": { + "type": "void" + } + } + ] +``` + +### Plugins +TODO + +### Wallet and dApp Support +TODO + +## Rationale + +### App vs Logic Sig +There have similar propsoals for reducing end-user friction, such as [ARC47](./arc-0047.md) which enables safer usage of delegated logic signatures. The major downside of logic signatures is that they are not + +### Plugins +TODO + +## Backwards Compatibility +Existing Algorand accounts can transition to an abstracted account by creating a new abstracted account application and setting the address to their current address. This requires them to create a new account to act as the admin. + +End-users can use an abstracted account with any dApp provided they rekey the account to an externally owned account. + +## Test Cases + +TODO: Some functional tests are in this repo https://github.com/joe-p/account_abstraction.git + +## Reference Implementation +https://github.com/joe-p/account_abstraction.git + +TODO: Migrate to ARC repo, but waiting until development has settled. + +## Security Considerations +By adding a plugin to an abstracted account, that plugin can be called by anyone which will initiate a rekey from the abstracted account to the plugin app address. While the plugin must rekey back, there is no safeguards on what the plugin does when it has authority over the abstracted account. As such, extreme diligance must be taken by the end-user to ensure they are adding safe and/or trusted plugins. + +## Copyright +Copyright and related rights waived via CCO. From b2063443d507f3e89993708fa0ec0fe14ccdd7bc Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Mon, 8 Jan 2024 19:16:19 -0500 Subject: [PATCH 02/16] fix typo --- ARCs/arc-draft_plugin_abstraction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCs/arc-draft_plugin_abstraction.md b/ARCs/arc-draft_plugin_abstraction.md index 3b65deb3f..f0e0de788 100644 --- a/ARCs/arc-draft_plugin_abstraction.md +++ b/ARCs/arc-draft_plugin_abstraction.md @@ -12,7 +12,7 @@ requires: 4 --- ## Abstract -This ARC proposes a standard for using stateful applications and rekey transactions to enable account abstraction on Algorand. The abstracted account is controlled by a single stateful application which is the auth address of the abstracted account. Other applications can be used as plugin to provide additional functionality to the abstraction account. +This ARC proposes a standard for using stateful applications and rekey transactions to enable account abstraction on Algorand. The abstracted account is controlled by a single stateful application which is the auth address of the abstracted account. Other applications can be used as plugin to provide additional functionality to the abstracted account. ## Motivation Manually signing transactions for every dApp interaction can be rather fatiguing for the end-user, which results in a frustrating UX. In some cases, it makes specfific app designs that require a lot of transactions borderline impossible without delegation or an escrow account. From f797187dfbed13f6727fb7f5c929fbc59f000494 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Mon, 8 Jan 2024 19:58:33 -0500 Subject: [PATCH 03/16] add EOA definition, finish app vs logic sig --- ARCs/arc-draft_plugin_abstraction.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ARCs/arc-draft_plugin_abstraction.md b/ARCs/arc-draft_plugin_abstraction.md index f0e0de788..38892e8a5 100644 --- a/ARCs/arc-draft_plugin_abstraction.md +++ b/ARCs/arc-draft_plugin_abstraction.md @@ -22,6 +22,8 @@ Another common point of friction for end-users in the Algorand ecosystem is ASA ## Specification ### Definitions +**External Owned Account (EOA)** - An account that is NOT controlled by a smart contract. + **Abstracted Account** - An account that has functionality beyond a typical keypair-based account. **Abstracted Account App** - The stateuful application used to control the abstracted account. This app's address is the `auth-addr` of the abstracted account. @@ -151,7 +153,7 @@ TODO ## Rationale ### App vs Logic Sig -There have similar propsoals for reducing end-user friction, such as [ARC47](./arc-0047.md) which enables safer usage of delegated logic signatures. The major downside of logic signatures is that they are not +There have similar propsoals for reducing end-user friction, such as [ARC47](./arc-0047.md) which enables safer usage of delegated logic signatures. The major downside of logic signatures is that they are not useable by smart contracts. This severely limits composability and potential use cases. ### Plugins TODO From 638d6667128373d0a9b8ebb4341300ad9ee0bd56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane?= Date: Tue, 9 Jan 2024 11:02:12 +0100 Subject: [PATCH 04/16] Assign number Fixing format --- ARCs/{arc-draft_plugin_abstraction.md => arc-0058.md} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename ARCs/{arc-draft_plugin_abstraction.md => arc-0058.md} (93%) diff --git a/ARCs/arc-draft_plugin_abstraction.md b/ARCs/arc-0058.md similarity index 93% rename from ARCs/arc-draft_plugin_abstraction.md rename to ARCs/arc-0058.md index 38892e8a5..5ba5ff97e 100644 --- a/ARCs/arc-draft_plugin_abstraction.md +++ b/ARCs/arc-0058.md @@ -1,9 +1,9 @@ --- -arc: +arc: 58 title: Plugin-Based Account Abstraction -description: An extendable standard for account abstraction using stateful applciations +description: Account abstraction using stateful applciations author: Joe Polny (@joe-p), Kyle Breeding aka krby.algo (@kylebeee) -discussions-to: +discussions-to: https://github.com/algorandfoundation/ARCs/issues/269/files status: Draft type: Standards Track category: ARC @@ -33,7 +33,7 @@ Another common point of friction for end-users in the Algorand ecosystem is ASA **Admin** - An account, sepererate from the **Abstracted Account**, that controls the **Abstracted Account App**. In patiular, this app can initiate rekeys, add plugins, and transfer admin. -### ARC4 Methods +### [ARC-4](./arc-0004.md) Methods An abstracted app account that adheres to this standard **MUST** implement the following methods @@ -153,7 +153,7 @@ TODO ## Rationale ### App vs Logic Sig -There have similar propsoals for reducing end-user friction, such as [ARC47](./arc-0047.md) which enables safer usage of delegated logic signatures. The major downside of logic signatures is that they are not useable by smart contracts. This severely limits composability and potential use cases. +There have similar propsoals for reducing end-user friction, such as [ARC-47](./arc-0047.md) which enables safer usage of delegated logic signatures. The major downside of logic signatures is that they are not useable by smart contracts. This severely limits composability and potential use cases. ### Plugins TODO From 03648519118ee7aa04913ab251ec609635fd81bc Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Tue, 9 Jan 2024 12:43:10 -0500 Subject: [PATCH 05/16] add reference contract in ARC, minor updates --- ARCs/arc-0058.md | 142 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 2 deletions(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index 5ba5ff97e..89112a051 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -156,7 +156,7 @@ TODO There have similar propsoals for reducing end-user friction, such as [ARC-47](./arc-0047.md) which enables safer usage of delegated logic signatures. The major downside of logic signatures is that they are not useable by smart contracts. This severely limits composability and potential use cases. ### Plugins -TODO +Rather than constantly updating the approval program of the abstracted account application to add functionality, it is safer and easier to simply add additional apps that enable the desired functionality. This also gives the end-user more control over what various dApps can do with their account at any time. ## Backwards Compatibility Existing Algorand accounts can transition to an abstracted account by creating a new abstracted account application and setting the address to their current address. This requires them to create a new account to act as the admin. @@ -168,12 +168,150 @@ End-users can use an abstracted account with any dApp provided they rekey the ac TODO: Some functional tests are in this repo https://github.com/joe-p/account_abstraction.git ## Reference Implementation + +```ts +import { Contract } from '@algorandfoundation/tealscript'; + +export class AbstractedAccount extends Contract { + /** Target AVM 10 */ + programVersion = 10; + + /** The admin of the abstracted account */ + admin = GlobalStateKey
(); + + /** + * The apps that are authorized to send itxns from the abstracted account + * The box map values aren't actually used and are always empty + */ + plugins = BoxMap>(); + + /** The address of the abstracted account */ + address = GlobalStateKey
(); + + /** + * 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.address.value || lastTxn.rekeyTo !== this.getAuthAddr()) { + verifyAppCallTxn(lastTxn, { + applicationID: this.app, + }); + assert(lastTxn.applicationArgs[0] === method('verifyAuthAddr()void')); + } + } + + /** + * 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.address.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 admin The admin for this app + */ + createApplication(address: Address, admin: Address): void { + verifyAppCallTxn(this.txn, { + sender: { includedIn: [address, admin] }, + }); + + assert(admin !== address); + + this.admin.value = admin; + this.address.value = address === Address.zeroAddress ? this.app.address : address; + } + + /** + * Verify the abstracted account is rekeyed to this app + */ + verifyAuthAddr(): void { + assert(this.address.value.authAddr === this.getAuthAddr()); + } + + /** + * Rekey the abstracted account to another address. Primarily useful for rekeying to an EOA. + * + * @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 { + verifyAppCallTxn(this.txn, { sender: this.admin.value }); + + sendPayment({ + sender: this.address.value, + receiver: addr, + rekeyTo: addr, + note: 'rekeying abstracted account', + }); + + if (flash) this.verifyRekeyToAbstractedAccount(); + } + + /** + * Temporarily rekey to an approved plugin app address + * + * @param plugin The app to rekey to + */ + rekeyToPlugin(plugin: Application): void { + assert(this.plugins(plugin).exists); + + sendPayment({ + sender: this.address.value, + receiver: this.address.value, + rekeyTo: plugin.address, + note: 'rekeying to plugin app', + }); + + this.verifyRekeyToAbstractedAccount(); + } + + /** + * Change the admin for this app + * + * @param newAdmin The new admin + */ + changeAdmin(newAdmin: Account): void { + verifyTxn(this.txn, { sender: this.admin.value }); + this.admin.value = newAdmin; + } + + /** + * Add an app to the list of approved plugins + * + * @param app The app to add + */ + addPlugin(app: Application): void { + assert(this.txn.sender === this.admin.value); + + this.plugins(app).create(0); + } + + /** + * Remove an app from the list of approved plugins + * + * @param app The app to remove + */ + removePlugin(app: Application): void { + assert(this.txn.sender === this.admin.value); + + this.plugins(app).delete(); + } +} + +``` https://github.com/joe-p/account_abstraction.git TODO: Migrate to ARC repo, but waiting until development has settled. ## Security Considerations -By adding a plugin to an abstracted account, that plugin can be called by anyone which will initiate a rekey from the abstracted account to the plugin app address. While the plugin must rekey back, there is no safeguards on what the plugin does when it has authority over the abstracted account. As such, extreme diligance must be taken by the end-user to ensure they are adding safe and/or trusted plugins. +By adding a plugin to an abstracted account, that plugin can be called by anyone which will initiate a rekey from the abstracted account to the plugin app address. While the plugin must rekey back, there is no safeguards on what the plugin does when it has authority over the abstracted 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. ## Copyright Copyright and related rights waived via CCO. From 11cce9a72c8db787bbb9bb18eda09c14a6655df5 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Tue, 9 Jan 2024 18:32:38 -0500 Subject: [PATCH 06/16] updated TEALScript --- ARCs/arc-0058.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index 89112a051..667c71051 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -198,8 +198,10 @@ export class AbstractedAccount extends Contract { if (lastTxn.sender !== this.address.value || lastTxn.rekeyTo !== this.getAuthAddr()) { verifyAppCallTxn(lastTxn, { applicationID: this.app, + applicationArgs: { + 0: method('verifyAuthAddr()void'), + }, }); - assert(lastTxn.applicationArgs[0] === method('verifyAuthAddr()void')); } } @@ -288,7 +290,7 @@ export class AbstractedAccount extends Contract { * @param app The app to add */ addPlugin(app: Application): void { - assert(this.txn.sender === this.admin.value); + verifyTxn(this.txn, { sender: this.admin.value }); this.plugins(app).create(0); } @@ -299,7 +301,7 @@ export class AbstractedAccount extends Contract { * @param app The app to remove */ removePlugin(app: Application): void { - assert(this.txn.sender === this.admin.value); + verifyTxn(this.txn, { sender: this.admin.value }); this.plugins(app).delete(); } From e04e42f8b7332de3f31742ebfeefb4bdd8fb1c04 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 10 Jan 2024 07:54:18 -0500 Subject: [PATCH 07/16] named plugins --- ARCs/arc-0058.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index 667c71051..5e3773217 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -59,7 +59,7 @@ An abstracted app account that adheres to this standard **MUST** implement the f } }, { - "name": "verifyAppAuthAddr", + "name": "verifyAuthAddr", "desc": "Verify the abstracted account is rekeyed to this app", "args": [], "returns": { @@ -99,6 +99,20 @@ An abstracted app account that adheres to this standard **MUST** implement the f "type": "void" } }, + { + "name": "rekeyToNamedPlugin", + "desc": "Temporarily rekey to a named plugin app address", + "args": [ + { + "name": "name", + "type": "string", + "desc": "The name of the plugin to rekey to" + } + ], + "returns": { + "type": "void" + } + }, { "name": "changeAdmin", "desc": "Change the admin for this app", @@ -140,6 +154,39 @@ An abstracted app account that adheres to this standard **MUST** implement the f "returns": { "type": "void" } + }, + { + "name": "addNamedPlugin", + "desc": "Add a named plugin", + "args": [ + { + "name": "app", + "type": "application", + "desc": "The plugin app" + }, + { + "name": "name", + "type": "string", + "desc": "The plugin name" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "removeNamedPlugin", + "desc": "Remove a named plugin", + "args": [ + { + "name": "name", + "type": "string", + "desc": "The plugin name" + } + ], + "returns": { + "type": "void" + } } ] ``` @@ -183,7 +230,12 @@ export class AbstractedAccount extends Contract { * The apps that are authorized to send itxns from the abstracted account * The box map values aren't actually used and are always empty */ - plugins = BoxMap>(); + plugins = BoxMap>({ prefix: 'p' }); + + /** + * Plugins that have been given a name for discoverability + */ + namedPlugins = BoxMap({ prefix: 'n' }); /** The address of the abstracted account */ address = GlobalStateKey
(); @@ -274,6 +326,15 @@ export class AbstractedAccount extends Contract { this.verifyRekeyToAbstractedAccount(); } + /** + * Temporarily rekey to a named plugin app address + * + * @param name The name of the plugin to rekey to + */ + rekeyToNamedPlugin(name: string): void { + this.rekeyToPlugin(this.namedPlugins(name).value); + } + /** * Change the admin for this app * @@ -305,8 +366,34 @@ export class AbstractedAccount extends Contract { this.plugins(app).delete(); } -} + /** + * Add a named plugin + * + * @param app The plugin app + * @param name The plugin name + */ + addNamedPlugin(app: Application, name: string): void { + verifyTxn(this.txn, { sender: this.admin.value }); + + assert(!this.namedPlugins(name).exists); + this.namedPlugins(name).value = app; + this.plugins(app).create(0); + } + + /** + * Remove a named plugin + * + * @param name The plugin name + */ + removeNamedPlugin(name: string): void { + verifyTxn(this.txn, { sender: this.admin.value }); + + const app = this.namedPlugins(name).value; + this.namedPlugins(name).delete(); + this.plugins(app).delete(); + } +} ``` https://github.com/joe-p/account_abstraction.git From 6257d607e183971e8c79254ca45f2aca8eb615aa Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 10 Jan 2024 07:59:54 -0500 Subject: [PATCH 08/16] add named plugins --- ARCs/arc-0058.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index 5e3773217..f246f0e62 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -192,7 +192,11 @@ An abstracted app account that adheres to this standard **MUST** implement the f ``` ### Plugins -TODO +Plugins are applications that the abstracted account applicaiton **MUST** rekey to when `rekeyToPlugin` or `rekeyToNamedPlugin` is called. After a plugin has been rekeyed to, the abstracted account **MUST** be rekeyed back to the abstracted account application. When and how this rekey is done does not matter, but it **MUST** be verified by a call `verifyAuthAddr` as the last transaction in the group OR the last transaction in the group must be an explicit rekey transaction. + +#### Named Plugins + +The admin can optionally add a named plugin to their abstracted account application. If the name starts with "ARC" then the name **MUST** match the regex `/^ARC\d+$/`, with the number corresponding to an ARC number. The ARC number **MUST NOT** be prefixed with any 0s. A plugin name beginning with "ARC" must correspond to a plugin that implements the interface(s) standardized in the respective ARC. ### Wallet and dApp Support TODO From a7503010fbd2654d414876eb6efc8238be6380a8 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Wed, 10 Jan 2024 09:55:38 -0500 Subject: [PATCH 09/16] reword named plugins --- ARCs/arc-0058.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index f246f0e62..0a53d9f5e 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -196,7 +196,7 @@ Plugins are applications that the abstracted account applicaiton **MUST** rekey #### Named Plugins -The admin can optionally add a named plugin to their abstracted account application. If the name starts with "ARC" then the name **MUST** match the regex `/^ARC\d+$/`, with the number corresponding to an ARC number. The ARC number **MUST NOT** be prefixed with any 0s. A plugin name beginning with "ARC" must correspond to a plugin that implements the interface(s) standardized in the respective ARC. +The admin can optionally add a named plugin to their abstracted account application. Any name that matches the regex `/^ARC\d+$/` **MUST** implement the interface(s) described in the respective ARC. The ARC number **MUST NOT** have any leading 0s. ### Wallet and dApp Support TODO From 55e630878c129fbddb2c157c6b5de8f1ba6c03d4 Mon Sep 17 00:00:00 2001 From: Joe Polny <50534337+joe-p@users.noreply.github.com> Date: Wed, 10 Jan 2024 11:15:54 -0500 Subject: [PATCH 10/16] fix typos Co-authored-by: John Jannotti --- ARCs/arc-0058.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index 0a53d9f5e..cd2dc27ed 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -15,27 +15,27 @@ requires: 4 This ARC proposes a standard for using stateful applications and rekey transactions to enable account abstraction on Algorand. The abstracted account is controlled by a single stateful application which is the auth address of the abstracted account. Other applications can be used as plugin to provide additional functionality to the abstracted account. ## Motivation -Manually signing transactions for every dApp interaction can be rather fatiguing for the end-user, which results in a frustrating UX. In some cases, it makes specfific app designs that require a lot of transactions borderline impossible without delegation or an escrow account. +Manually signing transactions for every dApp interaction can be rather fatiguing for the end-user, which results in a frustrating UX. In some cases, it makes specific app designs that require a lot of transactions borderline impossible without delegation or an escrow account. Another common point of friction for end-users in the Algorand ecosystem is ASA opt-in transactions. This is a paticularly high point of friction for onboarding new accounts since they must be funded and then initiate a transaction. This standard can be used to allow mass creation of non-custodial accounts and trigger opt-ins on their behalf. ## Specification ### Definitions -**External Owned Account (EOA)** - An account that is NOT controlled by a smart contract. +**External Owned Account (EOA)** - An account that is _not_ controlled by a smart contract. **Abstracted Account** - An account that has functionality beyond a typical keypair-based account. -**Abstracted Account App** - The stateuful application used to control the abstracted account. This app's address is the `auth-addr` of the abstracted account. +**Abstracted Account App** - The stateful application used to control the abstracted account. This app's address is the `auth-addr` of the abstracted account. **Plugin** - An additional application that adds functionality to the **Abstracted Account App** (and thus the **Abstracted Account**). -**Admin** - An account, sepererate from the **Abstracted Account**, that controls the **Abstracted Account App**. In patiular, this app can initiate rekeys, add plugins, and transfer admin. +**Admin** - An account, separate from the **Abstracted Account**, that controls the **Abstracted Account App**. In particular, this app can initiate rekeys, add plugins, and transfer admin. ### [ARC-4](./arc-0004.md) Methods -An abstracted app account that adheres to this standard **MUST** implement the following methods +An Abstracted Account App that adheres to this standard **MUST** implement the following methods ```json "methods": [ @@ -78,7 +78,7 @@ An abstracted app account that adheres to this standard **MUST** implement the f { "name": "flash", "type": "bool", - "desc": "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" + "desc": "Whether or not this should be a flash rekey. If true, the rekey back to the app address must be done in the same transaction group as this call" } ], "returns": { @@ -192,7 +192,7 @@ An abstracted app account that adheres to this standard **MUST** implement the f ``` ### Plugins -Plugins are applications that the abstracted account applicaiton **MUST** rekey to when `rekeyToPlugin` or `rekeyToNamedPlugin` is called. After a plugin has been rekeyed to, the abstracted account **MUST** be rekeyed back to the abstracted account application. When and how this rekey is done does not matter, but it **MUST** be verified by a call `verifyAuthAddr` as the last transaction in the group OR the last transaction in the group must be an explicit rekey transaction. +Plugins are applications that the Abstracted Account App **MUST** rekey to when `rekeyToPlugin` or `rekeyToNamedPlugin` is called. After a plugin has been rekeyed to, the abstracted account **MUST** be rekeyed back to the abstracted account application. When and how this rekey is done does not matter, but it **MUST** be verified by a call `verifyAuthAddr` as the last transaction in the group OR the last transaction in the group must be an explicit rekey transaction. #### Named Plugins @@ -204,7 +204,7 @@ TODO ## Rationale ### App vs Logic Sig -There have similar propsoals for reducing end-user friction, such as [ARC-47](./arc-0047.md) which enables safer usage of delegated logic signatures. The major downside of logic signatures is that they are not useable by smart contracts. This severely limits composability and potential use cases. +There have similar propsoals for reducing end-user friction, such as [ARC-47](./arc-0047.md) which enables safer usage of delegated logic signatures. The major downside of logic signatures is that they are not usable by smart contracts. This severely limits composability and potential use cases. ### Plugins Rather than constantly updating the approval program of the abstracted account application to add functionality, it is safer and easier to simply add additional apps that enable the desired functionality. This also gives the end-user more control over what various dApps can do with their account at any time. From e1b114e68229f0e252df7aaabea78f06b5772efb Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 18 Jan 2024 07:56:49 -0500 Subject: [PATCH 11/16] update implementation --- ARCs/arc-0058.md | 76 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index cd2dc27ed..6f0e85ee0 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -78,7 +78,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f { "name": "flash", "type": "bool", - "desc": "Whether or not this should be a flash rekey. If true, the rekey back to the app address must be done in the same transaction group as this call" + "desc": "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" } ], "returns": { @@ -135,6 +135,16 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f "name": "app", "type": "application", "desc": "The app to add" + }, + { + "name": "address", + "type": "address", + "desc": "The address of that's allowed to call the appor the global zero address for all addresses" + }, + { + "name": "end", + "type": "uint64", + "desc": "The timestamp when the permission expires" } ], "returns": { @@ -149,6 +159,10 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f "name": "app", "type": "application", "desc": "The app to remove" + }, + { + "name": "address", + "type": "address" } ], "returns": { @@ -159,15 +173,23 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f "name": "addNamedPlugin", "desc": "Add a named plugin", "args": [ + { + "name": "name", + "type": "string", + "desc": "The plugin name" + }, { "name": "app", "type": "application", "desc": "The plugin app" }, { - "name": "name", - "type": "string", - "desc": "The plugin name" + "name": "address", + "type": "address" + }, + { + "name": "end", + "type": "uint64" } ], "returns": { @@ -192,6 +214,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f ``` ### Plugins + Plugins are applications that the Abstracted Account App **MUST** rekey to when `rekeyToPlugin` or `rekeyToNamedPlugin` is called. After a plugin has been rekeyed to, the abstracted account **MUST** be rekeyed back to the abstracted account application. When and how this rekey is done does not matter, but it **MUST** be verified by a call `verifyAuthAddr` as the last transaction in the group OR the last transaction in the group must be an explicit rekey transaction. #### Named Plugins @@ -223,6 +246,8 @@ 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; address: Address }; + export class AbstractedAccount extends Contract { /** Target AVM 10 */ programVersion = 10; @@ -231,15 +256,17 @@ export class AbstractedAccount extends Contract { admin = GlobalStateKey
(); /** - * The apps that are authorized to send itxns from the abstracted account - * The box map values aren't actually used and are always empty + * The apps and addresses that are authorized to send itxns from the abstracted account, + * 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>({ prefix: 'p' }); + + plugins = BoxMap({ prefix: 'p' }); /** * Plugins that have been given a name for discoverability */ - namedPlugins = BoxMap({ prefix: 'n' }); + namedPlugins = BoxMap({ prefix: 'n' }); /** The address of the abstracted account */ address = GlobalStateKey
(); @@ -318,7 +345,13 @@ export class AbstractedAccount extends Contract { * @param plugin The app to rekey to */ rekeyToPlugin(plugin: Application): void { - assert(this.plugins(plugin).exists); + 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, address: this.txn.sender }; + assert(this.plugins(key).exists && this.plugins(key).value > globals.latestTimestamp); + } sendPayment({ sender: this.address.value, @@ -336,7 +369,7 @@ 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); + this.rekeyToPlugin(this.namedPlugins(name).value.application); } /** @@ -353,11 +386,14 @@ export class AbstractedAccount extends Contract { * 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 + * or the global zero address for all addresses + * @param end The timestamp when the permission expires */ - addPlugin(app: Application): void { + addPlugin(app: Application, address: Address, end: uint64): void { verifyTxn(this.txn, { sender: this.admin.value }); - - this.plugins(app).create(0); + const key: PluginsKey = { application: app, address: address }; + this.plugins(key).value = end; } /** @@ -365,10 +401,11 @@ export class AbstractedAccount extends Contract { * * @param app The app to remove */ - removePlugin(app: Application): void { + removePlugin(app: Application, address: Address): void { verifyTxn(this.txn, { sender: this.admin.value }); - this.plugins(app).delete(); + const key: PluginsKey = { application: app, address: address }; + this.plugins(key).delete(); } /** @@ -377,12 +414,13 @@ export class AbstractedAccount extends Contract { * @param app The plugin app * @param name The plugin name */ - addNamedPlugin(app: Application, name: string): void { + addNamedPlugin(name: string, app: Application, address: Address, end: uint64): void { verifyTxn(this.txn, { sender: this.admin.value }); - assert(!this.namedPlugins(name).exists); - this.namedPlugins(name).value = app; - this.plugins(app).create(0); + + const key: PluginsKey = { application: app, address: address }; + this.namedPlugins(name).value = key; + this.plugins(key).value = end; } /** From 20f89fd47b95b8c1f4fdc48cee4f77d2b92ff3d7 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 18 Jan 2024 08:06:04 -0500 Subject: [PATCH 12/16] add plugin permissions to ARC --- ARCs/arc-0058.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index 6f0e85ee0..7700eeff7 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -217,6 +217,10 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f Plugins are applications that the Abstracted Account App **MUST** rekey to when `rekeyToPlugin` or `rekeyToNamedPlugin` is called. After a plugin has been rekeyed to, the abstracted account **MUST** be rekeyed back to the abstracted account application. When and how this rekey is done does not matter, but it **MUST** be verified by a call `verifyAuthAddr` as the last transaction in the group OR the last transaction in the group must be an explicit rekey transaction. +### Plugin Permissions + +When adding a plugin, the the admin can specify an end time and a specific address that is allowed to call `rekeyToPlugin` for that specific plugin. Using the zero address will allow anyone to use the plugin. If `global LatestTimeStamp` has past the specified end time, the `rekeyToPlugin` call **MUST** fail. If the permitted address is not the zero address, the `rekeyToPlugin` call **MUST** fail. + #### Named Plugins The admin can optionally add a named plugin to their abstracted account application. Any name that matches the regex `/^ARC\d+$/` **MUST** implement the interface(s) described in the respective ARC. The ARC number **MUST NOT** have any leading 0s. @@ -232,6 +236,10 @@ There have similar propsoals for reducing end-user friction, such as [ARC-47](./ ### Plugins Rather than constantly updating the approval program of the abstracted account application to add functionality, it is safer and easier to simply add additional apps that enable the desired functionality. This also gives the end-user more control over what various dApps can do with their account at any time. +### Plugin Permisions + +A common use case for plugins will be end-users allowing specific apps to perform actions on their account. As such, implementing this in the Abstracted Account App allows for wallets and other ecosystem tools to easy display permissions to end users. The end time is also useful to ensure a user only enables a plugin for the time that it would be useful for them. The concept of plugins is similar to approvals on EVM chains and there have been cases where old approvals became an attack vector. + ## Backwards Compatibility Existing Algorand accounts can transition to an abstracted account by creating a new abstracted account application and setting the address to their current address. This requires them to create a new account to act as the admin. @@ -442,7 +450,9 @@ https://github.com/joe-p/account_abstraction.git TODO: Migrate to ARC repo, but waiting until development has settled. ## Security Considerations -By adding a plugin to an abstracted account, that plugin can be called by anyone which will initiate a rekey from the abstracted account to the plugin app address. While the plugin must rekey back, there is no safeguards on what the plugin does when it has authority over the abstracted 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. +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. + +The security assumptions of plugins are also similar to EVM approvals, which are a common source of exploits and scams. Implementations of plugins Algorand should learn from the lessons of EVM approvals. ## Copyright Copyright and related rights waived via CCO. From 78147573cbcb0d8c5dd27ce377c6a7c9f1f80d31 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 18 Jan 2024 08:21:29 -0500 Subject: [PATCH 13/16] Wallet and Application Support --- ARCs/arc-0058.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index 7700eeff7..82f8adfd3 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -225,8 +225,22 @@ When adding a plugin, the the admin can specify an end time and a specific addre The admin can optionally add a named plugin to their abstracted account application. Any name that matches the regex `/^ARC\d+$/` **MUST** implement the interface(s) described in the respective ARC. The ARC number **MUST NOT** have any leading 0s. -### Wallet and dApp Support -TODO +### Wallet and Application Support + +#### Adding Plugins + +An application may ask a user to add a plugin to their Abstracted Account App. The wallet **MUST** show the user the requested permitted caller, end time, and plugin app ID. Wallets **MAY** have a database of known plugins to describe the plugin in a more human-friendly way, but the exact details of this implementation is outside the scope of this ARC. + +#### Viewing Plugins + +For a given Abstracted Account, a wallet **SHOULD** allow the user to view all of the plugins added to their Abstracted Account App. Wallets also **SHOULD** provide the user a way to manually add and remove plugins from their Abstracted Account App. + + +#### Supporting EOA Rekeys + +If a user connects to an app with an Abstracted Account the app **SHOULD** allow the user to easily sign transactions with an externally owned account. It is easy for the admin to manually call `rekeyTo` prior to interacting with an app, but this does not gurantee the user will rekey back to the Abstracted Account (this breaking plugin functionality). As such, to improve user experience, applications **SHOULD** be able to send transactions groups that start with a `rekeyTo` call that rekeys to the admin admin account with `flash` set to true. Because `flash` is set to true, the last transaction in the group **MUST** be a rekey from the admin address to the Abstracted Account address. + +This requires wallets to be able to tell apps that the connected account is an Abstracted Account. ## Rationale From 57d7f2ba84d9661c2adc4913bc9b79ecbb52b4ad Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Fri, 2 Feb 2024 18:25:23 -0500 Subject: [PATCH 14/16] add sequence diagrams and new implementation --- ARCs/arc-0058.md | 138 +++++++++++++++++++++++++++++++---------------- 1 file changed, 93 insertions(+), 45 deletions(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index 82f8adfd3..4b057f2fc 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -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" }, @@ -59,7 +59,7 @@ 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": { @@ -67,7 +67,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f } }, { - "name": "rekeyTo", + "name": "arc58_rekeyTo", "desc": "Rekey the abstracted account to another address. Primarily useful for rekeying to an EOA.", "args": [ { @@ -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": [ { @@ -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": [ { @@ -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": [ { @@ -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": [ { @@ -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" }, @@ -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": [ { @@ -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" } ], @@ -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": [ { @@ -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" }, { @@ -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": [ { @@ -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 */ @@ -290,8 +292,8 @@ export class AbstractedAccount extends Contract { */ namedPlugins = BoxMap({ prefix: 'n' }); - /** The address of the abstracted account */ - address = GlobalStateKey
(); + /** The address this app controls */ + controlledAddress = GlobalStateKey
(); /** * Ensure that by the end of the group the abstracted account has control of its address @@ -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'), }, }); } @@ -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()); } /** @@ -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', @@ -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', }); @@ -390,8 +392,8 @@ 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); } /** @@ -399,8 +401,9 @@ export class AbstractedAccount extends Contract { * * @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; } @@ -408,13 +411,13 @@ export class AbstractedAccount extends Contract { * 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; } @@ -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(); } @@ -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; } @@ -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; @@ -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
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
the above three tranasctions upon create + Dapp->>Wallet: Abstracted App ID + note over Wallet: The user sees the
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
Auth Addr: EXISTING_ADDRESS + Wallet->>Wallet: Create new keypair + note over Wallet: Address: NEW_ADDRESS
Auth Addr: NEW_ADDRESS + Wallet->>Wallet: Rekey NEW_ADDRESS to EXISTING_ADDRESS + note over Wallet: Address: NEW_ADDRESS
Auth Addr: EXISTING_ADDRESS + Wallet->>Abstracted App: createApplication({ admin: NEW_ADDRESS, controlledAddress: EXISTING_ADDRESS }) + note over Abstracted App: Address: APP_ADDRESS
Admin: NEW_ADDRESS
Controlled Address: EXISTING_ADDRESS + Wallet->>Wallet: Rekey EXISTING_ADDRESS to APP_ADDRESS + note over Wallet: Address: EXISTING_ADDRESS
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. From d1754b156a2a37842c8e79ab88740837e3a6816a Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 15 Feb 2024 11:29:22 -0500 Subject: [PATCH 15/16] allow rekey/verify anywhere in group, implementation updates --- ARCs/arc-0058.md | 168 ++++++++++++++++++++++++++--------------------- 1 file changed, 93 insertions(+), 75 deletions(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index 4b057f2fc..5d8598d6f 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -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", @@ -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", @@ -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" }, @@ -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" } ], @@ -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" }, { @@ -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 */ @@ -284,7 +273,6 @@ 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({ prefix: 'p' }); /** @@ -292,24 +280,37 @@ export class AbstractedAccount extends Contract { */ namedPlugins = BoxMap({ prefix: 'n' }); - /** The address this app controls */ - controlledAddress = GlobalStateKey
(); + /** The address of the abstracted account */ + address = GlobalStateKey
(); /** * 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); } /** @@ -317,31 +318,50 @@ 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.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()); } /** @@ -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', @@ -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', }); @@ -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; } @@ -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(); } @@ -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; } @@ -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 @@ -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 @@ -511,6 +525,10 @@ sequenceDiagram note over Wallet: Address: EXISTING_ADDRESS
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. From b37c8bff21e9115a4bef7b21602cbd80b5a27e82 Mon Sep 17 00:00:00 2001 From: Joe Polny Date: Thu, 15 Feb 2024 14:27:12 -0500 Subject: [PATCH 16/16] minor updates/rewording --- ARCs/arc-0058.md | 56 ++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/ARCs/arc-0058.md b/ARCs/arc-0058.md index 5d8598d6f..1fa92e8c7 100644 --- a/ARCs/arc-0058.md +++ b/ARCs/arc-0058.md @@ -126,7 +126,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" }, @@ -150,7 +150,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" } ], @@ -173,7 +173,7 @@ An Abstracted Account App that adheres to this standard **MUST** implement the f "desc": "The plugin app" }, { - "name": "address", + "name": "allowedCaller", "type": "address" }, { @@ -259,14 +259,17 @@ 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; address: Address }; +type PluginsKey = { application: Application; allowedCaller: Address }; export class AbstractedAccount extends Contract { /** Target AVM 10 */ programVersion = 10; /** The admin of the abstracted account */ - admin = GlobalStateKey
(); + admin = GlobalStateKey
({ key: 'a' }); + + /** The address this app controls */ + controlledAddress = GlobalStateKey
({ key: 'c' }); /** * The apps and addresses that are authorized to send itxns from the abstracted account, @@ -280,9 +283,6 @@ export class AbstractedAccount extends Contract { */ namedPlugins = BoxMap({ prefix: 'n' }); - /** The address of the abstracted account */ - address = GlobalStateKey
(); - /** * Ensure that by the end of the group the abstracted account has control of its address */ @@ -293,7 +293,7 @@ export class AbstractedAccount extends Contract { const txn = this.txnGroup[i]; // The transaction is an explicit rekey back - if (txn.sender === this.address.value && txn.rekeyTo === this.getAuthAddr()) { + if (txn.sender === this.controlledAddress.value && txn.rekeyTo === this.getAuthAddr()) { rekeyedBack = true; break; } @@ -318,25 +318,25 @@ 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. * This is not part of ARC58 and implementation specific. * - * @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; } /** @@ -361,7 +361,7 @@ export class AbstractedAccount extends Contract { * Verify the abstracted account is rekeyed to this app */ arc58_verifyAuthAddr(): void { - assert(this.address.value.authAddr === this.getAuthAddr()); + assert(this.controlledAddress.value.authAddr === this.getAuthAddr()); } /** @@ -374,7 +374,7 @@ export class AbstractedAccount extends Contract { verifyAppCallTxn(this.txn, { sender: this.admin.value }); sendPayment({ - sender: this.address.value, + sender: this.controlledAddress.value, receiver: addr, rekeyTo: addr, note: 'rekeying abstracted account', @@ -389,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, address: globals.zeroAddress }; + 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', }); @@ -420,13 +420,13 @@ export class AbstractedAccount extends Contract { * 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 */ - arc58_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; } @@ -435,10 +435,10 @@ export class AbstractedAccount extends Contract { * * @param app The app to remove */ - arc58_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(); } @@ -448,11 +448,11 @@ export class AbstractedAccount extends Contract { * @param app The plugin app * @param name The plugin name */ - arc58_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; }