feat: Introduce MINTER_ROLE for minting control in CompliantFiatToken

- Added MINTER_ROLE constant to manage minting permissions.
- Updated mint function to restrict access to addresses with MINTER_ROLE, enhancing security and compliance.
- Granted MINTER_ROLE to the initial owner during contract deployment.
This commit is contained in:
defiQUG
2026-03-02 14:22:35 -08:00
parent af4152ac14
commit d36a8947b2
14 changed files with 1290 additions and 1 deletions

View File

@@ -0,0 +1,8 @@
# DBIS Rail — Build notes
- **Contracts:** All DBIS Rail contracts (RootRegistry, ParticipantRegistry, SignerRegistry, SettlementRouter, GRU_MintController, StablecoinReferenceRegistry, Conversion Router) are in this folder and implement Technical Spec v1 and v1.5 add-ons.
- **Tests:** `test/dbis/DBIS_Rail.t.sol` covers submitMintAuth success, replay revert, and signer-revoked-at-block.
- **Build:** With default Foundry config (`via_ir = true`, `optimizer_runs = 200`) the compiler may report a Yul stack-too-deep error. If so:
- Try `FOUNDRY_PROFILE=lite forge test --match-path "test/dbis/*.t.sol"` (note: lite uses `via_ir = false`, which can cause Solidity “Stack too deep” in other units).
- Or reduce complexity in the heaviest functions (e.g. further split `submitMintAuth` / `submitSwapAuth` or reduce locals in SignerRegistry) until the default profile builds.
- **Deploy:** Run `DeployDBISRail.s.sol` on Chain 138; then set GRU token on MintController, grant MINTER_ROLE on c* tokens to MintController, register stablecoins, and add venues/quote issuers as needed.

View File

@@ -0,0 +1,196 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./DBIS_RootRegistry.sol";
import "./DBIS_SignerRegistry.sol";
import "./StablecoinReferenceRegistry.sol";
interface IBlocklist {
function isBlocked(address account) external view returns (bool);
}
struct SwapAuth {
bytes32 messageId;
bytes32 lpaId;
bytes32 venue;
address tokenIn;
address tokenOut;
uint256 amountIn;
uint256 minAmountOut;
uint256 deadline;
bytes32 quoteHash;
address quoteIssuer;
uint256 chainId;
address verifyingContract;
}
contract DBIS_ConversionRouter is AccessControl, Pausable, ReentrancyGuard {
bytes32 public constant ROUTER_ADMIN_ROLE = keccak256("ROUTER_ADMIN");
uint256 public constant CHAIN_ID = 138;
bytes32 public constant SIGNER_REGISTRY_KEY = keccak256("SignerRegistry");
bytes32 public constant STABLECOIN_REGISTRY_KEY = keccak256("StablecoinReferenceRegistry");
bytes32 private constant EIP712_DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 private constant SWAPAUTH_TYPEHASH = keccak256(
"SwapAuth(bytes32 messageId,bytes32 lpaId,bytes32 venue,address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 deadline,bytes32 quoteHash,address quoteIssuer,uint256 chainId,address verifyingContract)"
);
DBIS_RootRegistry public rootRegistry;
mapping(bytes32 => bool) public usedSwapMessageIds;
mapping(bytes32 => bool) public venueAllowlist;
mapping(address => bool) public quoteIssuerAllowlist;
address public blocklistContract;
event ConversionExecuted(
bytes32 indexed messageId,
bytes32 indexed quoteHash,
bytes32 venue,
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 amountOut,
address quoteIssuer
);
constructor(address admin, address _rootRegistry) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(ROUTER_ADMIN_ROLE, admin);
rootRegistry = DBIS_RootRegistry(_rootRegistry);
}
function addVenue(bytes32 venue) external onlyRole(ROUTER_ADMIN_ROLE) {
venueAllowlist[venue] = true;
}
function removeVenue(bytes32 venue) external onlyRole(ROUTER_ADMIN_ROLE) {
venueAllowlist[venue] = false;
}
function addQuoteIssuer(address issuer) external onlyRole(ROUTER_ADMIN_ROLE) {
quoteIssuerAllowlist[issuer] = true;
}
function removeQuoteIssuer(address issuer) external onlyRole(ROUTER_ADMIN_ROLE) {
quoteIssuerAllowlist[issuer] = false;
}
function setBlocklist(address _blocklist) external onlyRole(ROUTER_ADMIN_ROLE) {
blocklistContract = _blocklist;
}
function pause() external onlyRole(ROUTER_ADMIN_ROLE) {
_pause();
}
function unpause() external onlyRole(ROUTER_ADMIN_ROLE) {
_unpause();
}
function _domainSeparator() private view returns (bytes32) {
return keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256("DBISConversionRouter"),
keccak256("1"),
CHAIN_ID,
address(this)
)
);
}
function _hashSwapAuth(SwapAuth calldata auth) private pure returns (bytes32) {
return keccak256(
abi.encode(
SWAPAUTH_TYPEHASH,
auth.messageId,
auth.lpaId,
auth.venue,
auth.tokenIn,
auth.tokenOut,
auth.amountIn,
auth.minAmountOut,
auth.deadline,
auth.quoteHash,
auth.quoteIssuer,
auth.chainId,
auth.verifyingContract
)
);
}
function getSwapAuthDigest(SwapAuth calldata auth) external view returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), _hashSwapAuth(auth)));
}
function submitSwapAuth(SwapAuth calldata auth, bytes[] calldata signatures, uint256 amountOut) external nonReentrant whenNotPaused {
require(auth.chainId == CHAIN_ID, "DBIS: wrong chain");
require(auth.verifyingContract == address(this), "DBIS: wrong contract");
require(block.timestamp <= auth.deadline, "DBIS: expired");
require(!usedSwapMessageIds[auth.messageId], "DBIS: replay");
require(venueAllowlist[auth.venue], "DBIS: venue not allowed");
require(quoteIssuerAllowlist[auth.quoteIssuer], "DBIS: quote issuer not allowed");
require(amountOut >= auth.minAmountOut, "DBIS: slippage");
_requireStablecoinActive(auth.tokenOut);
_requireNotBlocked();
address[] memory signers = _recoverSwapSigners(auth, signatures);
DBIS_SignerRegistry signerReg = DBIS_SignerRegistry(rootRegistry.getComponent(SIGNER_REGISTRY_KEY));
(bool ok, ) = signerReg.validateSignersForSwap(signers, auth.amountIn);
require(ok, "DBIS: quorum not met");
usedSwapMessageIds[auth.messageId] = true;
emit ConversionExecuted(
auth.messageId,
auth.quoteHash,
auth.venue,
auth.tokenIn,
auth.tokenOut,
auth.amountIn,
amountOut,
auth.quoteIssuer
);
}
function _requireStablecoinActive(address tokenOut) private view {
StablecoinReferenceRegistry stableReg = StablecoinReferenceRegistry(rootRegistry.getComponent(STABLECOIN_REGISTRY_KEY));
if (address(stableReg) != address(0)) require(stableReg.isActive(tokenOut), "DBIS: tokenOut not active");
}
function _requireNotBlocked() private view {
if (blocklistContract != address(0)) require(!IBlocklist(blocklistContract).isBlocked(msg.sender), "DBIS: blocked");
}
function _recoverSwapSigners(SwapAuth calldata auth, bytes[] calldata signatures) private view returns (address[] memory signers) {
DBIS_SignerRegistry signerReg = DBIS_SignerRegistry(rootRegistry.getComponent(SIGNER_REGISTRY_KEY));
require(address(signerReg) != address(0), "DBIS: no signer registry");
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), _hashSwapAuth(auth)));
signers = new address[](signatures.length);
for (uint256 i = 0; i < signatures.length; i++) {
require(signatures[i].length == 65, "DBIS: bad sig len");
address signer = _recover(digest, signatures[i]);
require(signer != address(0), "DBIS: invalid sig");
require(signerReg.isSignerActiveAtBlock(signer, block.number), "DBIS: signer not active");
for (uint256 j = 0; j < i; j++) require(signers[j] != signer, "DBIS: duplicate signer");
signers[i] = signer;
}
}
function _recover(bytes32 digest, bytes calldata signature) private pure returns (address) {
require(signature.length == 65, "DBIS: sig length");
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := calldataload(signature.offset)
s := calldataload(add(signature.offset, 32))
v := byte(0, calldataload(add(signature.offset, 64)))
}
if (v < 27) v += 27;
require(v == 27 || v == 28, "DBIS: invalid v");
return ecrecover(digest, v, r, s);
}
}

