23-02-22 update: The feature is implemented in https://github.com/MystenLabs/sui/pull/8273
This is a RFC post that intends to seek for feedback for Sponsored Transaction and Sponsor Station. Any feedback/suggestions are appreciated! We are looking to close the initial round of feedback by Dec 15th for Sui Core changes, but expecting Sponsor Station to iterate more frequently as discussions go on.
A Sponsored Transaction allows a transaction to be expensed by a gas owned by another address. Why is this interesting from the product/ecosystem’s perspective?
- improving web3 mass onboarding process. Web2 users can start their web3 journey before the lengthy on-ramping, increasing user conversion rate. For example, gaming studios convert their web2 gamers to web3 users by sponsoring early user transactions.
- incentivizing on-chain activity and web3 migration: encourage certain user behaviors such as voting and governance.
- customer acquisition: platforms and Dapps leverage this feature to attract new users by subsidizing the first few transactions interacting with their protocol.
- easier asset management - Dapps or institutions (particularly custodians) doing on-chain asset management no longer need SUI tokens in every account to transfer funds.
- other scenarios: facilitate using burner accounts to isolate on-chain activities; bootstrap across-chain transfers; a Dapp can give gases or gas promises as a reward to encourage users to use their products etc.
For more details, see Github Issue: Support sponsored transactions · Issue #2418 · MystenLabs/sui · GitHub for motivation (and more discussions).
Similar Features on other Blockchains
- Ethereum - Meta transaction & Ethereum Gas Station Network
- Solana - Gasless transaction
- Near - Prepaid Gas
Roles
In the context of a sponsored transaction, we define the roles below:
- Users
- the Sui address owner that has an transaction to execute
- Sponsors
- the Sui address owner that pays gas for the transaction
- Sponsor could run a Sponsor Station See below to accept
GasLessTransactionData
or distribute/airdrop “empty check” gases to users
Transaction Construction Diagram
This section describes the ways the a sponsored transaction can be constructed:
Case I. sponsor pays for a specific transaction that user initialized (diagram)
GasLessTransactionData
contains the transaction details and user address. It is not a sui-core data structure, but will be exposed in relevant SDKs so recognizable to both user and sponsor.
pub struct GasLessTransactionData {
pub kind: TransactionKind,
sender: SuiAddress,
}
- User constructs
GasLessTransactionData
and pass it to Sponsor for expensing. - If Sponsor decides this is the right transaction to sponsor, it picks a gas, build a
TransactionData
(see below), and signsTransactionData
to produce aSignature
. - Sponsor passes back
TransactionData
andSignature
to user. - User checks the signature, if all good, signs
TransactionData
to get the secondSignature
. - User then forms the dual-signed
SenderSignedData
and submits it to fullnode for execution.
Case II. sponsor pays for a specific transaction that sponsor initialized (diagram)
- Sponsor constructs
TransactionData
(containing the transaction details and gas data) that it thinks the user may like, signs it to getSignature
- User checks
TransactionData
and if it like it, signs it to get the secondSignature
- User submits the dual-signed transaction to fullnode or sponsor for execution.
This data flow is particularly interesting in scenarios where sponsor act as an advertiser, airdropper, or in general a party that wants to incentivize users to perform some behaviors. The unsigned TransactionData
can be sent over to user in various forms, such as email, SMS or simply a Dapp frontend.
Case III. sponsor gives an “empty check” that applies to any transactions (diagram)
- Sponsor hands out a
GasData
to eligible user - User constructs
TransactionData
signs it - User passes the
TransactionData
andSignature
to Sponsor - Sponsor checks it and signs it too
- Sponsor submits the dual-signed
TransactionData
to full node.
Sui Protocol Changes
Data Structure
We propose to change the current data structure to the following. Note this change is general enough that it also applies to normal transactions (non-sponsored). The beauty of a universal single data structure makes the code less-branched and more manageable.
Proposed Data Structure I
Make tx_signatures
a vector of Signature
. This would work for multi-agent transactions as well.
pub struct TransactionData {
pub kind: TransactionKind,
sender: SuiAddress,
pub gas_data: GasData,
}
/// I spin it off to make the structs/interfaces cleaner, but this is optional
pub struct GasData {
gas_payment: ObjectRef,
pub gas_price: u64,
pub gas_budget: u64,
pub gas_owner: SuiAddress,
}
pub struct SenderSignedData {
pub data: TransactionData,
/// tx_signatures is signed by the user and sponsor, applied on `data`.
/// when sender == gas_owner, only one sig is needed
pub tx_signatures: Vec<Signature>,
}
Alternative Data Structure II
Adding an optional field gas_owner_signature
.
pub struct TransactionData {
pub kind: TransactionKind,
sender: SuiAddress,
gas_payment: ObjectRef,
pub gas_price: u64,
pub gas_budget: u64,
}
pub struct SenderSignedData {
pub data: TransactionData,
/// tx_signatures is signed by the user applied on `data`.
pub tx_signatures: Signature,
/// gas_owner_signature is signed by the user applied on `data`.
/// when sender == gas_owner, this should be None
pub gas_owner_signature: Option<Signature>,
}
Alternative Data Structure III
To make this change easily extensible to Multi-agent Transaction in the future, we could make tx_details
a vector of (signer address, transaction kind)
:
pub struct TransactionData {
/* signer address */
pub tx_details: Vec<(SuiAddress, TransactionKind)>,
pub gas_data: GasData,
}
Before Sui protocol supports Multi-agent Transaction, the code will enforce the vector’s length to be 1.
TransactionDigest
Derivation
We agree to not commit user signature into TransactionDigest. The change will be released in 0.18.0.
Transaction Submission API
The sui_executeTransaction
API is compatible with this proposal, no additional change is needed.
Limitations
Sponsored Transaction cannot be used in *Sui transaction types, currently including:
TransferSui
PaySui
PayAllSui
This is because *Sui transactions treat user input object or one of the input objects as the gas. This contradicts with the idea of sponsor transaction that sender ≠sponsor.
Apparently Sponsored Transaction is also not usable in system transactions such as EpochChange
.
However Sponsored Transaction can be used with SingleTransaction or BatchTransaction. Today’s BatchTransaction does not support *Sui Transactions either.
Threat Models
Both user and sponsor should understand potential risks before doing a joint sponsored transaction. Equivocation risk is the major threat model present in Sponsored Transaction.
A sponsored transaction may risk being equivocated by the other party. This is because when the transaction is examined by validators, all of the owned objects are locked to prevent double spend. An equivocation happens when an owned objects pair (ObjectID, SequenceNumber)
is concurrently used in multiple non-finalized transactions. The worse case is we cannot get supermajority validator signatures for any of the transactions, leading some or all objects involved in these transaction lose liveness until the epoch change.
To equivocate, one party (user or sponsor) signs and submits another transaction that involves an owned object in the original transaction. Hence, only the user and sponsor is capable of doing it.
Risks
- User or sponsor’s owned objects that appear in the sponsored transaction lose liveness until end of epoch.
- This is a non-issue for normal transactions where user and sponsor is the same address.
Mitigations
- Sponsors gain reputation by helping users fulfill transactions. If the sponsor is incompetent, users will seek other players in the open market. Note sponsor has nothing to gain for equivocation, so it’s practical to assume no sponsors would cause equivocation on purpose, except for the aforementioned censorship.
- User’s input objects do not always include an owned object but gas must be owned. From this perspective, sponsor faces bigger risk if rogue users intend to abuse their gases. To mitigate this attack surface, we recommend sponsors to 1. dispense small amount of gases, 2. authorize gas request properly and 3. apply rate limiting if necessary
Note: under some circumstances, equivocation may be expected and fairly benign. For example, assuming a Dapp wants to hand out limited number of red pockets in Spring Festival. It could use the same gas object to create multiple transactions that interact with a red pocket smart contract. It’s expected at most one of few transaction will succeed, depending on which user acts faster enough. The bottom line is, the locked object only loses liveness temporarily, and will be revived at the end of epoch.
Sui Gas Station
Anyone can operate a Gas Station to fund user transactions. Depending on which data flow the sponsor is interested in handling, the Gas Station wants to support a different set of user facing functionalities/RPC interface. Below we draft a recommended specifications:
-
Track Gas Prices in Real-time
-
Sponsor wants to know the current gas prices on chain, to guide the gas price that they give
-
Track Gas Usage
-
Sponsor wants to know a rough gas usage range for the user’s transaction, to guide the gas limit they give
-
Gas Management
-
Sponsor wants to manage gas objects and only hand out gas objects that are nearly enough to cover the expenses, to minimize the risks of objects being locked and illiquid for a while.
-
API endpoint: Take
GaslessTransaction
(see above) and return sole-signed SenderSignedData (case I)
pub fn request_gas_and_signature(gasless_tx: GaslessTransaction) -> Result<SenderSignedData, Error>;
- API endpoint: Take sole-signed
SenderSignedData
and return execution result (case III)
pub fn submit_sole_signed_transaction(sole_signed_data: SenderSignedData) -> Result<(Transaction, CertifiedTransactionEffects), Error>;
- API endpoint: Take a dual-signed SenderSignedData, and return execution result (case I and II)
In Case I and II, users may submit the dual-signed transaction via sponsor, such that the sponsor knows the TransactionDigest and execution result.
pub fn submit_dual_signed_transaction(dual_signed_data: SenderSignedData) -> Result<(Transaction, CertifiedTransactionEffects), Error>;
- API endpoint: return empty check GasData (case III)
pub fn request_gas(/*perhaps some requirement data*/) -> Result<GasData, Error>;
Risk Control
Sponsors want to take the following recommended measures to control risks of running a Gas Station.
Authorization & Rate Limiting
Depending on the nature of the Gas Station, sponsors can apply different authorization rules to avoid being spammed by bad actors. Possible policies include:
- Rate limit gas request per account, or per IP
- Only take requests with valid authorization header, which is rate limited separately
Abuse Detection
For all gas objects that the sponsor gives out, track if users ever try to equivocate and lock objects. If such behavior is detected, blocklist this user/requestor accordingly.