Gas Station Network Audit

Zerotrust
Zerotrust
Published in
7 min readSep 16, 2019

The Gas Station Network team asked us to review the Gas Station Network contracts. We looked at the code and now publish our results.

The audited commit is fc2a8bce3430c2e53b4ce4de65268514eac39e96 and the files included in scope were GsnUtils.sol, IRelayHub.sol, IRelayRecipient.sol, RLPReader.sol, RelayHub.sol, RelayRecipient.sol, SampleRecipient.sol, and TestRecipientUtils.sol.

This audit was a full audit covering the entire project. This audit encompassed the system as a whole, as well as a deep inspection of each contract individually. Additionally, the auditors considered some potential off-chain attack vectors.

Here are our assessment and recommendations, in order of importance.

Update: The Gas Station Network team has reviewed all of the recommendations in this report and plans to incrementally improve the contracts and network based on these findings in combination with data from the contracts currently live on the main Ethereum network. None of the findings in this report present a significant security risk to the current participants of the network in its current state.

Issues

Critical

None

High

[H01] Users can cause relays to lose transaction fees without being refunded

There are several ways for users to grief relays, cutting into their profitability. The following scenarios result in a relay making a transaction that reverts, thus, costing them a transaction fee without receiving a fee for relaying in return:

  1. The recipient contract returns inconsistent results from the acceptRelayedCall function. First the contract returns true for the relay's initial read-only check, then false when the actual transaction is made causing it to revert. This is case is mentioned in the Gas Station Network EIP where it states:
    Relay keeps track of transactions it sent, and waits for TransactionRelayed events to see the charge. If a transaction reverts and goes unpaid, which means the recipient’s acceptRelayedCall() function was inconsistent, Relay refuses service to that recipient for a while (or blacklists it indefinitely, if it happens often).”
  2. A user engages multiple relays at the same time to relay the same transaction. One will succeed and the rest will revert due to the user’s nonce having already been used thus costing relays a transaction fee.
  3. The user front-runs a relayed transaction to withdraw the entire balance of the recipient contract. This would cause the balance check in the relayCall function to fail relatively early in the course of the transaction.

Scenario 3 can also be mitigated by blacklisting recipients or users off-chain as proposed for scenario 1. However, depending on the sophistication of the relays and the number of relays on the network, these mitigations may not be adequate to prevent non-trivial griefing attacks, possibly forcing relays to go offline. If relays maintain recipient contract blacklists independently, a malicious recipient contract could grief every active relay in the network one time.

In scenario 2, this mitigation will not work because the fault is not necessarily attributable to the user. A relay or multiple relays could withhold a transaction until the user chooses a different relay just to broadcast it at the same time causing one of the transactions to fail. If the naive relay in this scenario blacklists the user, it will not have been deserved and the malicious relay(s) will have successfully ended that relay-user relationship.

Additionally, there is clear upside for an attacker to execute these griefing attacks if they themselves are a relay. Damaging the profitability of other relays or forcing them to go offline will increase the profitability of the attacker’s relay assuming traffic will be rerouted to themselves.

Lastly, given these mitigations to griefing attacks, recipient contracts that do not explicitly maintain a whitelist of users must be very careful in how they are implemented. For example, this is a proposed implementation of the acceptRelayedCall function described in the Gas Station Network EIP:

Balance sheet of registered users, maintained by the dapp owner. Users pay the dapp with a credit card or other non-ETH means, and are credited in the RelayRecipient balance sheet. Users can never cost the dapp more than they were credited for.

If users can withdraw their balance or spend their balance in other ways, a malicious user will be able to force inconsistent behavior from the acceptRelayedCall function resulting in the contract being blacklisted.

Consider thoroughly documenting all griefing scenarios, their implications, and possible mitigations so implementers of relays and recipient contracts understand the risks and are well aware of the problem set.

Update: The Gas Station Network team has acknowledged the above scenarios as potential issues for some use cases but is confident they can be mitigated off-chain. As stated above, scenarios 1 and 3 can mitigated by relays by maintaining a blacklist of misbehaving recipient contracts. The burden of mitigating scenario 2 can be placed on the recipient contract as well. Recipient contracts whose users grief the relays will be blacklisted so all recipient contracts will be responsible for barring malicious users. For this reason, some permissionless contracts will not be well suited for integration with the gas station network.

