diff --git a/contracts/README.md b/contracts/README.md index 70e91a4..9465e1c 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -33,18 +33,21 @@ The FeeVault uses CREATE2 for deterministic addresses across chains. ### Environment Variables -| Variable | Deploy | Operational | Description | -|----------|--------|-------------|-------------| -| `OWNER` | Required | - | Owner address (can configure the vault) | -| `SALT` | Optional | - | CREATE2 salt (default: `0x0`). Use any bytes32 value | -| `DESTINATION_DOMAIN` | Optional | Required | Hyperlane destination chain ID | -| `RECIPIENT_ADDRESS` | Optional | Required | Recipient on destination chain (bytes32, left-padded) | -| `MINIMUM_AMOUNT` | Optional | Optional | Minimum wei to bridge | -| `CALL_FEE` | Optional | Optional | Fee in wei for calling `sendToCelestia()` | -| `BRIDGE_SHARE_BPS` | Optional | Optional | Basis points to bridge (default: 10000 = 100%) | -| `OTHER_RECIPIENT` | Optional | Required* | Address to receive non-bridged portion | - -*`OTHER_RECIPIENT` is required only if `BRIDGE_SHARE_BPS` < 10000 +All configuration is set via constructor arguments at deploy time: + +| Variable | Required | Description | +|----------|----------|-------------| +| `OWNER` | Yes | Owner address (can configure the vault post-deployment) | +| `SALT` | No | CREATE2 salt (default: `0x0`). Use any bytes32 value | +| `DESTINATION_DOMAIN` | Yes* | Hyperlane destination chain ID | +| `RECIPIENT_ADDRESS` | Yes* | Recipient on destination chain (bytes32, left-padded) | +| `MINIMUM_AMOUNT` | No | Minimum wei to bridge (default: 0) | +| `CALL_FEE` | No | Fee in wei for calling `sendToCelestia()` (default: 0) | +| `BRIDGE_SHARE_BPS` | No | Basis points to bridge (default: 10000 = 100%) | +| `OTHER_RECIPIENT` | No** | Address to receive non-bridged portion | + +*Required for the vault to be operational (can be set to 0 at deploy and configured later via setters) +**Required if `BRIDGE_SHARE_BPS` < 10000 **Note:** `HYP_NATIVE_MINTER` must be set via `setHypNativeMinter()` after deployment for the vault to be operational. diff --git a/contracts/script/DeployFeeVault.s.sol b/contracts/script/DeployFeeVault.s.sol index 6d9b986..54b7e0f 100644 --- a/contracts/script/DeployFeeVault.s.sol +++ b/contracts/script/DeployFeeVault.s.sol @@ -10,81 +10,66 @@ contract DeployFeeVault is Script { address owner = vm.envAddress("OWNER"); bytes32 salt = vm.envOr("SALT", bytes32(0)); - // Optional: Post-deployment configuration uint32 destinationDomain = uint32(vm.envOr("DESTINATION_DOMAIN", uint256(0))); bytes32 recipientAddress = vm.envOr("RECIPIENT_ADDRESS", bytes32(0)); uint256 minimumAmount = vm.envOr("MINIMUM_AMOUNT", uint256(0)); uint256 callFee = vm.envOr("CALL_FEE", uint256(0)); - uint256 bridgeShareBps = vm.envOr("BRIDGE_SHARE_BPS", uint256(10000)); + uint256 bridgeShareBps = vm.envOr("BRIDGE_SHARE_BPS", uint256(0)); // 0 defaults to 10000 in constructor address otherRecipient = vm.envOr("OTHER_RECIPIENT", address(0)); // =================================== - // Compute address before deployment - address predicted = computeAddress(salt, owner); - console.log("Predicted FeeVault address:", predicted); - vm.startBroadcast(); // Deploy FeeVault with CREATE2 - FeeVault feeVault = new FeeVault{salt: salt}(owner); - console.log("FeeVault deployed at:", address(feeVault)); - require(address(feeVault) == predicted, "Address mismatch"); - - // Configure if values provided - if (destinationDomain != 0 && recipientAddress != bytes32(0)) { - feeVault.setRecipient(destinationDomain, recipientAddress); - console.log("Recipient set - domain:", destinationDomain); - } - - if (minimumAmount > 0) { - feeVault.setMinimumAmount(minimumAmount); - console.log("Minimum amount set:", minimumAmount); - } - - if (callFee > 0) { - feeVault.setCallFee(callFee); - console.log("Call fee set:", callFee); - } - - if (bridgeShareBps != 10000) { - feeVault.setBridgeShare(bridgeShareBps); - console.log("Bridge share set:", bridgeShareBps, "bps"); - } - - if (otherRecipient != address(0)) { - feeVault.setOtherRecipient(otherRecipient); - console.log("Other recipient set:", otherRecipient); - } + FeeVault feeVault = new FeeVault{salt: salt}( + owner, destinationDomain, recipientAddress, minimumAmount, callFee, bridgeShareBps, otherRecipient + ); vm.stopBroadcast(); + console.log("FeeVault deployed at:", address(feeVault)); + console.log("Owner:", owner); + console.log("Destination domain:", destinationDomain); + console.log("Minimum amount:", minimumAmount); + console.log("Call fee:", callFee); + console.log("Bridge share bps:", feeVault.bridgeShareBps()); console.log(""); console.log("NOTE: Call setHypNativeMinter() after deploying HypNativeMinter"); } - - /// @notice Compute the CREATE2 address for FeeVault deployment - function computeAddress(bytes32 salt, address owner) public view returns (address) { - bytes32 bytecodeHash = keccak256(abi.encodePacked(type(FeeVault).creationCode, abi.encode(owner))); - return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash))))); - } } -/// @notice Standalone script to compute FeeVault address without deploying +/// @notice Compute FeeVault CREATE2 address off-chain +/// @dev Use this to predict the address before deploying +/// Requires env vars: DEPLOYER (EOA), OWNER, SALT (optional), and all constructor args contract ComputeFeeVaultAddress is Script { function run() external view { - address owner = vm.envAddress("OWNER"); - bytes32 salt = vm.envOr("SALT", bytes32(0)); address deployer = vm.envAddress("DEPLOYER"); + bytes32 salt = vm.envOr("SALT", bytes32(0)); + + address owner = vm.envAddress("OWNER"); + uint32 destinationDomain = uint32(vm.envOr("DESTINATION_DOMAIN", uint256(0))); + bytes32 recipientAddress = vm.envOr("RECIPIENT_ADDRESS", bytes32(0)); + uint256 minimumAmount = vm.envOr("MINIMUM_AMOUNT", uint256(0)); + uint256 callFee = vm.envOr("CALL_FEE", uint256(0)); + uint256 bridgeShareBps = vm.envOr("BRIDGE_SHARE_BPS", uint256(0)); + address otherRecipient = vm.envOr("OTHER_RECIPIENT", address(0)); - bytes32 bytecodeHash = keccak256(abi.encodePacked(type(FeeVault).creationCode, abi.encode(owner))); + bytes32 initCodeHash = keccak256( + abi.encodePacked( + type(FeeVault).creationCode, + abi.encode( + owner, destinationDomain, recipientAddress, minimumAmount, callFee, bridgeShareBps, otherRecipient + ) + ) + ); address predicted = - address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, bytecodeHash))))); + address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, initCodeHash))))); console.log("========== FeeVault Address Computation =========="); + console.log("Deployer (EOA):", deployer); console.log("Owner:", owner); console.log("Salt:", vm.toString(salt)); - console.log("Deployer:", deployer); console.log("Predicted address:", predicted); console.log("=================================================="); } diff --git a/contracts/src/FeeVault.sol b/contracts/src/FeeVault.sol index 296d33d..932538b 100644 --- a/contracts/src/FeeVault.sol +++ b/contracts/src/FeeVault.sol @@ -36,9 +36,26 @@ contract FeeVault { _; } - constructor(address _owner) { + constructor( + address _owner, + uint32 _destinationDomain, + bytes32 _recipientAddress, + uint256 _minimumAmount, + uint256 _callFee, + uint256 _bridgeShareBps, + address _otherRecipient + ) { + require(_owner != address(0), "FeeVault: owner is the zero address"); + require(_bridgeShareBps <= 10000, "FeeVault: invalid bps"); + owner = _owner; - bridgeShareBps = 10000; // Default to 100% bridge + destinationDomain = _destinationDomain; + recipientAddress = _recipientAddress; + minimumAmount = _minimumAmount; + callFee = _callFee; + bridgeShareBps = _bridgeShareBps == 0 ? 10000 : _bridgeShareBps; + otherRecipient = _otherRecipient; + emit OwnershipTransferred(address(0), _owner); } diff --git a/contracts/test/FeeVault.t.sol b/contracts/test/FeeVault.t.sol index 0af644c..dbb4301 100644 --- a/contracts/test/FeeVault.t.sol +++ b/contracts/test/FeeVault.t.sol @@ -35,15 +35,18 @@ contract FeeVaultTest is Test { user = address(0x1); otherRecipient = address(0x99); mockMinter = new MockHypNativeMinter(); - feeVault = new FeeVault(owner); - // Configure contract + feeVault = new FeeVault( + owner, + destination, + recipient, + minAmount, + fee, + 10000, // 100% bridge share + otherRecipient + ); + feeVault.setHypNativeMinter(address(mockMinter)); - feeVault.setRecipient(destination, recipient); - feeVault.setMinimumAmount(minAmount); - feeVault.setCallFee(fee); - feeVault.setOtherRecipient(otherRecipient); - // Default bridge share is 10000 (100%) } function test_Receive() public { @@ -207,11 +210,7 @@ contract FeeVaultTest is Test { function test_SendToCelestia_MinterNotSet() public { // Deploy fresh vault without minter - FeeVault freshVault = new FeeVault(owner); - freshVault.setRecipient(destination, recipient); - freshVault.setMinimumAmount(minAmount); - freshVault.setCallFee(fee); - freshVault.setOtherRecipient(otherRecipient); + FeeVault freshVault = new FeeVault(owner, destination, recipient, minAmount, fee, 10000, otherRecipient); (bool success,) = address(freshVault).call{value: minAmount}(""); require(success);