Action: PositionOpen
A PositionOpen
action opens a trading position that provides liquidity for swaps and contains just a position.
A position contains a state, which is one of Opened
, Closed
, Withdrawn
, or Claimed
; two reserve amounts; a TradingFunction
, a nonce, and a flag indicating whether it is a limit order (that should be automatically closed once one of its reserves reach zero).
A TradingFunction
specifies which pair of assets liquidity is being provided for, what fee should be paid for trades that go through this position, and two trading coefficients that specify what ratio this position permits swaps at.
The balance commitment associated with a PositionOpen
is transparent, debits the position's reserves from the balance, and credits the LPNFT for the new position to the transaction's balance.
An LPNFT is an asset whose generator is derived from a hash of the position ID and current state. A position ID is a hash of the position's nonce, asset types, fee, and trading coefficients (importantly, not of its reserves or state, which are mutable).
The transaction planner will by default store the LPNFT for the position in an Output
note as change, which can be used in a subsequent transaction with a Spend
+ PositionClose
, but other arrangements are possible (e.g., using a PositionOpen
and PositionClose
within the same transaction to only provide liquidity at a certain exchange rate for one block and then storing the closed position's LPNFT in an Output
note).
PositionOpen::check_stateless
invokes Position::check_stateless
, which checks that
both reserves are less than or equal to .
at least one of the reserves is nonzero.
both trading coefficients are nonzero (either being zero would imply an infinite price for the opposite asset).
both trading coefficients are less than or equal to .
the trading function's assets are distinct (to avoid creating self-edges in the position graph).
the fee is less than 50%.
PositionOpen::check_stateful
checks that the position ID did not occur in any previous transaction by checking the state under "dex/position/{id}"
.
PositionOpen::execute
calls PositionManager::put_position
, which stores the position in "dex/position/{id}"
and updates various indexes relating to the position.
There is a TOCTOU bug () that allows a position to be opened multiple times with the same ID within one transaction (see Finding ref↗), which should be prevented by adding a check to Transaction::check_stateless
that ensures that all transaction IDs opened are unique.
Action: PositionClose
A PositionClose
action closes a currently open position, specified by its ID.
The balance commitment associated with a PositionClose
is transparent, debits the LPNFT for the open position with the specified ID, and credits the LPNFT for the closed position with the specified ID to the transaction's balance.
PositionClose::check_stateless
and PositionClose::check_stateful
both unconditionally succeed.
PositionClose::execute
queues the position ID in the state under "dex/pending_position_closures"
to be closed at the end of the block.
Action: PositionWithdraw
A PositionWithdraw
action withdraws reserves from a closed position, specified by its ID, and specifies a transparent commitment to the reserves.
The balance commitment associated with a PositionWithdraw
is transparent, debits the LPNFT for the closed position with the specified ID, and credits both the LPNFT for the withdrawn position with the specified ID as well as the reserves to the transaction's balance.
PositionWithdraw::check_stateless
unconditionally succeeds.
PositionWithdraw::check_stateful
checks that the reserves currently associated with the position in the state match the specified reserves to withdraw.
PositionWithdraw::execute
retrieves the position from the state, checks that it is closed (returning an error if it is not), and stores that it is withdrawn in the state.
While there is a potential TOCTOU issue () if the reserves are modified (such as being traded against while open, then closed, then withdrawn, potentially resulting in the assets being swapped twice — once in the batch swap and again in the withdraw-with-stale-reserves), this seems to be unreachable in practice since PositionWithdraw
checks that the position is closed immediately, while PositionClose
defers the close to the end of the block.
The comments in PositionWithdraw::check_stateful
imply that submitting PositionClose
+ PositionWithdraw
for the same position in a single transaction is intended to be supported. This seems like it would require deferring withdrawals to the end of block to happen after the queue of position closures is processed and require checking that the reserves have not shifted there as well. But that would also require changing how the reserves are withdrawn, since while the state transition could be cancelled (and the position left closed instead of withdrawn) at the end of the block, there would be no straightforward way to cancel the withdrawn value at that point (since the value is not necessarily stored in a pair of Output
notes, there is not necessarily even a pair of nullifiers to burn).
Per discussion with Penumbra Labs, it's not intended that a position should be able to be closed + withdrawn in the same transaction, and the comment in PositionWithdraw::check_stateful
will be revised.
Action: PositionRewardClaim
The PositionRewardClaim
actions were intended, in the future, to allow providing retroactive liquidity incentives. They are currently an unimplemented placeholder, and PositionRewardClaim::{check_stateless,check_stateful,execute}
all unconditionally return an error.
PositionRewardClaim
and State::Claimed
were removed in in favour of a different mechanism involving adding sequence numbers to State::Withdrawn
.