View File

@@ -0,0 +1,77 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title DBIS_EIP712Lib
* @notice Library for EIP-712 hashing and ecrecover to avoid stack-too-deep in router.
*/
library DBIS_EIP712Lib {
function hashAddressArray(address[] calldata arr) internal pure returns (bytes32) {
bytes32[] memory hashes = new bytes32[](arr.length);
for (uint256 i = 0; i < arr.length; i++) {
hashes[i] = keccak256(abi.encode(arr[i]));
}
return keccak256(abi.encodePacked(hashes));
}
function hashUint256Array(uint256[] calldata arr) internal pure returns (bytes32) {
bytes32[] memory hashes = new bytes32[](arr.length);
for (uint256 i = 0; i < arr.length; i++) {
hashes[i] = keccak256(abi.encode(arr[i]));
}
return keccak256(abi.encodePacked(hashes));
}
function getMintAuthStructHash(
bytes32 typeHash,
bytes32 messageId,
bytes32 isoType,
bytes32 isoHash,
bytes32 accountingRef,
uint8 fundsStatus,
bytes32 corridor,
uint8 assetClass,
bytes32 recipientsHash,
bytes32 amountsHash,
uint64 notBefore,
uint64 expiresAt,
uint256 chainId,
address verifyingContract
) internal pure returns (bytes32) {
return keccak256(abi.encode(
typeHash,
messageId,
isoType,
isoHash,
accountingRef,
fundsStatus,
corridor,
assetClass,
recipientsHash,
amountsHash,
notBefore,
expiresAt,
chainId,
verifyingContract
));
}
function getDigest(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
}
function recover(bytes32 digest, bytes calldata signature) internal pure returns (address) {
require(signature.length == 65, "DBIS: sig length");
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := calldataload(signature.offset)
s := calldataload(add(signature.offset, 32))
v := byte(0, calldataload(add(signature.offset, 64)))
}
if (v < 27) v += 27;
require(v == 27 || v == 28, "DBIS: invalid v");
return ecrecover(digest, v, r, s);
}
}

View File

@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./IDBISTypes.sol";
interface IERC20Mintable {
function mint(address to, uint256 amount) external;
}
contract DBIS_GRU_MintController is AccessControl, Pausable, ReentrancyGuard {
bytes32 public constant ROUTER_ADMIN_ROLE = keccak256("ROUTER_ADMIN");
address public settlementRouter;
address public gruToken;
event MintFromAuthorization(bytes32 indexed messageId, address indexed recipient, uint256 amount, uint8 assetClass);
constructor(address admin, address _settlementRouter) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(ROUTER_ADMIN_ROLE, admin);
settlementRouter = _settlementRouter;
}
function setSettlementRouter(address _router) external onlyRole(ROUTER_ADMIN_ROLE) {
settlementRouter = _router;
}
function setGruToken(address _token) external onlyRole(ROUTER_ADMIN_ROLE) {
gruToken = _token;
}
function pause() external onlyRole(ROUTER_ADMIN_ROLE) {
_pause();
}
function unpause() external onlyRole(ROUTER_ADMIN_ROLE) {
_unpause();
}
function mintFromAuthorization(IDBISTypes.MintAuth calldata auth) external nonReentrant whenNotPaused {
require(msg.sender == settlementRouter, "DBIS: only router");
require(gruToken != address(0), "DBIS: token not set");
require(auth.recipients.length == auth.amounts.length, "DBIS: length mismatch");
IERC20Mintable token = IERC20Mintable(gruToken);
for (uint256 i = 0; i < auth.recipients.length; i++) {
require(auth.recipients[i] != address(0), "DBIS: zero recipient");
token.mint(auth.recipients[i], auth.amounts[i]);
emit MintFromAuthorization(auth.messageId, auth.recipients[i], auth.amounts[i], uint8(auth.assetClass));
}
}
}

View File

