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
47 changes: 47 additions & 0 deletions .github/workflows/contracts_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Contracts CI

on:
push:
paths:
- 'contracts/**'
pull_request:
paths:
- 'contracts/**'
workflow_dispatch:

env:
FOUNDRY_PROFILE: ci

jobs:
check:
name: Foundry project
runs-on: ubuntu-latest
defaults:
run:
working-directory: contracts
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Show Forge version
run: |
forge --version

- name: Run Forge fmt
run: |
forge fmt --check
id: fmt

- name: Run Forge build
run: |
forge build --sizes
id: build

- name: Run Forge tests
run: |
forge test -vvv
id: test
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "contracts/lib/forge-std"]
path = contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
14 changes: 14 additions & 0 deletions contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env
66 changes: 66 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## Foundry

**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**

Foundry consists of:

- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.

## Documentation

https://book.getfoundry.sh/

## Usage

### Build

```shell
$ forge build
```

### Test

```shell
$ forge test
```

### Format

```shell
$ forge fmt
```

### Gas Snapshots

```shell
$ forge snapshot
```

### Anvil

```shell
$ anvil
```

### Deploy

```shell
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
```

### Cast

```shell
$ cast <subcommand>
```

### Help

```shell
$ forge --help
$ anvil --help
$ cast --help
```
6 changes: 6 additions & 0 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions contracts/lib/forge-std
Submodule forge-std added at 8e4051
109 changes: 109 additions & 0 deletions contracts/src/FeeVault.sol

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice work, this makes sense to me. Looks like exactly what we described when talking about it 👍🏻

Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IHypNativeMinter {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can also consider moving this impl to this repo or somewhere else as we spoke about.

I'm happy to do that in a following PR tho if we decide that. Let's wait and see

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just want to point out that this implementation is maintained here rn https://github.com/celestiaorg/hyperlane-ops/tree/main/solidity

function transferRemote(uint32 _destination, bytes32 _recipient, uint256 _amount)
external
payable
returns (bytes32 messageId);
}

contract FeeVault {
IHypNativeMinter public immutable hypNativeMinter;

address public owner;
uint32 public destinationDomain;
bytes32 public recipientAddress;
uint256 public minimumAmount;
uint256 public callFee;

// Split accounting
address public otherRecipient;
uint256 public bridgeShareBps; // Basis points (0-10000) for bridge share

event SentToCelestia(uint256 amount, bytes32 recipient, bytes32 messageId);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event RecipientUpdated(uint32 destinationDomain, bytes32 recipientAddress);
event MinimumAmountUpdated(uint256 minimumAmount);
event CallFeeUpdated(uint256 callFee);
event BridgeShareUpdated(uint256 bridgeShareBps);
event OtherRecipientUpdated(address otherRecipient);
event FundsSplit(uint256 totalNew, uint256 bridgeAmount, uint256 otherAmount);

modifier onlyOwner() {
require(msg.sender == owner, "FeeVault: caller is not the owner");
_;
}

constructor(address _hypNativeMinter, address _owner) {
hypNativeMinter = IHypNativeMinter(_hypNativeMinter);
owner = _owner;
bridgeShareBps = 10000; // Default to 100% bridge
emit OwnershipTransferred(address(0), _owner);
}

receive() external payable {}

function sendToCelestia() external payable {
require(msg.value >= callFee, "FeeVault: insufficient fee");

uint256 currentBalance = address(this).balance;

// Calculate split
uint256 bridgeAmount = (currentBalance * bridgeShareBps) / 10000;
uint256 otherAmount = currentBalance - bridgeAmount;

require(bridgeAmount >= minimumAmount, "FeeVault: minimum amount not met");

emit FundsSplit(currentBalance, bridgeAmount, otherAmount);

// Send other amount if any
if (otherAmount > 0) {
require(otherRecipient != address(0), "FeeVault: other recipient not set");
(bool success,) = otherRecipient.call{value: otherAmount}("");
require(success, "FeeVault: transfer failed");
}

// Bridge the bridge amount
bytes32 messageId =
hypNativeMinter.transferRemote{value: bridgeAmount}(destinationDomain, recipientAddress, bridgeAmount);

emit SentToCelestia(bridgeAmount, recipientAddress, messageId);
}

// Admin functions

function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "FeeVault: new owner is the zero address");
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}

function setRecipient(uint32 _destinationDomain, bytes32 _recipientAddress) external onlyOwner {
destinationDomain = _destinationDomain;
recipientAddress = _recipientAddress;
emit RecipientUpdated(_destinationDomain, _recipientAddress);
}

function setMinimumAmount(uint256 _minimumAmount) external onlyOwner {
minimumAmount = _minimumAmount;
emit MinimumAmountUpdated(_minimumAmount);
}

function setCallFee(uint256 _callFee) external onlyOwner {
callFee = _callFee;
emit CallFeeUpdated(_callFee);
}

function setBridgeShare(uint256 _bridgeShareBps) external onlyOwner {
require(_bridgeShareBps <= 10000, "FeeVault: invalid bps");
bridgeShareBps = _bridgeShareBps;
emit BridgeShareUpdated(_bridgeShareBps);
}

function setOtherRecipient(address _otherRecipient) external onlyOwner {
require(_otherRecipient != address(0), "FeeVault: zero address");
otherRecipient = _otherRecipient;
emit OtherRecipientUpdated(_otherRecipient);
}
}
Loading