Proxy that routes to multiple logic contracts based on param

Hello,

I’m familiar with using a proxy to create an upgradable logic contract. But I’d like to go one step further. I’d like to use a proxy that allows me to route the delegatecall to a logic contract of my choosing based on a parameter that’s passed into the function call. The logic contracts will not be hard coded in the proxy nor will they be limited to just 1. I may have many of them and they will all be written by me.

For example, say I have 2 logic contracts, A and B. I will make a function call to the proxy with the function selector, function parameters, and some piece of information that tells the proxy which logic contract to use (A or B). My goal is to not only allow A and B to be upgradable, but also leave room for the future when I add new logic contracts C and D.

This is type of proxy I’m familiar with using. I feel like what I want to do is only be a small modification away from this example. Has anyone done this or can anyone point me in the right direction for how to do this?

It seems that I would want to add an extra parameter to the function call. Then have the proxy read that extra parameter and use that as its implementation address. Does that sound right? Thing is, in the fallback function, there’s no visible parameter here… I can see in assembly where it gets the parameters via calldatacopy. Maybe I should just use msg.data to extract the implementation address from the call parameters?

/**
* @dev Fallback function allowing to perform a delegatecall to the given implementation.
* This function will return whatever the implementation call returns
*/
function () payable external 
{
    address _impl = implementation();
    require(_impl != address(0));
    
    assembly 
    {
        let ptr := mload(0x40)
        calldatacopy(ptr, 0, calldatasize)
        let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
        let size := returndatasize
        returndatacopy(ptr, 0, size)
        
        switch result
        case 0 { revert(ptr, size) }
        default { return(ptr, size) }
    }
}

Thanks

Edit: I should note, I’m very familiar with writing inline assembly in the C language from back in the day But I haven’t ever written much assembly in Solidity and I’m not sure of the best way to write, test, and debug assembly in solidity.

2 Likes

Using msg.data seems to work. See code below. It extracts the first address passed into a function call. This should work in the fallback function.

    bytes memory data = msg.data;
    address parsed;
    // We want to ignore the function hash, so 32 + 4 = 36
    assembly {parsed := mload(add(data, 36))}

Then I can set implementation from this address I extract. But then it would be nice if I could remove that address from the arguments before I pass it to the logic contract. Logic contract does not need an argument with the address of itself... So how would I do that?

So say my arguments passed into the proxy contract are :

0x + functionHash + logicContractAddress + logicContractParams

I would want the proxy to read only the logicContractAddress, then remove it and pass only the following to the logic contract:

0x + functionHash + logicContractParams

Modify the calldatacopy part?

calldatacopy(t, f, s) - F copy s bytes from calldata at position f to mem at position t

I could update the f position to offset and ignore the first address parameter. But that would also remove the function hash and break the call! So I'd need to somehow prepend the function hash back to the arguments.... Not sure how to do that.

2 Likes

Hi @Asynchronous,

Welcome to the community :wave:

I haven’t done any assembly in Ethereum, so hopefully someone in the community may be able to help with this discussion.

Is there a reason that you can’t use OpenZeppelin upgradeable contracts and just have a single logic contract? See Upgrading Smart Contracts Learn guide. I would suggest looking at this first to see if it meets your needs.

I would be very cautious about rolling your own upgrade mechanism, and recommend any such mechanism you create be appropriately tested and audited. (OpenZeppelin perform security audits: https://openzeppelin.com/security-audits/)

@Nick_Mudge has been working on some alternative upgrade mechanisms, including https://github.com/ethereum/EIPs/issues/2535 that you could look at if you are going down that path.

1 Like

Wow, the Diamond Standard looks like exactly what I want! I will read up on that more.

I’m currently using upgradable contacts but I’ve gone down a road of needing a shared state between multiple upgradable logic contracts along with some sort of routing mechanism. Which appears to be the exact goal of the Diamond Standard!

Correct. I’m aware of audit risks.

1 Like

Hi @Asynchronous,

Glad to help.

I would get in contact with @Nick_Mudge to find out how far the EIP has got and see if it has been used in production and/or what audits it has had.

2 Likes

Yes, you are exactly right about the goal of the Diamond Standard! Let me know of you have any questions!

@Asynchronous I tweeted your comment: https://twitter.com/mudgen/status/1246987999436517379
Got to get the word out.

Thanks @abcoathup for helping @Asynchronous.

1 Like

The Diamond Standard is awesome. Nice work. It’s incredibly easy to use and does exactly what it claims. I’ll be up and running with this very quickly.

2 Likes

Awesome! So happy to read that!

1 Like