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
30 changes: 30 additions & 0 deletions crates/forge/assets/tempo/MailTemplate.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script} from "forge-std/Script.sol";
import {ITIP20} from "tempo-std/interfaces/ITIP20.sol";
import {ITIP20RolesAuth} from "tempo-std/interfaces/ITIP20RolesAuth.sol";
import {StdPrecompiles} from "tempo-std/StdPrecompiles.sol";
import {StdTokens} from "tempo-std/StdTokens.sol";
import {Mail} from "../src/Mail.sol";

contract MailScript is Script {
function setUp() public {}

function run() public {
vm.startBroadcast();

StdPrecompiles.TIP_FEE_MANAGER.setUserToken(StdTokens.ALPHA_USD_ADDRESS);

ITIP20 token =
ITIP20(StdPrecompiles.TIP20_FACTORY.createToken("testUSD", "tUSD", "USD", StdTokens.PATH_USD, msg.sender));

ITIP20RolesAuth(address(token)).grantRole(token.ISSUER_ROLE(), msg.sender);

token.mint(msg.sender, 1_000_000 * 10 ** token.decimals());

new Mail(token);

vm.stopBroadcast();
}
}
24 changes: 24 additions & 0 deletions crates/forge/assets/tempo/MailTemplate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {ITIP20} from "tempo-std/interfaces/ITIP20.sol";

contract Mail {
event MailSent(address indexed from, address indexed to, string message, Attachment attachment);

struct Attachment {
uint256 amount;
bytes32 memo;
}

ITIP20 public token;

constructor(ITIP20 token_) {
token = token_;
}

function sendMail(address to, string memory message, Attachment memory attachment) external {
token.transferFromWithMemo(msg.sender, to, attachment.amount, attachment.memo);
emit MailSent(msg.sender, to, message, attachment);
}
}
62 changes: 62 additions & 0 deletions crates/forge/assets/tempo/MailTemplate.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import {ITIP20} from "tempo-std/interfaces/ITIP20.sol";
import {ITIP20RolesAuth} from "tempo-std/interfaces/ITIP20RolesAuth.sol";
import {StdPrecompiles} from "tempo-std/StdPrecompiles.sol";
import {StdTokens} from "tempo-std/StdTokens.sol";
import {Mail} from "../src/Mail.sol";

contract MailTest is Test {
ITIP20 public token;
Mail public mail;

address public constant ALICE = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266);
address public constant BOB = address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8);

function setUp() public {
StdPrecompiles.TIP_FEE_MANAGER.setUserToken(StdTokens.ALPHA_USD_ADDRESS);

token = ITIP20(
StdPrecompiles.TIP20_FACTORY.createToken("testUSD", "tUSD", "USD", StdTokens.PATH_USD, address(this))
);

ITIP20RolesAuth(address(token)).grantRole(token.ISSUER_ROLE(), address(this));

mail = new Mail(token);
}

function test_SendMail() public {
token.mint(ALICE, 100_000 * 10 ** token.decimals());

Mail.Attachment memory attachment =
Mail.Attachment({amount: 100 * 10 ** token.decimals(), memo: "Invoice #1234"});

vm.prank(ALICE);
token.approve(address(mail), attachment.amount);

vm.prank(ALICE);
mail.sendMail(BOB, "Hello Alice, this is a unit test mail.", attachment);

assertEq(token.balanceOf(BOB), attachment.amount);
assertEq(token.balanceOf(ALICE), 100_000 * 10 ** token.decimals() - attachment.amount);
}

function testFuzz_SendMail(uint128 mintAmount, uint128 sendAmount, string memory message, bytes32 memo) public {
mintAmount = uint128(bound(mintAmount, 0, type(uint128).max));
sendAmount = uint128(bound(sendAmount, 0, mintAmount));

token.mint(ALICE, mintAmount);

Mail.Attachment memory attachment = Mail.Attachment({amount: sendAmount, memo: memo});

vm.startPrank(ALICE);
token.approve(address(mail), sendAmount);
mail.sendMail(BOB, message, attachment);
vm.stopPrank();

assertEq(token.balanceOf(BOB), sendAmount);
assertEq(token.balanceOf(ALICE), mintAmount - sendAmount);
}
}
57 changes: 57 additions & 0 deletions crates/forge/assets/tempo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
## Tempo Foundry

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

Tempo's fork of 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.

## Documentation

https://book.getfoundry.sh/

## Usage

### Build

```shell
$ forge build
```

### Test

```shell
$ forge test
```

### Format

```shell
$ forge fmt
```

### Gas Snapshots

```shell
$ forge snapshot
```

### Deploy

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

### Cast

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

### Help

```shell
$ forge --help
$ cast --help
```
42 changes: 42 additions & 0 deletions crates/forge/assets/tempo/workflowTemplate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI

permissions: {}

on:
push:
pull_request:
workflow_dispatch:

env:
FOUNDRY_PROFILE: ci

jobs:
check:
name: Tempo Foundry project
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
network: tempo

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

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

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

- name: Run Forge tests
run: forge test -vvv
62 changes: 58 additions & 4 deletions crates/forge/src/cmd/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ use foundry_config::Config;
use std::path::{Path, PathBuf};
use yansi::Paint;

/// Supported networks for `forge init --network <NETWORK>`
#[derive(Clone, Debug, clap::ValueEnum)]
pub enum Networks {
Tempo,
}

