Skip to content
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

ARC-63 Delegated multisig-account #303

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
320 changes: 320 additions & 0 deletions ARCs/arc-0063.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
---
arc: 63
title: Lsig Plug-In Signer for Msig Vault
description: Delegated multisig-account controlled by one account
author: Stéphane BARROSO (@SudoWeezy)
discussions-to: https://github.com/algorandfoundation/ARCs/issues/303
status: Draft
type: Standards Track
category: ARC
created: 2024-07-16
---

## Abstract

This ARC proposes a method for creating a delegated multisig account controlled by one account and a Logic Signature (Lsig).

## Motivation

The motivation behind this ARC is to extend Algorand account features by enabling third-party "Plug-Ins" using a combination of delegated Lsig and Multi-Signature accounts, which act as vaults. This approach allows anyone to sign the Lsig for the vault, while maintaining security and control through a classic algorand account.

## Specification

The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in <a href = "https://www.ietf.org/rfc/rfc2119.txt">RFC-2119</a>

### Components

1. **Owner Account**: Vault's Owner
1. **Lsig Plug-In**: Provided by a third party.
SudoWeezy marked this conversation as resolved.
Show resolved Hide resolved
1. **Plug-In Signer**: Created by generating a new key pair
1. **1/2 Msig Account**: Comprises the owner's address, and the plug-in signer.

### Implementation 1

We will use the following Logic plug-in for illustrative purposes:

**DO NOT USE IN PRODUCTION**

```python
teal_program = """
#pragma version 10
txn TypeEnum
pushint 4
==
txn AssetAmount
pushint 0
==
&&
txn RekeyTo
global ZeroAddress
==

Choose a reason for hiding this comment

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

Suggested change
==
==
&&
txn AssetCloseTo
global ZeroAddress
==

Copy link
Collaborator Author

@SudoWeezy SudoWeezy Oct 1, 2024

Choose a reason for hiding this comment

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

Good catch, to be even more strict, we could also add this:

==
global GenesisHash
byte base64(wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=)
==
&&
==
&&
txn FreezeAssetAccount
global ZeroAddress
==

&&
txn Fee
global MinTxnFee
==
&&
return
"""
compiled_program = client.compile(teal_program)
program = base64.b64decode(compiled_program["result"])
lsig = transaction.LogicSigAccount(program)
```

> This give opt-in control over the signer

#### 1. **Generate Plug-In Signer**

- Generate a random new account that we will only use once.

```python
plug_in_sk, plug_in_addr = account.generate_account()
```

#### 2. **Sign Lsig with Plug-In Signer**

- Sign the Lsig using the plug-in signer.
- Publish the public signature on the blockchain.

```python
public_key, secret_key = nacl.bindings.crypto_sign_seed_keypair(base64.b64decode(plug_in_sk)[: constants.key_len_bytes])
message = constants.logic_prefix + program
raw_signed = nacl.bindings.crypto_sign(message, secret_key)
crypto_sign_BYTES = nacl.bindings.crypto_sign_BYTES
signature = nacl.encoding.RawEncoder.encode(raw_signed[:crypto_sign_BYTES])
plug_in_public_sig = base64.b64encode(signature).decode()
```

> You can achieve the same result like this:

```python
lsig = transaction.LogicSigAccount(program).sign(plug_in_sk)
plug_in_public_sig = lsig.lsig.sig
```

#### 3. **Create 1/2 Msig Account**

- Create a multi-signature account with owner address, and the plug-in signer.

```python
owner_vault_msig = transaction.Multisig(1,1,[owner_addr, plug_in_addr])
```

- Add a transaction note to the transaction to help third party to retrieve signer and vault information.

```json
{
"pk": plug_in_addr,
"sk": plug_in_public_sig,
"lsig": lsig.address(),
}
```

- Prefix the note following the [ARC-2](./arc-0002.md) standard. `arc63:j`

```python
ptxn = transaction.PaymentTxn(
owner_addr, sp, owner_vault_msig.address(), int(1e6), note=f"arc_63:j{note_field}"
).sign(owner_sk)
```

#### 4. **Opt-In to Msig Vault**

- Anyone can opt-in to the Msig vault using the plug-in signer’s public address and the published signature.

```python
optin_txn = AssetTransferTxn(
sender=owner_vault_msig.address(),
sp=sp,
receiver=owner_vault_msig.address(),
amt=0,
index=a_id,
)
lsig.lsig.msig = owner_vault_msig
lsig.append_to_multisig(plug_in_sk) # signature from plug_in_public
lstx = LogicSigTransaction(optin_txn, lsig)
```

### Implementation 2

We will use the following Lsig plug-in for our illustrative purposes:

**DO NOT USE IN PRODUCTION**

```python
teal_program = f"""
#pragma version 10
txn TypeEnum
int appl
==
txn ApplicationID
int {app_client.app_id}
==
&&
txn RekeyTo
global ZeroAddress
==
&&
txn Fee
global MinTxnFee
==
&&
return
"""
compiled_program = client.compile(teal_program)
program = base64.b64decode(compiled_program["result"])
lsig = transaction.LogicSigAccount(program)
```

> This allow the application with the id `app_client.app_id` to control the signer

