Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fa06654
feat: add governance-controlled rewarder v2
dangerousfood Aug 29, 2026
3284eef
feat: add CREATE2 rewarder market factory
dangerousfood Aug 30, 2026
82065c6
refactor: delegate router hooks through governance controller
dangerousfood Aug 30, 2026
e30142c
feat: enforce vested recipient allocation on DEEP mints
dangerousfood Aug 30, 2026
61e1f2b
ci: size-check production contracts only
dangerousfood Aug 30, 2026
90cf4bd
feat: return DEEP administration after issuance term
dangerousfood Aug 30, 2026
d81bfc7
fix: lock token administration for full term
dangerousfood Aug 30, 2026
539f5eb
fix: require minter role for all controller mints
dangerousfood Aug 30, 2026
45df2f3
refactor: name Deepstate V1 controller accurately
dangerousfood Aug 30, 2026
6c77db6
fix: burn retired rewarder balances in place
dangerousfood Aug 30, 2026
929f196
fix: vest recipient allocations for one year
dangerousfood Aug 30, 2026
c26d885
style: format basis points as percentages
dangerousfood Aug 30, 2026
e1303f9
fix: burn each rewarder's live balance
dangerousfood Aug 30, 2026
0f7c263
refactor: rename token administration lifecycle
dangerousfood Aug 30, 2026
c3960a3
fix: make recipient carry thirty percent
dangerousfood Aug 30, 2026
4c6b0bf
refactor: name controller token explicitly
dangerousfood Aug 30, 2026
1cd282f
fix: authorize controller owner to mint
dangerousfood Aug 30, 2026
1d8e5b1
refactor: use Solady roles for mint controller
dangerousfood Aug 30, 2026
8278d18
refactor: clarify factory authority names
dangerousfood Aug 30, 2026
7f8dfcd
fix: allow independent V1 controller ownership
dangerousfood Aug 30, 2026
8b091ab
refactor: encode returned token administration state
dangerousfood Aug 30, 2026
ff13cdc
perf: rely on exact Sablier allowance consumption
dangerousfood Aug 30, 2026
a11186a
refactor: deploy rewarders with CREATE
dangerousfood Aug 30, 2026
cae5b25
refactor: unify controller role authority
dangerousfood Aug 30, 2026
39a336f
refactor: inline factory authority check
dangerousfood Aug 30, 2026
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: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
case "$CHECK" in
quality)
forge fmt --check
forge build --sizes --threads 0
forge build --sizes --threads 0 --skip test
bash test/DeployProductionScript.sh
;;
tests)
Expand Down
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts.git
[submodule "lib/sablier"]
path = lib/sablier
url = https://github.com/sablier-labs/evm-monorepo.git
[submodule "lib/prb-math"]
path = lib/prb-math
url = https://github.com/PaulRBerg/prb-math.git
[submodule "lib/chainlink-contracts"]
path = lib/chainlink-contracts
url = https://github.com/smartcontractkit/chainlink-brownie-contracts.git
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,54 @@ The deployment deliberately does not install a timelock. The Governor remains
the direct owner and executor for the vault, rewarder, and router. Successful
proposals can therefore execute immediately after voting ends.

## Controlled Minting

`DeepstateMinterController` is the operational DEEP minter. It uses Solady
`OwnableRoles`: governance is the owner and may grant the controller's
`MINTER_ROLE` bit only to approved issuance contracts, such as
`DeepstateRewarderFactory`. For every requested mint `M`, the controller mints
`M` as the primary 70% tranche and mints `floor(M * 30 / 70)` to Sablier
Lockup v4.0.1 as the recipient's 30% tranche. Each recipient allocation gets
its own linear one-year stream.
The vesting recipient and Sablier contract are immutable constructor settings;
streams are non-cancelable and their NFTs are non-transferable.

The controller also has an immutable deployment-time live-supply cap. The
intended production value is 20,000,000,000 DEEP. Before every mint, the
controller checks the existing DEEP `totalSupply()` plus both the requested
amount and its corresponding 30% tranche. Burns reduce total supply and reopen
capacity below the cap. This is a controller-level soft cap: governance can
bypass it only by authorizing a different token-level minter after token
administration returns.

The requested address always receives the complete `M`; the recipient amount is
minted in addition so that it represents 30% of the combined issuance. A
factory market therefore receives its complete 100,000,000 DEEP initial funding
while a separate `floor(100,000,000 * 30 / 70)` DEEP stream is created. If a
market is retired, its unspent rewarder balance is burned, but the independent
recipient stream continues vesting.

This policy is enforceable only while `DeepstateMinterController` is the sole
operational holder of `DeepstateToken.MINTER_ROLE`. Governance must not grant
the token-level role directly to the factory or another minter that can bypass
the controller.