/// CLI arguments for `forge init`.
#[derive(Clone, Debug, Default, Parser)]
pub struct InitArgs {
Expand Down Expand Up @@ -41,6 +47,10 @@ pub struct InitArgs {
#[arg(long, conflicts_with = "template")]
pub vyper: bool,

/// Initialize a project template for the specified network in Foundry.
#[arg(long, short, conflicts_with_all = &["vyper", "template"])]
pub network: Option<Networks>,

/// Use the parent git repository instead of initializing a new one.
/// Only valid if the target is in a git repository.
#[arg(long, conflicts_with = "template")]
Expand All @@ -66,10 +76,13 @@ impl InitArgs {
vscode,
use_parent_git,
vyper,
network,
empty,
} = self;
let DependencyInstallOpts { shallow, no_git, commit } = install;

let tempo = matches!(network, Some(Networks::Tempo));

// create the root dir if it does not exist
if !root.exists() {
fs::create_dir_all(&root)?;
Expand Down Expand Up @@ -170,6 +183,24 @@ impl InitArgs {
contract_path,
include_str!("../../assets/vyper/CounterTemplate.s.sol"),
)?;
} else if tempo {
// write the contract file
let contract_path = src.join("Mail.sol");
fs::write(contract_path, include_str!("../../assets/tempo/MailTemplate.sol"))?;

// write the tests
let contract_path = test.join("Mail.t.sol");
fs::write(
contract_path,
include_str!("../../assets/tempo/MailTemplate.t.sol"),
)?;

// write the script
let contract_path = script.join("Mail.s.sol");
fs::write(
contract_path,
include_str!("../../assets/tempo/MailTemplate.s.sol"),
)?;
} else {
// write the contract file
let contract_path = src.join("Counter.sol");
Expand All @@ -194,9 +225,13 @@ impl InitArgs {
}
}

// Write the default README file
// Write the README file
let readme_path = root.join("README.md");
fs::write(readme_path, include_str!("../../assets/README.md"))?;
if tempo {
fs::write(readme_path, include_str!("../../assets/tempo/README.md"))?;
} else {
fs::write(readme_path, include_str!("../../assets/README.md"))?;
}

// write foundry.toml, if it doesn't exist already
let dest = root.join(Config::FILE_NAME);
Expand All @@ -208,7 +243,7 @@ impl InitArgs {

// set up the repo
if !no_git {
init_git_repo(git, commit, use_parent_git, vyper)?;
init_git_repo(git, commit, use_parent_git, vyper, tempo)?;
}

// install forge-std
Expand All @@ -220,6 +255,17 @@ impl InitArgs {
let dep = "https://github.com/foundry-rs/forge-std".parse()?;
self.install.install(&mut config, vec![dep]).await?;
}

// install tempo-std
if tempo {
if root.join("lib/tempo-std").exists() {
sh_warn!("\"lib/tempo-std\" already exists, skipping install...")?;
self.install.install(&mut config, vec![]).await?;
} else {
let dep = "https://github.com/tempoxyz/tempo-std".parse()?;
self.install.install(&mut config, vec![dep]).await?;
}
}
}

// init vscode settings
Expand All @@ -239,7 +285,13 @@ impl InitArgs {
/// Creates `.gitignore` and `.github/workflows/test.yml`, if they don't exist already.
///
/// Commits everything in `root` if `commit` is true.
fn init_git_repo(git: Git<'_>, commit: bool, use_parent_git: bool, vyper: bool) -> Result<()> {
fn init_git_repo(
git: Git<'_>,
commit: bool,
use_parent_git: bool,
vyper: bool,
tempo: bool,
) -> Result<()> {
// `git init`
if !git.is_in_repo()? || (!use_parent_git && !git.is_repo_root()?) {
git.init()?;
Expand All @@ -258,6 +310,8 @@ fn init_git_repo(git: Git<'_>, commit: bool, use_parent_git: bool, vyper: bool)

if vyper {
fs::write(workflow, include_str!("../../assets/vyper/workflowTemplate.yml"))?;
} else if tempo {
fs::write(workflow, include_str!("../../assets/tempo/workflowTemplate.yml"))?;
} else {
fs::write(workflow, include_str!("../../assets/solidity/workflowTemplate.yml"))?;
}
Expand Down
34 changes: 34 additions & 0 deletions crates/forge/tests/cli/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,40 @@ Installing forge-std in [..] (url: https://github.com/foundry-rs/forge-std, tag:
assert!(prj.root().join(".github").join("workflows").join("test.yml").exists());
});

// checks that `forge init --network tempo` works.
forgetest!(can_init_tempo_project, |prj, cmd| {
prj.wipe();

cmd.args(["init", "--network", "tempo"]).arg(prj.root()).assert_success().stdout_eq(str![[
r#"
Initializing [..]...
Installing forge-std in [..] (url: https://github.com/foundry-rs/forge-std, tag: None)
Installed forge-std[..]
Installing tempo-std in [..] (url: https://github.com/tempoxyz/tempo-std, tag: None)
Installed tempo-std[..]
Initialized forge project

"#
]]);

assert!(prj.root().join("foundry.toml").exists());
assert!(prj.root().join("lib/forge-std").exists());

assert!(prj.root().join("src").exists());
assert!(prj.root().join("src").join("Mail.sol").exists());

assert!(prj.root().join("test").exists());
assert!(prj.root().join("test").join("Mail.t.sol").exists());

assert!(prj.root().join("script").exists());
assert!(prj.root().join("script").join("Mail.s.sol").exists());

assert!(prj.root().join(".github").join("workflows").exists());
assert!(prj.root().join(".github").join("workflows").join("test.yml").exists());

assert!(prj.root().join("README.md").exists());
});

// checks that clone works with raw src containing `node_modules`
// <https://github.com/foundry-rs/foundry/issues/10115>
forgetest!(can_clone_with_node_modules, |prj, cmd| {
Expand Down
Loading