@@ -0,0 +1,106 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract DBIS_ParticipantRegistry is AccessControl {
bytes32 public constant PARTICIPANT_ADMIN_ROLE = keccak256("PARTICIPANT_ADMIN");
enum EntityType { BANK, MSB, CUSTODIAN, TRUSTEE, OPERATOR, AUDITOR }
enum Status { ACTIVE, SUSPENDED, REVOKED }
struct Participant {
bytes32 participantId;
string legalName;
string jurisdiction;
EntityType entityType;
Status status;
bytes32[] policyTags;
address[] operationalWallets;
}
mapping(bytes32 => Participant) private _participants;
mapping(bytes32 => uint8) private _participantStatus;
mapping(address => bytes32) private _walletToParticipant;
event ParticipantRegistered(bytes32 indexed participantId, string legalName, string jurisdiction, uint8 entityType);
event ParticipantStatusChanged(bytes32 indexed participantId, uint8 status);
event OperationalWalletAdded(bytes32 indexed participantId, address wallet);
event OperationalWalletRemoved(bytes32 indexed participantId, address wallet);
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(PARTICIPANT_ADMIN_ROLE, admin);
}
function registerParticipant(Participant calldata p) external onlyRole(PARTICIPANT_ADMIN_ROLE) {
require(p.participantId != bytes32(0), "DBIS: zero participantId");
require(_participants[p.participantId].participantId == bytes32(0), "DBIS: already registered");
_participants[p.participantId] = Participant({
participantId: p.participantId,
legalName: p.legalName,
jurisdiction: p.jurisdiction,
entityType: p.entityType,
status: p.status,
policyTags: p.policyTags,
operationalWallets: p.operationalWallets
});
_participantStatus[p.participantId] = uint8(p.status);
_linkWalletsToParticipant(p.participantId, p.operationalWallets);
emit ParticipantRegistered(p.participantId, p.legalName, p.jurisdiction, uint8(p.entityType));
}
function setParticipantStatus(bytes32 participantId, Status s) external onlyRole(PARTICIPANT_ADMIN_ROLE) {
require(_participants[participantId].participantId != bytes32(0), "DBIS: unknown participant");
_participants[participantId].status = s;
_participantStatus[participantId] = uint8(s);
emit ParticipantStatusChanged(participantId, uint8(s));
}
function addOperationalWallet(bytes32 participantId, address wallet) external onlyRole(PARTICIPANT_ADMIN_ROLE) {
require(_participants[participantId].participantId != bytes32(0), "DBIS: unknown participant");
require(wallet != address(0), "DBIS: zero wallet");
_participants[participantId].operationalWallets.push(wallet);
_walletToParticipant[wallet] = participantId;
emit OperationalWalletAdded(participantId, wallet);
}
function removeOperationalWallet(bytes32 participantId, address wallet) external onlyRole(PARTICIPANT_ADMIN_ROLE) {
require(_participants[participantId].participantId != bytes32(0), "DBIS: unknown participant");
address[] storage wallets = _participants[participantId].operationalWallets;
for (uint256 i = 0; i < wallets.length; i++) {
if (wallets[i] == wallet) {
wallets[i] = wallets[wallets.length - 1];
wallets.pop();
if (_walletToParticipant[wallet] == participantId) delete _walletToParticipant[wallet];
emit OperationalWalletRemoved(participantId, wallet);
return;
}
}
revert("DBIS: wallet not found");
}
function isOperationalWallet(address wallet) external view returns (bool) {
bytes32 pid = _walletToParticipant[wallet];
if (pid == bytes32(0)) return false;
return _participantStatus[pid] == uint8(Status.ACTIVE);
}
function getParticipantByWallet(address wallet) external view returns (bytes32 participantId) {
return _walletToParticipant[wallet];
}
function getParticipantStatus(bytes32 participantId) external view returns (Status) {
return _participants[participantId].status;
}
function getParticipant(bytes32 participantId) external view returns (Participant memory) {
return _participants[participantId];
}
function _linkWalletsToParticipant(bytes32 participantId, address[] calldata wallets) private {
for (uint256 i = 0; i < wallets.length; i++) {
if (wallets[i] != address(0)) _walletToParticipant[wallets[i]] = participantId;
}
}
}

View File

@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract DBIS_RootRegistry is AccessControl {
bytes32 public constant ROOT_ADMIN_ROLE = keccak256("ROOT_ADMIN");
mapping(bytes32 => address) private _components;
string public railVersion;
event ComponentUpdated(bytes32 indexed key, address indexed addr, string railVersion);
constructor(address admin, string memory _railVersion) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(ROOT_ADMIN_ROLE, admin);
railVersion = _railVersion;
}
function setComponent(bytes32 key, address addr) external onlyRole(ROOT_ADMIN_ROLE) {
_components[key] = addr;
emit ComponentUpdated(key, addr, railVersion);
}
function getComponent(bytes32 key) external view returns (address) {
return _components[key];
}
function setRailVersion(string calldata _version) external onlyRole(ROOT_ADMIN_ROLE) {
railVersion = _version;
}
}

View File

