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()
lockAndOpenQuote()
For efficiency, solvers can use combined methods like lockAndOpenQuote(). These methods execute the locking and opening steps in a single transaction.
/**
* @notice Locks and opens the specified quote with the provided details and signatures.
* @param quoteId The ID of the quote to be locked and opened.
* @param filledAmount PartyB has the option to open the position with either the full amount requested by the user or a specific fraction of it
* @param openedPrice The price at which the position is opened.
* @param upnlSig The Muon signature containing the single UPNL value used to lock the quote.
* @param pairUpnlSig The Muon signature containing the pair UPNL and price values used to open the position.
*/
function lockAndOpenQuote(
uint256 quoteId,
uint256 filledAmount,
uint256 openedPrice,
SingleUpnlSig memory upnlSig,
PairUpnlAndPriceSig memory pairUpnlSig
) external whenNotPartyBActionsPaused onlyPartyB notLiquidated(quoteId) {}
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