Article

Adapting to Change in an Immutable World

August 15, 2024

Proxy Architectures for Dynamic Smart Contracts

Developing a web3 financial platform that stands the test of time and evolves with user needs requires a deep understanding of smart contract (SC) architecture. Before you embark on building the next DeltaPrime, GMX, or Uniswap, it's crucial to design your platform with scalability, flexibility, and security in mind. As part of that design, your smart contracts should be able to evolve with feature requests, bug findings, and improved implementations over time. While smart contracts themselves are immutable, proxy architectures allow changing implementations in a transparent, seamless way. This article will guide you through essential smart contract proxy patterns, explaining what problems are being solved by each pattern and how proxies can be used to create a robust web3 financial platform. This article progresses from simplest architecture to most complicated (in regards to components involved). As you build your own smart contract architecture, consider implementing the simplest approach that sufficiently meets the need of your use-case.

The Blessing and Curse of Immutability

One of the core tenants of most blockchains is that smart contracts are immutable once deployed. This means that once the code for a given smart contract is deployed, no code changes can be made to that deployed contract. This immutability is generally seen as a benefit; it allows a given contract to be reviewed once by all parties and guarantees what was reviewed is what will be executed. This helps build user trust in smart contracts.

However, there are valid reasons that smart contract developers need to change behavior for contract(s) they manage:

  1. Bug fixes: If a bug (functional, security, etc.) is discovered, that bug should be fixed.
  2. New features: If users/developers of a contract want a new feature, it should be possible to add that feature. Completely static contracts will lose relevancy over time as user demands change.
  3. Improved implementations: As blockchain ecosystems evolve, new frameworks, techniques, etc. will arise that improve contract functionality. Completely static contracts will not be able to take advantage of these improvements.

Because of these needs for changes in behavior, proxy contracts have been introduced as a solution to support changes in the immutable world of smart contracts.

The Naive Approach: Point Users to a New Contract

Before introducing proxies, you can consider the simplest architectural solution to making contract changes:

  1. Just make code changes to your contract code and deploy a new contract.
  2. Update all references you have to point to the new contract
  3. Tell any end users to update their references to use the new contract

Figure 1: The naive approach; all end users need to point to the new contract address

This approach, while technically simple, can be incredibly error prone & logistically complicated:

  • You need to coordinate changing all references, both internally and to the broader community.
  • Additionally, this approach requires moving all state stored in v1 of the contract into v2 of the contract which is its own technical burden. E.g. if balances are stored in v1, those balances need to be moved at time-of-upgrade to v2.

The Simple Proxy Solution

A straightforward solution to the problem of updating all those smart contract references is to use an entrypoint contract that routes all users to the correct implementation SC. This can be achieved with a simple proxy (e.g. a transparent upgradable proxy), which allows you to change the implementation transparently to the user when bug fixes or new features are required.

In essence, the simple proxy acts as middleware, forwarding user interactions to the latest version of your smart contract. This means when a bug is fixed or a feature is added, only the reference in the proxy contract needs updating, not all user-facing references. Additionally, state can be stored in the entrypoint contract while behavior is derived from the implementation contract; this eliminates the state migration problem described in the previous section.

Figure 2: The entrypoint contract uses a simple proxy to route users to the correct smart contract, simplifying updates by requiring changes to only one contract.

A Problem with the Simple Proxy Solution

While the simple proxy solution works for basic transactions, it falls short when the entrypoint contract needs to retain complex information about each user. For example, in web3 financial transactions like smart loans, the entrypoint contract must store detailed user-specific data, such as loan amounts, repayment schedules, and collateral information. While state for all users could potentially be stored in the entrypoint contract, this contract code can become complex to manage as the state of your contract becomes more complex.

As one way to address this, each user could have a unique entrypoint contract pointing to the correct proxied contract. However, this approach reintroduces a similar problem to our initial naive solution because whenever the implementation SC is updated, each individual’s entrypoint contract must also be updated. This can be mitigated by management contracts and scripts to manage all known entrypoint contracts, but that comes with its own technical overhead.

Figure 3: Each user has a unique entrypoint contract directing them to the appropriate proxied contract, but updates still need to be applied individually.

The Beacon Proxy Pattern Solution

The beacon proxy pattern offers a more scalable solution when multiple entrypoint contracts are needed. This solution contains a “beacon” contract that points to the implementation contract. Each entrypoint contract points to the single beacon proxy.

