Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: update UserNonces #6

Merged
merged 3 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ WE MANDATE THAT ONLY SIGNER AND XSAFE CAN SUBMIT SIGNATURES FOR DEPLOYMENT

DEPLOYMENT REQUIRES SIGNATURE, NOT ADDRESS DIRECTLY

- SALT IS A FUNCTION OF ADDRESS - ANY ONE CAN SUBMIT AN ADDRESS. ONLY PRIVATE KEY HOLDER CAN SUBMIT SIGNATURE
- 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.
File renamed without changes.
40 changes: 31 additions & 9 deletions src/PredictiveDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable {
bytes32 internal domainSeparator;

// Factory Storage
mapping(address => uint256) public userNonces;
//mapping(address => uint256) public userNonces;
cucupac marked this conversation as resolved.
Show resolved Hide resolved
mapping(address => mapping(bytes32 => uint256)) public userNonces;
0xernesto marked this conversation as resolved.
Show resolved Hide resolved
mapping(address => address[]) public deploymentHistory;

// Events
event Deploy(address indexed principal, address indexed child, bytes32 hashedBytecode, uint256 nonce);
Expand Down Expand Up @@ -52,8 +54,12 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable {
* @param _nonce The principal account's current nonce on this factory contract.
* @return messageHash The expected signed message hash.
*/
function _computeMessageHash(address _principal, uint256 _nonce) internal view returns (bytes32) {
bytes32 txHash = getTransactionHash(_principal, _nonce);
function _computeMessageHash(address _principal, bytes memory _bytecode, uint256 _nonce)
internal
view
returns (bytes32)
{
bytes32 txHash = getTransactionHash(_principal, _bytecode, _nonce);
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash));
}

Expand All @@ -63,8 +69,12 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable {
* @param _nonce The principal account's current nonce on this factory contract.
* @return txHash The unique deployment transaction hash to be signed.
*/
cucupac marked this conversation as resolved.
Show resolved Hide resolved
function getTransactionHash(address _principal, uint256 _nonce) public view returns (bytes32) {
return keccak256(abi.encodePacked(domainSeparator, _principal, _nonce));
function getTransactionHash(address _principal, bytes memory _bytecode, uint256 _nonce)
public
view
returns (bytes32)
{
return keccak256(abi.encodePacked(domainSeparator, _principal, _bytecode, _nonce));
}

/**
Expand All @@ -74,11 +84,20 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable {
* @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];
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)));
}

/**
* @dev Returns the keccak256 hash of the provided bytecode.
* @param _bytecode The bytecode of the contract to be deployed.
rahul0eth marked this conversation as resolved.
Show resolved Hide resolved
* @return hash The keccak256 hash of the provided bytecode.
*/
function getBytecodeHash(bytes memory _bytecode) public pure returns (bytes32) {
return keccak256(_bytecode);
}

/**
* @dev Deploys arbitrary child contracts at predictable addresses, derived from account signatures.
* @param _principal The address of the account that signed the message hash.
Expand All @@ -87,8 +106,8 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable {
* @return child The address of the deployed contract.
*/
function deploy(address _principal, bytes memory _signature, bytes memory _bytecode) public returns (address) {
cucupac marked this conversation as resolved.
Show resolved Hide resolved
uint256 currentNonce = userNonces[_principal];
bytes32 expectedMessageHash = _computeMessageHash(_principal, currentNonce);
uint256 currentNonce = userNonces[_principal][keccak256(_bytecode)];
bytes32 expectedMessageHash = _computeMessageHash(_principal, _bytecode, currentNonce);

// Recover principal address
address recoveredSigner = ECDSA.recover(expectedMessageHash, _signature);
Expand All @@ -100,7 +119,7 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable {
uint256 salt = uint256(uint160(_principal)) + currentNonce;

// Update nonce state
userNonces[_principal]++;
userNonces[_principal][keccak256(_bytecode)]++;

// Deploy contract
address child;
Expand All @@ -109,6 +128,9 @@ contract PredictiveDeployer is Initializable, UUPSUpgradeable, Ownable {
if iszero(extcodesize(child)) { revert(0, 0) }
}

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

emit Deploy(_principal, child, keccak256(_bytecode), currentNonce);
return child;
}
Expand Down
7 changes: 5 additions & 2 deletions src/interfaces/IPredictiveDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
pragma solidity ^0.8.21;

interface IPredictiveDeployer {
function userNonces(address _principal) external returns (uint256);
function getTransactionHash(address _principal, uint256 _nonce) external returns (bytes32);
function userNonces(address _principal, bytes32 _hashedBytecode) external returns (uint256);
function getTransactionHash(address _principal, bytes memory _bytecode, uint256 _nonce)
external
returns (bytes32);
function getBytecodeHash(bytes memory _bytecode) external pure returns (bytes32);
function getAddress(address _principal, bytes memory _bytecode) external returns (address);
function deploy(address _principal, bytes memory _signature, bytes memory _bytecode) external returns (address);
}
88 changes: 77 additions & 11 deletions test/PredictiveDeployer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ contract PredictiveDeployerTest is Test {
ERC1967Proxy public proxy;
Child public child;
bytes public childBytecode;
bytes32 public hashedChildBytecode;

// Events
event Deploy(address indexed sender, address indexed child, bytes32 hashedBytecode, uint256 nonce);
Expand All @@ -29,16 +30,18 @@ contract PredictiveDeployerTest is Test {
// Get Bytecode
bytes memory bytecode = type(Child).creationCode;
childBytecode = abi.encodePacked(bytecode, abi.encode(address(this)));
hashedChildBytecode = keccak256(childBytecode);
}

function testFuzz_GetAddress(uint256 pkNum, address sender) public {
// Setup
VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum)))));

uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr);
uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode);

