Skip to content

Commit 29464dd

Browse files
committed
feat: new validator that blocks V1 calls on selected addresses
1 parent 514bee7 commit 29464dd

File tree

4 files changed

+268
-1
lines changed

4 files changed

+268
-1
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
4141
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
4242
"@nomicfoundation/hardhat-verify": "^1.0.0",
43+
"@openzeppelin/contracts": "^4.9.3",
4344
"@openzeppelin/contracts-upgradeable": "^4.9.3",
4445
"@typechain/ethers-v6": "^0.4.0",
4546
"@typechain/hardhat": "^8.0.0",
@@ -52,5 +53,6 @@
5253
"typechain": "^8.2.0",
5354
"typescript": "^5.4.3",
5455
"web3": "^4.7.0"
55-
}
56+
},
57+
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
5658
}

src/Validator.sol

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
5+
import "./IValidator.sol";
6+
7+
/**
8+
* @title Validator
9+
* @dev Role-based access control for transfer validation and account management.
10+
* Uses OpenZeppelin AccessControl for secure role management.
11+
* docs: https://docs.openzeppelin.com/contracts/4.x/api/access#AccessControl
12+
* audit: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/audits/2023-05-v4.9.pdf
13+
*/
14+
contract Validator is AccessControl, IValidator {
15+
// Role identifiers
16+
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
17+
bytes32 public constant V1_BLOCKED_ROLE = keccak256("V1_BLOCKED_ROLE");
18+
bytes32 public constant BLACKLISTED_ROLE = keccak256("BLACKLISTED_ROLE");
19+
bytes32 public constant V1_FRONTEND_ROLE = keccak256("V1_FRONTEND_ROLE");
20+
21+
// Contract identifier for interface compliance
22+
bytes32 private constant ID =
23+
0x5341d189213c4172d0c7256f80bc5f8e6350af3aaff7a029625d8dd94f0f82a5;
24+
25+
/**
26+
* @dev Returns the contract identifier.
27+
*/
28+
function CONTRACT_ID() public pure returns (bytes32) {
29+
return ID;
30+
}
31+
32+
/**
33+
* @dev Sets up initial admin and role relationships.
34+
* The deployer is the default admin.
35+
* ADMIN_ROLE is the admin for blocked, blacklisted, and frontend roles.
36+
*/
37+
constructor() {
38+
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
39+
_setRoleAdmin(V1_BLOCKED_ROLE, ADMIN_ROLE);
40+
_setRoleAdmin(BLACKLISTED_ROLE, ADMIN_ROLE);
41+
_setRoleAdmin(V1_FRONTEND_ROLE, ADMIN_ROLE);
42+
}
43+
44+
/**
45+
* @dev Validates a transfer between two accounts.
46+
* - If called by a V1 frontend, checks if either account is blocked.
47+
* - Always checks if either account is blacklisted.
48+
* @return valid True if transfer is allowed, false otherwise.
49+
*/
50+
function validate(
51+
address from,
52+
address to,
53+
uint256 /* amount */
54+
) external view override returns (bool valid) {
55+
if (isV1Frontend(msg.sender)) {
56+
valid = !(isV1Blocked(from) || isV1Blocked(to));
57+
if (!valid) {
58+
return false;
59+
}
60+
}
61+
if (isBlacklisted(from) || isBlacklisted(to)) {
62+
return false;
63+
}
64+
return true;
65+
}
66+
67+
// --- Admin role management ---
68+
69+
/**
70+
* @dev Grants ADMIN_ROLE to an account. Only callable by an admin.
71+
*/
72+
function setAdmin(address account) external {
73+
grantRole(ADMIN_ROLE, account);
74+
}
75+
76+
/**
77+
* @dev Revokes ADMIN_ROLE from an account. Only callable by an admin.
78+
*/
79+
function revokeAdmin(address account) external {
80+
revokeRole(ADMIN_ROLE, account);
81+
}
82+
83+
/**
84+
* @dev Checks if an account has ADMIN_ROLE.
85+
*/
86+
function isAdminAccount(address account) public view returns (bool) {
87+
return hasRole(ADMIN_ROLE, account);
88+
}
89+
90+
// --- Blocked role management ---
91+
92+
/**
93+
* @dev Grants V1_BLOCKED_ROLE to an account. Only callable by an admin.
94+
*/
95+
function setV1Blocked(address account) external {
96+
grantRole(V1_BLOCKED_ROLE, account);
97+
}
98+
99+
/**
100+
* @dev Revokes V1_BLOCKED_ROLE from an account. Only callable by an admin.
101+
*/
102+
function revokeV1Blocked(address account) external {
103+
revokeRole(V1_BLOCKED_ROLE, account);
104+
}
105+
106+
/**
107+
* @dev Checks if an account has V1_BLOCKED_ROLE.
108+
*/
109+
function isV1Blocked(address account) public view returns (bool) {
110+
return hasRole(V1_BLOCKED_ROLE, account);
111+
}
112+
113+
// --- Blacklisted role management ---
114+
115+
/**
116+
* @dev Grants BLACKLISTED_ROLE to an account. Only callable by an admin.
117+
*/
118+
function setBlacklisted(address account) external {
119+
grantRole(BLACKLISTED_ROLE, account);
120+
}
121+
122+
/**
123+
* @dev Revokes BLACKLISTED_ROLE from an account. Only callable by an admin.
124+
*/
125+
function revokeBlacklisted(address account) external {
126+
revokeRole(BLACKLISTED_ROLE, account);
127+
}
128+
129+
/**
130+
* @dev Checks if an account has BLACKLISTED_ROLE.
131+
*/
132+
function isBlacklisted(address account) public view returns (bool) {
133+
return hasRole(BLACKLISTED_ROLE, account);
134+
}
135+
136+
// --- Frontend role management ---
137+
138+
/**
139+
* @dev Grants V1_FRONTEND_ROLE to an account. Only callable by an admin.
140+
*/
141+
function setV1Frontend(address account) external {
142+
grantRole(V1_FRONTEND_ROLE, account);
143+
}
144+
145+
/**
146+
* @dev Revokes V1_FRONTEND_ROLE from an account. Only callable by an admin.
147+
*/
148+
function revokeV1Frontend(address account) external {
149+
revokeRole(V1_FRONTEND_ROLE, account);
150+
}
151+
152+
/**
153+
* @dev Checks if an account has V1_FRONTEND_ROLE.
154+
*/
155+
function isV1Frontend(address account) public view returns (bool) {
156+
return hasRole(V1_FRONTEND_ROLE, account);
157+
}
158+
}

