Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
"@nomicfoundation/hardhat-verify": "^1.0.0",
"@openzeppelin/contracts": "^4.9.3",
"@openzeppelin/contracts-upgradeable": "^4.9.3",
"@typechain/ethers-v6": "^0.4.0",
"@typechain/hardhat": "^8.0.0",
Expand All @@ -52,5 +53,6 @@
"typechain": "^8.2.0",
"typescript": "^5.4.3",
"web3": "^4.7.0"
}
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
200 changes: 200 additions & 0 deletions src/Validator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import "./IValidator.sol";

/**
* @title Validator
* @dev Role-based access control for transfer validation and account management.
* Uses OpenZeppelin AccessControl for secure role management.
* docs: https://docs.openzeppelin.com/contracts/4.x/api/access#AccessControl
* audit: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/audits/2023-05-v4.9.pdf
*/
contract Validator is AccessControl, IValidator {
// Role identifiers
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant V1_BLOCKED_ROLE = keccak256("V1_BLOCKED_ROLE");
bytes32 public constant BLACKLISTED_ROLE = keccak256("BLACKLISTED_ROLE");
bytes32 public constant V1_FRONTEND_ROLE = keccak256("V1_FRONTEND_ROLE");

// Contract identifier for interface compliance
bytes32 private constant ID =
0x5341d189213c4172d0c7256f80bc5f8e6350af3aaff7a029625d8dd94f0f82a5;

/**
* @dev Returns the contract identifier.
*/
function CONTRACT_ID() public pure returns (bytes32) {
return ID;
}

/**
* @dev Sets up initial admin and role relationships.
* The deployer is the default admin.
* ADMIN_ROLE is the admin for blocked, blacklisted, and frontend roles.
*/
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setRoleAdmin(V1_BLOCKED_ROLE, ADMIN_ROLE);
_setRoleAdmin(BLACKLISTED_ROLE, ADMIN_ROLE);
_setRoleAdmin(V1_FRONTEND_ROLE, ADMIN_ROLE);
}

/**
* @dev Validates a transfer between two accounts.
* - If called by a V1 frontend, checks if either account is blocked.
* - Always checks if either account is blacklisted.
* @return valid True if transfer is allowed, false otherwise.
*/
function validate(
address from,
address to,
uint256 amount
) external override returns (bool valid) {
if (isV1Frontend(msg.sender)) {
if (isV1Blocked(from)) {
emit Decision(from, to, amount, false);
revert(
string(
abi.encodePacked(
"Transfer not supported:",
Strings.toHexString(from),
" is blocked in V1. Please use V2 instead. See https://monerium.dev/docs/tokens"
)
)
);
}
if (isV1Blocked(to)) {
emit Decision(from, to, amount, false);
revert(
string(
abi.encodePacked(
"Transfer not supported:",
Strings.toHexString(to),
" is blocked in V1. Please use V2 instead. See https://monerium.dev/docs/tokens"
)
)
);
}
}
if (isBlacklisted(from)) {
emit Decision(from, to, amount, false);
revert(
string(
abi.encodePacked(
"Transfer not supported:",
Strings.toHexString(from),
" is blacklisted."
)
)
);
}
if (isBlacklisted(to)) {
emit Decision(from, to, amount, false);
revert(
string(
abi.encodePacked(
"Transfer not supported:",
Strings.toHexString(to),
" is blacklisted."
)
)
);
}
return true;
}

// --- Admin role management ---

/**
* @dev Grants ADMIN_ROLE to an account. Only callable by an admin.
*/
function setAdmin(address account) external {
grantRole(ADMIN_ROLE, account);
}

/**
* @dev Revokes ADMIN_ROLE from an account. Only callable by an admin.
*/
function revokeAdmin(address account) external {
revokeRole(ADMIN_ROLE, account);
}

/**
* @dev Checks if an account has ADMIN_ROLE.
*/
function isAdminAccount(address account) public view returns (bool) {
return hasRole(ADMIN_ROLE, account);
}

// --- Blocked role management ---

/**
* @dev Grants V1_BLOCKED_ROLE to an account. Only callable by an admin.
*/
function setV1Blocked(address account) external {
grantRole(V1_BLOCKED_ROLE, account);
}

/**
* @dev Revokes V1_BLOCKED_ROLE from an account. Only callable by an admin.
*/
function revokeV1Blocked(address account) external {
revokeRole(V1_BLOCKED_ROLE, account);
}

/**
* @dev Checks if an account has V1_BLOCKED_ROLE.
*/
function isV1Blocked(address account) public view returns (bool) {
return hasRole(V1_BLOCKED_ROLE, account);
}

// --- Blacklisted role management ---

/**
* @dev Grants BLACKLISTED_ROLE to an account. Only callable by an admin.
*/
function setBlacklisted(address account) external {
grantRole(BLACKLISTED_ROLE, account);
}

