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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ axum = "0.7.9"
seismic-alloy-network = { git = "https://github.com/SeismicSystems/seismic-alloy.git", rev = "c8fad0b93da53f2b423d2f6720abdb4a6cbb9082" }
seismic-alloy-provider = { git = "https://github.com/SeismicSystems/seismic-alloy.git", rev = "c8fad0b93da53f2b423d2f6720abdb4a6cbb9082" }
seismic-alloy-rpc-types = { git = "https://github.com/SeismicSystems/seismic-alloy.git", rev = "c8fad0b93da53f2b423d2f6720abdb4a6cbb9082" }
alloy-consensus = "=1.1.0"
alloy-contract = "=1.1.0"
alloy-chains = "=0.2.17"
alloy-eips = "=1.1.0"
Expand Down
36 changes: 36 additions & 0 deletions contracts/script/DeployBaseTestnetUSDC.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.30;

import {Script, console} from "forge-std/Script.sol";
import {TestnetUSDC} from "../src/TestnetUSDC.sol";

/// @notice Deploys the controlled six-decimal test USDC on Base Sepolia and
/// mints the initial supply into the machine-funding reserve. There is no
/// faucet contract on Base: the reserve key transfers directly, so the
/// reserve holds the tokens and the deployer keeps mint authority.
contract DeployBaseTestnetUSDCScript is Script {
uint256 internal constant DEFAULT_INITIAL_RESERVE_SUPPLY = 1_000_000e6;

function run() external {
uint256 deployerPrivateKey = vm.envUint("BASE_DEPLOYER_PRIVATE_KEY");
address reserveAccount = vm.envAddress("INTERNAL_FUNDING_BASE_ADDRESS");
uint256 initialSupply = vm.envOr("BASE_ERC20_USDC_INITIAL_RESERVE_SUPPLY", DEFAULT_INITIAL_RESERVE_SUPPLY);

address deployer = vm.addr(deployerPrivateKey);
require(reserveAccount != address(0), "Invalid reserve account");
require(initialSupply > 0, "Invalid initial supply");
vm.startBroadcast(deployerPrivateKey);

TestnetUSDC usdc = new TestnetUSDC(deployer);
usdc.mint(reserveAccount, initialSupply);

vm.stopBroadcast();

console.log("=== Base Testnet USDC Deployment ===");
console.log("USDC token:", address(usdc));
console.log("Chain ID:", block.chainid);
console.log("Token owner / minter:", deployer);
console.log("Machine funding reserve:", reserveAccount);
console.log("Initial reserve supply (USDC, 6d):", initialSupply);
}
}
79 changes: 79 additions & 0 deletions deploy/bash_aliases
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ deploy_machine_faucet_server() (
http://127.0.0.1:3002/api/internal/erc20-usdc/readiness
echo
fi
if [ "${INTERNAL_FUNDING_BASE_ENABLED:-false}" = true ]; then
curl --fail --silent --show-error \
-H "Authorization: Bearer $INTERNAL_FUNDING_TOKEN" \
http://127.0.0.1:3002/api/internal/base/readiness
echo
fi
)