// Get signature information
bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce);
bytes32 txHash =
IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce);

bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash));

Expand Down Expand Up @@ -66,10 +69,11 @@ contract PredictiveDeployerTest is Test {
// Setup
VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum)))));

uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr);
uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode);

// Get signature information
bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce);
bytes32 txHash =
IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce);

bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash));

Expand All @@ -90,14 +94,42 @@ contract PredictiveDeployerTest is Test {
assertEq(actualChild, expectedChild);
}

function test_CannotDeployReplay(uint256 pkNum) public {
// helper function
function deployChild(VmSafe.Wallet memory wallet, bytes memory bytecode, bytes32 hashedBytecode)
cucupac marked this conversation as resolved.
Show resolved Hide resolved
internal
returns (address)
{
uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedBytecode);
bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, bytecode, currentNonce);
bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash));
(uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, messageHash);
bytes memory signature = abi.encodePacked(r, s, v);
return IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, bytecode);
}

function testFuzz_DeployTwice(uint256 pkNum) public {
// Setup
VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum)))));

uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr);
// First Deployment
address actualChild1 = deployChild(wallet, childBytecode, hashedChildBytecode);

// Second Deployment
address actualChild2 = deployChild(wallet, childBytecode, hashedChildBytecode);

// Assertions
require(actualChild1 != actualChild2, "Addresses should not be equal");
}

function testFuzz_CannotDeployReplay(uint256 pkNum) public {
// Setup
VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum)))));

uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode);

// Get signature information
bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce);
bytes32 txHash =
IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce);

bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash));

Expand All @@ -112,17 +144,18 @@ contract PredictiveDeployerTest is Test {
IPredictiveDeployer(address(proxy)).deploy(wallet.addr, signature, childBytecode);
}

function test_CannotDeployWithoutApproval(uint256 pkNum, address invalidPrincipal) public {
function testFuzz_CannotDeployWithoutApproval(uint256 pkNum, address invalidPrincipal) public {
// Setup
VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum)))));

// Failing condition: principal is not the signer
vm.assume(invalidPrincipal != wallet.addr);

uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr);
uint256 currentNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode);

// Get signature information
bytes32 txHash = IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, currentNonce);
bytes32 txHash =
IPredictiveDeployer(address(proxy)).getTransactionHash(wallet.addr, childBytecode, currentNonce);

bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", txHash));

Expand All @@ -134,7 +167,40 @@ contract PredictiveDeployerTest is Test {
IPredictiveDeployer(address(proxy)).deploy(invalidPrincipal, signature, childBytecode);
}

function test_Receive(uint256 transferAmount) public {
function testFuzz_DeployAndUpdateNonce(uint256 pkNum) public {
cucupac marked this conversation as resolved.
Show resolved Hide resolved
// Setup
VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(pkNum)))));

// Get initial nonce
uint256 initialNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode);

// Deploy
address actualChild = deployChild(wallet, childBytecode, hashedChildBytecode);

// Get updated nonce
uint256 updatedNonce = IPredictiveDeployer(address(proxy)).userNonces(wallet.addr, hashedChildBytecode);

// Assertions
require(actualChild != address(0), "Deployment failed");
require(updatedNonce == initialNonce + 1, "Nonce did not update correctly");
}

function testFuzz_getBytecodeHash(bytes memory _childBytecode) public {
if (_childBytecode.length == 0) {
return; // Skip the test for empty bytecode
}

// Act
bytes32 actualHash = IPredictiveDeployer(address(proxy)).getBytecodeHash(_childBytecode);

// Expected
bytes32 expectedHash = keccak256(_childBytecode);

// Assertions
assertEq(actualHash, expectedHash, "Hash mismatch");
}

function testFuzz_Receive(uint256 transferAmount) public {
// Assumptions
vm.assume(transferAmount != 0 && transferAmount <= 1000 ether);

Expand Down
Loading