CAP: 0018
Title: Fine-Grained Control of Authorization
Author: Jonathan Jove
Status: Final
Created: 2019-01-31
Discussion: https://github.com/stellar/stellar-protocol/issues/146
Protocol version: 13
This proposal provides issuers with a level of authorization between unauthorized and fully authorized. This level of authorization will allow a trustline to maintain liabilities (see CAP-0003) without permitting any other operations.
This proposal adds a new flag to TrustLineFlags
which offers a level of authorization intermediate between unauthorized and fully authorized. It is then shown how this new flag enables similar behavior to CAP-0016 by carefully setting and toggling the level of authorization. Additional flags could also be added, which would enable fine-grained control of authorization on a per-account basis.
A number of Stellar asset issuers need greater control over assets than simply authorizing particular accounts to hold the asset. The TrustLineEntry.flags
field allows an issuer to toggle between the following two states:
TrustLineEntry.flags == 0
: The account can hold a balance but cannot receive payments, send payments, maintain offers, or manage offersTrustLineEntry.flags == AUTHORIZED_FLAG
: The account can hold a balance, receive payments, send payments, maintain offers, and manage offers
If the issuer does not intend to allow the account to maintain offers, then toggling between the two states described above can be used to control what accounts do with their assets. To achieve this, an issuer could leave accounts in the unauthorized state. In order to do anything with the assets an account holds, that account would need to become authorized. This would require the signature of the issuer. The owner of such an account could then request that the issuer sign a transaction; the issuer should refuse to sign any transaction that does not return the account to the unauthorized state. A transaction that an issuer might be willing to sign could look like
- Operation 1: Issuer uses
AllowTrust
to fully authorize account A, asset X - Operation 2: Issuer uses
AllowTrust
to fully authorize account B, asset X - Operation 3: Payment from A to B
- Operation 4: Issuer uses
AllowTrust
to unauthorize account B, asset X - Operation 5: Issuer uses
AllowTrust
to unauthorize account A, asset X
which would require the signatures of the issuer and account A.
But this approach does not work if the account should be allowed to maintain offers. As soon as the account becomes unauthorized, all of its offers will be removed from the ledger. So some issuers have taken a more drastic approach, where the issuer is actually a signer on the asset holders' accounts. This solution has several drawbacks:
- The owner of an account cannot unilaterally utilize assets unrelated to the issuer without the signature of that issuer
- If multiple issuers want to become signers on a single account, then many signatures are potentially required for simple operations
- Issuers cannot easily rotate keys, because their keys are on every account holding their asset
This proposal requires the addition of AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
to TrustLineFlags
, so the XDR becomes
enum TrustLineFlags
{
AUTHORIZED_FLAG = 1, // issuer has authorized account to perform transactions with its credit
AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG = 2 // issuer has authorized account to maintain and reduce liabilities for its credit
};
// mask for all trustline flags
const MASK_TRUSTLINE_FLAGS = 3;
In order to interact with these flags, the type of the authorize
field of AllowTrustOp
must be changed from bool
to uint32
. This provides binary compatibility with clients that do not know about the new flags (since AUTHORIZED_FLAG == TRUE
in XDR), but having a uint32
provides the additional option of setting it to any combination of TrustLineFlags
. The XDR becomes
struct AllowTrustOp
{
AccountID trustor;
union switch (AssetType type)
{
// ASSET_TYPE_NATIVE is not allowed
case ASSET_TYPE_CREDIT_ALPHANUM4:
opaque assetCode4[4];
case ASSET_TYPE_CREDIT_ALPHANUM12:
opaque assetCode12[12];
// add other asset types here in the future
}
asset;
// 0, or any bitwise combination of TrustLineFlags
uint32 authorize;
};
The new behavior of ALLOW_TRUST
is to set flags = authorize
on the relevant trustline. Semantically, this is equivalent to the existing behavior of ALLOW_TRUST
because
authorize == FALSE
becomesauthorize == 0
which removes any authorization from the trustlineauthorize == TRUE
becomesauthorize == AUTHORIZED_FLAG
which makes the trustline fully authorized
The combination AUTHORIZED_FLAG | AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
is not valid because AUTHORIZED_FLAG
implies AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
. Trying to set authorize
to any value above AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
is also invalid. ALLOW_TRUST_MALFORMED
is the error code that will be returned.
If AUTH_REVOCABLE_FLAG
is not set, the transition from AUTHORIZED_FLAG
to AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
is invalid. ALLOW_TRUST_CANT_REVOKE
is the error code that will be returned.
If a trustline tl
has tl.flags == AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
then
tl.balance > 0
is permitted (this is always true for any value oftl.flags
)tl.liabilities.buying > 0
is permitted (this is not true iftl.flags == 0
)tl.liabilities.selling > 0
is permitted (this is not true iftl.flags == 0
)tl.balance
can only change if an existing liability is converted into a balance. (this is not true iftl.flags == AUTHORIZED
)- No operation which creates or modifies an offer buying or selling
tl.asset
is permitted but deleting such offers is permitted. (this is not true iftl.flags == AUTHORIZED
) - Upgrades should treat
AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
the same way asAUTHORIZED_FLAG
because liabilities can only be decreased on upgrades (since protocol version 11). An invariant for this should be added.
Using AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
enables issuers to control what accounts do with their assets even if the accounts should be allowed to maintain offers. To achieve this, an issuer could leave accounts in the AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
state. Such an account is allowed to maintain liabilities, meaning it can own offers but cannot otherwise do anything with the asset. In order to do anything with the assets an account holds, that account would need to become authorized. This would require the signature of the issuer. The owner of such an account could then request that the issuer sign a transaction; the issuer should refuse to sign any transaction that does not return the account to the AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
state. A transaction that an issuer might be willing to sign could look like
- Operation 1: Issuer uses
AllowTrust
to fully authorize account A, asset X - Operation 2: Issuer uses
AllowTrust
to fully authorize account B, asset X - Operation 3: Payment from A to B
- Operation 4: Issuer uses
AllowTrust
to set account B, asset X toAUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
state - Operation 5: Issuer uses
AllowTrust
to set account A, asset X toAUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
state
or
- Operation 1: Issuer uses
AllowTrust
to fully authorize account A, asset X - Operation 2: Account A manages offer to buy or sell X
- Operation 3: Issuer uses
AllowTrust
to set account A, asset X toAUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
state
both of which would require the signatures of the issuer and account A.
In comparison to CAP-0016 this proposal would prohibit a fully authorized account from sending to an account in the AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
state without the signature of the issuer, which could be a desirable feature because it means issuers entirely control their asset. But this solution is flexible in that it would be possible to add other flags to TrustLineFlags
which would control other aspects of authorization. For example, one could add the flag AUTHORIZED_TO_INCREASE_BALANCE_FLAG
which would permit an account to receive payments. The behavior of CAP-0016 could be emulated entirely with this additional flag. An appropriate set of such flags would provide issuers with fine-grained control of authorization on a per-account basis, which justifies the name of this proposal.
Note that requirement (5) implies but is not equivalent to
- No operation which increases
tl.liabilities.buying
is permitted - No operation which increases
tl.liabilities.selling
is permitted
Although requirements (6) and (7) seem more natural than requirement (5), since they refer only to individual values in the ledger like the other requirements, they are actually insufficient. If requirements (6) and (7) were included instead of (5) then it would be possible for account A to modify an offer selling asset X for asset Y and change it into an offer selling asset X for asset Z without a signature from the issuers of X or Y even if A is only AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
for one or both of those assets. This would allow accounts to easily evade restrictions from the issuer on which assets can be exchanged for X. Requirements (6) and (7) would also make it possible for accounts to change the price of offers without a signature from the relevant issuer or issuers.
The only backwards incompatibility with this proposal is if downstream systems relied on the fact that !(TrustLineEntry.flags & AUTHORIZED_FLAG)
implies unauthorized. In this case, downstream systems might erroneously conclude that an account is unauthorized when it is in fact in the AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG
state.
None yet.