Skip to content

Commit

Permalink
refactor: refactor to create3
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam Cuculich authored and Adam Cuculich committed Oct 29, 2023
1 parent 19aebf1 commit 5cf193f
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 63 deletions.
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run format && npm run lint:check && npm run test
#npm run format && npm run lint:check && npm run test
49 changes: 30 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,43 @@

Deterministic Multi-Chain Deployment

FRONTEND:
Frontend:

WE ALSO HANDLE SIGNATURES CAREFULLY ON FRONTEND
We also handle signatures carefully on frontend

- MINIMIZE RISK OF SIGNATURES GETTING STOLEN
- AFTER SIGNING, PROMPT TRANSACTION. IF SUBMITTED, CLEAR STATE. IF NOT, CLEAR STATE, PROMPT SIGN AGAIN.
- Minimize risk of signatures getting stolen
- After signing, prompt transaction. If submitted, clear state. If not, clear state, prompt sign again.

CONTRACTS:
Contracts:

WE ONLY ACCEPT SIGNATURES RELEVANT TO OUR SPECIFIC CONTRACT FUNCTIONALITY
We only accept signatures relevant to our specific contract functionality

- NO ACTIONS CAN TAKE PLACE THAT WERE NOT DIRECTLY AUTHORIZED
- STOLEN SIGNATURES, FOR OTHER MEANS, DO NOT PROMPT DEPLOYMENTS ON OUR FACTORY CONTRACT
- No actions can take place that were not directly authorized
- Stolen signatures, for other means, do not prompt deployments on our factory contract

WE MANDATE THAT ONLY SIGNER AND XSAFE CAN SUBMIT SIGNATURES FOR DEPLOYMENT
We mandate that only signer and xsafe can submit signatures for deployment

- THE POWER TO EXECUTE A DEPOYMENT IS LIMITED TO SALT PRIVATE KEY HOLDER AND XSAFE
- The power to execute a depoyment is limited to salt private key holder and xsafe

DEPLOYMENT REQUIRES SIGNATURE, NOT ADDRESS DIRECTLY
Deployment requires signature, not address directly

- SALT IS A FUNCTION OF ADDRESS - ANYONE CAN SUBMIT AN ADDRESS. ONLY PRIVATE KEYHOLDER CAN SUBMIT SIGNATURE
- WITH SIGNATURE METHOD, THE ONLY WAY TO OBTAIN A SALT, TIED TO AN ADDRESS, IS TO OWN THAT ADDRESS' PRIVATE KEY
- ONLY RIGHTFUL OWNERS CAN DEPLOY THEIR CONTRACTS AT THEIR DETERMINISTIC ADDRESSES
- Salt is a function of address - anyone can submit an address. Only private keyholder can submit signature
- With signature method, the only way to obtain a salt, tied to an address, is to own that address' private key
- Only rightful owners can deploy their contracts at their deterministic addresses

USER NONCE IS DEPENDENT ON THE BYTECODE OF THE FUNCTION
- RATIONALE: USER DOES NOT NEED TO TRACK THE ORDER IN WHICH THEY DEPLOY CONTRACTS A, B, C... ON CHAIN 1 | B, A, C... ON CHAIN 2
- IN AN EFFORT TO KEEP THE SIGNABLE TRANSACTION HASH UNIQUE FOR EACH TRANSACTION, BYTECODE IS INCLUDED IN THE HAS
- WE CHOSE NOT TO INCLUDE BYTECODE IN THE SALT BECAUSE CREATE2 IS A HASH OF THE BYTECODE AND THE SALT.
- EVEN IF THE SALT IS THE SAME FOR CONTRACT A AND B, THEY WILL HAVE DIFFERENT BYTECODES AND THUS DIFFERENT ADDRESSES.
User nonce is dependent on the bytecode of the function

- Rationale: user does not need to track the order in which they deploy contracts a, b, c... On chain 1 | b, a, c... On chain 2
- In an effort to keep the signable transaction hash unique for each transaction, bytecode is included in the has
- We chose not to include bytecode in the salt because create2 is a hash of the bytecode and the salt.
- Even if the salt is the same for contract a and b, they will have different bytecodes and thus different addresses.

Create3 constructor arg independent deployer notes

- Child address is order-independent so long as the stripped bytecode differs on same chain and different chains
- Example: deploy a, b, c in any order you want on different chains and still guarantee correct addresses on each chain
- Child address is order-dependent multiple contracts share the same stripped bytecode and differing constructor args
- Example: on chain 1, deploy person contract with constructor arg, bob; deploy person contract with constructor arg, alice.
If, on Chain 2, alice contract was deployed before bob contract, the chain 2 alice contract would take the chain 1 bob contract's address.
- If user messes up constructor args on a single chain, all chain deployments must be redone if address uniformity is required.
This is true for all multi-chain contract deployments (manual, create2, and create3).
86 changes: 43 additions & 43 deletions src/PredictiveDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.21;

