Skip to content

Commit 779afd8

Browse files
Minimal factory (#288)
* draft generic factory * TWMinimalFactory * add test, script * v3.2.8-1 * salt-hash in minimal factory * generic stateless factory * forge update * add thirdweb attribution --------- Co-authored-by: Krishang <[email protected]>
1 parent 9b5238c commit 779afd8

File tree

7 files changed

+230
-2
lines changed

7 files changed

+230
-2
lines changed

contracts/TWMinimalFactory.sol

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.11;
3+
4+
/// @author thirdweb
5+
6+
// $$\ $$\ $$\ $$\ $$\
7+
// $$ | $$ | \__| $$ | $$ |
8+
// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\
9+
// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\
10+
// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ |
11+
// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ |
12+
// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ |
13+
// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/
14+
15+
import "@openzeppelin/contracts/utils/Address.sol";
16+
import "@openzeppelin/contracts/proxy/Clones.sol";
17+
18+
contract TWMinimalFactory {
19+
/// @dev Deploys a proxy that points to the given implementation.
20+
constructor(
21+
address _implementation,
22+
bytes memory _data,
23+
bytes32 _salt
24+
) payable {
25+
address instance;
26+
bytes32 salthash = keccak256(abi.encodePacked(msg.sender, _salt));
27+
assembly {
28+
let ptr := mload(0x40)
29+
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
30+
mstore(add(ptr, 0x14), shl(0x60, _implementation))
31+
mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
32+
instance := create2(0, ptr, 0x37, salthash)
33+
}
34+
35+
if (_data.length > 0) {
36+
// instance.call{ value: msg.value }(_data);
37+
38+
// solhint-disable-next-line avoid-low-level-calls
39+
(bool success, bytes memory result) = instance.call{ value: msg.value }(_data);
40+
41+
if (!success) {
42+
// Next 5 lines from https://ethereum.stackexchange.com/a/83577
43+
if (result.length < 68) revert("Transaction reverted silently");
44+
assembly {
45+
result := add(result, 0x04)
46+
}
47+
revert(abi.decode(result, (string)));
48+
}
49+
}
50+
}
51+
}

contracts/TWStatelessFactory.sol

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.11;
3+
4+
/// @author thirdweb
5+
6+
// $$\ $$\ $$\ $$\ $$\
7+
// $$ | $$ | \__| $$ | $$ |
8+
// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\
9+
// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\
10+
// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ |
11+
// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ |
12+
// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ |
13+
// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/
14+
15+
import "./extension/interface/IContractFactory.sol";
16+
17+
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
18+
import "@openzeppelin/contracts/utils/Multicall.sol";
19+
import "@openzeppelin/contracts/proxy/Clones.sol";
20+
21+
contract TWStatelessFactory is Multicall, ERC2771Context, IContractFactory {
22+
/// @dev Emitted when a proxy is deployed.
23+
event ProxyDeployed(address indexed implementation, address proxy, address indexed deployer);
24+
25+
constructor(address _trustedForwarder) ERC2771Context(_trustedForwarder) {}
26+
27+
/// @dev Deploys a proxy that points to the given implementation.
28+
function deployProxyByImplementation(
29+
address _implementation,
30+
bytes memory _data,
31+
bytes32 _salt
32+
) public override returns (address deployedProxy) {
33+
bytes32 salthash = keccak256(abi.encodePacked(_msgSender(), _salt));
34+
deployedProxy = Clones.cloneDeterministic(_implementation, salthash);
35+
36+
emit ProxyDeployed(_implementation, deployedProxy, _msgSender());
37+
38+
if (_data.length > 0) {
39+
// slither-disable-next-line unused-return
40+
Address.functionCall(deployedProxy, _data);
41+
}
42+
}
43+
44+
function _msgSender() internal view virtual override returns (address sender) {
45+
return ERC2771Context._msgSender();
46+
}
47+
48+
function _msgData() internal view virtual override returns (bytes calldata) {
49+
return ERC2771Context._msgData();
50+
}
51+
}

docs/TWMinimalFactory.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# TWMinimalFactory
2+
3+
4+
5+
6+
7+
8+
9+
10+
11+
12+

lib/forge-std

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.0;
3+
4+
import "contracts/TWMinimalFactory.sol";
5+
import "contracts/TWProxy.sol";
6+
7+
import "@openzeppelin/contracts/proxy/Clones.sol";
8+
import "@openzeppelin/contracts/utils/Strings.sol";
9+
10+
import "../utils/BaseTest.sol";
11+
12+
contract DummyUpgradeable {
13+
uint256 public number;
14+
15+
constructor() {}
16+
17+
function initialize(uint256 _num) public {
18+
number = _num;
19+
}
20+
}
21+
22+
contract TWNotMinimalFactory {
23+
/// @dev Deploys a proxy that points to the given implementation.
24+
function deployProxyByImplementation(
25+
address _implementation,
26+
bytes memory _data,
27+
bytes32 _salt
28+
) public {
29+
address deployedProxy = Clones.cloneDeterministic(_implementation, _salt);
30+
31+
if (_data.length > 0) {
32+
// slither-disable-next-line unused-return
33+
Address.functionCall(deployedProxy, _data);
34+
}
35+
}
36+
}
37+
38+
contract MinimalFactoryTest is BaseTest {
39+
address internal implementation;
40+
bytes32 internal salt;
41+
bytes internal data;
42+
address admin;
43+
44+
TWNotMinimalFactory notMinimal;
45+
46+
function setUp() public override {
47+
super.setUp();
48+
admin = getActor(5000);
49+
vm.startPrank(admin);
50+
implementation = getContract("TokenERC20");
51+
salt = keccak256("yooo");
52+
data = abi.encodeWithSelector(
53+
TokenERC20.initialize.selector,
54+
admin,
55+
"MinimalToken",
56+
"MT",
57+
"ipfs://notCentralized",
58+
new address[](0),
59+
admin,
60+
admin,
61+
50
62+
);
63+
64+
notMinimal = new TWNotMinimalFactory();
65+
}
66+
67+
// gas: Baseline + 140k
68+
function test_gas_twProxy() public {
69+
new TWProxy(implementation, data);
70+
}
71+
72+
// gas: Baseline + 41.5k
73+
function test_gas_notMinimalFactory() public {
74+
notMinimal.deployProxyByImplementation(implementation, data, salt);
75+
}
76+
77+
// gas: Baseline
78+
function test_gas_minimal() public {
79+
new TWMinimalFactory(implementation, data, salt);
80+
}
81+
82+
function test_verify_deployedProxy() public {
83+
vm.stopPrank();
84+
vm.prank(address(0x123456));
85+
address minimalFactory = address(new TWMinimalFactory(implementation, data, salt));
86+
bytes32 salthash = keccak256(abi.encodePacked(address(0x123456), salt));
87+
address deployedProxy = Clones.predictDeterministicAddress(implementation, salthash, minimalFactory);
88+
89+
bytes32 contractType = TokenERC20(deployedProxy).contractType();
90+
assertEq(contractType, bytes32("TokenERC20"));
91+
}
92+
}

src/test/scripts/getCloneAddress.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const { MerkleTree } = require("merkletreejs");
2+
const keccak256 = require("keccak256");
3+
const { ethers } = require("ethers");
4+
5+
const process = require("process");
6+
7+
let implementationAddress = process.argv[2];
8+
let signer = process.argv[3];
9+
let salthash = process.argv[4];
10+
11+
const cloneBytecode = [
12+
"0x3d602d80600a3d3981f3363d3d373d3d3d363d73",
13+
implementationAddress.replace(/0x/, "").toLowerCase(),
14+
"5af43d82803e903d91602b57fd5bf3",
15+
].join("");
16+
17+
const initCodeHash = ethers.utils.solidityKeccak256(["bytes"], [cloneBytecode]);
18+
19+
const create2Address = ethers.utils.getCreate2Address(signer, salthash, initCodeHash);
20+
21+
process.stdout.write(create2Address);
22+
// process.stdout.write(ethers.utils.defaultAbiCoder.encode(["bytes32"], [create2Address]));

0 commit comments

Comments
 (0)