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.
It can be thought of as a 1-out-of-∞ multisig to interact with other smart contracts.
AuthForwardProxy is an extension of ForwardPorxy that is useful for contracts whose permissioned methods should not
be made completely permissionless by using ForwardProxy.
It can be thought of as a 1-out-of-N multisig to interact with other smart contracts.
Both ForwardProxy and AuthForwardProxy provide 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.
interfaceForwardProxyLike {
function __to() externalviewreturns (address);
function _() externalreturns (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/AuthForwardProxy also forward any ether sent through it to the target.
contractPayableTarget {
event A(addresswho, uint256wad);
function funcA() publicpayablereturns (address, uint256) {
emitA(msg.sender, msg.value);
return (msg.sender, msg.value);
}
receive() externalpayable {}
}
PayableTarget payableTarget =newPayableTarget();
(addresssender, uint256value) =PayableTarget(
proxy._(address(payableTarget))
).funcA{value: 20ether}();However, it's NOT possible to make plain ether transfers to a ForwardProxy/AuthForwardProxy:
payable(proxy).transfer(1 ether); // This will REVERT!ForwardProxy usr1 =newForwardProxy();
ForwardProxy usr2 =newForwardProxy();
System system =newSystem(/* ... */);
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:
constusr1=newethers.Wallet('<private key 1>');constusr2=newethers.Wallet('<private key 2>');constsystem=newethers.Contract('<address>','<abi>');consttx1=awaitsystem.authorize(address(usr1),'role-A');awaittx1.wait()consttx2=awaitsystem.authorize(address(usr2),'role-B');awaittx2.wait()system.connect(usr1);consttx3=awaitsystem.authorizedMethodA();awaittx3.wait();system.connect(usr2);consttx4=awaitsystem.authorizedMethodB();awaittx4.wait();Each address allowed into AuthForwarProxy is a ward. Once an address receives a ward status, it can add or remove
other wards, but it cannot modify the owner. The owner has root access to the contract.
To avoid naming clashes with the target contract, every public function on AuthForwardProxy that is not part of the
ForwardProxyLike interface is appended of its own selector:
interfaceAuthForwardProxyLike {
///@notice Get the owner of the contract.///@dev The selector for `owner()` is `0x8da5cb5b` and so on...function owner_8da5cb5b() externalviewreturns (address);
///@notice Transfer the ownership of the contract.function transferOwnership_f2fde38b(addresswho) external;
///@notice Give `who` the ward role.function rely_65fae35e(addresswho) external;
///@notice Revokes the ward role for `who`.function deny_9c52a7f1(addresswho) external;
///@notice Gets the ward status of `who`.function wards_bf353dbb(addresswho) externalviewreturns (uint256);
}After deploying the contract, the owner can call rely_65fae35e(address) to authorize a different address:
seth send "$AUTH_FORWARD_PROXY_ADDRESS""rely_65fae35e(address)""$WARD_ADDRESS"