For the initial two-year issuance term, the controller temporarily holds
`DeepstateToken.DEFAULT_ADMIN_ROLE` while governance remains the controller's
owner. Governance calls `lockTokenAdministration()` only after granting the
token admin role to the controller. Locking also ensures the controller has the
token minter role. The controller owner may rotate during the term, but
administration cannot be unlocked early. At or after the exact two-year
deadline, anyone may call `unlockTokenAdministration()`. Unlocking grants the
token admin role to the controller's current owner before the controller
renounces it, preserving the token's final-admin invariant throughout the
transition. The controller owner receives
independent mint authority without needing `DeepstateMinterController.MINTER_ROLE`.
Ownership transfers move that owner authority without changing separately
delegated minter roles. Every non-owner mint requires `MINTER_ROLE`.
The controller retains its ordinary token minter role until governance revokes
it after regaining token administration.

## Reward Schedule

The deployment creates one immutable rewarder for NVDA/USDG. Each side starts
Expand Down
1 change: 1 addition & 0 deletions lib/chainlink-contracts
Submodule chainlink-contracts added at 5cb41f
1 change: 1 addition & 0 deletions lib/prb-math
Submodule prb-math added at 280fc5
1 change: 1 addition & 0 deletions lib/sablier
Submodule sablier added at fae38d
4 changes: 4 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
@chainlink/contracts/=lib/chainlink-contracts/contracts/
forge-std/=lib/forge-std/src/
solady/=lib/solady/src/
deepstate-contracts/=lib/deepstate-contracts/src/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
@prb/math/=lib/prb-math/
@sablier/evm-utils/=lib/sablier/utils/
@sablier/lockup/=lib/sablier/lockup/
20 changes: 20 additions & 0 deletions src/DeepstateController.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {OwnableRoles} from "solady/auth/OwnableRoles.sol";

/// @title Deepstate Controller
/// @notice Shared governance ownership and delegated-role authority for protocol controllers.
abstract contract DeepstateController is OwnableRoles {
error InvalidOwner();

constructor(address owner_) {
if (owner_ == address(0)) revert InvalidOwner();
_initializeOwner(owner_);
}

/// @notice Controller ownership cannot be renounced.
function renounceOwnership() public payable virtual override onlyOwner {
revert NewOwnerIsZeroAddress();
}
}
154 changes: 154 additions & 0 deletions src/DeepstateMinterController.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.28;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Lockup} from "@sablier/lockup/src/types/Lockup.sol";
import {LockupLinear} from "@sablier/lockup/src/types/LockupLinear.sol";
import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
import {SafeCastLib} from "solady/utils/SafeCastLib.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";

import {DeepstateToken} from "./DeepstateToken.sol";
import {DeepstateController} from "./DeepstateController.sol";
import {ISablierLockupLinearV4} from "./interfaces/ISablierLockupLinearV4.sol";

