Skip to content

Commit

Permalink
feat(contracts): add UsernamesV1.sol contract (#773)
Browse files Browse the repository at this point in the history
* 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
sebastijankuzner authored Nov 22, 2024
1 parent 04b8fe5 commit e05a907
Show file tree
Hide file tree
Showing 9 changed files with 1,218 additions and 28 deletions.
1 change: 1 addition & 0 deletions contracts/src/consensus/ConsensusV1.sol
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: GNU GENERAL PUBLIC LICENSE
pragma solidity ^0.8.27;

import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
Expand Down
176 changes: 176 additions & 0 deletions contracts/src/usernames/UsernamesV1.sol
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);
}
}
83 changes: 83 additions & 0 deletions contracts/test/usernames/Usernames-Proxy.sol
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"));
}
}
Loading

0 comments on commit e05a907

Please sign in to comment.