The ForwardProxy
is useful for permissioned smart contracts systems in environments where EOAs are not available
(i.e.: tests written with ds-test
) and there is a need to emulate different actors interacting with components of
the system.
The ForwardProxy
provides a fallback function that forwards all calls to another contract using the EVM instruction
call
. The success and return data of the call will be returned back to the caller of the proxy.
Notice that this is different from OpenZeppelin's base Proxy
contract, which uses delegatecall
instead.
This largely alleviates the security issues that come with delegatecall
, since the call to the target contract will be
made on its own context, but this code has not been audited and I DO NOT recommend using it in production.
As it currently stands, this contract could be seen as a bare-bones permissionless 1-out-of-∞ multisig that allows interacting with smart contracts.
interface ForwardProxyLike {
function __to() external view returns (address);
function _() external returns (address);
}
__to()
: returns the address of the target contract._()
: updates the address of the target contract.
The methods have these peculiar names for 2 reasons:
- Keep it short, reducing the noise when reading the code.
- Minimize the chances of clashing names, which would prevent the proxy from working properly.
The setter method _()
implements method chaining to make it more ergonomic:
Instead of:
proxy._(target);
Target(proxy).targetMethod();
You can write:
Target(proxy._(target)).targetMethod();
This is specially useful when the proxy needs to be used with multiple targets:
TargetA(proxy._(targetA)).targetAMethod();
TargetB(proxy._(targetB)).targetBMethod();
I'm glad you asked!
ForwardProxy
also forwards any ether
sent through it to the target.
contract PayableTarget {
event A(address who, uint256 wad);
function funcA() public payable returns (address, uint256) {
emit A(msg.sender, msg.value);
return (msg.sender, msg.value);
}
receive() external payable {}
}
PayableTarget payableTarget = new PayableTarget();
(address sender, uint256 value) = PayableTarget(
proxy._(address(payableTarget))
).funcA{value: 20 ether}();
However, it's NOT possible to make plain ether
transfers to a ForwardProxy
:
payable(proxy).transfer(1 ether); // This will REVERT!
ForwardProxy usr1 = new ForwardProxy();
ForwardProxy usr2 = new ForwardProxy();
System system = new System(/* ... */);
system.authorize(address(usr1), 'role-A');
system.authorize(address(usr2), 'role-B');
// "Impersonate" a contract of type `System`
System(
// Set the `system` contract as the target `to` and gets the reference to the proxy address.
usr1._(address(system))
)
// Call a method in the proxy which will be forwarded to the system
.authorizedMethodA();
// Do the same for `usr2`:
System(usr2._(address(system))).authorizedMethodB();
The example above is roughly equivalent to the following using ethers.js
:
const usr1 = new ethers.Wallet('<private key 1>');
const usr2 = new ethers.Wallet('<private key 2>');
const system = new ethers.Contract('<address>', '<abi>');
const tx1 = await system.authorize(address(usr1), 'role-A');
await tx1.wait()
const tx2 = await system.authorize(address(usr2), 'role-B');
await tx2.wait()
system.connect(usr1);
const tx3 = await system.authorizedMethodA();
await tx3.wait();
system.connect(usr2);
const tx4 = await system.authorizedMethodB();
await tx4.wait();