4. Opening/Closing a Position On-Chain

Opening a Position

At this stage, the solver calls openPosition() or a combined function (lockAndOpenQuote()).

  • Partial Fills are possible: if Hedger only wants to open half the quantity, the remainder might get reposted as a new Intent.

  • The contract does a final solvency check via Muon. If any party is insolvent, the contract disallows opening.

Before a Hedger can lockQuote() or openPosition(), sufficient collateral must be allocated to PartyB. This allocation ensures that PartyB meets the margin requirements for handling the position. If the allocated balance is insufficient, the contract will reject the lockQuote or openPosition transaction.

Allocating Collateral for partyB

Below is a short script that shows how a solver would allocate collateral to a partyA. More information can be found here.

async function allocateForPartyB(amount, partyB, partyA) {
  try {
    console.log(`Allocating ${amount} tokens for PartyB with address ${partyA} from ${partyB}...`);

    // Estimate gas for the transaction
    const allocateGasEstimate = await diamondContract.methods.allocateForPartyB(amount, partyA).estimateGas({ from: partyB });
    console.log("Estimated Gas: ", allocateGasEstimate);

    // Fetch current gas price
    const allocateGasPrice = await web3.eth.getGasPrice();
    console.log("Current Gas Price: ", allocateGasPrice);

    // Execute the allocation transaction
    const receipt = await diamondContract.methods.allocateForPartyB(amount, partyA).send({
      from: partyB,
      gas: allocateGasEstimate,
      gasPrice: allocateGasPrice,
    });

    console.log("Allocation successful!");
    return { success: true, receipt: receipt };
  } catch (error) {
    console.error("Error during allocation:", error);
    return { success: false, error: error.toString() };
  }
}

Locking the Quote

Locking the quote ensures that no other Hedger can act on the same quoteId. This step requires a valid Muon signature for the uPnl (uPnl_B) of the position. You can read more about this method here. The formatted signature and quoteId must be passed to the lockQuote() method.

async function lockQuote(accountAddress, partyA, quoteId, increaseNonce, muonUrls, chainId, diamondAddress) {
  const lockQuoteClient = LockQuoteClient.createInstance(true);
  const signatureResult = await lockQuoteClient.getMuonSig(accountAddress, partyA, 'symmio', muonUrls, chainId, diamondAddress);

  if (signatureResult.success) {
    const { reqId, timestamp, upnl, gatewaySignature, sigs } = signatureResult.signature;
    const upnlSigFormatted = {
      reqId: web3.utils.hexToBytes(reqId),
      timestamp: timestamp.toString(),
      upnl: upnl.toString(),
      gatewaySignature: web3.utils.hexToBytes(gatewaySignature),
      sigs: {
        signature: sigs.signature.toString(),
        owner: sigs.owner,
        nonce: sigs.nonce,
      },
    };

    const gasEstimate = await diamondContract.methods.lockQuote(quoteId, upnlSigFormatted, increaseNonce).estimateGas({ from: accountAddress });
    const gasPrice = await web3.eth.getGasPrice();

    const receipt = await diamondContract.methods.lockQuote(quoteId, upnlSigFormatted, increaseNonce).send({
      from: accountAddress,
      gas: gasEstimate,
      gasPrice,
    });

    console.log("Lock Quote successful!", receipt);
  } else {
    console.error("Failed to fetch Muon signature:", signatureResult.error);
  }
}

Opening the Position Script

Once the quote is locked, the solver calls openPosition() to finalize the transaction. This method performs a solvency check before opening.

async function openPosition(accountAddress, partyA, quoteId, filledAmount, openedPrice, symbolId, muonUrls, chainId, diamondAddress) {
  const openPositionClient = OpenPositionClient.createInstance(true);
  const signatureResult = await openPositionClient.getPairUpnlAndPriceSig(accountAddress, partyA, symbolId, 'symmio', muonUrls, chainId, diamondAddress);

  if (signatureResult.success) {
    const { reqId, timestamp, uPnlA, uPnlB, price, gatewaySignature, sigs } = signatureResult.signature;
    const upnlSigFormatted = {
      reqId: web3.utils.hexToBytes(reqId),
      timestamp: timestamp.toString(),
      upnlPartyA: uPnlA.toString(),
      upnlPartyB: uPnlB.toString(),
      price: price.toString(),
      gatewaySignature: web3.utils.hexToBytes(gatewaySignature),
      sigs: {
        signature: sigs.signature.toString(),
        owner: sigs.owner,
        nonce: sigs.nonce,
      },
    };

    const gasEstimate = await diamondContract.methods.openPosition(quoteId, filledAmount, openedPrice, upnlSigFormatted).estimateGas({ from: accountAddress });
    const gasPrice = await web3.eth.getGasPrice();

    const receipt = await diamondContract.methods.openPosition(quoteId, filledAmount, openedPrice, upnlSigFormatted).send({
      from: accountAddress,
      gas: gasEstimate,
      gasPrice,
    });

    console.log("Open Position successful!", receipt);
  } else {
    console.error("Failed to fetch Muon signature:", signatureResult.error);
  }
}

