Skip to content

Instantly share code, notes, and snippets.

@izqui
Created August 16, 2021 16:34
Show Gist options
  • Select an option

  • Save izqui/7c0700bd18976fe95bd041af779f6ea7 to your computer and use it in GitHub Desktop.

Select an option

Save izqui/7c0700bd18976fe95bd041af779f6ea7 to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.8.7+commit.e28d00a7.js&optimize=false&runs=200&gist=
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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment