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.
            }
        }
Example #10
0
        /// <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;
            }
        }
Example #11
0
        /// <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
                });
            }
        }