To be consistant with the previous example, we will use a similar opt-in process.

```python
class SmartApp(ARC4Contract):
def __init__(self) -> None:
self.db = BoxMap(Account, String, key_prefix="")

@abimethod()
def opt_in(self, id: UInt64, account: Account) -> None:
itxn.AssetTransfer(
asset_amount=0,
xfer_asset=id,
sender=account,
asset_receiver=account,
fee=1000,
).submit()

@abimethod()
def set_public_sig(self, account: Account, sig: String) -> bool:
self.db[account] = sig
return self.db[account] == sig

@abimethod(readonly=True)
def get_public_sig(self, account: Account) -> String:
return self.db[account]
```

### 1. [Generate Plug-In Signer](./arc-0063.md#1-generate-plug-in-signer)

### 2. [Sign Lsig with Plug-In Signer](./arc-0063.md#2-sign-lsig-with-plug-in-signer)

#### 3. **Create 1/2 Msig Account** and execute an app Call

- Create a multi-signature account with owner address, and the plug-in signer.

```python
owner_vault_msig = transaction.Multisig(1,1,[owner_addr, plug_in_addr])
```

- Publish the public signature by using set_public_sig to the app.

```python
response = app_client.set_public_sig(
account=owner_addr,
sig=plug_in_public_sig,
transaction_parameters=algokit_utils.TransactionParameters(
boxes=[(app_client.app_id, encoding.decode_address(owner_addr))],
accounts=[owner_addr]
)
)
```

- Get the transaction set_public_sig from the app.

```python
response = app_client.get_public_sig(
account=owner_addr,
transaction_parameters=algokit_utils.TransactionParameters(
boxes=[(app_client.app_id, encoding.decode_address(owner_addr))]
),
)
```

#### 4. **App in control of Msig Vault**

- Anyone can now call the app to opt-in any asset to the Msig vault using the plug-in signer’s public address and the published signature.

```python
composer.opt_in(
id=a_id,
account=app_client.app_address,
transaction_parameters=algokit_utils.TransactionParameters(
foreign_assets=[a_id], signer=app_client.signer,
sender=owner_vault_msig.address()
),
)
opt_in_txn = composer.atc.txn_list[0].txn

lsig.lsig.msig = owner_vault_msig
lsig.lsig.msig.subsigs[1].signature = base64.b64decode(plug_in_public_sig)
lstx = transaction.LogicSigTransaction(opt_in_txn, lsig)
```

### Diagram

```mermaid
graph TD
subgraph signer
PKS[PK]
SKS[SK]
PKS ~~~ SKS
end
subgraph msigVault
SKVS[Public Signature]
SKVA[Public Address]
PKV[ownerPK]
PKV ~~~ SKVA
PKV ~~~ SKVS
end
subgraph owner
PKO[PK]
end

ta[[Throwaway Account]]

lsig((Lsig Plug In))

sign((Sign))

ta --> signer
PKO --> PKV
SKS --> sign
lsig --- sign
sign --> SKVS
PKS --> SKVA
msigVault ~~~ lsig
```

## Rationale

The rationale for this design is to leverage third-party Lsig plug-ins. By note storing the plug-in signer private key, we mitigate risks associated with its misuse, while the multi-signature account setup ensures controlled access and flexibility in asset management.

## Backwards Compatibility

This ARC introduces no backward incompatibilities. It builds upon existing Algorand functionalities, ensuring seamless integration with current systems.

## Reference Implementation

An example implementation in Python is provided, demonstrating the creation of a plug-in signer, signing an Lsig, and opting into a multi-signature vault.

### Only with Lsig

[Create_Opt_in_Plug_in](../assets/arc-0063/create_plugin.py)

### Delegating the Lsig to an App

[Deploy config](../assets/arc-0063/deploy_config.py)
[Contract](../assets/arc-0063/contract.py)

> This need to be run with Algokit

## Security Considerations

Even if the plug-in signer is rekeyed, the private key can still sign new lsigs, which is why the private key should not be accessible by anyone after the signature.
This step is crucial to prevent any unauthorized use of the signer post-creation.

If the Multi-signature vault accounts is rekeyd to any other account (Msig or traditional), it will keep the same public address and will not be delegated anymore.

## Copyright

Copyright and related rights waived via <a href="https://creativecommons.org/publicdomain/zero/1.0/">CCO</a>.
26 changes: 26 additions & 0 deletions assets/arc-0063/contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from algopy import ARC4Contract, Account, UInt64, itxn, BoxMap
from algopy.arc4 import abimethod, String


class SmartApp(ARC4Contract):
def __init__(self) -> None:
self.db = BoxMap(Account, String, key_prefix="")

@abimethod()
def opt_in(self, id: UInt64, account: Account) -> None:
itxn.AssetTransfer(
asset_amount=0,
xfer_asset=id,
sender=account,
asset_receiver=account,
fee=1000,
).submit()

@abimethod()
def set_public_sig(self, account: Account, sig: String) -> bool:
self.db[account] = sig
return self.db[account] == sig

@abimethod(readonly=True)
def get_public_sig(self, account: Account) -> String:
return self.db[account]
Loading
Loading