-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(contracts): add UsernamesV1.sol contract (#773)
* Initial contract * Add username conditions * Add error * getUsername * Prevent double usernames * Allow updates * Add isUsernameRegistered * Add isUsernameValid * Remove empty lines * Add comment * addUsername * resignUsername * Add version * Add emits * Proxy test * getUsernames * Add usernames * Set deployer address in serviceProvider * Extract initialize * Extract blockContext and specId * Extract deploy proxyContracts * Move method * Initialize usernames * Typo * Typo * Include previous username * Update abi
- Loading branch information
1 parent
04b8fe5
commit e05a907
Showing
9 changed files
with
1,218 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
// SPDX-License-Identifier: GNU GENERAL PUBLIC LICENSE | ||
pragma solidity ^0.8.27; | ||
|
||
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; | ||
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; | ||
|
||
error CallerIsNotOwner(); | ||
error CallerIsOwner(); | ||
|
||
error InvalidUsername(); | ||
error TakenUsername(); | ||
error UsernameNotRegistered(); | ||
|
||
event UsernameRegistered(address addr, string username, string previousUsername); | ||
|
||
event UsernameResigned(address addr, string username); | ||
|
||
struct User { | ||
address addr; | ||
string username; | ||
} | ||
|
||
contract UsernamesV1 is Initializable, UUPSUpgradeable { | ||
address private _owner; | ||
|
||
mapping(address => string) private _usernames; | ||
mapping(bytes32 => bool) private _usernameExists; | ||
|
||
// Modifiers | ||
modifier onlyOwner() { | ||
if (msg.sender != _owner) { | ||
revert CallerIsNotOwner(); | ||
} | ||
_; | ||
} | ||
|
||
modifier preventOwner() { | ||
if (msg.sender == _owner) { | ||
revert CallerIsOwner(); | ||
} | ||
_; | ||
} | ||
|
||
// Initializers | ||
function initialize() public initializer { | ||
_owner = msg.sender; | ||
} | ||
|
||
// Overrides | ||
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} | ||
|
||
// External functions | ||
function addUsername(address user, string memory username) external onlyOwner { | ||
bytes memory b = bytes(username); | ||
// Full username verification is intentionally skipped for backwards compatibility | ||
if (b.length < 1 || b.length > 20) { | ||
revert InvalidUsername(); | ||
} | ||
|
||
_registerUsername(user, username, b); | ||
} | ||
|
||
function registerUsername(string memory username) external preventOwner { | ||
// Register username | ||
bytes memory b = bytes(username); | ||
if (!_verifyUsername(b)) { | ||
revert InvalidUsername(); | ||
} | ||
|
||
_registerUsername(msg.sender, username, b); | ||
} | ||
|
||
function resignUsername() external { | ||
string memory username = _usernames[msg.sender]; | ||
// If user already has a username | ||
if (bytes(username).length > 0) { | ||
_usernameExists[keccak256(bytes(_usernames[msg.sender]))] = false; | ||
delete _usernames[msg.sender]; | ||
|
||
emit UsernameResigned(msg.sender, username); | ||
} else { | ||
revert UsernameNotRegistered(); | ||
} | ||
} | ||
|
||
// External functions that are view | ||
function version() external pure returns (uint256) { | ||
return 1; | ||
} | ||
|
||
function getUsername(address user) external view returns (string memory) { | ||
return _usernames[user]; | ||
} | ||
|
||
function isUsernameRegistered(string memory username) external view returns (bool) { | ||
return _usernameExists[keccak256(bytes(username))]; | ||
} | ||
|
||
function isUsernameValid(string memory username) external pure returns (bool) { | ||
return _verifyUsername(bytes(username)); | ||
} | ||
|
||
function getUsernames(address[] calldata addresses) external view returns (User[] memory) { | ||
User[] memory users = new User[](addresses.length); | ||
uint256 count = 0; | ||
for (uint256 i = 0; i < addresses.length; i++) { | ||
if (bytes(_usernames[addresses[i]]).length != 0) { | ||
users[count++] = User(addresses[i], _usernames[addresses[i]]); | ||
} | ||
} | ||
|
||
// Slice the array to remove empty slots | ||
User[] memory result = new User[](count); | ||
for (uint256 i = 0; i < count; i++) { | ||
result[i] = users[i]; | ||
} | ||
|
||
return result; | ||
} | ||
|
||
// Internal function | ||
// RULES: | ||
// minimum length of 1 character | ||
// maximum length of 20 characters | ||
// only lowercase letters, numbers and underscores are allowed | ||
// cannot start or end with underscore | ||
// cannot contain two or more consecutive underscores | ||
function _verifyUsername(bytes memory username) internal pure returns (bool) { | ||
// minimum length of 1 character | ||
// maximum length of 20 characters | ||
if (username.length < 1 || username.length > 20) { | ||
return false; | ||
} | ||
|
||
// cannot start or end with underscore | ||
if (username[0] == 0x5F || username[username.length - 1] == 0x5F) { | ||
return false; | ||
} | ||
|
||
for (uint256 i = 0; i < username.length; i++) { | ||
// only lowercase letters, numbers and underscores are allowed | ||
if ( | ||
!(username[i] >= 0x30 && username[i] <= 0x39) // 0-9 | ||
&& !(username[i] >= 0x61 && username[i] <= 0x7A) // a-z | ||
&& !(username[i] == 0x5F) // _ | ||
) { | ||
return false; | ||
} | ||
|
||
// No need to care out ot bound access at i + 1, because previous test already check that latest character is not underscore | ||
if (username[i] == 0x5F && username[i + 1] == 0x5F) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
function _registerUsername(address user, string memory username, bytes memory b) internal { | ||
bytes32 usernameHash = keccak256(b); | ||
if (_usernameExists[usernameHash]) { | ||
revert TakenUsername(); | ||
} | ||
|
||
string memory previousUsername = _usernames[user]; | ||
// If user already has a username | ||
if (bytes(previousUsername).length > 0) { | ||
_usernameExists[keccak256(bytes(previousUsername))] = false; // Remove old username | ||
} | ||
|
||
_usernames[user] = username; | ||
_usernameExists[usernameHash] = true; | ||
|
||
emit UsernameRegistered(user, username, previousUsername); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
// SPDX-License-Identifier: GNU GENERAL PUBLIC LICENSE | ||
pragma solidity ^0.8.13; | ||
|
||
import {Test, console} from "@forge-std/Test.sol"; | ||
import { | ||
UsernamesV1, | ||
CallerIsOwner, | ||
CallerIsNotOwner, | ||
InvalidUsername, | ||
TakenUsername, | ||
UsernameNotRegistered, | ||
UsernameRegistered, | ||
UsernameResigned | ||
} from "@contracts/usernames/UsernamesV1.sol"; | ||
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; | ||
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; | ||
|
||
contract UsernamesVTest is UsernamesV1 { | ||
function versionv2() external pure returns (uint256) { | ||
return 99; | ||
} | ||
} | ||
|
||
contract ProxyTest is Test { | ||
UsernamesV1 public usernames; | ||
|
||
function setUp() public { | ||
bytes memory data = abi.encode(UsernamesV1.initialize.selector); | ||
address proxy = address(new ERC1967Proxy(address(new UsernamesV1()), data)); | ||
usernames = UsernamesV1(proxy); | ||
} | ||
|
||
function test_initialize_should_revert() public { | ||
vm.expectRevert(Initializable.InvalidInitialization.selector); | ||
usernames.initialize(); | ||
} | ||
|
||
function test_shoudl_have_valid_UPGRADE_INTERFACE_VERSION() public view { | ||
assertEq(usernames.UPGRADE_INTERFACE_VERSION(), "5.0.0"); | ||
} | ||
|
||
function test_proxy_should_update() public { | ||
assertEq(usernames.version(), 1); | ||
assertEq(usernames.UPGRADE_INTERFACE_VERSION(), "5.0.0"); | ||
usernames.upgradeToAndCall(address(new UsernamesVTest()), bytes("")); | ||
|
||
// Cast proxy to new contract | ||
UsernamesVTest usernamesNew = UsernamesVTest(address(usernames)); | ||
assertEq(usernamesNew.versionv2(), 99); | ||
|
||
// Should keep old data | ||
vm.expectRevert(Initializable.InvalidInitialization.selector); | ||
usernamesNew.initialize(); | ||
} | ||
|
||
function test_proxy_should_update_and_perserve_variables() public { | ||
assertEq(usernames.version(), 1); | ||
|
||
// Register valdiators | ||
usernames.addUsername(address(1), "test"); | ||
usernames.addUsername(address(2), "test2"); | ||
|
||
// Assert | ||
assertEq(usernames.getUsername(address(1)), "test"); | ||
assertEq(usernames.getUsername(address(2)), "test2"); | ||
assertTrue(usernames.isUsernameRegistered("test")); | ||
assertTrue(usernames.isUsernameRegistered("test2")); | ||
|
||
// Upgrade | ||
usernames.upgradeToAndCall(address(new UsernamesVTest()), bytes("")); | ||
|
||
// Cast proxy to new contract | ||
UsernamesVTest usernamesNew = UsernamesVTest(address(usernames)); | ||
assertEq(usernamesNew.versionv2(), 99); | ||
assertEq(usernamesNew.version(), 1); | ||
|
||
// Assert | ||
assertEq(usernamesNew.getUsername(address(1)), "test"); | ||
assertEq(usernamesNew.getUsername(address(2)), "test2"); | ||
assertTrue(usernamesNew.isUsernameRegistered("test")); | ||
assertTrue(usernamesNew.isUsernameRegistered("test2")); | ||
} | ||
} |
Oops, something went wrong.