@@ -0,0 +1,193 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./IDBISTypes.sol";
import "./DBIS_EIP712Lib.sol";
import "./DBIS_RootRegistry.sol";
import "./DBIS_ParticipantRegistry.sol";
import "./DBIS_SignerRegistry.sol";
import "./DBIS_GRU_MintController.sol";
contract DBIS_SettlementRouter is AccessControl, Pausable, ReentrancyGuard {
bytes32 public constant ROUTER_ADMIN_ROLE = keccak256("ROUTER_ADMIN");
uint256 public constant CHAIN_ID = 138;
bytes32 public constant PARTICIPANT_REGISTRY_KEY = keccak256("ParticipantRegistry");
bytes32 public constant SIGNER_REGISTRY_KEY = keccak256("SignerRegistry");
bytes32 public constant GRU_MINT_CONTROLLER_KEY = keccak256("GRUMintController");
bytes32 private constant EIP712_DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 private constant MINTAUTH_TYPEHASH = keccak256(
"MintAuth(bytes32 messageId,bytes32 isoType,bytes32 isoHash,bytes32 accountingRef,uint8 fundsStatus,bytes32 corridor,uint8 assetClass,bytes32 recipientsHash,bytes32 amountsHash,uint64 notBefore,uint64 expiresAt,uint256 chainId,address verifyingContract)"
);
DBIS_RootRegistry public rootRegistry;
mapping(bytes32 => bool) public usedMessageIds;
uint256 public maxAmountPerMessage;
mapping(bytes32 => uint256) public corridorDailyCap;
mapping(bytes32 => mapping(uint256 => uint256)) public corridorUsedToday;
event SettlementRecorded(
bytes32 indexed messageId,
bytes32 indexed isoType,
bytes32 isoHash,
bytes32 accountingRef,
uint8 fundsStatus,
bytes32 corridor,
uint8 assetClass,
uint256 totalAmount
);
event MintExecuted(bytes32 indexed messageId, address indexed recipient, uint256 amount, uint8 assetClass);
event MessageIdConsumed(bytes32 indexed messageId);
event RouterPaused(bool paused);
constructor(address admin, address _rootRegistry) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(ROUTER_ADMIN_ROLE, admin);
rootRegistry = DBIS_RootRegistry(_rootRegistry);
maxAmountPerMessage = type(uint256).max;
}
function setMaxAmountPerMessage(uint256 cap) external onlyRole(ROUTER_ADMIN_ROLE) {
maxAmountPerMessage = cap;
}
function setCorridorDailyCap(bytes32 corridor, uint256 cap) external onlyRole(ROUTER_ADMIN_ROLE) {
corridorDailyCap[corridor] = cap;
}
function pause() external onlyRole(ROUTER_ADMIN_ROLE) {
_pause();
emit RouterPaused(true);
}
function unpause() external onlyRole(ROUTER_ADMIN_ROLE) {
_unpause();
emit RouterPaused(false);
}
function _domainSeparator() private view returns (bytes32) {
return keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256("DBISSettlementRouter"),
keccak256("1"),
CHAIN_ID,
address(this)
)
);
}
function _hashMintAuth(IDBISTypes.MintAuth calldata auth) private pure returns (bytes32) {
bytes32 rh = DBIS_EIP712Lib.hashAddressArray(auth.recipients);
bytes32 ah = DBIS_EIP712Lib.hashUint256Array(auth.amounts);
return DBIS_EIP712Lib.getMintAuthStructHash(
MINTAUTH_TYPEHASH,
auth.messageId,
auth.isoType,
auth.isoHash,
auth.accountingRef,
uint8(auth.fundsStatus),
auth.corridor,
uint8(auth.assetClass),
rh,
ah,
auth.notBefore,
auth.expiresAt,
auth.chainId,
auth.verifyingContract
);
}
function getMintAuthDigest(IDBISTypes.MintAuth calldata auth) external view returns (bytes32) {
return DBIS_EIP712Lib.getDigest(_domainSeparator(), _hashMintAuth(auth));
}
function submitMintAuth(IDBISTypes.MintAuth calldata auth, bytes[] calldata signatures) external nonReentrant whenNotPaused {
require(auth.chainId == CHAIN_ID, "DBIS: wrong chain");
require(auth.verifyingContract == address(this), "DBIS: wrong contract");
require(block.timestamp >= auth.notBefore && block.timestamp <= auth.expiresAt, "DBIS: time window");
require(!usedMessageIds[auth.messageId], "DBIS: replay");
require(auth.recipients.length == auth.amounts.length, "DBIS: length mismatch");
require(auth.recipients.length > 0, "DBIS: no recipients");
uint256 totalAmount = _sumAmounts(auth.amounts);
require(totalAmount <= maxAmountPerMessage, "DBIS: cap exceeded");
uint256 day = block.timestamp / 1 days;
require(_checkCorridorCap(auth.corridor, day, totalAmount), "DBIS: corridor cap");
_requireRecipientsOperational(auth.recipients);
address[] memory signers = _recoverSigners(auth, signatures);
DBIS_SignerRegistry signerReg = DBIS_SignerRegistry(rootRegistry.getComponent(SIGNER_REGISTRY_KEY));
(bool ok, ) = signerReg.validateSigners(signers);
require(ok, "DBIS: quorum not met");
usedMessageIds[auth.messageId] = true;
corridorUsedToday[auth.corridor][day] = corridorUsedToday[auth.corridor][day] + totalAmount;
_emitSettlementEvents(auth, totalAmount);
DBIS_GRU_MintController mintController = DBIS_GRU_MintController(rootRegistry.getComponent(GRU_MINT_CONTROLLER_KEY));
require(address(mintController) != address(0), "DBIS: no mint controller");
mintController.mintFromAuthorization(auth);
}
function _sumAmounts(uint256[] calldata amounts) private pure returns (uint256) {
uint256 total = 0;
for (uint256 i = 0; i < amounts.length; i++) total += amounts[i];
return total;
}
function _checkCorridorCap(bytes32 corridor, uint256 day, uint256 totalAmount) private view returns (bool) {
uint256 cap = corridorDailyCap[corridor];
if (cap == 0) return true;
return corridorUsedToday[corridor][day] + totalAmount <= cap;
}
function _requireRecipientsOperational(address[] calldata recipients) private view {
DBIS_ParticipantRegistry participantReg = DBIS_ParticipantRegistry(rootRegistry.getComponent(PARTICIPANT_REGISTRY_KEY));
require(address(participantReg) != address(0), "DBIS: no participant registry");
for (uint256 i = 0; i < recipients.length; i++) {
require(participantReg.isOperationalWallet(recipients[i]), "DBIS: not operational wallet");
}
}
function _recoverSigners(IDBISTypes.MintAuth calldata auth, bytes[] calldata signatures) private view returns (address[] memory signers) {
DBIS_SignerRegistry signerReg = DBIS_SignerRegistry(rootRegistry.getComponent(SIGNER_REGISTRY_KEY));
require(address(signerReg) != address(0), "DBIS: no signer registry");
bytes32 digest = DBIS_EIP712Lib.getDigest(_domainSeparator(), _hashMintAuth(auth));
signers = new address[](signatures.length);
for (uint256 i = 0; i < signatures.length; i++) {
require(signatures[i].length == 65, "DBIS: bad sig len");
address signer = DBIS_EIP712Lib.recover(digest, signatures[i]);
require(signer != address(0), "DBIS: invalid sig");
require(signerReg.isSignerActiveAtBlock(signer, block.number), "DBIS: signer not active at block");
for (uint256 j = 0; j < i; j++) require(signers[j] != signer, "DBIS: duplicate signer");
signers[i] = signer;
}
}
function _emitSettlementEvents(IDBISTypes.MintAuth calldata auth, uint256 totalAmount) private {
emit SettlementRecorded(
auth.messageId,
auth.isoType,
auth.isoHash,
auth.accountingRef,
uint8(auth.fundsStatus),
auth.corridor,
uint8(auth.assetClass),
totalAmount
);
for (uint256 i = 0; i < auth.recipients.length; i++) {
emit MintExecuted(auth.messageId, auth.recipients[i], auth.amounts[i], uint8(auth.assetClass));
}
emit MessageIdConsumed(auth.messageId);
}
}

View File