The Muon uPnlWithSymbolPrice method is used for solvency verification. The positionState will change to OPENED upon successful execution. If any party is insolvent, the contract will reject the transaction.

Combined Methods: lockAndOpenQuote()

For efficiency, solvers can use combined methods like lockAndOpenQuote(). These methods execute the locking and opening steps in a single transaction.

const receipt = await diamondContract.methods.lockAndOpenQuote(
  quoteId, filledAmount, openedPrice, upnlSigFormatted
).send({ from: accountAddress, gas, gasPrice });

The same signature is required as for opening a Quote.

Once opened, the position remains active until fully closed or liquidated. PartyA can place limit or market close requests, which the solver can fill. If the solver fails to respond within a certain timeframe the partyA can execute force closes or force cancels.

Closing a Position

PartyA calls requestToCloseQuote() with parameters (quantity, price if limit, etc.). The solver can close his corresponding off-chain hedge, then call fillCloseRequest() on-chain.

Closing a position on SYMMIO involves calling the fillCloseRequest() function. The solver (PartyB) can close an open position partially or fully, depending on the request type and specified amount.

How fillCloseRequest Works

Parameters:

  • quoteId: The unique identifier of the quote to be closed.

  • filledAmount: The amount being closed.

  • closedPrice: The final price at which the position is being closed.

  • upnlSig: A Muon signature that ensures solvency and validates the closing action.

LIMIT requests can be closed incrementally in multiple steps however MARKET requests must be closed in a single transaction.

Preconditions for Closing

  • Ensure the Hedger (PartyB) is authorized to act on the quote. If not, contact SYMMIO developers to enable whitelisting.

  • A Muon signature validates solvency for both parties before the close action is processed.

  • The position must not be in the LIQUIDATED state, and the partyA must be solvent after the close.

Closing the Position Script

const Web3 = require('web3');
const OpenPositionClient = require('./OpenPositionClient'); // Assuming OpenPositionClient exists in your setup
const { abi } = require('./DiamondContractABI.json'); // ABI for the diamond contract

async function fillCloseRequest(accountAddress, partyA, partyB, quoteId, filledAmount, closedPrice, symbolId, muonUrls, chainId, diamondAddress) {
  console.log("Filling close request...");

  const closeRequestClient = OpenPositionClient.createInstance(true);
  if (!closeRequestClient) {
    console.error("OpenPositionClient is not enabled.");
    return { success: false, error: "Initialization failed" };
  }

  const web3 = new Web3(process.env.RPC_URL);
  const diamondContract = new web3.eth.Contract(abi, diamondAddress);
  const appName = "symmio";
  const urls = [muonUrls];

  try {
    // Fetch Muon signature
    const signatureResult = await closeRequestClient.getPairUpnlAndPriceSig(accountAddress, partyA, symbolId, appName, urls, chainId, diamondAddress);
    if (!signatureResult.success) {
      throw new Error(signatureResult.error || "Muon signature fetch failed");
    }

    const { reqId, timestamp, uPnlA, uPnlB, price, gatewaySignature, sigs } = signatureResult.signature;
    const upnlSigFormatted = {
      reqId: web3.utils.hexToBytes(reqId),
      timestamp: timestamp.toString(),
      upnlPartyA: uPnlA.toString(),
      upnlPartyB: uPnlB.toString(),
      price: price.toString(),
      gatewaySignature: web3.utils.hexToBytes(gatewaySignature),
      sigs: {
        signature: sigs.signature.toString(),
        owner: sigs.owner,
        nonce: sigs.nonce,
      },
    };

    // Call fillCloseRequest
    const receipt = await diamondContract.methods.fillCloseRequest(
      quoteId,
      filledAmount,
      closedPrice,
      upnlSigFormatted
    ).send({
      from: partyB,
      gas: 500000,
    });

    console.log("Close request successfully filled!");
    return { success: true, receipt };
  } catch (error) {
    console.error("Error filling close request:", error);
    return { success: false, error: error.toString() };
  }
}

module.exports = fillCloseRequest;

The script allows PartyB to close a trade initiated by PartyA using the fillCloseRequest() function on the smart contract. The Muon signature is used for solvency validation. The correct Muon method here is also uPnlWithSymbolPrice

Last updated