deploy_contracts() (
Expand Down Expand Up @@ -282,3 +288,76 @@ deploy_erc20_usdc_contracts() (
echo "ERC20 USDC preflight passed and configuration is enabled in $machine_env"
echo "activate it with: deploy_machine_faucet_server"
)

# Base Sepolia test USDC: a plain six-decimal ERC20 minted into the dedicated
# Base reserve. No faucet contract is deployed on Base — the reserve key
# transfers directly — so only the token address is recorded. The human must
# have funded INTERNAL_FUNDING_BASE_ADDRESS with Base Sepolia ETH first; the
# deployer key pays for the deployment itself. Uses stock foundry (forge/cast)
# because Base is a standard EVM network.
deploy_base_erc20_usdc_contract() (
set -e
_faucet_lock_acquire deploy_base_erc20_usdc_contract
machine_env=$_faucet_repo/.env.machine-funding

_faucet_require_file "$machine_env"
_faucet_require_command jq
_faucet_require_command cast
_faucet_require_command forge

set -a
source "$machine_env"
set +a
_faucet_require_env BASE_RPC_URL
_faucet_require_env BASE_CHAIN_ID
_faucet_require_env BASE_DEPLOYER_PRIVATE_KEY
_faucet_require_env INTERNAL_FUNDING_BASE_ADDRESS
_faucet_require_env INTERNAL_FUNDING_BASE_PRIVATE_KEY
_faucet_require_env INTERNAL_FUNDING_BASE_GAS_ETH_AMOUNT
_faucet_require_env INTERNAL_FUNDING_MAX_BASE_ERC20_USDC_AMOUNT
_faucet_require_env INTERNAL_FUNDING_GLOBAL_BASE_GAS_ETH_BUDGET
_faucet_require_env INTERNAL_FUNDING_GLOBAL_BASE_ERC20_USDC_BUDGET

chain_id=$(cast chain-id --rpc-url "$BASE_RPC_URL")
[ "$chain_id" = "$BASE_CHAIN_ID" ] || {
echo "BASE_RPC_URL reports chain id $chain_id, expected BASE_CHAIN_ID=$BASE_CHAIN_ID" >&2
exit 1
}

_faucet_set_env_value "$machine_env" INTERNAL_FUNDING_BASE_ENABLED false

cd "$_faucet_repo/contracts"
forge script script/DeployBaseTestnetUSDC.s.sol \
--rpc-url "$BASE_RPC_URL" \
--broadcast \
--private-key "$BASE_DEPLOYER_PRIVATE_KEY"

broadcast=$_faucet_repo/contracts/broadcast/DeployBaseTestnetUSDC.s.sol/$chain_id/run-latest.json
_faucet_require_file "$broadcast"
usdc_address=$(jq -er '
[.transactions[]
| select(.contractName == "TestnetUSDC")
| .contractAddress
| select(. != null)]
| last
' "$broadcast")
[[ "$usdc_address" =~ ^0x[0-9a-fA-F]{40}$ ]] || {
echo "invalid Base ERC20 USDC address in $broadcast: $usdc_address" >&2
exit 1
}

_faucet_set_env_value "$machine_env" BASE_ERC20_USDC_TOKEN_ADDRESS "$usdc_address"

cd "$_faucet_repo"
cargo build --release --locked -p faucet-machine-funding
INTERNAL_FUNDING_BASE_ENABLED=true \
BASE_ERC20_USDC_TOKEN_ADDRESS="$usdc_address" \
target/release/faucet-machine-funding --check-base

_faucet_set_env_value "$machine_env" INTERNAL_FUNDING_BASE_ENABLED true

echo "deployed Base Sepolia test USDC: $usdc_address"
echo "Base preflight passed and configuration is enabled in $machine_env"
echo "record BASE_ERC20_USDC_ADDRESS=$usdc_address on the sandbox machine; integrations must read it from there, never hardcode it"
echo "activate it with: deploy_machine_faucet_server"
)
1 change: 1 addition & 0 deletions machine-funding-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ uuid.workspace = true
url.workspace = true

[dev-dependencies]
alloy-consensus = { workspace = true, features = ["k256"] }
http-body-util.workspace = true
tempfile = "3.21.0"
tower = { workspace = true, features = ["util"] }
29 changes: 29 additions & 0 deletions machine-funding-server/RISK_REGISTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,35 @@ actor can consume it before other testers. Likewise, a poisoned queue can keep
legitimate work behind low-value requests. Per-principal quotas and a bounded or
fair queue are separate future controls.

## Base Sepolia Funding

The Base Sepolia routes (`/api/internal/base/gas`, `/api/internal/base/erc20-usdc/transfers`,
`/api/internal/base/readiness`) reuse every existing control: the same nginx
source-IP gate and bearer token, the same Redis reservation, idempotency, and
operator serialization, and per-asset budgets and rate windows. What differs:

- **The signer is the reserve.** There is no faucet contract on Base, so the
reserve key holds both the native ETH gas reserve and the ERC20 USDC supply
and transfers them directly. A stolen Base key drains both balances outright;
the containment is the small, explicitly funded reserve and the dedicated key.
`INTERNAL_FUNDING_BASE_PRIVATE_KEY` is refused when it matches a Seismic
funding key unless `INTERNAL_FUNDING_BASE_ALLOW_SHARED_KEY=true` is set with
explicit approval.
- **Native gas is real value on other networks.** Base Sepolia ETH has no
real-world value, but the same key format on Base mainnet would. The chain id
is verified at connect time and on every readiness probe, and every Base
ledger key is scoped to `chain id + token + reserve`, so a mispointed RPC
fails closed rather than replaying a testnet ledger against another network.
- **Reserve depletion is a diagnostic, not a surprise.** Startup preflight and
`/api/internal/base/readiness` compare both balances to configured floors
(`INTERNAL_FUNDING_BASE_ETH_RESERVE_FLOOR`,
`INTERNAL_FUNDING_BASE_ERC20_USDC_RESERVE_FLOOR`); readiness answers
`503 reserve_low` below either floor so an alert fires before a drip fails
on-chain with `insufficient funds`, which is otherwise a terminal `422`.
- **EIP-1559 fees are estimated, not capped.** The estimate is doubled for
headroom without a configured ceiling, the same deferred control as the
Seismic gas price.

## Deferred Hardening

### Staging Hardening
Expand Down
Loading
Loading