@@ -0,0 +1,160 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract DBIS_SignerRegistry is AccessControl {
bytes32 public constant SIGNER_ADMIN_ROLE = keccak256("SIGNER_ADMIN");
uint8 public constant CATEGORY_OPS = 0;
uint8 public constant CATEGORY_COMPLIANCE = 1;
uint8 public constant CATEGORY_CUSTODY = 2;
uint8 public constant CATEGORY_RISK = 3;
uint8 public constant CATEGORY_AUDITOR = 4;
uint256 private constant NEVER_REVOKED = type(uint256).max;
struct SignerInfo {
uint8 category;
uint256 effectiveFromBlock;
uint256 revokedAtBlock;
bool exists;
}
mapping(address => SignerInfo) private _signers;
address[] private _signerList;
uint8 public requiredSignatures = 3;
uint256 public categoryMaskRequired = 1 << CATEGORY_COMPLIANCE;
uint256 public categoryMaskAllowed = (1 << CATEGORY_OPS) | (1 << CATEGORY_COMPLIANCE) | (1 << CATEGORY_CUSTODY) | (1 << CATEGORY_RISK) | (1 << CATEGORY_AUDITOR);
event SignerAdded(address indexed signer, uint8 category);
event SignerRemoved(address indexed signer);
event QuorumUpdated(uint8 requiredSigs, uint256 requiredMask, uint256 allowedMask);
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(SIGNER_ADMIN_ROLE, admin);
}
function addSigner(address signer, uint8 category) external onlyRole(SIGNER_ADMIN_ROLE) {
require(signer != address(0), "DBIS: zero signer");
require(!_signers[signer].exists, "DBIS: already signer");
require(category <= CATEGORY_AUDITOR, "DBIS: invalid category");
_signers[signer] = SignerInfo({
category: category,
effectiveFromBlock: block.number,
revokedAtBlock: NEVER_REVOKED,
exists: true
});
_signerList.push(signer);
emit SignerAdded(signer, category);
}
function removeSigner(address signer) external onlyRole(SIGNER_ADMIN_ROLE) {
require(_signers[signer].exists, "DBIS: not signer");
if (_signers[signer].revokedAtBlock == NEVER_REVOKED) {
_signers[signer].revokedAtBlock = block.number;
}
_signers[signer].exists = false;
for (uint256 i = 0; i < _signerList.length; i++) {
if (_signerList[i] == signer) {
_signerList[i] = _signerList[_signerList.length - 1];
_signerList.pop();
break;
}
}
emit SignerRemoved(signer);
}
function revokeSignerAtBlock(address signer) external onlyRole(SIGNER_ADMIN_ROLE) {
require(_signers[signer].exists, "DBIS: not signer");
_signers[signer].revokedAtBlock = block.number;
}
function setQuorum(uint8 requiredSigs, uint256 requiredMask, uint256 allowedMask) external onlyRole(SIGNER_ADMIN_ROLE) {
requiredSignatures = requiredSigs;
categoryMaskRequired = requiredMask;
categoryMaskAllowed = allowedMask;
emit QuorumUpdated(requiredSigs, requiredMask, allowedMask);
}
function isSigner(address signer) external view returns (bool) {
return _signers[signer].exists && _signers[signer].revokedAtBlock == NEVER_REVOKED;
}
function isSignerActiveAtBlock(address signer, uint256 blockNum) external view returns (bool) {
SignerInfo memory info = _signers[signer];
if (!info.exists) return false;
if (blockNum < info.effectiveFromBlock) return false;
if (info.revokedAtBlock != NEVER_REVOKED && blockNum >= info.revokedAtBlock) return false;
return true;
}
function getSignerInfo(address signer) external view returns (uint8 category, uint256 effectiveFromBlock, uint256 revokedAtBlock) {
SignerInfo memory info = _signers[signer];
require(info.exists, "DBIS: not signer");
return (info.category, info.effectiveFromBlock, info.revokedAtBlock);
}
function validateSigners(address[] calldata signers) external view returns (bool ok, string memory reason) {
if (signers.length < requiredSignatures) return (false, "insufficient count");
uint256 categoryMask = 0;
for (uint256 i = 0; i < signers.length; i++) {
address s = signers[i];
(bool exists, uint8 cat, uint256 revoked) = _getSignerInfo(s);
if (!exists) return (false, "not signer");
if (revoked != NEVER_REVOKED) return (false, "signer revoked");
if ((categoryMaskAllowed & (1 << cat)) == 0) return (false, "category not allowed");
if (_hasDuplicate(signers, i, s)) return (false, "duplicate signer");
categoryMask |= (1 << cat);
}
if ((categoryMask & categoryMaskRequired) != categoryMaskRequired) return (false, "required category missing");
return (true, "");
}
function _getSignerInfo(address signer) private view returns (bool exists, uint8 category, uint256 revokedAtBlock) {
SignerInfo memory info = _signers[signer];
return (info.exists, info.category, info.revokedAtBlock);
}
function _hasDuplicate(address[] calldata signers, uint256 currentIndex, address signer) private pure returns (bool) {
for (uint256 j = currentIndex + 1; j < signers.length; j++) {
if (signers[j] == signer) return true;
}
return false;
}
function getSignerCount() external view returns (uint256) {
return _signerList.length;
}
uint256 public largeSwapAmountThreshold;
uint8 public requiredSignaturesSmall = 2;
uint8 public requiredSignaturesLarge = 3;
function setSwapQuorum(uint256 largeThreshold, uint8 smallSigs, uint8 largeSigs) external onlyRole(SIGNER_ADMIN_ROLE) {
largeSwapAmountThreshold = largeThreshold;
requiredSignaturesSmall = smallSigs;
requiredSignaturesLarge = largeSigs;
}
function validateSignersForSwap(address[] calldata signers, uint256 amountIn) external view returns (bool ok, string memory reason) {
uint8 required = amountIn > largeSwapAmountThreshold ? requiredSignaturesLarge : requiredSignaturesSmall;
if (signers.length < required) return (false, "insufficient count");
uint256 categoryMask = 0;
for (uint256 i = 0; i < signers.length; i++) {
address s = signers[i];
(bool exists, uint8 cat, uint256 revoked) = _getSignerInfo(s);
if (!exists) return (false, "not signer");
if (revoked != NEVER_REVOKED) return (false, "signer revoked");
if ((categoryMaskAllowed & (1 << cat)) == 0) return (false, "category not allowed");
if (_hasDuplicate(signers, i, s)) return (false, "duplicate signer");
categoryMask |= (1 << cat);
}
if (amountIn > largeSwapAmountThreshold && (categoryMask & categoryMaskRequired) != categoryMaskRequired) {
return (false, "required category missing");
}
return (true, "");
}
}

View File

@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IDBISTypes {
enum FundsStatus { ON_LEDGER_FINAL, OFF_LEDGER_FINAL }
enum AssetClass { GRU_M00, GRU_M0, GRU_M1 }
struct MintAuth {
bytes32 messageId;
bytes32 isoType;
bytes32 isoHash;
bytes32 accountingRef;
FundsStatus fundsStatus;
bytes32 corridor;
AssetClass assetClass;
address[] recipients;
uint256[] amounts;
uint64 notBefore;
uint64 expiresAt;
uint256 chainId;
address verifyingContract;
}
}

View File

