📄 BouncerStorage.sol
D-OPEN SOVEREIGN
Documentation & Insights
@dev Directly imports a user account, bypassing all normal validation.
Called exclusively by off-chain migration scripts when redeploying storage.
Handles location code mapping and activeLocations bookkeeping.1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 *
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 *
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15
16// SPDX-License-Identifier: Apache-2.0
17pragma solidity ^0.8.24;
18
19import "./IBouncerStorage.sol";
20import {ErrorLibrary} from "./ErrorLibrary.sol";
21import {Identity} from "./Identity.sol";
22
23contract BouncerStorage is Identity, IBouncerStorage {
24 mapping(address => bool) public _isAuthorized; // contracts allowed to call write methods
25 address public admin; // single admin or multi-sig if you choose
26
27 event AuthorizedAdded(address indexed caller);
28 event AuthorizedRemoved(address indexed caller);
29
30 modifier onlyAdmin() {
31 require(msg.sender == admin, "not admin");
32 _;
33 }
34
35 modifier onlyAuthorized() {
36 require(_isAuthorized[msg.sender], "not authorized");
37 _;
38 }
39
40 modifier only() {
41 require(
42 msg.sender == admin || _isAuthorized[msg.sender],
43 "not admin and not authorized"
44 );
45 _;
46 }
47
48 function isAuthorized(address caller) public view returns (bool) {
49 require(caller != address(0), "zero addr");
50 return _isAuthorized[caller];
51 }
52
53 function addAuthorized(address caller) public onlyAdmin {
54 require(caller != address(0), "zero addr");
55 _isAuthorized[caller] = true;
56 emit AuthorizedAdded(caller);
57 }
58
59 function removeAuthorized(address caller) public onlyAdmin {
60 _isAuthorized[caller] = false;
61 emit AuthorizedRemoved(caller);
62 }
63
64 // Deterministic location counts
65 mapping(bytes32 => uint32) public locationCounts;
66 mapping(bytes32 => uint256) private _locationIndex;
67
68 // Active locations tracking - eliminates the need for loops
69 LocationCount[] public activeLocations; // Array of active location counts
70
71 mapping(address => uint32) private accounts;
72 mapping(uint32 => UserAccount) private _userData;
73
74 // Persistent username-ID linkage (survives unregister)
75 mapping(string => uint32) private _usernameToUserId;
76 mapping(uint32 => string) private _userIdToUsername;
77
78 uint32 private _nbAccounts;
79
80 constructor() Identity() {
81 admin = msg.sender;
82 }
83
84 // Override defineCombinations to satisfy both Identity and IBouncerStorage
85 function defineCombinations(
86 string[] calldata adjectives,
87 string[] calldata verbs
88 ) public override(Identity, IBouncerStorage) onlyAdmin {
89 Identity.defineCombinations(adjectives, verbs);
90 }
91
92
93
94 function _locationKey(string memory country, string memory language) internal pure returns (bytes32) {
95 return keccak256(abi.encodePacked(toLowercase(country), "-", toLowercase(language)));
96 }
97
98 function locationToId(string calldata country, string calldata language) external view returns (uint16) {
99 return uint16(_locationIndex[_locationKey(country, language)]);
100 }
101
102 // ===== Pure helpers replicated for compatibility =====
103 function toLowercase(
104 string memory str
105 ) public pure returns (string memory) {
106 bytes memory strBytes = bytes(str);
107 for (uint256 i = 0; i < strBytes.length; i++) {
108 if (strBytes[i] >= 0x41 && strBytes[i] <= 0x5A) {
109 strBytes[i] = bytes1(uint8(strBytes[i]) + 32);
110 }
111 }
112 return string(strBytes);
113 }
114
115
116
117
118 function getUsernameByAddress(
119 address addr
120 ) public view override(IBouncerStorage, Identity) returns (string memory) {
121 // Delegate to Identity's implementation to avoid storage duplication
122 return Identity.getUsernameByAddress(addr);
123 }
124
125 // ===== Account reads =====
126 function hasAccount(address addr) public view returns (bool ok) {
127 return accounts[addr] > 0;
128 }
129
130 function userIdOf(address addr) public view returns (uint32) {
131 if (!hasAccount(addr)) {
132 revert ErrorLibrary.ErrNoAccount();
133 }
134 return accounts[addr];
135 }
136
137 function userAddr(uint32 userId) public view returns (address addr) {
138 if (userId <= 0 || userId > _nbAccounts) {
139 revert ErrorLibrary.InvalidUserId();
140 }
141 return _userData[userId].addr;
142 }
143
144 function userData(uint32 userId) public view returns (UserAccount memory) {
145 if (userId <= 0 || userId > _nbAccounts) {
146 revert ErrorLibrary.InvalidUserId();
147 }
148 return _userData[userId];
149 }
150
151 function userIdToUsername(
152 uint32 userId
153 ) public view returns (string memory) {
154 if (userId <= 0 || userId > _nbAccounts) {
155 revert ErrorLibrary.InvalidUserId();
156 }
157 return _userIdToUsername[userId];
158 }
159
160 function usernameToUserId(
161 string calldata username
162 ) public view returns (uint32) {
163 return _usernameToUserId[username]; // 0 if not found
164 }
165
166 function nbAccounts() public view returns (uint32) {
167 return _nbAccounts;
168 }
169
170 function userInfos(
171 address addr
172 )
173 public
174 view
175 returns (string memory username, string memory country, string memory language, uint32 newUserId)
176 {
177 if (!hasAccount(addr)) {
178 revert ErrorLibrary.ErrNoAccount();
179 }
180 newUserId = accounts[addr];
181 username = _userData[newUserId].username;
182 country = _userData[newUserId].country;
183 language = _userData[newUserId].language;
184 }
185
186 // ===== Location reads =====
187
188
189
190 function getAccountCountByCountryLanguage(
191 string calldata country,
192 string calldata language
193 ) public view returns (uint32 count) {
194 bytes32 key = _locationKey(country, language);
195 return locationCounts[key];
196 }
197
198 function getAllLocationCounts()
199 public
200 view
201 returns (LocationCount[] memory activeLocationCounts)
202 {
203 return activeLocations;
204 }
205
206 function getActiveLocationCount() public view returns (uint256 count) {
207 return activeLocations.length;
208 }
209
210 // ===== Internal helpers =====
211 function _addLocationToActive(
212 bytes32 key,
213 string memory country,
214 string memory language
215 ) internal {
216 activeLocations.push(
217 LocationCount({
218 country: country,
219 language: language,
220 count: 1
221 })
222 );
223 _locationIndex[key] = activeLocations.length; // 1-based index (0 means not found)
224 }
225
226 function _removeLocationFromActive(bytes32 key) internal {
227 uint256 indexToRemove = _locationIndex[key] - 1; // back to 0-based
228 uint256 lastIndex = activeLocations.length - 1;
229 if (indexToRemove != lastIndex) {
230 LocationCount memory lastLocation = activeLocations[lastIndex];
231 activeLocations[indexToRemove] = lastLocation;
232 bytes32 lastKey = _locationKey(lastLocation.country, lastLocation.language);
233 _locationIndex[lastKey] = indexToRemove + 1; // Update index to 1-based
234 }
235 activeLocations.pop();
236 delete _locationIndex[key];
237 }
238
239 // ===== Account lifecycle =====
240 function newAccount(
241 address caller,
242 string calldata country,
243 string calldata language,
244 uint32 referredBy
245 )
246 external
247 only
248 returns (string memory username, string memory countryStr, string memory languageStr, uint32 newUserId)
249 {
250 if (hasAccount(caller)) {
251 revert ErrorLibrary.ErrAlreadyRegistered();
252 }
253 _nbAccounts++;
254 newUserId = _nbAccounts;
255
256 username = _generateUsername(newUserId);
257 linkUsernameToAddress(caller, username);
258
259 accounts[caller] = newUserId;
260
261 _userData[newUserId] = UserAccount({
262 id: newUserId,
263 addr: caller,
264 username: username,
265 language: language,
266 country: country,
267 registeredAt: uint64(block.timestamp),
268 referredBy: referredBy
269 });
270
271 if (_usernameToUserId[username] == 0) {
272 _usernameToUserId[username] = newUserId;
273 _userIdToUsername[newUserId] = username;
274 }
275
276 bytes32 key = _locationKey(country, language);
277 locationCounts[key]++;
278
279 uint256 sentinel = _locationIndex[key];
280 if (sentinel == 0) {
281 _addLocationToActive(key, country, language);
282 } else {
283 activeLocations[sentinel - 1].count++;
284 }
285
286 return (username, country, language, newUserId);
287 }
288
289 function unregister(address caller) external override onlyAuthorized {
290 if (!hasAccount(caller)) {
291 revert ErrorLibrary.ErrNoAccount();
292 }
293
294 uint32 index = accounts[caller];
295 string memory country = _userData[index].country;
296 string memory language = _userData[index].language;
297 bytes32 key = _locationKey(country, language);
298
299 // Get username before clearing data to unlink from Identity mappings
300 string memory username = _userData[index].username;
301
302 // Clear Identity mappings (from inherited Identity contract)
303 usernames[username] = address(0);
304 usernamesInverted[caller] = "";
305
306 accounts[caller] = 0;
307
308 uint32 cnt = locationCounts[key];
309 if (cnt > 0) {
310 unchecked {
311 locationCounts[key] = cnt - 1;
312 }
313 uint256 sentinel = _locationIndex[key];
314 if (sentinel > 0) {
315 uint32 newCount = activeLocations[sentinel - 1].count - 1;
316 activeLocations[sentinel - 1].count = newCount;
317 if (newCount == 0) {
318 _removeLocationFromActive(key);
319 }
320 }
321 }
322 }
323
324 // ===== Admin bulk-import (migration scripts only) =====
325
326 /**
327 * @dev Directly imports a user account, bypassing all normal validation.
328 * Called exclusively by off-chain migration scripts when redeploying storage.
329 * Handles location code mapping and activeLocations bookkeeping.
330 */
331 function importAccount(
332 uint32 userId,
333 address addr,
334 string calldata username,
335 string calldata country,
336 string calldata language,
337 uint64 registeredAt,
338 uint32 referredBy
339 ) external onlyAdmin {
340 require(addr != address(0), "zero addr");
341 require(userId > 0, "invalid userId");
342
343 // Account address → userId
344 accounts[addr] = userId;
345
346 // User data
347 _userData[userId] = UserAccount({
348 id: userId,
349 addr: addr,
350 username: username,
351 language: language,
352 country: country,
353 registeredAt: registeredAt,
354 referredBy: referredBy
355 });
356
357 // Username linkage (persistent)
358 if (_usernameToUserId[username] == 0) {
359 _usernameToUserId[username] = userId;
360 _userIdToUsername[userId] = username;
361 }
362
363 // Identity mappings (inherited)
364 linkUsernameToAddress(addr, username);
365
366 // Location counts
367 bytes32 key = _locationKey(country, language);
368 locationCounts[key]++;
369
370 uint256 sentinel = _locationIndex[key];
371 if (sentinel == 0) {
372 _addLocationToActive(key, country, language);
373 } else {
374 activeLocations[sentinel - 1].count++;
375 }
376 }
377
378 /// @dev Sets _nbAccounts directly. Call after all importAccount() calls.
379 function setNbAccounts(uint32 count) external onlyAdmin {
380 _nbAccounts = count;
381 }
382
383
384}
385