When implementation upgrades need to occur, you simply update the pointer in the beacon contract. The updated beacon will redirect all entrypoint contracts to the new implementation. 

This streamlined process not only reduces the maintenance burden but also ensures a more efficient, cost-effective, scalable solution.

Figure 4: The beacon proxy pattern routes individual entrypoint contracts to a central beacon, ensuring seamless updates by modifying only the beacon.

Managing Smart Contract Code Size Limitations

While the beacon proxy addresses many issues, it doesn't solve the problem of code size limitations.

As your SC's feature set grows, it can quickly reach the maximum code size limit. On the EVM for example, the code size limit is currently 24 KB. This size limitation can hinder the development of complex financial applications that require extensive logic and functionality.

Additionally, from a developer & user perspective, upgrading an entire contract each time a small bug fix or feature is released can be heavy; this comes with both larger technical and reputational risk.

For instance, a decentralized exchange (DEX) might have numerous features, from simple swaps to complex order routing and liquidity management. Cramming all these features into a single contract can exceed the size limit, forcing developers to find alternative solutions.

Figure 5: As the proxied contract’s features expand, it approaches the 24 KB size limit, necessitating an alternative approach to manage code complexity.

The Facet Pattern Solution

The Facet Pattern solves the code size limitation by allowing you to create modular smart contracts. Instead of a single, monolithic contract, you develop multiple contracts that each handle specific functions. This modular approach enables dynamic routing of users to different "facet contracts" based on the function called in the entrypoint contract.

For example, one facet could handle user authorization, another could manage token transfers, and a third could oversee loan issuance. In addition to providing smaller slices of functionality, facets can also be used to restrict users from performing specific requests as well as "translate" allowed requests into requests suitable for integration points with your protocol.

Using a facet pattern allows you to add or remove functionality by adding or replacing individual facets without altering the entire contract system, giving you empowerment and flexibility over your code.

While this is the most cognitively complex solution architecturally, this modularity provides a flexible and scalable solution to managing complex web3 applications.

Figure 6: The facet pattern routes users to specialized contracts based on functionality, allowing for modular updates and avoiding the size limitations of monolithic contracts.

But What About the Promises of Immutability?

At this point, you might be asking "But what about all the promises of immutability on the blockchain?" By leveraging proxies, aren't you essentially side-stepping immutability and eroding user trust in your contracts? Couldn't you just switch out implementations at any point in time and cause malicious or haphazard issues. If you implement proxies irresponsibly, the answer is yes. However, there are existing smart contract solutions that help restore that trust to get the best of both the immutability of smart contracts and the flexibility of proxy solutions. Some examples of things you can include are:

  1. Timelock'ing any proxy upgrade methods: This will allow users to see any attempts to upgrade your implementation and review the upgrades before they happen. Upgrades will only happen after the timelock elapses.
  2. Multisignature admin accounts: By requiring more than one signer to complete upgrades, you ensure users that a single rogue, haphazard, or compromised account can't unexpectedly trigger upgrades.
  3. Use well established proxy libraries. OpenZeppelin for example has published an Upgrades Plugin and proxy interfaces that have been battle-tested and provide something familiar to the web3 community.
  4. Lastly, just be open and transparent with your users about upgrades in off-chain communication. By transparently communicating upgrades, users will have assurance you aren't trying to sneak upgrades by them.

Conclusion

Building a sophisticated web3 financial platform involves more than just connecting users to smart contracts. It requires a well-thought-out architecture that addresses immutability, complexity, scalability, and code size limitations. Even after you’ve deployed your desired proxy architecture, monitoring and managing the platform's real-time events is crucial for security and efficiency. It’s one thing to deploy one of the above architectures, but it’s another to ensure it’s functioning as-expected and that it isn’t being exploited.

That's where Ledger Works ("LWorks") comes in. Our RiskOps as a Service platform gives you a sense of security and control over your platform, by capturing all on-chain events then enriching that data to provide you with real-time insights, ML financial models, security alerts, and more.

DeFi platforms like DeltaPrime benefit from LWorks’ comprehensive monitoring, observability, and advanced computational algorithms, to ensure they can manage the people, protocol, and market risks that can occur on their ecosystems effectively.

By developing advanced architectural patterns and leveraging leading-edge, next-gen risk management fintech, you can build robust, scalable, and secure web3 financial platforms that meet the evolving needs of the decentralized finance landscape.

To learn more about Ledger Works and how we can help you manage risk on your protocol, feel free to contact our team.

References

Aerial road photo from Unsplash