@@ -0,0 +1,89 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract StablecoinReferenceRegistry is AccessControl {
bytes32 public constant STABLECOIN_REGISTRAR_ROLE = keccak256("STABLECOIN_REGISTRAR");
enum StablecoinStatus { ACTIVE, SUSPENDED, REVOKED }
struct StablecoinEntry {
string tokenSymbol;
address tokenAddress;
string issuerOrBridge;
string legalClaimType;
string redemptionPath;
string reserveDisclosureRef;
uint8 riskTier;
address pauseAuthority;
StablecoinStatus status;
bool exists;
}
mapping(address => StablecoinEntry) private _byAddress;
address[] private _addressList;
event StablecoinRegistered(
address indexed tokenAddress,
string tokenSymbol,
StablecoinStatus status
);
event StablecoinStatusUpdated(address indexed tokenAddress, StablecoinStatus status);
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(STABLECOIN_REGISTRAR_ROLE, admin);
}
function register(
address tokenAddress,
string calldata tokenSymbol,
string calldata issuerOrBridge,
string calldata legalClaimType,
string calldata redemptionPath,
string calldata reserveDisclosureRef,
uint8 riskTier,
address pauseAuthority
) external onlyRole(STABLECOIN_REGISTRAR_ROLE) {
require(tokenAddress != address(0), "DBIS: zero address");
require(!_byAddress[tokenAddress].exists, "DBIS: already registered");
_byAddress[tokenAddress] = StablecoinEntry({
tokenSymbol: tokenSymbol,
tokenAddress: tokenAddress,
issuerOrBridge: issuerOrBridge,
legalClaimType: legalClaimType,
redemptionPath: redemptionPath,
reserveDisclosureRef: reserveDisclosureRef,
riskTier: riskTier,
pauseAuthority: pauseAuthority,
status: StablecoinStatus.ACTIVE,
exists: true
});
_addressList.push(tokenAddress);
emit StablecoinRegistered(tokenAddress, tokenSymbol, StablecoinStatus.ACTIVE);
}
function setStatus(address tokenAddress, StablecoinStatus status) external onlyRole(STABLECOIN_REGISTRAR_ROLE) {
require(_byAddress[tokenAddress].exists, "DBIS: not registered");
_byAddress[tokenAddress].status = status;
emit StablecoinStatusUpdated(tokenAddress, status);
}
function getEntry(address tokenAddress) external view returns (StablecoinEntry memory) {
return _byAddress[tokenAddress];
}
function isActive(address tokenAddress) external view returns (bool) {
return _byAddress[tokenAddress].exists && _byAddress[tokenAddress].status == StablecoinStatus.ACTIVE;
}
function getRegisteredCount() external view returns (uint256) {
return _addressList.length;
}
function getRegisteredAt(uint256 index) external view returns (address) {
require(index < _addressList.length, "DBIS: index");
return _addressList[index];
}
}

View File

