public async Task <IActionResult> MultisigConfirmationsAsync(DestinationChain destinationChain, int transactionId) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); List <string> owners = await client.GetOwnersAsync().ConfigureAwait(false); var ownersConfirmed = new List <string>(); foreach (string multisig in owners) { bool confirmed = await client.AddressConfirmedTransactionAsync(transactionId, multisig).ConfigureAwait(false); if (confirmed) { ownersConfirmed.Add(multisig); } } return(this.Json(ownersConfirmed)); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public IActionResult ChangeRequirement(DestinationChain destinationChain, int requirement, int gasPrice) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); string data = client.EncodeChangeRequirementParams(requirement); ETHInteropSettings settings = this.interopSettings.GetSettingsByChain(destinationChain); // TODO: Maybe for convenience the gas price could come from the external API poller return(this.Json(client.SubmitTransactionAsync(settings.MultisigWalletAddress, 0, data).GetAwaiter().GetResult())); } catch (Exception e) { this.logger.Error("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public async Task <IActionResult> MultisigTransactionAsync(DestinationChain destinationChain, int transactionId, bool raw) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); if (raw) { return(this.Json(await client.GetRawMultisigTransactionAsync(transactionId).ConfigureAwait(false))); } TransactionDTO transaction = await client.GetMultisigTransactionAsync(transactionId).ConfigureAwait(false); var response = new TransactionResponseModel() { Destination = transaction.Destination, Value = transaction.Value.ToString(), Data = Encoders.Hex.EncodeData(transaction.Data), Executed = transaction.Executed }; return(this.Json(response)); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public ETHCompatibleClientProvider(IETHClient ethClient, IBNBClient bnbClient, InteropSettings interopSettings) { this.supportedChains = new Dictionary <DestinationChain, IETHClient>() { { DestinationChain.ETH, ethClient }, { DestinationChain.BNB, bnbClient }, }; this.interopSettings = interopSettings; }
public IActionResult ConfirmTransaction(DestinationChain destinationChain, int transactionId, int gasPrice) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); return(this.Json(client.ConfirmTransactionAsync(transactionId).GetAwaiter().GetResult())); } catch (Exception e) { this.logger.Error("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public async Task <IActionResult> BalanceAsync(DestinationChain destinationChain, string account) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); return(this.Json((await client.GetErc20BalanceAsync(account).ConfigureAwait(false)).ToString())); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public async Task <IActionResult> ConfirmTransactionAsync(DestinationChain destinationChain, int transactionId, int gasPrice) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); // TODO: Maybe for convenience the gas price could come from the external API poller return(this.Json(await client.ConfirmTransactionAsync(transactionId, gasPrice).ConfigureAwait(false))); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public async Task <IActionResult> RemoveOwnerAsync(DestinationChain destinationChain, string existingOwnerAddress, int gasPrice) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); string data = client.EncodeRemoveOwnerParams(existingOwnerAddress); ETHInteropSettings settings = this.interopSettings.GetSettingsByChain(destinationChain); // TODO: Maybe for convenience the gas price could come from the external API poller return(this.Json(await client.SubmitTransactionAsync(settings.MultisigWalletAddress, 0, data, gasPrice).ConfigureAwait(false))); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
/// <summary> /// Iterates through all unprocessed mint requests in the repository. /// If this node is regarded as the designated originator of the multisig transaction, it will submit the transfer transaction data to /// the multisig wallet contract on the Ethereum chain. This data consists of a method call to the transfer() method on the wrapped STRAX contract, /// as well as the intended recipient address and amount of tokens to be transferred. /// </summary> private async Task ProcessConversionRequestsAsync() { List <ConversionRequest> mintRequests = this.conversionRequestRepository.GetAllMint(true); if (mintRequests == null) { this.logger.LogDebug("No requests."); return; } this.logger.LogInformation("There are {0} unprocessed conversion mint requests.", mintRequests.Count); foreach (ConversionRequest request in mintRequests) { this.logger.LogInformation("Processing conversion mint request {0} on {1} chain.", request.RequestId, request.DestinationChain); IETHClient clientForDestChain = this.clientProvider.GetClientForChain(request.DestinationChain); // We are not able to simply use the entire federation member list, as only multisig nodes can be transaction originators. List <IFederationMember> federation = this.federationHistory.GetFederationForBlock(this.chainIndexer.GetHeader(request.BlockHeight)); var multisig = new List <CollateralFederationMember>(); foreach (IFederationMember member in federation) { if (!(member is CollateralFederationMember collateralMember)) { continue; } if (!collateralMember.IsMultisigMember) { continue; } if (this.network.NetworkType == NetworkType.Mainnet && !this.multisigPubKeys.Contains(collateralMember.PubKey)) { continue; } multisig.Add(collateralMember); } // This should be impossible. if (multisig.Count == 0) { this.logger.LogError("Sanity check failed, there are no multisig members!"); return; } IFederationMember designatedMember = multisig[request.BlockHeight % multisig.Count]; bool originator = designatedMember.Equals(this.federationManager.GetCurrentFederationMember()); // Regardless of whether we are the originator, this is a good time to check the multisig's remaining reserve // token balance. It is necessary to maintain a reserve as mint transactions are many times more expensive than // transfers. As we don't know precisely what value transactions are expected, the sole determining factor is // whether the reserve has a large enough balance to service the current conversion request. If not, trigger a // mint for a predetermined amount. // BigInteger reserveBalanace = await this.ETHClient.GetErc20BalanceAsync(this.interopSettings.MultisigWalletAddress).ConfigureAwait(false); // The request is denominated in satoshi and needs to be converted to wei. BigInteger amountInWei = this.CoinsToWei(Money.Satoshis(request.Amount)); /* Temporarily disabled mint logic * if (amountInWei > reserveBalanace) * { * if (this.minting) * { * this.logger.LogInformation("Minting transaction has not yet confirmed. Waiting."); * * return; * } * * this.minting = true; * * this.logger.LogInformation("Insufficient reserve balance remaining, initiating mint transaction to replenish reserve."); * * string mintData = this.ETHClient.EncodeMintParams(this.interopSettings.MultisigWalletAddress, ReserveBalanceTarget); * * BigInteger mintTransactionId = await this.ETHClient.SubmitTransactionAsync(request.DestinationAddress, 0, mintData).ConfigureAwait(false); * * // Now we need to broadcast the mint transactionId to the other multisig nodes so that they can sign it off. * string mintSignature = this.federationManager.CurrentFederationKey.SignMessage(MintPlaceHolderRequestId + ((int)mintTransactionId)); * // TODO: The other multisig nodes must be careful not to blindly trust that any given transactionId relates to a mint transaction. Need to validate the recipient * await this.federatedPegBroadcaster.BroadcastAsync(new InteropCoordinationPayload(MintPlaceHolderRequestId, (int)mintTransactionId, mintSignature)).ConfigureAwait(false); * * return; * } */ // TODO: Perhaps the transactionId coordination should actually be done within the multisig contract. This will however increase gas costs for each mint. Maybe a Cirrus contract instead? switch (request.RequestStatus) { case ((int)ConversionRequestStatus.Unprocessed): { if (originator) { // If this node is the designated transaction originator, it must create and submit the transaction to the multisig. this.logger.LogInformation("This node selected as originator for transaction {0}.", request.RequestId); request.RequestStatus = ConversionRequestStatus.OriginatorNotSubmitted; } else { this.logger.LogInformation("This node was not selected as the originator for transaction {0}. The originator is: {1}.", request.RequestId, designatedMember.PubKey.ToHex()); request.RequestStatus = ConversionRequestStatus.NotOriginator; } break; } case (ConversionRequestStatus.OriginatorNotSubmitted): { // First construct the necessary transfer() transaction data, utilising the ABI of the wrapped STRAX ERC20 contract. // When this constructed transaction is actually executed, the transfer's source account will be the account executing the transaction i.e. the multisig contract address. string abiData = clientForDestChain.EncodeTransferParams(request.DestinationAddress, amountInWei); // Submit the unconfirmed transaction data to the multisig contract, returning a transactionId used to refer to it. // Once sufficient multisig owners have confirmed the transaction the multisig contract will execute it. // Note that by submitting the transaction to the multisig wallet contract, the originator is implicitly granting it one confirmation. BigInteger transactionId = await clientForDestChain.SubmitTransactionAsync(this.interopSettings.ETHSettings.WrappedStraxContractAddress, 0, abiData).ConfigureAwait(false); this.logger.LogInformation("Originator submitted transaction to multisig and was allocated transactionId {0}.", transactionId); // TODO: Need to persist vote storage across node shutdowns this.interopTransactionManager.AddVote(request.RequestId, transactionId, this.federationManager.CurrentFederationKey.PubKey); request.RequestStatus = ConversionRequestStatus.OriginatorSubmitted; break; } case (ConversionRequestStatus.OriginatorSubmitted): { // It must then propagate the transactionId to the other nodes so that they know they should confirm it. // The reason why each node doesn't simply maintain its own transaction counter, is that it can't be guaranteed // that a transaction won't be submitted out-of-turn by a rogue or malfunctioning federation multisig node. // The coordination mechanism safeguards against this, as any such spurious transaction will not receive acceptance votes. // TODO: The transactionId should be accompanied by the hash of the submission transaction on the Ethereum chain so that it can be verified BigInteger transactionId2 = this.interopTransactionManager.GetCandidateTransactionId(request.RequestId); if (transactionId2 != BigInteger.MinusOne) { await this.BroadcastCoordinationAsync(request.RequestId, transactionId2, request.DestinationChain).ConfigureAwait(false); BigInteger agreedTransactionId = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); if (agreedTransactionId != BigInteger.MinusOne) { this.logger.LogInformation("Transaction {0} has received sufficient votes, it should now start getting confirmed by each peer.", agreedTransactionId); request.RequestStatus = ConversionRequestStatus.VoteFinalised; } } break; } case (ConversionRequestStatus.VoteFinalised): { BigInteger transactionId3 = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); if (transactionId3 != BigInteger.MinusOne) { // The originator isn't responsible for anything further at this point, except for periodically checking the confirmation count. // The non-originators also need to monitor the confirmation count so that they know when to mark the transaction as processed locally. BigInteger confirmationCount = await clientForDestChain.GetConfirmationCountAsync(transactionId3).ConfigureAwait(false); if (confirmationCount >= 6) { this.logger.LogInformation("Transaction {0} has received at least 6 confirmations, it will be automatically executed by the multisig contract.", transactionId3); request.RequestStatus = ConversionRequestStatus.Processed; request.Processed = true; // We no longer need to track votes for this transaction. this.interopTransactionManager.RemoveTransaction(request.RequestId); } else { this.logger.LogInformation("Transaction {0} has finished voting but does not yet have 8 confirmations, re-broadcasting votes to peers.", transactionId3); // There are not enough confirmations yet. // Even though the vote is finalised, other nodes may come and go. So we re-broadcast the finalised votes to all federation peers. // Nodes will simply ignore the messages if they are not relevant. await this.BroadcastCoordinationAsync(request.RequestId, transactionId3, request.DestinationChain).ConfigureAwait(false); // No state transition here, we are waiting for sufficient confirmations. } } break; } case (ConversionRequestStatus.NotOriginator): { // If not the originator, this node needs to determine what multisig wallet transactionId it should confirm. // Initially there will not be a quorum of nodes that agree on the transactionId. // So each node needs to satisfy itself that the transactionId sent by the originator exists in the multisig wallet. // This is done within the InteropBehavior automatically, we just check each poll loop if a transaction has enough votes yet. // Each node must only ever confirm a single transactionId for a given conversion transaction. BigInteger agreedUponId = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); if (agreedUponId != BigInteger.MinusOne) { this.logger.LogInformation("Quorum reached for conversion transaction {0} with transactionId {1}, submitting confirmation to contract.", request.RequestId, agreedUponId); // Once a quorum is reached, each node confirms the agreed transactionId. // If the originator or some other nodes renege on their vote, the current node will not re-confirm a different transactionId. string confirmationHash = await clientForDestChain.ConfirmTransactionAsync(agreedUponId).ConfigureAwait(false); this.logger.LogInformation("The hash of the confirmation transaction for conversion transaction {0} was {1}.", request.RequestId, confirmationHash); request.RequestStatus = ConversionRequestStatus.VoteFinalised; } else { BigInteger transactionId4 = this.interopTransactionManager.GetCandidateTransactionId(request.RequestId); if (transactionId4 != BigInteger.MinusOne) { this.logger.LogInformation("Broadcasting vote (transactionId {0}) for conversion transaction {1}.", transactionId4, request.RequestId); this.interopTransactionManager.AddVote(request.RequestId, transactionId4, this.federationManager.CurrentFederationKey.PubKey); await this.BroadcastCoordinationAsync(request.RequestId, transactionId4, request.DestinationChain).ConfigureAwait(false); } // No state transition here, as we are waiting for the candidate transactionId to progress to an agreed upon transactionId via a quorum. } break; } } // Make sure that any state transitions are persisted to storage. this.conversionRequestRepository.Save(request); // Unlike the mint requests, burns are not initiated by the multisig wallet. // Instead they are initiated by the user, via a contract call to the burn() method on the WrappedStrax contract. // They need to provide a destination STRAX address when calling the burn method. // Properly processing burn transactions requires emulating a withdrawal on the main chain from the multisig wallet. // It will be easier when conversion can be done directly to and from a Cirrus contract instead. // Currently the processing is done in the WithdrawalExtractor. } }
/// <summary> /// Creates filters that the RPC interfaces uses to listen for events against the desired contract. /// In this case the filter is specifically listening for Transfer events emitted by the wrapped strax /// contracts deployed on supported chains. /// </summary> private async Task CreateEventFiltersIfRequired(DestinationChain targetChain, IETHClient chainClient) { if (this.eventFilterCreationRequired[DestinationChain.ETH]) { // The filter should only be set up once IBD completes. await chainClient.CreateTransferEventFilterAsync().ConfigureAwait(false); this.eventFilterCreationRequired[targetChain] = false; } }
/// <summary> /// Retrieves any Transfer events from the logs of the Wrapped Strax contract deployed on specified chain. /// Transfers with the zero (0x0000...) address as their destination can be considered to be burn transactions and are saved for processing as withdrawals on the mainchain. /// </summary> private async Task CheckForContractEventsAsync(DestinationChain targetChain, IETHClient chainClient) { await this.CreateEventFiltersIfRequired(targetChain, chainClient).ConfigureAwait(false); // Check for all Transfer events against the WrappedStrax contract since the last time we checked. // In future this could also poll for other events as the need arises. List <EventLog <TransferEventDTO> > transferEvents = await chainClient.GetTransferEventsForWrappedStraxAsync().ConfigureAwait(false); foreach (EventLog <TransferEventDTO> transferEvent in transferEvents) { // Will probably never be the case, but check anyway. if (string.IsNullOrWhiteSpace(transferEvent.Log.TransactionHash)) { continue; } // These could be mints or something else, either way ignore them. if (transferEvent.Event.From == ETHClient.ETHClient.ZeroAddress) { continue; } // Transfers can only be burns if they are made with the zero address as the destination. if (transferEvent.Event.To != ETHClient.ETHClient.ZeroAddress) { continue; } this.logger.LogInformation("Conversion burn transaction {0} received from contract events, sender {1}.", transferEvent.Log.TransactionHash, transferEvent.Event.From); if (this.conversionRequestRepository.Get(transferEvent.Log.TransactionHash) != null) { this.logger.LogInformation("Conversion burn transaction {0} already exists, ignoring.", transferEvent.Log.TransactionHash); continue; } if (transferEvent.Event.Value == BigInteger.Zero) { this.logger.LogInformation("Ignoring zero-valued burn transaction {0}.", transferEvent.Log.TransactionHash); continue; } if (transferEvent.Event.Value < BigInteger.Zero) { this.logger.LogInformation("Ignoring negative-valued burn transaction {0}.", transferEvent.Log.TransactionHash); continue; } this.logger.LogInformation("Conversion burn transaction {0} has value {1}.", transferEvent.Log.TransactionHash, transferEvent.Event.Value); // Look up the desired destination address for this account. string destinationAddress = await chainClient.GetDestinationAddressAsync(transferEvent.Event.From).ConfigureAwait(false); this.logger.LogInformation("Conversion burn transaction {0} has destination address {1}.", transferEvent.Log.TransactionHash, destinationAddress); try { // Validate that it is a mainchain address here before bothering to add it to the repository. BitcoinAddress.Create(destinationAddress, this.counterChainNetwork); } catch (Exception) { this.logger.LogWarning("Error validating destination address {0} for transaction {1}.", destinationAddress, transferEvent.Log.TransactionHash); continue; } // Schedule this transaction to be processed at the next block height that is divisible by 10. If the current block height is divisible by 10, add a further 10 to it. // In this way, burns will always be scheduled at a predictable future time across the multisig. // This is because we cannot predict exactly when each node is polling the Ethereum chain for events. ulong blockHeight = (ulong)this.chainIndexer.Tip.Height - ((ulong)this.chainIndexer.Tip.Height % 10) + 10; if (blockHeight <= 0) { blockHeight = 10; } this.conversionRequestRepository.Save(new ConversionRequest() { RequestId = transferEvent.Log.TransactionHash, RequestType = ConversionRequestType.Burn, Processed = false, RequestStatus = ConversionRequestStatus.Unprocessed, Amount = this.ConvertWeiToSatoshi(transferEvent.Event.Value), BlockHeight = (int)blockHeight, DestinationAddress = destinationAddress, DestinationChain = targetChain }); } }