Proxy using `call` instead of `delegatecall` (no shared storage)


I want to implement a filtering proxy to restrict calls to a logic contract.
It's meant to act kind of like a TX firewall - following certain routing rules to determine which msg.senders can send TX's to which contract functions.

I want to be able to deploy this proxy to already existing smart contracts (including proxied ones).
I don't need to have any shared storage between my filtering proxy and the smart contract it is protecting - just to be able to get a call, decide whether to approve or decline, and if approved forward it to the logic contract and have it run in its context and alter its storage.

As I understand it, this comes down to the difference between the call and delegatecall opcodes.
Does OpenZeppelin have any proxy implementations using call?
Do you know of any example or documentation of something like what I am looking for?

Thank you!

What you are trying to achieve is impossible.

Using a delegatecall means that you are using the code from the implementation and storing data on the proxy (the contract that initiates the delegatecall).

Using a call you are just calling a contract method that anyone can call. Placing a filter in front of it does not work. There is no way to force the usage of the proxy instead of calling the contract directly

You are right.

The scenarios I'm thinking about are:

  1. When deploying a brand new contract, set onlyFirewall modifiers on functions to enforce that only the firewall can call the logic contract.
  2. For upgradable (already existing) contracts this modifier can be added via an upgrade
  3. For non-upgradable (already existing) contracts there can't be any "hard" code enforcement, only "soft" encouragement to use the firewall + monitoring of what percentage of traffic passed through the firewall. This wouldn't be acceptable as a security mechanism but I imagine it might still be useful in certain situations.

Technically speaking - it looks like I've managed to get it working by slightly changing the OZ Proxy contract and its descendants. I would still be happy to hear if anyone has any thoughts about the best way to implement this or certain edge cases I might need to lookout for.

You might still run into problems in scenarios 1 and 2. msg.sender is always gonna be the address of the proxy contract (when call()ing the a firewalled method). Doesn't msg.sender needs to be passed to the proxyed method??

Have you tried

That's a really good point!
I'm sure how to resolve that... do you have any idea?

I know delegatecall is like callcode except it preserves the msg.sender, what I would need is the equivalent of delegatecall for call.

Don't really think that it is possible. You would need to "fake" msg.sender, this would be a massive security hole.

Preserving msg.sender for a call would convert the meaning of msg.sender (in the callee) into tx.origin, which is unsafe to use. This would render a otherwise secure contract insecure

You call it "fake" but I don't think there's any issue with preserving the msg.sender. How is this different than using delegatecall which also preserves msg.sender (other than the technical difference of the storage not being shared)? This architecture would simply render the firewall proxy transparent.

Again, I don't really see the issue. There's no problem with using tx.origin per se, it's just a problem if you misuse it like in the example described in your link - the Wallet contract wants to make sure that the address asking for the transfer is the owner but instead it checks that the address which initiated the transaction is the owner. So if the owner calls a 2nd contract, it can then call the Wallet contract and the Wallet contract would give it owner permissions. As they suggest in the article, changing the require to look at msg.sender instead of tx.origin would fix this. So bottom line my point is that the logic here was wrong, no problem with tx.origin.

Just to reiterate - the point of the firewall proxy is to be a transparent filter. Instead of sending a TX directly to Wallet, you send it through the firewall - if the TX is approved it is forwarded to Wallet which should be oblivious to the fact that the TX went through the firewall.

The are two "components" to account for: execution context (storage, balance, nonce) and call data (msg.* fields). The closest thing to "shared storage" is delegating to different contracts

call uses the callee's context and sets msg.sender to the caller
delegatecall uses the caller context and keeps the original msg.sender

keeping the original msg.sender and using the calle's would make the usage of msg.sender on the callee behave differently (somewhat like tx.origin).

The type of call instruction doesn't exist but might in the future (or something similar). Check IMPERSONATECALL

tx.origin might not be meaningful in the future (according to this post by Vitalik, point 6) and can't be used for authorization. The only real-world use i've seen is tx.origin==msg.sender (which is probably a bad idea). It's more of a footgun than anything else

Well it looks like I've hit a wall with this idea, if there's no way to preserve the msg.sender then the firewall is not really transparent and only specific contracts could make use of it.

Thank you @helio.rosa for all your help and input!