test/Validator.t.sol

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import "forge-std/Test.sol";
5+
import "../src/Validator.sol";
6+
7+
contract ValidatorTest is Test {
8+
Validator validator;
9+
address owner = address(0x1);
10+
address admin = address(0x2);
11+
address user = address(0x3);
12+
address blocked = address(0x4);
13+
address blacklisted = address(0x5);
14+
address frontend = address(0x6);
15+
16+
function setUp() public {
17+
vm.prank(owner);
18+
validator = new Validator();
19+
20+
// Grant roles for testing
21+
vm.prank(owner);
22+
validator.setAdmin(admin);
23+
24+
vm.prank(admin);
25+
validator.setV1Blocked(blocked);
26+
27+
vm.prank(admin);
28+
validator.setBlacklisted(blacklisted);
29+
30+
vm.prank(admin);
31+
validator.setV1Frontend(frontend);
32+
}
33+
34+
function testAdminRole() public {
35+
assertTrue(validator.isAdminAccount(admin));
36+
vm.prank(owner);
37+
validator.revokeAdmin(admin);
38+
assertFalse(validator.isAdminAccount(admin));
39+
}
40+
41+
function testBlockedRole() public {
42+
assertTrue(validator.isV1Blocked(blocked));
43+
vm.prank(admin);
44+
validator.revokeV1Blocked(blocked);
45+
assertFalse(validator.isV1Blocked(blocked));
46+
}
47+
48+
function testBlacklistedRole() public {
49+
assertTrue(validator.isBlacklisted(blacklisted));
50+
vm.prank(admin);
51+
validator.revokeBlacklisted(blacklisted);
52+
assertFalse(validator.isBlacklisted(blacklisted));
53+
}
54+
55+
function testFrontendRole() public {
56+
assertTrue(validator.isV1Frontend(frontend));
57+
vm.prank(admin);
58+
validator.revokeV1Frontend(frontend);
59+
assertFalse(validator.isV1Frontend(frontend));
60+
}
61+
62+
function testValidateTransfer() public {
63+
// Not blocked or blacklisted
64+
vm.prank(user);
65+
assertTrue(validator.validate(user, admin, 100));
66+
67+
// Blocked by frontend
68+
vm.prank(frontend);
69+
assertFalse(validator.validate(blocked, admin, 100));
70+
vm.prank(frontend);
71+
assertFalse(validator.validate(admin, blocked, 100));
72+
73+
// Blacklisted always fails
74+
vm.prank(user);
75+
assertFalse(validator.validate(blacklisted, admin, 100));
76+
vm.prank(user);
77+
assertFalse(validator.validate(admin, blacklisted, 100));
78+
}
79+
80+
function testContractId() public {
81+
bytes32 expected = 0x5341d189213c4172d0c7256f80bc5f8e6350af3aaff7a029625d8dd94f0f82a5;
82+
assertEq(validator.CONTRACT_ID(), expected);
83+
}
84+
85+
function testIsAdminAccount() public {
86+
assertFalse(validator.isAdminAccount(owner));
87+
assertTrue(validator.isAdminAccount(admin));
88+
assertFalse(validator.isAdminAccount(address(0xdead)));
89+
}
90+
function testIsV1Blocked() public {
91+
assertTrue(validator.isV1Blocked(blocked));
92+
assertFalse(validator.isV1Blocked(address(0xdead)));
93+
}
94+
function testIsBlacklisted() public {
95+
assertTrue(validator.isBlacklisted(blacklisted));
96+
assertFalse(validator.isBlacklisted(address(0xdead)));
97+
}
98+
function testIsV1Frontend() public {
99+
assertTrue(validator.isV1Frontend(frontend));
100+
assertFalse(validator.isV1Frontend(address(0xdead)));
101+
}
102+
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,11 @@
686686
resolved "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.6.tgz"
687687
integrity sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA==
688688

689+
"@openzeppelin/contracts@^4.9.3":
690+
version "4.9.6"
691+
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.6.tgz#2a880a24eb19b4f8b25adc2a5095f2aa27f39677"
692+
integrity sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA==
693+
689694
"@openzeppelin/defender-admin-client@^1.52.0":
690695
version "1.54.6"
691696
resolved "https://registry.npmjs.org/@openzeppelin/defender-admin-client/-/defender-admin-client-1.54.6.tgz"

0 commit comments

Comments
 (0)