@@ -13,6 +13,8 @@ import "../compliance/LegallyCompliantBase.sol";
* Full ERC-20 for DEX liquidity pools; inherits LegallyCompliantBase.
*/
contract CompliantFiatToken is ERC20, Pausable, Ownable, LegallyCompliantBase {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
uint8 private immutable _decimalsStorage;
string private _currencyCode;
@@ -41,6 +43,7 @@ contract CompliantFiatToken is ERC20, Pausable, Ownable, LegallyCompliantBase {
{
_decimalsStorage = decimals_;
_currencyCode = currencyCode_;
_grantRole(MINTER_ROLE, initialOwner);
if (initialSupply > 0) {
_mint(msg.sender, initialSupply);
}
@@ -82,7 +85,8 @@ contract CompliantFiatToken is ERC20, Pausable, Ownable, LegallyCompliantBase {
_unpause();
}
function mint(address to, uint256 amount) public onlyOwner {
/// @notice Mint (DBIS Rail: grant MINTER_ROLE only to DBIS_GRU_MintController; revoke from others)
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}

View File

@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
import "../../contracts/dbis/DBIS_RootRegistry.sol";
import "../../contracts/dbis/DBIS_ParticipantRegistry.sol";
import "../../contracts/dbis/DBIS_SignerRegistry.sol";
import "../../contracts/dbis/DBIS_GRU_MintController.sol";
import "../../contracts/dbis/DBIS_SettlementRouter.sol";
import "../../contracts/dbis/StablecoinReferenceRegistry.sol";
import "../../contracts/dbis/DBIS_ConversionRouter.sol";
/**
* @title DeployDBISRail
* @notice Deploy all DBIS Rail contracts (Spec v1 + v1.5) and wire RootRegistry.
* @dev Run on Chain 138. Env: PRIVATE_KEY; optional ADMIN_ADDRESS (default deployer).
* After deploy: set GRU token on MintController; grant MINTER_ROLE on c* tokens to MintController; register stablecoins; add venues/quote issuers.
*/
contract DeployDBISRail is Script {
function run() external {
uint256 pk = vm.envUint("PRIVATE_KEY");
address admin = vm.envOr("ADMIN_ADDRESS", vm.addr(pk));
vm.startBroadcast(pk);
DBIS_RootRegistry root = new DBIS_RootRegistry(admin, "v1");
DBIS_ParticipantRegistry participantReg = new DBIS_ParticipantRegistry(admin);
DBIS_SignerRegistry signerReg = new DBIS_SignerRegistry(admin);
DBIS_GRU_MintController mintController = new DBIS_GRU_MintController(admin, address(0));
DBIS_SettlementRouter router = new DBIS_SettlementRouter(admin, address(root));
StablecoinReferenceRegistry stableReg = new StablecoinReferenceRegistry(admin);
DBIS_ConversionRouter conversionRouter = new DBIS_ConversionRouter(admin, address(root));
root.setComponent(keccak256("ParticipantRegistry"), address(participantReg));
root.setComponent(keccak256("SignerRegistry"), address(signerReg));
root.setComponent(keccak256("GRUMintController"), address(mintController));
mintController.setSettlementRouter(address(router));
root.setComponent(keccak256("SettlementRouter"), address(router));
root.setComponent(keccak256("StablecoinReferenceRegistry"), address(stableReg));
root.setComponent(keccak256("ConversionRouter"), address(conversionRouter));
signerReg.setSwapQuorum(1e24, 2, 3);
console.log("DBIS_RootRegistry", address(root));
console.log("DBIS_ParticipantRegistry", address(participantReg));
console.log("DBIS_SignerRegistry", address(signerReg));
console.log("DBIS_GRU_MintController", address(mintController));
console.log("DBIS_SettlementRouter", address(router));
console.log("StablecoinReferenceRegistry", address(stableReg));
console.log("DBIS_ConversionRouter", address(conversionRouter));
vm.stopBroadcast();
}
}

110
scripts/mint-for-liquidity.sh Executable file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# Mint cUSDT and cUSDC to the deployer for adding PMM liquidity on Chain 138.
# Deployer must be owner of both token contracts. After minting, run add-liquidity (see step 2).
#
# Usage:
# cd smom-dbis-138 && ./scripts/mint-for-liquidity.sh
# MINT_CUSDT_AMOUNT=2000000 MINT_CUSDC_AMOUNT=2000000 ./scripts/mint-for-liquidity.sh # 2M each
# ./scripts/mint-for-liquidity.sh --add-liquidity # mint then run AddLiquidityPMMPoolsChain138
#
# Env (in smom-dbis-138/.env): PRIVATE_KEY, RPC_URL_138 (or RPC_URL).
# Optional: MINT_CUSDT_AMOUNT, MINT_CUSDC_AMOUNT (human units, default 1000000 = 1M each).
# For --add-liquidity: ADD_LIQUIDITY_BASE_AMOUNT, ADD_LIQUIDITY_QUOTE_AMOUNT (base units, 6 decimals).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_ROOT"
[[ -f .env ]] && set -a && source .env && set +a
RPC="${RPC_URL_138:-${RPC_URL:-http://192.168.11.211:8545}}"
CUSDT="${COMPLIANT_USDT:-0x93E66202A11B1772E55407B32B44e5Cd8eda7f22}"
CUSDC="${COMPLIANT_USDC:-0xf22258f57794CC8E06237084b353Ab30fFfa640b}"
DECIMALS=6
# Amounts in human units (e.g. 1000000 = 1M tokens)
MINT_CUSDT="${MINT_CUSDT_AMOUNT:-1000000}"
MINT_CUSDC="${MINT_CUSDC_AMOUNT:-1000000}"
RUN_ADD_LIQUIDITY=false
for a in "$@"; do [[ "$a" == "--add-liquidity" ]] && RUN_ADD_LIQUIDITY=true && break; done
if [[ -z "${PRIVATE_KEY:-}" ]]; then
echo "ERROR: PRIVATE_KEY not set. Source smom-dbis-138/.env"
exit 1
fi
DEPLOYER=$(cast wallet address "$PRIVATE_KEY" 2>/dev/null || true)
if [[ -z "$DEPLOYER" ]]; then
echo "ERROR: Could not derive address from PRIVATE_KEY"
exit 1
fi
# Base units (6 decimals)
BASE_CUSDT=$((MINT_CUSDT * 10**DECIMALS))
BASE_CUSDC=$((MINT_CUSDC * 10**DECIMALS))
echo "=== Mint cUSDT / cUSDC for liquidity (Chain 138) ==="
echo " Deployer: $DEPLOYER"
echo " RPC: $RPC"
echo " cUSDT: $MINT_CUSDT tokens ($BASE_CUSDT base units) -> $CUSDT"
echo " cUSDC: $MINT_CUSDC tokens ($BASE_CUSDC base units) -> $CUSDC"
echo ""
mint_one() {
local addr="$1"
local name="$2"
local amount_base="$3"
echo "Minting $name to deployer..."
OWNER=$(cast call "$addr" "owner()(address)" --rpc-url "$RPC" 2>/dev/null || echo "")
if [[ -n "$OWNER" && "$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')" != "$(echo "$DEPLOYER" | tr '[:upper:]' '[:lower:]')" ]]; then
echo " SKIP $name: contract owner is $OWNER, deployer is $DEPLOYER (only owner can mint)"
return 0
fi
if cast send "$addr" "mint(address,uint256)" "$DEPLOYER" "$amount_base" \
--rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy; then
echo " OK $name"
else
echo " FAIL $name"
return 1
fi
}
mint_one "$CUSDT" "cUSDT" "$BASE_CUSDT"
mint_one "$CUSDC" "cUSDC" "$BASE_CUSDC"
echo ""
echo "Mint done. Deployer balances:"
cast call "$CUSDT" "balanceOf(address)(uint256)" "$DEPLOYER" --rpc-url "$RPC" 2>/dev/null | xargs -I {} echo " cUSDT: {}"
cast call "$CUSDC" "balanceOf(address)(uint256)" "$DEPLOYER" --rpc-url "$RPC" 2>/dev/null | xargs -I {} echo " cUSDC: {}"
echo ""
if [[ "$RUN_ADD_LIQUIDITY" == true ]]; then
if [[ -z "${ADD_LIQUIDITY_BASE_AMOUNT:-}" || -z "${ADD_LIQUIDITY_QUOTE_AMOUNT:-}" ]]; then
# Default: use half of what we just minted (so we add liquidity for the cUSDT/cUSDC pool)
ADD_LIQUIDITY_BASE_AMOUNT=${ADD_LIQUIDITY_BASE_AMOUNT:-$((BASE_CUSDT / 2))}
ADD_LIQUIDITY_QUOTE_AMOUNT=${ADD_LIQUIDITY_QUOTE_AMOUNT:-$((BASE_CUSDC / 2))}
echo "Using default add-liquidity amounts (half of minted): base=$ADD_LIQUIDITY_BASE_AMOUNT quote=$ADD_LIQUIDITY_QUOTE_AMOUNT"
fi
export ADD_LIQUIDITY_BASE_AMOUNT ADD_LIQUIDITY_QUOTE_AMOUNT
# Default pool addresses (Chain 138) if not in .env
export POOL_CUSDTCUSDC="${POOL_CUSDTCUSDC:-0x9fcB06Aa1FD5215DC0E91Fd098aeff4B62fEa5C8}"
export POOL_CUSDTUSDT="${POOL_CUSDTUSDT:-0xa3Ee6091696B28e5497b6F491fA1e99047250c59}"
export POOL_CUSDCUSDC="${POOL_CUSDCUSDC:-0x90bd9Bf18Daa26Af3e814ea224032d015db58Ea5}"
if [[ -n "${DODO_PMM_INTEGRATION:-}" || -n "${DODO_PMM_INTEGRATION_ADDRESS:-}" ]]; then
echo "Running AddLiquidityPMMPoolsChain138 (cUSDT/cUSDC pool only if base/quote set)..."
forge script script/dex/AddLiquidityPMMPoolsChain138.s.sol:AddLiquidityPMMPoolsChain138 \
--rpc-url "$RPC" --broadcast --private-key "$PRIVATE_KEY" --with-gas-price 1000000000
echo "Add-liquidity done."
else
echo "Set DODO_PMM_INTEGRATION (or DODO_PMM_INTEGRATION_ADDRESS) and POOL_* in .env, then run:"
echo " forge script script/dex/AddLiquidityPMMPoolsChain138.s.sol:AddLiquidityPMMPoolsChain138 --rpc-url \$RPC_URL_138 --broadcast --private-key \$PRIVATE_KEY"
fi
else
echo "To add liquidity next: set ADD_LIQUIDITY_BASE_AMOUNT and ADD_LIQUIDITY_QUOTE_AMOUNT (base units, 6 decimals),"
echo "POOL_CUSDTCUSDC (and optional POOL_CUSDTUSDT, POOL_CUSDCUSDC), DODO_PMM_INTEGRATION in .env, then run:"
echo " forge script script/dex/AddLiquidityPMMPoolsChain138.s.sol:AddLiquidityPMMPoolsChain138 --rpc-url \$RPC_URL_138 --broadcast --private-key \$PRIVATE_KEY"
echo "Or run this script with --add-liquidity to mint and add in one go (uses half of minted for cUSDT/cUSDC pool)."
fi

183
test/dbis/DBIS_Rail.t.sol Normal file
View File

@@ -0,0 +1,183 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../../contracts/dbis/IDBISTypes.sol";
import "../../contracts/dbis/DBIS_RootRegistry.sol";
import "../../contracts/dbis/DBIS_ParticipantRegistry.sol";
import "../../contracts/dbis/DBIS_SignerRegistry.sol";
import "../../contracts/dbis/DBIS_GRU_MintController.sol";
import "../../contracts/dbis/DBIS_SettlementRouter.sol";
import "../../contracts/dbis/StablecoinReferenceRegistry.sol";
import "../../contracts/tokens/CompliantFiatToken.sol";
contract DBIS_RailTest is Test, IDBISTypes {
DBIS_RootRegistry public root;
DBIS_ParticipantRegistry public participantReg;
DBIS_SignerRegistry public signerReg;
DBIS_GRU_MintController public mintController;
DBIS_SettlementRouter public router;
StablecoinReferenceRegistry public stableReg;
CompliantFiatToken public token;
address public admin;
address public alice;
address public signer1;
address public signer2;
address public signer3;
uint256 signer1Key;
uint256 signer2Key;
uint256 signer3Key;
function setUp() public {
admin = makeAddr("admin");
alice = makeAddr("alice");
(signer1, signer1Key) = makeAddrAndKey("signer1");
(signer2, signer2Key) = makeAddrAndKey("signer2");
(signer3, signer3Key) = makeAddrAndKey("signer3");
vm.startPrank(admin);
root = new DBIS_RootRegistry(admin, "v1");
participantReg = new DBIS_ParticipantRegistry(admin);
signerReg = new DBIS_SignerRegistry(admin);
mintController = new DBIS_GRU_MintController(admin, address(0));
router = new DBIS_SettlementRouter(admin, address(root));
stableReg = new StablecoinReferenceRegistry(admin);
root.setComponent(keccak256("ParticipantRegistry"), address(participantReg));
root.setComponent(keccak256("SignerRegistry"), address(signerReg));
root.setComponent(keccak256("GRUMintController"), address(mintController));
mintController.setSettlementRouter(address(router));
token = new CompliantFiatToken("Test GRU", "tGRU", 6, "USD", admin, admin, 0);
mintController.setGruToken(address(token));
token.grantRole(token.MINTER_ROLE(), address(mintController));
signerReg.addSigner(signer1, 0);
signerReg.addSigner(signer2, 1);
signerReg.addSigner(signer3, 2);
address[] memory wallets = new address[](1);
wallets[0] = alice;
bytes32[] memory tags;
participantReg.registerParticipant(DBIS_ParticipantRegistry.Participant({
participantId: keccak256("participant1"),
legalName: "Test FI",
jurisdiction: "US",
entityType: DBIS_ParticipantRegistry.EntityType.BANK,
status: DBIS_ParticipantRegistry.Status.ACTIVE,
policyTags: tags,
operationalWallets: wallets
}));
vm.stopPrank();
}
function test_submitMintAuth_success() public {
address[] memory recipients = new address[](1);
recipients[0] = alice;
uint256[] memory amounts = new uint256[](1);
amounts[0] = 1000 * 1e6;
MintAuth memory auth = MintAuth({
messageId: keccak256("msg1"),
isoType: keccak256("pacs.008"),
isoHash: keccak256("isobundle"),
accountingRef: keccak256("acctref"),
fundsStatus: FundsStatus.ON_LEDGER_FINAL,
corridor: bytes32(0),
assetClass: AssetClass.GRU_M00,
recipients: recipients,
amounts: amounts,
notBefore: uint64(block.timestamp - 1),
expiresAt: uint64(block.timestamp + 300),
chainId: 138,
verifyingContract: address(router)
});
bytes32 digest = router.getMintAuthDigest(auth);
bytes memory sig1 = _sign(digest, signer1Key);
bytes memory sig2 = _sign(digest, signer2Key);
bytes memory sig3 = _sign(digest, signer3Key);
bytes[] memory sigs = new bytes[](3);
sigs[0] = sig1;
sigs[1] = sig2;
sigs[2] = sig3;
vm.prank(alice);
router.submitMintAuth(auth, sigs);
assertEq(token.balanceOf(alice), 1000 * 1e6);
assertTrue(router.usedMessageIds(auth.messageId));
}
function test_submitMintAuth_replayReverts() public {
address[] memory recipients = new address[](1);
recipients[0] = alice;
uint256[] memory amounts = new uint256[](1);
amounts[0] = 1000 * 1e6;
MintAuth memory auth = MintAuth({
messageId: keccak256("msg2"),
isoType: keccak256("pacs.008"),
isoHash: keccak256("isobundle"),
accountingRef: keccak256("acctref"),
fundsStatus: FundsStatus.ON_LEDGER_FINAL,
corridor: bytes32(0),
assetClass: AssetClass.GRU_M00,
recipients: recipients,
amounts: amounts,
notBefore: uint64(block.timestamp - 1),
expiresAt: uint64(block.timestamp + 300),
chainId: 138,
verifyingContract: address(router)
});
bytes32 digest = router.getMintAuthDigest(auth);
bytes[] memory sigs = new bytes[](3);
sigs[0] = _sign(digest, signer1Key);
sigs[1] = _sign(digest, signer2Key);
sigs[2] = _sign(digest, signer3Key);
vm.prank(alice);
router.submitMintAuth(auth, sigs);
vm.expectRevert("DBIS: replay");
vm.prank(alice);
router.submitMintAuth(auth, sigs);
}
function test_signerActiveAtBlock_afterRevoke() public {
vm.prank(admin);
signerReg.revokeSignerAtBlock(signer1);
address[] memory recipients = new address[](1);
recipients[0] = alice;
uint256[] memory amounts = new uint256[](1);
amounts[0] = 1000 * 1e6;
MintAuth memory auth = MintAuth({
messageId: keccak256("msg3"),
isoType: keccak256("pacs.008"),
isoHash: keccak256("isobundle"),
accountingRef: keccak256("acctref"),
fundsStatus: FundsStatus.ON_LEDGER_FINAL,
corridor: bytes32(0),
assetClass: AssetClass.GRU_M00,
recipients: recipients,
amounts: amounts,
notBefore: uint64(block.timestamp - 1),
expiresAt: uint64(block.timestamp + 300),
chainId: 138,
verifyingContract: address(router)
});
bytes32 digest = router.getMintAuthDigest(auth);
bytes[] memory sigs = new bytes[](3);
sigs[0] = _sign(digest, signer1Key);
sigs[1] = _sign(digest, signer2Key);
sigs[2] = _sign(digest, signer3Key);
vm.prank(alice);
vm.expectRevert("DBIS: signer not active at block");
router.submitMintAuth(auth, sigs);
}
function _sign(bytes32 digest, uint256 pk) internal pure returns (bytes memory) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest);
return abi.encodePacked(r, s, v);
}
}