/// @title Deepstate Minter Controller
/// @notice Allocates 30% of every authorized DEEP issuance to a vesting recipient.
/// @dev The recipient allocation is placed in a new non-cancelable, non-transferable Sablier
/// Lockup v4 linear stream. This contract temporarily administers DEEP while remaining owned by governance.
contract DeepstateMinterController is DeepstateController, ReentrancyGuard {
using SafeTransferLib for address;

uint256 public constant MINTER_ROLE = 1 << 0;
uint256 public constant RECIPIENT_ALLOCATION_BPS = 30_00;
uint256 public constant PRIMARY_ALLOCATION_BPS = 70_00;
uint40 public constant VESTING_DURATION = 365 days;
uint40 public constant TOKEN_ADMINISTRATION_DURATION = 2 * 365 days;

DeepstateToken public immutable deepstateToken;
ISablierLockupLinearV4 public immutable sablierLockup;
address public immutable recipient;
/// @notice Maximum live DEEP supply this controller will permit after a mint.
uint256 public immutable mintCap;

/// @notice Administration deadline, zero before locking, and uint40 max after permanent return.
uint40 public tokenAdministrationEndsAt;

event MintedWithVesting(
address indexed caller,
address indexed mintRecipient,
uint256 mintAmount,
address indexed vestingRecipient,
uint256 vestingAmount,
uint256 streamId
);
event TokenAdministrationActivated(uint40 indexed endsAt);
event TokenAdministrationReturned(address indexed owner, address indexed caller);

error InvalidDeepstateToken();
error InvalidSablierLockup();
error InvalidRecipient();
error InvalidMintCap();
error InvalidMintRecipient();
error MintAmountTooSmall();
error VestingAmountTooLarge(uint256 amount);
error ControllerNotTokenAdmin();
error TokenAdministrationAlreadyActivated();
error TokenAdministrationAlreadyReturned();
error TokenAdministrationNotActive();
error TokenAdministrationActive(uint40 endsAt);
error MintCapExceeded(uint256 cap, uint256 attemptedSupply);

constructor(address owner_, address deepstateToken_, address sablierLockup_, address recipient_, uint256 mintCap_)
DeepstateController(owner_)
{
if (deepstateToken_ == address(0) || deepstateToken_.code.length == 0) revert InvalidDeepstateToken();
if (sablierLockup_ == address(0) || sablierLockup_.code.length == 0) revert InvalidSablierLockup();
if (recipient_ == address(0)) revert InvalidRecipient();
if (mintCap_ == 0) revert InvalidMintCap();

deepstateToken = DeepstateToken(deepstateToken_);
sablierLockup = ISablierLockupLinearV4(sablierLockup_);
recipient = recipient_;
mintCap = mintCap_;
}

/// @notice Lock DEEP administration in this contract for the initial two-year term.
/// @dev Also ensures this controller holds DEEP's operational minter role.
function lockTokenAdministration() external onlyOwner {
if (tokenAdministrationEndsAt != 0) revert TokenAdministrationAlreadyActivated();

bytes32 tokenAdminRole = deepstateToken.DEFAULT_ADMIN_ROLE();
if (!deepstateToken.hasRole(tokenAdminRole, address(this))) revert ControllerNotTokenAdmin();
if (!deepstateToken.hasRole(deepstateToken.MINTER_ROLE(), address(this))) {
deepstateToken.grantRole(deepstateToken.MINTER_ROLE(), address(this));
}

uint40 endsAt = SafeCastLib.toUint40(block.timestamp + TOKEN_ADMINISTRATION_DURATION);
tokenAdministrationEndsAt = endsAt;
emit TokenAdministrationActivated(endsAt);
}

/// @notice Unlock DEEP administration to this contract's current governance owner after the term expires.
/// @dev Anyone may trigger the unlock at or after the exact deadline.
function unlockTokenAdministration() external {
uint40 endsAt = tokenAdministrationEndsAt;
if (endsAt == 0) revert TokenAdministrationNotActive();
if (endsAt == type(uint40).max) revert TokenAdministrationAlreadyReturned();

address owner_ = owner();
if (block.timestamp < endsAt) revert TokenAdministrationActive(endsAt);

bytes32 tokenAdminRole = deepstateToken.DEFAULT_ADMIN_ROLE();
if (!deepstateToken.hasRole(tokenAdminRole, address(this))) revert ControllerNotTokenAdmin();

tokenAdministrationEndsAt = type(uint40).max;
// Grant first so DeepstateToken's final-admin invariant cannot strand the token.
deepstateToken.grantRole(tokenAdminRole, owner_);
deepstateToken.renounceRole(tokenAdminRole, address(this));

emit TokenAdministrationReturned(owner_, msg.sender);
}

/// @notice Mint the 70% primary tranche `amount` to `to` and the 30% tranche into a one-year stream.
/// @dev The recipient amount is `floor(amount * 30 / 70)`. Amounts that round it to zero revert.
function mint(address to, uint256 amount)
external
onlyOwnerOrRoles(MINTER_ROLE)
nonReentrant
returns (uint256 streamId)
{
if (to == address(0)) revert InvalidMintRecipient();

uint256 vestingAmount = FixedPointMathLib.fullMulDiv(amount, RECIPIENT_ALLOCATION_BPS, PRIMARY_ALLOCATION_BPS);
if (vestingAmount == 0) revert MintAmountTooSmall();
if (vestingAmount > type(uint128).max) revert VestingAmountTooLarge(vestingAmount);
uint128 streamAmount = SafeCastLib.toUint128(vestingAmount);

uint256 mintSupply = amount + vestingAmount;
uint256 attemptedSupply = deepstateToken.totalSupply() + mintSupply;
if (attemptedSupply > mintCap) revert MintCapExceeded(mintCap, attemptedSupply);

deepstateToken.mint(to, amount);
deepstateToken.mint(address(this), vestingAmount);

address(deepstateToken).safeApproveWithRetry(address(sablierLockup), vestingAmount);
streamId = sablierLockup.createWithDurationsLL(
Lockup.CreateWithDurations({
sender: address(this),
recipient: recipient,
depositAmount: streamAmount,
token: IERC20(address(deepstateToken)),
cancelable: false,
transferable: false,
shape: "Deepstate allocation"
}),
LockupLinear.UnlockAmounts({start: 0, cliff: 0}),
0,
LockupLinear.Durations({cliff: 0, total: VESTING_DURATION})
);

emit MintedWithVesting(msg.sender, to, amount, recipient, vestingAmount, streamId);
}
}
Loading