Making an ERC20 Token Bridge using OpenZeppelin Defender

Making an ERC20 Token Bridge using OpenZeppelin Defender

In the world of Web3, tokens can be used in a variety of ways. But cannot be shared between networks.

This is where bridges come in handy, allowing users to "move" tokens from one network to another.

Backend

Usually bridges are developed by connecting a smart contract that is deployed on two different network, with a backend that would listen for important events and initiating transactions on the target networks.

This means that usually in order for the user to get the token they are transferring and locking wether ERC20 or ERC721 from a source network into a target network. The user then would have to switch to the target network and claim the token on the other side.

This is not a bad UX at all, but it got me thinking if there is a possibility to streamline the process by eliminating the step where the user has to change networks, and implement a system that does it for them automatically.

OpenZeppelin Defender

OpenZeppelin Defender is a perfect solution for this, with it's two services Relayer and AutoTask, this process was made into a reality.

AimBridge FlowChart

By taking advantage of AutoTask, that runs a AWS Lambda every time a call is made, it is possible to just make a POST request the moment the locking of tokens is complete.

But how would we be able to communicate the data to the target network?

The main reason why users need to switch networks, is that you cannot communicate data and requests from one network to another, the whole point a bridge existing.

This is where OpenZeppelin Relayer comes in, Relayers are basically wallets created and held in the OpenZeppelin Defender panel, and are connected to a certain network, and then can be injected and used within the AutoTask.

So to answer the question, all we need to do is send a POST request to the AutoTask connected to our target network. And this will lead to us communicating the data directly.

Architecture.png

Security

Now that we have solved the way to communicate our request, comes the issue of security.

  • Can everyone just call the requests to the smart contract and mint new tokens?
  • Since we are getting as webhook url from Defender, how can we avoid malicious requests?
  • How to secure a successful transaction against replay attacks

Relayers

For the first issue, Relayers are back again to save us. By just implementing roles in the smart contracts we are able to limit access to critical methods to only the relayers, preventing random people from calling our sensitive methods.

Abstraction

To avoid the webhook from having too many needless requests and attacks, it is abstracted by using NextJS's - framework used in the project - route to abstract the call to the webhook, and hide also hide the actual webhook url from the network tab.

What's to prevent users from calling the custom API directly then?

In order to prevent that a secret message is used to secure the API, unless that secret key is provided the request will be rejected.

Replay Attacks.

The above mentioned methods are not nearly enough to prevent attackers from abusing our system, replay attacks are one the biggest issues in this architecture, since an attacker can submit a valid request to mint or release tokens, then repeat the same request infinite amount of times to take advantage of the system.

giphy.webp

In order to prevent that, a timestamp+ hash method has been implemented, the way this method works is that instead of for the webhook to receive regular data from the front-end, it receives additional data with it. For example:

{
  timestamp : 1668893990, // A unix timestamp for when the transaction was initiated
  content : {....}  // Object of actual data
  hash : "f688ae26e9cfa3ba6235477831d5122e" // timestamp + data hashed together using a private key
}

Timestamp

The reason a timestamp is attached to the request, is to check how long ago was the transaction initiated.

If the transaction is older then a certain interval it is rejected.

if(current_timestamp - timestamp > 10 minutes) return `Timeout`

Hash

If the time is within the interval the webhook then proceeds to hash the data and timestamp again with the same private key as the front-end, if the hashes are equal , the webhook then calls the smart contract on the target network, if not it is rejected.

Example:

if( "f688ae26e9cfa3ba6235477831d5122e" === "f688ae26e9cfa3ba6235477831d5122e") // Returns true, allowed

if( "f688ae26e9cfa3ba6235477831d5122e" === "bfa99df33b137bc8fb5f5407d7e58da8") // Returns false, transaction is cancled

This means that if the attacker changes the timestamp or the data the hash is then invalid, leading to a rejected transaction.

And by implementing everything together, we would have our final result :)

Please not that this way of making a bridge is highly experimental and needs more security, and is not advised to actually go to production with

Smart Contract repo

Frontend repo