Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
27 changes: 15 additions & 12 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
79 changes: 32 additions & 47 deletions contracts/script/DeployFeeVault.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines 26 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The CREATE2 address prediction and verification check has been removed. This is a crucial safety measure to ensure the contract is deployed at the expected address, protecting against unintended changes in bytecode, constructor arguments, or salt. It's highly recommended to re-add this verification within the deployment script.

        // Compute address before deployment
        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), address(this), salt, initCodeHash))))
        );
        console.log("Predicted FeeVault address:", predicted);

        vm.startBroadcast();

        // Deploy FeeVault with CREATE2
        FeeVault feeVault = new FeeVault{salt: salt}(
            owner,
            destinationDomain,
            recipientAddress,
            minimumAmount,
            callFee,
            bridgeShareBps,
            otherRecipient
        );
        require(address(feeVault) == predicted, "FeeVault: address mismatch");

        vm.stopBroadcast();

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("==================================================");
}
Expand Down
21 changes: 19 additions & 2 deletions contracts/src/FeeVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
23 changes: 11 additions & 12 deletions contracts/test/FeeVault.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is 10k used here?

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 {
Expand Down Expand Up @@ -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);
Expand Down