Added instructions and commands for account delegation.

This commit is contained in:
Geoff Taylor 2022-01-18 17:51:11 +00:00
parent f9b7d7b03f
commit f1b14acdcf
12 changed files with 349 additions and 24 deletions

47
bin/delegate-account Executable file
View File

@ -0,0 +1,47 @@
#!/usr/bin/env python3
import argparse
import os
import os.path
import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Delegate operational authority of a Mango account to another account.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--account-address", type=PublicKey, required=True,
help="address of the Mango account to delegate")
parser.add_argument("--delegate-address", type=PublicKey, required=False,
help="address of the account to which to delegate operational control of the Mango account")
parser.add_argument("--revoke", action="store_true", default=False,
help="revoke any previous account delegation")
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account: mango.Account = mango.Account.load(context, args.account_address, group)
if account.owner != wallet.address:
raise Exception(f"Account {account.address} is not owned by current wallet {wallet.address}.")
all_instructions: mango.CombinableInstructions = mango.CombinableInstructions.from_signers([wallet.keypair])
if args.revoke:
unset_delegate_instructions = mango.build_unset_account_delegate_instructions(context, wallet, group, account)
all_instructions += unset_delegate_instructions
else:
if args.delegate_address is None:
raise Exception("No delegate address specified")
set_delegate_instructions = mango.build_set_account_delegate_instructions(
context, wallet, group, account, args.delegate_address)
all_instructions += set_delegate_instructions
transaction_ids = all_instructions.execute(context)
mango.output(f"Transaction IDs: {transaction_ids}")

View File

@ -6,6 +6,7 @@ import os.path
import sys
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
@ -16,18 +17,15 @@ mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True, help="token symbol to deposit (e.g. USDC)")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity token to deposit")
parser.add_argument("--account-index", type=int, default=0,
help="index of the account to use, if more than one available")
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
if len(accounts) == 0:
raise Exception(f"Could not find any Mango accounts for '{wallet.address}'.")
account = accounts[args.account_index]
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
instrument = context.instrument_lookup.find_by_symbol(args.symbol)
if instrument is None:

View File

@ -5,6 +5,8 @@ import os
import os.path
import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
import mango # nopep8
@ -12,9 +14,9 @@ import mango # nopep8
parser = argparse.ArgumentParser(description="Ensure an OpenOrders account exists for the wallet and market.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--account-index", type=int, default=0,
help="index of the account to use, if more than one available")
parser.add_argument("--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)")
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
args: argparse.Namespace = mango.parse_args(parser)
@ -23,10 +25,7 @@ context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context)
accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
if len(accounts) == 0:
raise Exception(f"No Mango account exists for group {group.address} and wallet {wallet.address}")
account = accounts[args.account_index]
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
market = context.market_lookup.find_by_symbol(args.market)
if market is None:

View File

@ -11,7 +11,7 @@ sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows details of a Merps account.")
parser = argparse.ArgumentParser(description="Shows details of a Mango account.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=False,

33
bin/show-delegated-accounts Executable file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env python3
import argparse
import os
import os.path
import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows details of a Mango account.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=False,
help="address of the delegate of the account (defaults to the root address of the wallet)")
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
delegate = args.address
if delegate is None:
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
delegate = wallet.address
group = mango.Group.load(context, context.group_address)
mango_accounts = mango.Account.load_all_for_delegate(context, delegate, group)
if len(mango_accounts) == 0:
mango.output(f"Account {delegate} has no accounts delegated to it.")
else:
mango.output(mango_accounts)

View File

@ -17,8 +17,8 @@ mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey,
help="Root address to check (if not provided, the wallet address is used)")
parser.add_argument("--account-index", type=int, default=0,
help="index of the account to use, if more than one available")
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -28,8 +28,7 @@ if address is None:
address = wallet.address
group = mango.Group.load(context)
accounts = mango.Account.load_all_for_owner(context, address, group)
account = accounts[args.account_index]
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
at_least_one_open_orders_account = False
quote_token_bank = group.shared_quote_token

View File