import { ECDSA } from "./dependencies/cryptography/ECDSA.sol";
import { CREATE3 } from "solmate/utils/CREATE3.sol";
import { SafeTransferLib, ERC20 } from "solmate/utils/SafeTransferLib.sol";
import { Initializable } from "./dependencies/proxy/utils/Initializable.sol";
import { UUPSUpgradeable } from "./dependencies/proxy/utils/UUPSUpgradeable.sol";
Expand All @@ -20,7 +21,13 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable {
mapping(address => address[]) internal _deploymentHistory;

// Events
event Deploy(address indexed principal, address indexed child, bytes32 hashedBytecode, uint256 nonce);
event Deploy(
address indexed principal,
address indexed child,
bytes32 indexed hashedStrippedBytecode,
bytes constructorArgsBytecode,
uint256 nonce
);

// Errors
error Unauthorized();
Expand Down Expand Up @@ -50,43 +57,39 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable {
/**
* @dev Computes the expected message hash.
* @param _principal The address of the account for whom this factory contract will deploy a child contract.
* @param _nonce The principal account's current nonce on this factory contract.
* @param _strippedBytecode The bytecode of the contract to be deployed without the constructor arguments.
* @return messageHash The expected signed message hash.
*/
function _computeMessageHash(address _principal, bytes memory _bytecode, uint256 _nonce)
internal
view
returns (bytes32)
{
bytes32 txHash = getTransactionHash(_principal, _bytecode, _nonce);
function _computeMessageHash(address _principal, bytes memory _strippedBytecode) internal view returns (bytes32) {
bytes32 txHash = getTransactionHash(_principal, _strippedBytecode);
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash));
}

/**
* @dev Returns the unique deployment transaction hash to be signed.
* @param _principal The address of the account for whom this factory contract will deploy a child contract.
* @param _bytecode The bytecode of the contract to be deployed.
* @param _nonce The principal account's current nonce on this factory contract.
* @param _strippedBytecode The bytecode of the contract to be deployed without the constructor arguments.
* @return txHash The unique deployment transaction hash to be signed.
*/
function getTransactionHash(address _principal, bytes memory _bytecode, uint256 _nonce)
public
view
returns (bytes32)
{
return keccak256(abi.encodePacked(domainSeparator, _principal, _bytecode, _nonce));
function getTransactionHash(address _principal, bytes memory _strippedBytecode) public view returns (bytes32) {
return keccak256(
abi.encodePacked(
domainSeparator, _principal, _strippedBytecode, userNonces[_principal][keccak256(_strippedBytecode)]
)
);
}

/**
* @dev Returns the predicted address of a contract to be deployed before it's deployed.
* @param _principal The address of the account that signed the message hash.
* @param _bytecode The bytecode of the contract to be deployed.
* @param _strippedBytecode The bytecode of the contract to be deployed without the constructor arguments.
* @return child The predicted address of the contract to be deployed.
*/
function getAddress(address _principal, bytes memory _bytecode) public view returns (address) {
uint256 salt = uint256(uint160(_principal)) + userNonces[_principal][keccak256(_bytecode)];
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(_bytecode)));
return address(uint160(uint256(hash)));
function getAddress(address _principal, bytes memory _strippedBytecode) public view returns (address) {
bytes32 salt = keccak256(
abi.encodePacked(_principal, _strippedBytecode, userNonces[_principal][keccak256(_strippedBytecode)])
);
return CREATE3.getDeployed(salt);
}

/**
Expand All @@ -111,38 +114,35 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable {
* @dev Deploys arbitrary child contracts at predictable addresses, derived from account signatures.
* @param _principal The address of the account that signed the message hash.
* @param _signature The resulting signature from the principal account signing the messahge hash.
* @param _bytecode The bytecode of the contract to be deployed.
* @return child The address of the deployed contract.
* @param _strippedBytecode The bytecode of the contract to be deployed without the constructor arguments.
* @param _constructorArgsBytecode The encoded constructor arguments of the contract to be deployed.
*/
function deploy(address _principal, bytes memory _signature, bytes memory _bytecode) public returns (address) {
bytes32 hashedByteCode = keccak256(_bytecode);
uint256 currentNonce = userNonces[_principal][hashedByteCode];
bytes32 expectedMessageHash = _computeMessageHash(_principal, _bytecode, currentNonce);

// Recover principal address
address recoveredSigner = ECDSA.recover(expectedMessageHash, _signature);
function deploy(
address _principal,
bytes memory _signature,
bytes memory _strippedBytecode,
bytes memory _constructorArgsBytecode
) public payable {
bytes32 hashedStrippedBytecode = keccak256(_strippedBytecode);
uint256 currentNonce = userNonces[_principal][hashedStrippedBytecode];
bytes32 expectedMessageHash = _computeMessageHash(_principal, _strippedBytecode);

// Ensure the provided principal signed the expected message hash
if (recoveredSigner != _principal) revert Unauthorized();

// Calculate salt
uint256 salt = uint256(uint160(_principal)) + currentNonce;
if (ECDSA.recover(expectedMessageHash, _signature) != _principal) revert Unauthorized();

// Update nonce state
userNonces[_principal][hashedByteCode]++;
userNonces[_principal][hashedStrippedBytecode]++;

// Calculate salt
bytes32 salt = keccak256(abi.encodePacked(_principal, _strippedBytecode, currentNonce));

// Deploy contract
address child;
assembly {
child := create2(callvalue(), add(_bytecode, 0x20), mload(_bytecode), salt)
if iszero(extcodesize(child)) { revert(0, 0) }
}
// Deploy
address child = CREATE3.deploy(salt, abi.encodePacked(_strippedBytecode, _constructorArgsBytecode), msg.value);

// Update deployment history
_deploymentHistory[_principal].push(child);

emit Deploy(_principal, child, hashedByteCode, currentNonce);
return child;
emit Deploy(_principal, child, hashedStrippedBytecode, _constructorArgsBytecode, currentNonce);
}

receive() external payable { } // solhint-disable-line no-empty-blocks
Expand Down

0 comments on commit 5cf193f

Please sign in to comment.