Medium

[M01] Penalize function rewards can be stolen

In the RelayHub contract, there are two functions, penalizeRepeatedNonceand penalizeIllegalTransaction, that are used to claim a reward by proving a relay made an illegal transaction. Both of these functions can be front-ran by an attacker to steal the full reward. The minimumStake is currently 1 Ether meaning the reward for submitting proof of an illegal transaction will be at least 0.5 ETH, one half of the stake. The other half of the stake is burned to prevent a malicious actor from reporting themselves and going unpenalized. As with most front-runnable contracts on Ethereum, attackers will also compete to front-run each other driving the transaction fee up and the profitability of the attack to nearly 0. Regular participants will be forced to use transaction fees that are just below the rewards or expect their transactions to fail. This may result in an inadequate incentive to report misbehaving relays. Consider implementing a commit reveal scheme or exploring other options to mitigate front-running.

Update: The Gas Station Network team has opted to keep the current implementation. The team stated that while the reward may be taken by front-runners, it will still incentivize relays to behave honestly. A user who has the information necessary to penalize a relay could share it publicly or directly with a group of reporters rather than reporting themselves.

Low

[L01] Use assert for invariants

In the RelayHub contract's relayCall function, there is a require statement that checks that the recipient has a large enough balance to pay for the transaction that should never revert. Consider using assert rather than require for invariants to clarify intentions and make use of tooling that can verify assert statements.

[L02] Lack of RLP Validation

Even though the toList function handles data parsing, sanity checks should be put in place in RLPReader.sol to guarantee expected behavior. The following are locations in the code where sanity checks may be used

  • The address length check in RLPReader should check strictly for a length of 21, as all addresses are always 20 bytes in length plus one byte for the item length.
  • toUint should check that item.len is less than or equal to 33 bytes, the max length of a uint plus one byte for the item length.

In general, this file should reflect the latest updates made in the original repository.

[L03] The transaction overhead cost is calculated as a fixed cost but is dynamic

In the RelayHub contract the total gas cost is calculated by getting the gasleft() at the beginning and towards the end of the transaction and adding an overhead cost that covers the base transaction fee and the gas cost to reward the relay. However, this will not always result in an accurate gas cost calculation because the overhead is not constant. For example, the relayCall function has dynamic inputs and a transaction will cost 68 more gas per non-zero byte in the transaction. This cost will not be reflected in the gas cost calculation but will cost the relay and may be used as a griefing attack. Consider making the gas calculation more precise or allowing it to be calculated completely off-chain.

Notes

  • The transactionFee param docstring on the relayCall function is listed twice. Consider removing one instance of this docstring.
  • In RelayRecipient.sol, a comment states, "Implement acceptRelayedCall, which acts as a whitelist/blacklist of senders". Consider adding comments stating that the preRelayCall function and postRelayCall function must also be implemented.
  • The registerRelay function is callable by either a "staked" or a "registered" relay. A successful call of this function updates the state of the relay to "staked" (if this is not already the case) and emits an event that includes the passed in transactionFee and url parameters.
  • Since an already registered relay can successfully call this with different transactionFee and url values than originally passed in, this function's name should reflect the fact that it is also possible to update a registered relay's parameters (as opposed to simply register). Consider renaming this function to something more descriptive, such as registerOrUpdateRelay. Alternatively, consider making a separate function that has the single purpose of updating the already-registered relay.
  • maximumRecipientDeposit is a constant set to a relatively low value for its use case. This value is checked when depositFor is called, and the purpose of this function is to “repay relay calls into this contract”.
  • Consider setting maximumRecipientDeposit to a higher value in order to reduce repetitive transactions and still achieve its goal of preventing user error. The price of ETH and the network gas markets fluctuate wildly. Because of this, there may be a time in which the price of ETH is low and the network gas usage is high, forcing high transaction fees relative to ETH, but low relative to USD.

Conclusion

Zero critical and one high issue were found. Some changes were proposed to follow best practices and reduce the potential attack surface.

--

--