@ -6,6 +6,7 @@ import os.path
import sys
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
@ -16,8 +17,8 @@ mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True, help="token symbol to withdraw (e.g. USDC)")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity token to withdraw")
parser.add_argument("--account-index", type=int, default=0,
help="index of the account to use, if more than one available")
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
parser.add_argument("--allow-borrow", action="store_true", default=False,
help="allow borrowing to fund the withdrawal")
args: argparse.Namespace = mango.parse_args(parser)
@ -26,10 +27,7 @@ context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
if len(accounts) == 0:
raise Exception(f"Could not find any margin accounts for '{wallet.address}'.")
account = accounts[args.account_index]
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
instrument = context.instrument_lookup.find_by_symbol(args.symbol)
if instrument is None:

173
docs/Delegation.md Normal file
View File

@ -0,0 +1,173 @@
# 🥭 Mango Explorer
# 🔥 Delegation
Mango Account Delegation is a feature that allows a separate account limited access to the Mango Account's features.
Delegation can provide some additional security for more sophisticated key management procedures.
A delegated account *can*, for example:
* Deposit funds
* Place orders
* Cancel orders
A delegated account *cannot*, for example:
* Withdraw funds
* Close the Mango Account
A Mango Account can have at most one delegate. The owner of the Mango Account continues to have full control of the Mango Account irrespective of delegation.
# 🆒 Commands
`mango-explorer` provides two commands for delegation: `delegate-account` and `show-delegated-accounts`.
You can see exactly what additional parameters a command takes by specifying the `--help` parameter.
## `delegate-account`
> Accepts parameter: `--account-address <ACCOUNT-PUBLIC-KEY>` (required, `PublicKey`, no default)
> Accepts parameter: `--delegate-address <DELEGATE-PUBLIC-KEY>` (optional, `PublicKey`, no default)
> Accepts parameter: `--revoke` (optional, TRUE if specified otherwise FALSE)
`delegate-account` allows you to set or revoke delegation of a Mango Account. It must be run by the owner of the Mango Account.
## `show-delegated-accounts`
> Accepts parameter: `--address <DELEGATE-PUBLIC-KEY>` (optional, `PublicKey`, default value: wallet root address)
`show-delegated-accounts` will list all accounts for which the current wallet is nominated as the delegate. **It will not** list accounts *owned* by the current wallet, only those in which it is listed as a delegate.
# ✅ Example Walkthrough
To run this walkthrough, you need to set up 2 different Solana wallets and 1 Mango Account. Choose one of the wallets as the Owner and use it to create a Mango Account. The other wallet is the Delegate.
In these examples,
* `ALBdwdFnHsmTAQ3xGqkDK8hcTMXeuMNyCNkz1ikBzv3L` is the address of the Mango Account Owner's wallet
* `7zTdp1YhPdBQw8nFJJXkmTewZf4zPwDbjktKzQLaQr62` is the address of the Mango Account
* `6aNN6nbAm9Dbqex9kKGAYcMFJg7Jhertwew9JhRv4QfB` is the address of Delegate's wallet
## Show No Delegate
*Run as Owner of the Mango Account*
This command will show the Mango Account, including what account (if any) is listed as a delegate:
```
show-accounts
```
This will output something like (abridged):
```
« Account (un-named), Version.V3 [7zTdp1YhPdBQw8nFJJXkmTewZf4zPwDbjktKzQLaQr62]
« Metadata Version.V2 - Account: Initialized »
Owner: ALBdwdFnHsmTAQ3xGqkDK8hcTMXeuMNyCNkz1ikBzv3L
Delegated To: None
```
## Delegate Account
*Run as Owner of the Mango Account*
This command will grant the Delegate limited access to the Mango Account:
```
delegate-account --account-address 7zTdp1YhPdBQw8nFJJXkmTewZf4zPwDbjktKzQLaQr62 --delegate-address 6aNN6nbAm9Dbqex9kKGAYcMFJg7Jhertwew9JhRv4QfB
```
## Show Delegate
*Run as Owner of the Mango Account*
This command will now show the Mango Account with `6aNN6nbAm9Dbqex9kKGAYcMFJg7Jhertwew9JhRv4QfB` listed as a delegate:
```
show-accounts
```
This will output the address of the Delegate in the 'Delegated To' line (abridged):
```
« Account (un-named), Version.V3 [7zTdp1YhPdBQw8nFJJXkmTewZf4zPwDbjktKzQLaQr62]
« Metadata Version.V2 - Account: Initialized »
Owner: ALBdwdFnHsmTAQ3xGqkDK8hcTMXeuMNyCNkz1ikBzv3L
Delegated To: 6aNN6nbAm9Dbqex9kKGAYcMFJg7Jhertwew9JhRv4QfB
```
## Show Delegated Account
*Run as Delegate*
This command will show all Mango Accounts with the current user listed as a delegate. If it is run as the user with the wallet address `6aNN6nbAm9Dbqex9kKGAYcMFJg7Jhertwew9JhRv4QfB` (the Delegate)
```
show-delegated-accounts
```
It will show the same output as `show-account` run by the Owner:
```
« Account (un-named), Version.V3 [7zTdp1YhPdBQw8nFJJXkmTewZf4zPwDbjktKzQLaQr62]
« Metadata Version.V2 - Account: Initialized »
Owner: ALBdwdFnHsmTAQ3xGqkDK8hcTMXeuMNyCNkz1ikBzv3L
Delegated To: 6aNN6nbAm9Dbqex9kKGAYcMFJg7Jhertwew9JhRv4QfB
```
## Deposit
*Run as Delegate*
Delegates can deposit funds from their own wallet (not the Owner's wallet).
```
deposit --symbol SOL --quantity 1 --account-address 7zTdp1YhPdBQw8nFJJXkmTewZf4zPwDbjktKzQLaQr62
```
Note that to target an account which has been delegated, the `--account-address` parameter must be specified. (This is the same way an account is specified for wallets that hold multiple Mango Accounts.)
## Place Order
*Run as Delegate*
Delegates can place and cancel orders using the delegated Mango Account.
```
place-order --market SOL-PERP --quantity 1 --side BUY --order-type LIMIT --price 10 --skip-preflight --account-address 7zTdp1YhPdBQw8nFJJXkmTewZf4zPwDbjktKzQLaQr62
```
**Note**: it is the Delegate running this command so it is the Delegate that pays the SOL for the transaction.
## Withdraw
*Run as Delegate*
Delegates **cannot** withdraw funds from the delegated Mango Account. **This will fail**:
```
withdraw --symbol SOL --quantity 1 --account-address 7zTdp1YhPdBQw8nFJJXkmTewZf4zPwDbjktKzQLaQr62
```
**This will fail** with an error containing the following in the transaction logs:
```
Program log: MangoErrorCode::InvalidOwner; src/processor.rs
```
## Revoke Delegation
*Run as Owner of the Mango Account*
This command will revoke the delegated authority to the Delegate, removing their access to the Mango Account:
```
delegate-account --account-address 7zTdp1YhPdBQw8nFJJXkmTewZf4zPwDbjktKzQLaQr62 --revoke
```
## Show No Delegated Accounts
*Run as Delegate*
Running the same command as earlier:
```
show-delegated-accounts
```
Now shows no delegated accounts, since the delegation was revoked:
```
Account 6aNN6nbAm9Dbqex9kKGAYcMFJg7Jhertwew9JhRv4QfB has no accounts delegated to it.
```

View File

@ -88,8 +88,10 @@ from .instructions import build_redeem_accrued_mango_instructions as build_redee
from .instructions import build_serum_consume_events_instructions as build_serum_consume_events_instructions
from .instructions import build_serum_place_order_instructions as build_serum_place_order_instructions
from .instructions import build_serum_settle_instructions as build_serum_settle_instructions
from .instructions import build_set_account_delegate_instructions as build_set_account_delegate_instructions
from .instructions import build_spot_place_order_instructions as build_spot_place_order_instructions
from .instructions import build_transfer_spl_tokens_instructions as build_transfer_spl_tokens_instructions
from .instructions import build_unset_account_delegate_instructions as build_unset_account_delegate_instructions
from .instructions import build_withdraw_instructions as build_withdraw_instructions
from .instructionreporter import InstructionReporter as InstructionReporter
from .instructionreporter import SerumInstructionReporter as SerumInstructionReporter

View File

@ -344,6 +344,34 @@ class Account(AddressableAccount):
accounts += [account]
return accounts
@staticmethod
def load_all_for_delegate(context: Context, delegate: PublicKey, group: Group) -> typing.Sequence["Account"]:
# mango_group is just after the METADATA, which is the first entry.
group_offset = layouts.METADATA.sizeof()
# delegate is a PublicKey which is 32 bytes that ends 5 bytes before the end of the layout
delegate_offset = layouts.MANGO_ACCOUNT.sizeof() - 37
filters = [
MemcmpOpts(
offset=group_offset,
bytes=encode_key(group.address)
),
MemcmpOpts(
offset=delegate_offset,
bytes=encode_key(delegate)
)
]
results = context.client.get_program_accounts(
context.mango_program_address, memcmp_opts=filters, data_size=layouts.MANGO_ACCOUNT.sizeof())
cache: Cache = group.fetch_cache(context)
accounts: typing.List[Account] = []
for account_data in results:
address = PublicKey(account_data["pubkey"])
account_info = AccountInfo._from_response_values(account_data["account"], address)
account = Account.parse(account_info, group, cache)
accounts += [account]
return accounts
@staticmethod
def load_for_owner_by_address(context: Context, owner: PublicKey, group: Group, account_address: typing.Optional[PublicKey]) -> "Account":
if account_address is not None:

View File

@ -879,3 +879,37 @@ def build_faucet_airdrop_instructions(token_mint: PublicKey, destination: Public
})
)
return CombinableInstructions(signers=[], instructions=[faucet_airdrop_instruction])
# # 🥭 build_set_account_delegate_instructions function
#
# Creates an instruction to delegate account operations (except Withdraw and CloseAccount) to a
# different account.
#
# Set to SYSTEM_PROGRAM_ADDRESS to revoke delegate.
#
def build_set_account_delegate_instructions(context: Context, wallet: Wallet, group: Group, account: Account, delegate: PublicKey) -> CombinableInstructions:
# /// https://github.com/blockworks-foundation/mango-v3/pull/97/
# /// Set delegate authority to mango account which can do everything regular account can do
# /// except Withdraw and CloseMangoAccount. Set to Pubkey::default() to revoke delegate
# ///
# /// Accounts expected: 4
# /// 0. `[]` mango_group_ai - MangoGroup
# /// 1. `[writable]` mango_account_ai - MangoAccount
# /// 2. `[signer]` owner_ai - Owner of Mango Account
# /// 3. `[]` delegate_ai - delegate
set_delegate_instruction = TransactionInstruction(
keys=[
AccountMeta(is_signer=False, is_writable=False, pubkey=group.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=account.address),
AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address),
AccountMeta(is_signer=False, is_writable=False, pubkey=delegate)
],
program_id=context.mango_program_address,
data=layouts.SET_DELEGATE.build({})
)
return CombinableInstructions(signers=[], instructions=[set_delegate_instruction])
def build_unset_account_delegate_instructions(context: Context, wallet: Wallet, group: Group, account: Account) -> CombinableInstructions:
return build_set_account_delegate_instructions(context, wallet, group, account, SYSTEM_PROGRAM_ADDRESS)