/**
* @dev Revokes BLACKLISTED_ROLE from an account. Only callable by an admin.
*/
function revokeBlacklisted(address account) external {
revokeRole(BLACKLISTED_ROLE, account);
}

/**
* @dev Checks if an account has BLACKLISTED_ROLE.
*/
function isBlacklisted(address account) public view returns (bool) {
return hasRole(BLACKLISTED_ROLE, account);
}

// --- Frontend role management ---

/**
* @dev Grants V1_FRONTEND_ROLE to an account. Only callable by an admin.
*/
function setV1Frontend(address account) external {
grantRole(V1_FRONTEND_ROLE, account);
}

/**
* @dev Revokes V1_FRONTEND_ROLE from an account. Only callable by an admin.
*/
function revokeV1Frontend(address account) external {
revokeRole(V1_FRONTEND_ROLE, account);
}

/**
* @dev Checks if an account has V1_FRONTEND_ROLE.
*/
function isV1Frontend(address account) public view returns (bool) {
return hasRole(V1_FRONTEND_ROLE, account);
}
}
108 changes: 108 additions & 0 deletions test/Validator.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/Validator.sol";

contract ValidatorTest is Test {
Validator validator;
address owner = address(0x1);
address admin = address(0x2);
address user = address(0x3);
address blocked = address(0x4);
address blacklisted = address(0x5);
address frontend = address(0x6);

function setUp() public {
vm.prank(owner);
validator = new Validator();

// Grant roles for testing
vm.prank(owner);
validator.setAdmin(admin);

vm.prank(admin);
validator.setV1Blocked(blocked);

vm.prank(admin);
validator.setBlacklisted(blacklisted);

vm.prank(admin);
validator.setV1Frontend(frontend);
}

function testAdminRole() public {
assertTrue(validator.isAdminAccount(admin));
vm.prank(owner);
validator.revokeAdmin(admin);
assertFalse(validator.isAdminAccount(admin));
}

function testBlockedRole() public {
assertTrue(validator.isV1Blocked(blocked));
vm.prank(admin);
validator.revokeV1Blocked(blocked);
assertFalse(validator.isV1Blocked(blocked));
}

function testBlacklistedRole() public {
assertTrue(validator.isBlacklisted(blacklisted));
vm.prank(admin);
validator.revokeBlacklisted(blacklisted);
assertFalse(validator.isBlacklisted(blacklisted));
}

function testFrontendRole() public {
assertTrue(validator.isV1Frontend(frontend));
vm.prank(admin);
validator.revokeV1Frontend(frontend);
assertFalse(validator.isV1Frontend(frontend));
}

function testValidateTransfer() public {
// Not blocked or blacklisted
vm.prank(user);
assertTrue(validator.validate(user, admin, 100));

// Blocked by frontend
vm.prank(frontend);
vm.expectRevert("Transfer not supported:0x0000000000000000000000000000000000000004 is blocked in V1. Please use V2 instead. See https://monerium.dev/docs/tokens");
validator.validate(blocked, admin, 100);

vm.prank(frontend);
vm.expectRevert("Transfer not supported:0x0000000000000000000000000000000000000004 is blocked in V1. Please use V2 instead. See https://monerium.dev/docs/tokens");
validator.validate(admin, blocked, 100);

// Blacklisted always reverts
vm.prank(user);
vm.expectRevert("Transfer not supported:0x0000000000000000000000000000000000000005 is blacklisted.");
validator.validate(blacklisted, admin, 100);

vm.prank(user);
vm.expectRevert("Transfer not supported:0x0000000000000000000000000000000000000005 is blacklisted.");
validator.validate(admin, blacklisted, 100);
}

function testContractId() public {
bytes32 expected = 0x5341d189213c4172d0c7256f80bc5f8e6350af3aaff7a029625d8dd94f0f82a5;
assertEq(validator.CONTRACT_ID(), expected);
}

function testIsAdminAccount() public {
assertFalse(validator.isAdminAccount(owner));
assertTrue(validator.isAdminAccount(admin));
assertFalse(validator.isAdminAccount(address(0xdead)));
}
function testIsV1Blocked() public {
assertTrue(validator.isV1Blocked(blocked));
assertFalse(validator.isV1Blocked(address(0xdead)));
}
function testIsBlacklisted() public {
assertTrue(validator.isBlacklisted(blacklisted));
assertFalse(validator.isBlacklisted(address(0xdead)));
}
function testIsV1Frontend() public {
assertTrue(validator.isV1Frontend(frontend));
assertFalse(validator.isV1Frontend(address(0xdead)));
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,11 @@
resolved "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.6.tgz"
integrity sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA==

"@openzeppelin/contracts@^4.9.3":
version "4.9.6"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.6.tgz#2a880a24eb19b4f8b25adc2a5095f2aa27f39677"
integrity sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA==

"@openzeppelin/defender-admin-client@^1.52.0":
version "1.54.6"
resolved "https://registry.npmjs.org/@openzeppelin/defender-admin-client/-/defender-admin-client-1.54.6.tgz"
Expand Down