|
pragma solidity >= 0.8.7; |
|
|
|
// HiddenCalldataProxy reads the version that identifies the implementation contract from 4 bytes at the |
|
// end of the calldata |
|
contract HiddenCalldataProxy { |
|
mapping (uint32 => address) public implementationForVersion; |
|
|
|
constructor() { |
|
// set up a couple of dummy test target contracts that have a function that acts slightly differently |
|
// depending on its constructor param |
|
implementationForVersion[1] = address(new TestTarget(1)); |
|
implementationForVersion[2] = address(new TestTarget(2)); |
|
} |
|
|
|
// Testing: send 0xaeae3802[version] to call 'emitNumber()'. Eg. 0xaeae380200000002 |
|
fallback (bytes calldata _calldata) external payable returns (bytes memory) { |
|
// Using as little assembly as possible for clarity, can be optimized with more assembly |
|
uint32 version; |
|
|
|
assembly { |
|
// last 4 bytes (uint32) of calldata are assigned to version |
|
version := shr(224, calldataload(sub(calldatasize(), 4))) |
|
} |
|
|
|
address implementation = implementationForVersion[version]; |
|
require(implementation != address(0), "unset version"); |
|
|
|
(bool ok, bytes memory returndata) = implementation.delegatecall(_calldata); |
|
require(ok, "delegatecall failed"); |
|
|
|
return returndata; |
|
} |
|
} |
|
|
|
contract TestTarget { |
|
// since number is immutable, the number inputted in the constructor is added to the contract code |
|
uint256 immutable internal number; |
|
|
|
event Number(uint256 number); |
|
|
|
constructor(uint256 _number) { |
|
number = _number; |
|
} |
|
|
|
// 0xaeae3802 |
|
function emitNumber() external { |
|
emit Number(number); |
|
} |
|
|
|
fallback () external { |
|
revert(); // needed to allow testing in remix |
|
} |
|
} |
|
|
|
// Using a HiddenCalldataProxy externally wouldn't require that many changes (the client library would just need to append the 4-byte version) |
|
// however, it breaks the ability to use Solidity's contract call syntax (since it will just properly ABI-encode the call without allowing to modify) |
|
// IMO, this alone could be a deal breaker for using this method since it will make contract-level integrations cumbersome. |
|
// Anyway, I prototyped an example of a wrapper library that could be used to make syntax nicer (see 'testTarget.emitNumberWithVersion(version)') |
|
library TestTargetWrapper { |
|
function emitNumberWithVersion(TestTarget self, uint32 version) internal { |
|
callWithVersion( |
|
self, |
|
abi.encodeWithSelector(self.emitNumber.selector), |
|
version |
|
); |
|
} |
|
|
|
function callWithVersion(TestTarget self, bytes memory data, uint32 version) internal returns (bytes memory) { |
|
(bool ok, bytes memory returndata) = address(self).call(abi.encodePacked(data, version)); |
|
|
|
if (!ok) { |
|
// forward returned error |
|
assembly { revert(add(returndata, 0x20), mload(returndata)) } |
|
} |
|
|
|
return returndata; |
|
} |
|
} |
|
|
|
// Wrapping it all together in a dummy contract that interacts with the proxy (sets it up in the constructor to ease testing) |
|
contract TestTargetConsumer { |
|
using TestTargetWrapper for TestTarget; |
|
|
|
TestTarget testTarget; |
|
|
|
constructor() { |
|
testTarget = TestTarget(address(new HiddenCalldataProxy())); |
|
} |
|
|
|
function emitNumberOnVersion(uint32 version) external { |
|
testTarget.emitNumberWithVersion(version); |
|
} |
|
} |