View File

@ -1570,6 +1570,20 @@ CREATE_MANGO_ACCOUNT = construct.Struct(
)
# /// https://github.com/blockworks-foundation/mango-v3/pull/97/
# /// Set delegate authority to mango account which can do everything regular account can do
# /// except Withdraw and CloseMangoAccount. Set to Pubkey::default() to revoke delegate
# ///
# /// Accounts expected: 4
# /// 0. `[]` mango_group_ai - MangoGroup
# /// 1. `[writable]` mango_account_ai - MangoAccount
# /// 2. `[signer]` owner_ai - Owner of Mango Account
# /// 3. `[]` delegate_ai - delegate
SET_DELEGATE = construct.Struct(
"variant" / construct.Const(58, construct.BytesInteger(4, swapped=True))
)
UNSPECIFIED = construct.Struct(
"variant" / DecimalAdapter(4)
)
@ -1633,7 +1647,7 @@ InstructionParsersByVariant = {
55: CREATE_MANGO_ACCOUNT, # CREATE_MANGO_ACCOUNT,
56: UNSPECIFIED, # UPGRADE_MANGO_ACCOUNT_V0_V1,
57: UNSPECIFIED, # CANCEL_PERP_ORDER_SIDE,
58: UNSPECIFIED, # SET_DELEGATE,
58: SET_DELEGATE, # SET_DELEGATE,
59: UNSPECIFIED, # CHANGE_SPOT_MARKET_PARAMS,
60: UNSPECIFIED, # CREATE_SPOT_OPEN_ORDERS,
}