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:
8
contracts/dbis/BUILD_NOTES.md
Normal file
8
contracts/dbis/BUILD_NOTES.md
Normal 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.
|
||||
196
contracts/dbis/DBIS_ConversionRouter.sol
Normal file
196
contracts/dbis/DBIS_ConversionRouter.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
77
contracts/dbis/DBIS_EIP712Lib.sol
Normal file
77
contracts/dbis/DBIS_EIP712Lib.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
54
contracts/dbis/DBIS_GRU_MintController.sol
Normal file
54
contracts/dbis/DBIS_GRU_MintController.sol
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
106
contracts/dbis/DBIS_ParticipantRegistry.sol
Normal file
106
contracts/dbis/DBIS_ParticipantRegistry.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
contracts/dbis/DBIS_RootRegistry.sol
Normal file
31
contracts/dbis/DBIS_RootRegistry.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
193
contracts/dbis/DBIS_SettlementRouter.sol
Normal file
193
contracts/dbis/DBIS_SettlementRouter.sol
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
160
contracts/dbis/DBIS_SignerRegistry.sol
Normal file
160
contracts/dbis/DBIS_SignerRegistry.sol
Normal 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, "");
|
||||
}
|
||||
}
|
||||
23
contracts/dbis/IDBISTypes.sol
Normal file
23
contracts/dbis/IDBISTypes.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
89
contracts/dbis/StablecoinReferenceRegistry.sol
Normal file
89
contracts/dbis/StablecoinReferenceRegistry.sol
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
55
script/deploy/DeployDBISRail.s.sol
Normal file
55
script/deploy/DeployDBISRail.s.sol
Normal 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
110
scripts/mint-for-liquidity.sh
Executable 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
183
test/dbis/DBIS_Rail.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user