@plutolabs/submarine
v1.0.1
Published
Recurring 1 DOT fee engine (Submarine) and Treasury reference implementation for PolkaVM-compatible chains.
Maintainers
Readme
Submarine
Submarine is a smart contract that helps developers earn money through a subscription-based model. If the deployed smart contract owns tokens, each month a part is sent to the creator of the contract
Installation
Foundry (recommended)
forge install plutolabs/SubmarineThis makes the contracts importable via:
import {Submarine} from "Submarine/src/Submarine.sol";
import {Treasury} from "Submarine/src/Treasury.sol";NPM package
npm install @plutolabs/submarineThen point your Solidity import path (via remappings.txt or your build tool) to node_modules/@plutolabs/submarine.
Contract functionality
Submarine.sol (fee engine)
- Constructor args
feeCollector: recipient of every 1 DOT payment (non-zero address enforced).feeIntervalInBlocks: block spacing between payments (e.g. ~30-day cadence).
- State
lastPaymentBlock: starts at 0 and is updated every time a fee is paid.nextPaymentBlock(): convenience getter returninglastPaymentBlock + interval.
- Core flow
_submarineHook()/runSubmarine()attempt a payout.- If the contract balance is below 1 DOT or the interval hasn’t elapsed, the hook exits quietly.
- Once both conditions are satisfied, exactly 1 DOT is transferred to
feeCollectorand an event is emitted.
- Safety guards
- Zero-address collectors and zero intervals are rejected at construction.
- Transfers revert if the low-level call fails, preventing silent fund loss.
Treasury.sol (DOT vault + fee engine)
- Owner-only vault that inherits
Submarine. - Public entry points
deposit()&receive(): accept DOT, log aDepositedevent, and invoke the Submarine hook.withdraw(address,uint256): owner can send DOT to any address (non-zero) with balance checks; hook executes after the transfer.tickSubmarine(): owner helper to manually run the fee hook without moving funds.runSubmarine(): inherited public trigger so any keeper can enforce the schedule.
- Views
treasuryBalance()returns current holdings.lastPaymentBlock()/nextPaymentBlock()inherited fromSubmarinereveal timing.
Activation behavior
The first time the Treasury balance reaches at least 1 DOT, the next run of _submarineHook() sets lastPaymentBlock and waits the configured interval. Every subsequent deposit/withdrawal automatically attempts a payout, so there is no need for external cron jobs unless you want guaranteed on-time payments via runSubmarine().
Quick usage
- Deploy
Treasurysupplying the fee collector and the desired block interval. - Fund the contract with at least 1 DOT. The Submarine engine activates as soon as the balance crosses the threshold, but the first payout only occurs after the interval has elapsed.
- Call
runSubmarine()(or let regular deposits/withdrawals trigger_submarineHook()) every interval to release the 1 DOT fee, provided enough DOT is available. - Use
withdrawto move any remaining DOT as the owner.
Example: integrating Submarine in your own contract
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Submarine} from "./src/Submarine.sol";
contract CreatorVault is Submarine {
address public owner;
constructor(address feeCollector, uint64 feeInterval) Submarine(feeCollector, feeInterval) {
owner = msg.sender;
}
receive() external payable {
_submarineHook(); // attempt to pay the fee every time funds arrive
}
function withdraw(uint256 amount) external {
require(msg.sender == owner, "not owner");
payable(owner).transfer(amount);
_submarineHook();
}
}Deployment & usage walkthrough:
- Deploy
CreatorVault(or the providedTreasury) with your DOT creator wallet asfeeCollectorand choose a block interval (e.g., 216_000 blocks ≈ 30 days). - Fund the contract with user deposits. As soon as it holds ≥ 1 DOT, the next
_submarineHook()run schedules the fee window. - Either:
- Let normal user interactions trigger
_submarineHook()implicitly, or - Run
runSubmarine()from any keeper/cron job right after each interval to guarantee payment timing.
- Let normal user interactions trigger
- Track payouts using the emitted
SubmarineFeePaidevents.
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://paritytech.github.io/foundry-book-polkadot/
Commands
Build
$ forge buildTest
$ forge testFormat
$ forge fmtGas Snapshots
$ forge snapshotAnvil
$ anvilDeploy Treasury
forge create src/Treasury.sol:Treasury \
--rpc-url <INSERT_RPC_URL> \
--private-key <INSERT_PRIVATE_KEY> \
--constructor-args <FEE_COLLECTOR_ADDRESS> <FEE_INTERVAL_BLOCKS> \
--resolcUse your preferred block interval (e.g. 216000 ≈ 30 days at 6s block time). After deployment, send at least 1 DOT to the Treasury to activate Submarine and optionally schedule external keepers to call runSubmarine() each interval.
Cast
$ cast <subcommand>Help
$ forge --help
$ anvil --help
$ cast --help