Skip to content
Open
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
63 changes: 53 additions & 10 deletions foundry/script/DeployPlatform.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {DinToken} from "../src/DinToken.sol";
import {DinCoordinator} from "../src/DinCoordinator.sol";
import {DinValidatorStake} from "../src/DinValidatorStake.sol";
import {DINModelRegistry} from "../src/DINModelRegistry.sol";
import {DinTreasury} from "../src/DinTreasury.sol";
import {DinFeeRouter} from "../src/DinFeeRouter.sol";

/// @notice Deploys the four DIN platform contracts behind Transparent Proxies
/// on a local anvil chain, wires them together, and writes
Expand All @@ -33,27 +35,47 @@ contract DeployPlatform is Script {
function run() external {
vm.startBroadcast();

// 1. DinToken — no init args
// 1. DinTreasury — no dependencies
address dinTreasuryProxy = Upgrades.deployTransparentProxy(
"DinTreasury.sol:DinTreasury",
msg.sender,
abi.encodeCall(DinTreasury.initialize, ())
);
console.log("DinTreasury proxy: ", dinTreasuryProxy);

// 2. DinToken — no init args
address dinTokenProxy = Upgrades.deployTransparentProxy(
"DinToken.sol:DinToken",
msg.sender,
abi.encodeCall(DinToken.initialize, ())
);
console.log("DinToken proxy: ", dinTokenProxy);

// 2. DinCoordinator — receives the DinToken proxy address
// 3. DinCoordinator — receives the DinToken proxy address
address dinCoordinatorProxy = Upgrades.deployTransparentProxy(
"DinCoordinator.sol:DinCoordinator",
msg.sender,
abi.encodeCall(DinCoordinator.initialize, (dinTokenProxy))
);
console.log("DinCoordinator proxy: ", dinCoordinatorProxy);

// 3. Wire DinToken → DinCoordinator (one-shot setter)
// 4. Wire DinToken → DinCoordinator (one-shot setter)
DinToken(dinTokenProxy).setCoordinator(dinCoordinatorProxy);
console.log("DinToken coordinator wired");

// 4. DinValidatorStake — receives both token and coordinator proxies
// 5. Wire DinCoordinator → DinTreasury
DinCoordinator(payable(dinCoordinatorProxy)).setTreasury(dinTreasuryProxy);
console.log("DinCoordinator treasury wired");

// 6. DinFeeRouter — receives DinToken and DinTreasury proxies
address dinFeeRouterProxy = Upgrades.deployTransparentProxy(
"DinFeeRouter.sol:DinFeeRouter",
msg.sender,
abi.encodeCall(DinFeeRouter.initialize, (dinTokenProxy, dinTreasuryProxy))
);
console.log("DinFeeRouter proxy: ", dinFeeRouterProxy);

// 7. DinValidatorStake — receives both token and coordinator proxies
address dinValidatorStakeProxy = Upgrades.deployTransparentProxy(
"DinValidatorStake.sol:DinValidatorStake",
msg.sender,
Expand All @@ -64,12 +86,12 @@ contract DeployPlatform is Script {
);
console.log("DinValidatorStake proxy:", dinValidatorStakeProxy);

// 5. Wire DinCoordinator → DinValidatorStake
// 8. Wire DinCoordinator → DinValidatorStake
DinCoordinator(payable(dinCoordinatorProxy))
.updateValidatorStakeContract(dinValidatorStakeProxy);
console.log("DinCoordinator stake contract wired");

// 6. DINModelRegistry — receives the stake proxy
// 9. DINModelRegistry — receives the stake proxy
address dinModelRegistryProxy = Upgrades.deployTransparentProxy(
"DINModelRegistry.sol:DINModelRegistry",
msg.sender,
Expand All @@ -80,33 +102,54 @@ contract DeployPlatform is Script {
);
console.log("DINModelRegistry proxy: ", dinModelRegistryProxy);

// 7. ProxyAdmin — shared across all four proxies; read from ERC1967 slot
// of any proxy (they all share the same admin per OZ TransparentProxy)
// 10. Wire DINModelRegistry → DinToken
DINModelRegistry(dinModelRegistryProxy).setDinToken(dinTokenProxy);
console.log("DINModelRegistry dinToken wired");

// 11. Wire DINModelRegistry → DinFeeRouter
DINModelRegistry(dinModelRegistryProxy).setFeeRouter(dinFeeRouterProxy);
console.log("DINModelRegistry feeRouter wired");

// 12. Authorise DINModelRegistry as a fee source on DinFeeRouter
DinFeeRouter(dinFeeRouterProxy).addFeeSource(dinModelRegistryProxy);
console.log("DINModelRegistry added as fee source");

// 13. Set DIN-denominated fees on DINModelRegistry (do not skip)
// Values: openSource=1 DIN, proprietary=10 DIN, osUpdate=0.1 DIN, propUpdate=1 DIN
DINModelRegistry(dinModelRegistryProxy).setDinFees(1e18, 10e18, 1e17, 1e18);
console.log("DINModelRegistry DIN fees set");

// ProxyAdmin — OZ v5 deploys one ProxyAdmin per proxy; read from any one
address proxyAdmin = Upgrades.getAdminAddress(dinTokenProxy);
console.log("ProxyAdmin: ", proxyAdmin);
console.log("ProxyAdmin (DinToken): ", proxyAdmin);

vm.stopBroadcast();

// 8. Write deployments JSON — same schema as hardhat/deployments/localhost.json
_writeDeployments(
dinTreasuryProxy,
dinTokenProxy,
dinCoordinatorProxy,
dinFeeRouterProxy,
dinValidatorStakeProxy,
dinModelRegistryProxy,
proxyAdmin
);
}

function _writeDeployments(
address dinTreasury,
address dinToken,
address dinCoordinator,
address dinFeeRouter,
address dinValidatorStake,
address dinModelRegistry,
address proxyAdmin
) internal {
string memory json = "deployments";
vm.serializeAddress(json, "dinTreasury", dinTreasury);
vm.serializeAddress(json, "dinToken", dinToken);
vm.serializeAddress(json, "dinCoordinator", dinCoordinator);
vm.serializeAddress(json, "dinFeeRouter", dinFeeRouter);
vm.serializeAddress(json, "dinValidatorStake", dinValidatorStake);
vm.serializeAddress(json, "dinModelRegistry", dinModelRegistry);
string memory finalJson = vm.serializeAddress(
Expand Down
142 changes: 141 additions & 1 deletion foundry/src/DINModelRegistry.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

Expand All @@ -14,10 +16,16 @@ interface IOwnable {
function owner() external view returns (address);
}

interface IDinFeeRouter {
function routeFeeDIN(address payer, uint256 amount) external;
function routeFeeETH(address payer) external payable;
}

/// @title DIN Model Registry
/// @notice Manages model registration requests, manifest updates, and per-model
/// lifecycle controls. Deployed once per network behind a Transparent Proxy.
contract DINModelRegistry is Initializable, OwnableUpgradeable {
using SafeERC20 for IERC20;
error NotModelOwner();
error InvalidModelId();
error InvalidRequestId();
Expand All @@ -35,6 +43,7 @@ contract DINModelRegistry is Initializable, OwnableUpgradeable {
error CoordinatorOwnershipChanged();
error AuditorOwnershipChanged();
error TransferFailed();
error FeeRouterNotSet();

event ModelRegistrationRequested(
uint256 indexed requestId,
Expand Down Expand Up @@ -66,6 +75,14 @@ contract DINModelRegistry is Initializable, OwnableUpgradeable {
);
event FeesWithdrawn(address indexed to, uint256 amount);
event DAOAdminUpdated(address indexed oldAdmin, address indexed newAdmin);
event DinTokenUpdated(address indexed dinToken);
event FeeRouterUpdated(address indexed feeRouter);
event DinFeesUpdated(
uint256 openSource,
uint256 proprietary,
uint256 openSourceUpdate,
uint256 proprietaryUpdate
);

struct Model {
address owner;
Expand Down Expand Up @@ -112,8 +129,18 @@ contract DINModelRegistry is Initializable, OwnableUpgradeable {
mapping(address => uint256) private _modelIdByTaskAuditor;
mapping(uint256 => bool) public modelDisabled;

IERC20 public dinToken;
IDinFeeRouter public feeRouter;
uint256 public openSourceFeeDIN;
uint256 public proprietaryFeeDIN;
uint256 public openSourceUpdateFeeDIN;
uint256 public proprietaryUpdateFeeDIN;
mapping(uint256 => bool) public modelRequestPaidInDIN;
mapping(uint256 => bool) public manifestRequestPaidInDIN;

// Reserved for future state variables at this inheritance level.
uint256[50] private __gap;
// Reduced from [50] by 8: dinToken, feeRouter, 4× DIN fees, 2× payment-flag mappings.
uint256[42] private __gap;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand Down Expand Up @@ -190,6 +217,10 @@ contract DINModelRegistry is Initializable, OwnableUpgradeable {
})
);

if (msg.value > 0 && address(feeRouter) != address(0)) {
feeRouter.routeFeeETH{value: msg.value}(msg.sender);
}

emit ModelRegistrationRequested(requestId, msg.sender);
}

Expand Down Expand Up @@ -288,6 +319,10 @@ contract DINModelRegistry is Initializable, OwnableUpgradeable {
})
);

if (msg.value > 0 && address(feeRouter) != address(0)) {
feeRouter.routeFeeETH{value: msg.value}(msg.sender);
}

emit ManifestUpdateRequested(requestId, modelId);
}

Expand Down Expand Up @@ -467,6 +502,111 @@ contract DINModelRegistry is Initializable, OwnableUpgradeable {
);
}

/// @notice Sets the DIN token address used for DIN-denominated fee paths.
function setDinToken(address dinToken_) external onlyOwner {
if (dinToken_ == address(0)) revert ZeroAddress();
dinToken = IERC20(dinToken_);
emit DinTokenUpdated(dinToken_);
}

/// @notice Sets the fee router used to split and route DIN and ETH fees.
function setFeeRouter(address feeRouter_) external onlyOwner {
if (feeRouter_ == address(0)) revert ZeroAddress();
feeRouter = IDinFeeRouter(feeRouter_);
emit FeeRouterUpdated(feeRouter_);
}

/// @notice Sets all four DIN-denominated fee tiers. Must be called after wiring.
/// Default is 0, which allows free registrations — do not skip this call.
function setDinFees(
uint256 _openSourceFeeDIN,
uint256 _proprietaryFeeDIN,
uint256 _openSourceUpdateFeeDIN,
uint256 _proprietaryUpdateFeeDIN
) external onlyOwner {
openSourceFeeDIN = _openSourceFeeDIN;
proprietaryFeeDIN = _proprietaryFeeDIN;
openSourceUpdateFeeDIN = _openSourceUpdateFeeDIN;
proprietaryUpdateFeeDIN = _proprietaryUpdateFeeDIN;
emit DinFeesUpdated(
_openSourceFeeDIN,
_proprietaryFeeDIN,
_openSourceUpdateFeeDIN,
_proprietaryUpdateFeeDIN
);
}

/// @notice Submits a model registration request paying with DIN.
/// @dev Payer must approve DinFeeRouter (not this contract) for the fee amount
/// before calling, as the router's routeFeeDIN executes the transferFrom.
function requestModelRegistrationDIN(
bytes32 manifestCID,
address taskCoordinator,
address taskAuditor,
bool isOpenSource
) external returns (uint256 requestId) {
if (!dinValidatorStake.isSlasherContract(taskCoordinator))
revert CoordinatorNoLongerSlasher();
if (!dinValidatorStake.isSlasherContract(taskAuditor))
revert AuditorNoLongerSlasher();
if (taskCoordinator == taskAuditor)
revert TaskCoordinatorEqualsTaskAuditor();
if (IOwnable(taskCoordinator).owner() != msg.sender)
revert NotOwnerOfTaskCoordinator();
if (IOwnable(taskAuditor).owner() != msg.sender)
revert NotOwnerOfTaskAuditor();

uint256 requiredFee = isOpenSource ? openSourceFeeDIN : proprietaryFeeDIN;
requestId = modelRequests.length;
modelRequests.push(
ModelRequest({
requester: msg.sender,
isOpenSource: isOpenSource,
manifestCID: manifestCID,
taskCoordinator: taskCoordinator,
taskAuditor: taskAuditor,
feePaid: requiredFee,
processed: false,
approved: false,
createdAt: block.timestamp
})
);
modelRequestPaidInDIN[requestId] = true;
if (requiredFee > 0) feeRouter.routeFeeDIN(msg.sender, requiredFee);
emit ModelRegistrationRequested(requestId, msg.sender);
}

/// @notice Submits a manifest update request paying with DIN.
/// @dev Same DinFeeRouter approval requirement as requestModelRegistrationDIN.
function requestManifestUpdateDIN(
uint256 modelId,
bytes32 newManifestCID
)
external
onlyModelOwner(modelId)
notDisabled(modelId)
returns (uint256 requestId)
{
Model storage m = models[modelId];
uint256 requiredFee = m.isOpenSource
? openSourceUpdateFeeDIN
: proprietaryUpdateFeeDIN;
requestId = manifestRequests.length;
manifestRequests.push(
ManifestUpdateRequest({
modelId: modelId,
newManifestCID: newManifestCID,
requester: msg.sender,
feePaid: requiredFee,
processed: false,
approved: false
})
);
manifestRequestPaidInDIN[requestId] = true;
if (requiredFee > 0) feeRouter.routeFeeDIN(msg.sender, requiredFee);
emit ManifestUpdateRequested(requestId, modelId);
}

/// @notice Transfers the contract's entire ETH balance to the specified address.
/// @param to Destination address for the fee withdrawal.
function withdrawFees(address payable to) external onlyOwner {
Expand Down
20 changes: 17 additions & 3 deletions foundry/src/DinCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ contract DinCoordinator is

uint256 public dinPerEth;

address public treasury;

// Reserved for future state variables at this inheritance level.
uint256[50] private __gap;
// Reduced from [50] by 1: treasury
uint256[49] private __gap;

event EthDepositAndDINminted(
address indexed user,
Expand All @@ -37,11 +40,13 @@ contract DinCoordinator is
event SlasherContractRemoved(address indexed slasher);
event ValidatorStakeContractUpdated(address indexed validatorStakeContract);
event DinPerEthUpdated(uint256 newRate);
event TreasuryUpdated(address indexed treasury);

error InvalidAddress();
error ValidatorStakeContractNotSet();
error ZeroValue();
error TransferFailed();
error TreasuryNotSet();

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand Down Expand Up @@ -70,14 +75,23 @@ contract DinCoordinator is
emit EthDepositAndDINminted(msg.sender, msg.value, mintAmount);
}

/// @notice Withdraws the contract's entire ETH balance to the owner address.
/// @notice Withdraws the contract's entire ETH balance to the treasury address.
/// @dev Reverts if treasury has not been set.
function withdraw() external onlyOwner nonReentrant {
if (treasury == address(0)) revert TreasuryNotSet();
uint256 balance = address(this).balance;
if (balance == 0) return;
(bool success, ) = payable(owner()).call{value: balance}("");
(bool success, ) = payable(treasury).call{value: balance}("");
if (!success) revert TransferFailed();
}

/// @notice Sets the treasury address. Required before withdraw() can be called.
function setTreasury(address treasury_) external onlyOwner {
if (treasury_ == address(0)) revert InvalidAddress();
treasury = treasury_;
emit TreasuryUpdated(treasury_);
}

/// @notice Registers a task contract as an authorised slasher on the
/// validator stake contract.
/// @param slasherContract Address of the task contract to authorise.
Expand Down
Loading