Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ced8cb5
💥 introduced register-unregister wallet in identity
mohamadhammoud Jan 3, 2026
8203ce4
🚨 fix lint
mohamadhammoud Jan 3, 2026
bc8e6bd
Merge branch 'develop' into feature/BT-1447-global-identity-registry-…
mohamadhammoud Jan 3, 2026
b11f323
💥 used require statement
mohamadhammoud Jan 5, 2026
25c74c2
💥 test coverage
mohamadhammoud Jan 5, 2026
a4271e9
💥 resolve require statement
mohamadhammoud Feb 10, 2026
aad1cbc
🚨 fix lint
mohamadhammoud Feb 11, 2026
1a7c009
added nonce per wallet
mohamadhammoud Feb 23, 2026
777b4c9
🚨 fix linter validation
mohamadhammoud Feb 23, 2026
c0dc8fa
🔧 Merge branch 'develop' into feature/BT-1447-global-identity-registr…
mohamadhammoud Feb 24, 2026
c357fb9
♻️ added eip 712 and resolved unlink method
mohamadhammoud Feb 25, 2026
9875667
Merge branch 'develop' into feature/BT-1447-global-identity-registry-…
mohamadhammoud Mar 5, 2026
1dcc2ad
Merge branch 'develop' into feature/BT-1447-global-identity-registry-…
mohamadhammoud Mar 5, 2026
7445cd6
💥 applied nonce for erc712, linked wallet just to the same ID
mohamadhammoud Mar 6, 2026
25faad4
Merge branch 'develop' into feature/BT-1447-global-identity-registry-…
mohamadhammoud Mar 6, 2026
9434b6f
return format section
mohamadhammoud Mar 6, 2026
ba2332f
fix lint
mohamadhammoud Mar 6, 2026
b88aff1
🔀 Merge branch 'develop' into feature/BT-1447-global-identity-registr…
mohamadhammoud Mar 9, 2026
6c71d8a
🔀 Merge branch 'develop' into feature/BT-1447-global-identity-registr…
mohamadhammoud Mar 9, 2026
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
3 changes: 1 addition & 2 deletions .github/scripts/coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ echo "$COVERAGE_OUTPUT"
echo "======================="

TOTAL_LINE=$(echo "$COVERAGE_OUTPUT" | grep "| Total.*|")

if [ -z "$TOTAL_LINE" ]; then
echo "❌ Could not find Total coverage line"
exit 1
Expand Down Expand Up @@ -44,7 +43,7 @@ fi

if [ $FAIL = 1 ]; then
echo ""
echo "Coverage check failed! All metrics must be 100%"
echo "Coverage check failed! All coverage metrics must be 100%"
exit 1
else
echo "✅ Coverage requirements met!"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- develop
pull_request:
workflow_dispatch:

Expand Down Expand Up @@ -44,4 +45,5 @@ jobs:
run: forge test -vvv

- name: Run coverage
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')
run: ./.github/scripts/coverage.sh
5 changes: 4 additions & 1 deletion .husky/commit-msg
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
npx commitlint --edit $1
#!/usr/bin/env sh
. "$(dirname "$0")/_/h"

npx commitlint --edit $1
5 changes: 4 additions & 1 deletion .husky/post-checkout
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
npm i
#!/usr/bin/env sh
. "$(dirname "$0")/_/h"

npm i
5 changes: 4 additions & 1 deletion .husky/post-merge
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
npm i
#!/usr/bin/env sh
. "$(dirname "$0")/_/h"

npm i
5 changes: 3 additions & 2 deletions .husky/pre-commit
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
npm run lint
.github/scripts/coverage.sh
STAGED_FILES=$(git diff --cached --name-only --diff-filter=d)
forge fmt
echo "$STAGED_FILES" | xargs -r git add
3 changes: 0 additions & 3 deletions contracts/ClaimIssuer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,6 @@ contract ClaimIssuer is IClaimIssuer, Identity, UUPSUpgradeable {
*/
// solhint-disable-next-line func-name-mixedcase
function __ClaimIssuer_init(address initialManagementKey) internal {
// Initialize UUPS upgradeability
__UUPSUpgradeable_init();

// Initialize Identity functionality
__Identity_init(initialManagementKey);
}
Expand Down
2 changes: 1 addition & 1 deletion contracts/IdentityUtilities.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.27;
pragma solidity 0.8.30;

import { IClaimIssuer } from "./interface/IClaimIssuer.sol";
import { IIdentity } from "./interface/IIdentity.sol";
Expand Down
5 changes: 2 additions & 3 deletions contracts/KeyManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@ contract KeyManager is IERC734 {
* Formula: keccak256(abi.encode(uint256(keccak256(bytes(id))) - 1)) & ~bytes32(uint256(0xff))
* where id is the namespace identifier
*/
bytes32 internal constant _KEY_STORAGE_SLOT = keccak256(
abi.encode(uint256(keccak256(bytes("onchainid.keymanager.storage"))) - 1)
) & ~bytes32(uint256(0xff));
bytes32 internal constant _KEY_STORAGE_SLOT =
keccak256(abi.encode(uint256(keccak256(bytes("onchainid.keymanager.storage"))) - 1)) & ~bytes32(uint256(0xff));

/**
* @notice Prevent any direct calls to the implementation contract (marked by _canInteract = false).
Expand Down
38 changes: 35 additions & 3 deletions contracts/factory/IIdFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ interface IIdFactory {
/**
* @dev function used to link a new wallet to an existing identity
* @param _newWallet the address of the wallet to link
* requires msg.sender to be linked to an existing onchainid
* requires msg.sender to be actively linked to an existing onchainid
* the _newWallet will be linked to the same OID contract as msg.sender
* _newWallet cannot be linked to an OID yet
* _newWallet cannot be actively linked to an OID yet
* if _newWallet was previously unlinked, it can only be re-linked to the same identity
* _newWallet cannot be address 0
* cannot link more than 100 wallets to an OID, for gas consumption reason
*/
Expand All @@ -80,12 +81,36 @@ interface IIdFactory {
/**
* @dev function used to unlink a wallet from an existing identity
* @param _oldWallet the address of the wallet to unlink
* requires msg.sender to be linked to the same onchainid as _oldWallet
* requires msg.sender to be actively linked to the same onchainid as _oldWallet
* msg.sender cannot be _oldWallet to keep at least 1 wallet linked to any OID
* _oldWallet cannot be address 0
* unlinked wallets remain bound to their identity and can only be re-linked to the same identity
*/
function unlinkWallet(address _oldWallet) external;

/**
* @dev function used to link a wallet to an identity using signature verification
* @param wallet the address of the wallet to link
* @param signature EIP-712 signature provided by the wallet
* @param nonce the current nonce of the wallet (prevents replay attacks)
* @param expiry expiry timestamp for the signature
* requires the wallet to sign an EIP-712 typed message binding wallet, identity, nonce, and expiry
* requires msg.sender to be the identity contract (called via execute())
* if the wallet was previously unlinked, it can only be re-linked to the same identity
* wallet cannot be address 0
* signature must not be expired
* nonce must match the current nonce of the wallet
*/
function linkWalletWithSignature(address wallet, bytes calldata signature, uint256 nonce, uint256 expiry) external;

/**
* @dev function used to unlink a wallet from an identity, callable by the identity contract
* @param wallet the address of the wallet to unlink
* requires msg.sender to be the identity contract that the wallet is linked to
* wallet cannot be address 0
*/
function unlinkWalletByIdentity(address wallet) external;

/**
* @dev function used to register an address as a token factory
* @param _factory the address of the token factory
Expand Down Expand Up @@ -131,6 +156,13 @@ interface IIdFactory {
*/
function isTokenFactory(address _factory) external view returns (bool);

/**
* @dev getter for the current nonce of a wallet address (used for EIP-712 replay protection)
* @param owner the wallet address to check the nonce for
* @return the current nonce value
*/
function nonces(address owner) external view returns (uint256);

/**
* @dev getter to know if a salt is taken for the create2 deployment
* @param _salt the salt used for deployment
Expand Down
135 changes: 106 additions & 29 deletions contracts/factory/IdFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
pragma solidity ^0.8.27;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import { IERC734 } from "../interface/IERC734.sol";
import { Errors } from "../libraries/Errors.sol";
Expand All @@ -10,7 +14,14 @@ import { KeyTypes } from "../libraries/KeyTypes.sol";
import { IdentityProxy } from "../proxy/IdentityProxy.sol";
import { IIdFactory } from "./IIdFactory.sol";

contract IdFactory is IIdFactory, Ownable {
contract IdFactory is IIdFactory, Ownable, EIP712, Nonces {

using EnumerableSet for EnumerableSet.AddressSet;

uint256 private constant _MAX_WALLETS_PER_IDENTITY = 101;

bytes32 private constant _LINK_WALLET_TYPEHASH =
keccak256("LinkWallet(address wallet,address identity,uint256 nonce,uint256 expiry)");

// address of the _implementationAuthority contract making the link to the implementation contract
address public immutable implementationAuthority;
Expand All @@ -21,11 +32,11 @@ contract IdFactory is IIdFactory, Ownable {
// salt is taken and which is not
mapping(string => bool) private _saltTaken;

// ONCHAINID of the wallet owner
// ONCHAINID of the wallet owner (never cleared — wallet stays bound to its identity forever)
mapping(address => address) private _userIdentity;

// wallets currently linked to an ONCHAINID
mapping(address => address[]) private _wallets;
// wallets actively linked to an ONCHAINID
mapping(address => EnumerableSet.AddressSet) private _wallets;

// ONCHAINID of the token
mapping(address => address) private _tokenIdentity;
Expand All @@ -34,7 +45,7 @@ contract IdFactory is IIdFactory, Ownable {
mapping(address => address) private _tokenAddress;

// setting
constructor(address implementationAuthorityAddress) Ownable(msg.sender) {
constructor(address implementationAuthorityAddress) Ownable(msg.sender) EIP712("IdentityFactory", "1") {
require(implementationAuthorityAddress != address(0), Errors.ZeroAddress());
implementationAuthority = implementationAuthorityAddress;
}
Expand Down Expand Up @@ -71,7 +82,7 @@ contract IdFactory is IIdFactory, Ownable {
address identity = _deployIdentity(oidSalt, _wallet);
_saltTaken[oidSalt] = true;
_userIdentity[_wallet] = identity;
_wallets[identity].push(_wallet);
_wallets[identity].add(_wallet);
emit WalletLinked(_wallet, identity);
return identity;
}
Expand Down Expand Up @@ -105,7 +116,7 @@ contract IdFactory is IIdFactory, Ownable {

_saltTaken[oidSalt] = true;
_userIdentity[_wallet] = identity;
_wallets[identity].push(_wallet);
_wallets[identity].add(_wallet);
emit WalletLinked(_wallet, identity);

return identity;
Expand All @@ -119,7 +130,7 @@ contract IdFactory is IIdFactory, Ownable {
override
returns (address)
{
require(isTokenFactory(msg.sender) || msg.sender == owner(), OwnableUnauthorizedAccount(msg.sender));
require(isTokenFactory(msg.sender) || msg.sender == owner(), Ownable.OwnableUnauthorizedAccount(msg.sender));
require(_token != address(0), Errors.ZeroAddress());
require(_tokenOwner != address(0), Errors.ZeroAddress());
require(keccak256(abi.encode(_salt)) != keccak256(abi.encode("")), Errors.EmptyString());
Expand All @@ -139,14 +150,12 @@ contract IdFactory is IIdFactory, Ownable {
*/
function linkWallet(address _newWallet) external override {
require(_newWallet != address(0), Errors.ZeroAddress());
require(_userIdentity[msg.sender] != address(0), Errors.WalletNotLinkedToIdentity(msg.sender));
require(_userIdentity[_newWallet] == address(0), Errors.WalletAlreadyLinkedToIdentity(_newWallet));
require(_tokenIdentity[_newWallet] == address(0), Errors.TokenAlreadyLinked(_newWallet));
address identity = _userIdentity[msg.sender];
require(_wallets[identity].length < 101, Errors.MaxWalletsPerIdentityExceeded());
_userIdentity[_newWallet] = identity;
_wallets[identity].push(_newWallet);
emit WalletLinked(_newWallet, identity);
require(
identity != address(0) && _wallets[identity].contains(msg.sender),
Errors.WalletNotLinkedToIdentity(msg.sender)
);
_linkWallet(_newWallet, identity);
}

/**
Expand All @@ -155,18 +164,42 @@ contract IdFactory is IIdFactory, Ownable {
function unlinkWallet(address _oldWallet) external override {
require(_oldWallet != address(0), Errors.ZeroAddress());
require(_oldWallet != msg.sender, Errors.CannotBeCalledOnSenderAddress());
require(_userIdentity[msg.sender] == _userIdentity[_oldWallet], Errors.OnlyLinkedWalletCanUnlink());
address _identity = _userIdentity[_oldWallet];
delete _userIdentity[_oldWallet];
uint256 length = _wallets[_identity].length;
for (uint256 i = 0; i < length; i++) {
if (_wallets[_identity][i] == _oldWallet) {
_wallets[_identity][i] = _wallets[_identity][length - 1];
_wallets[_identity].pop();
break;
}
}
emit WalletUnlinked(_oldWallet, _identity);
address identity = _userIdentity[msg.sender];
require(identity != address(0) && _wallets[identity].contains(msg.sender), Errors.OnlyLinkedWalletCanUnlink());
require(
_userIdentity[_oldWallet] == identity && _wallets[identity].contains(_oldWallet),
Errors.OnlyLinkedWalletCanUnlink()
);
_unlinkWallet(_oldWallet, identity);
}

/**
* @dev See {IIdFactory-linkWalletWithSignature}.
*/
function linkWalletWithSignature(address wallet, bytes calldata signature, uint256 nonce, uint256 expiry)
external
override
{
require(wallet != address(0), Errors.ZeroAddress());
require(block.timestamp <= expiry, Errors.ExpiredSignature(signature));

address identity = msg.sender;

_useCheckedNonce(wallet, nonce);
_verifyWalletSignature(wallet, identity, nonce, expiry, signature);
_linkWallet(wallet, identity);
}

/**
* @dev See {IIdFactory-unlinkWalletByIdentity}.
*/
function unlinkWalletByIdentity(address wallet) external override {
require(wallet != address(0), Errors.ZeroAddress());
require(
_userIdentity[wallet] == msg.sender && _wallets[msg.sender].contains(wallet),
Errors.WalletNotLinkedToIdentity(wallet)
);
_unlinkWallet(wallet, msg.sender);
}

/**
Expand All @@ -177,7 +210,11 @@ contract IdFactory is IIdFactory, Ownable {
return _tokenIdentity[_wallet];
}

return _userIdentity[_wallet];
address identity = _userIdentity[_wallet];
if (identity != address(0) && _wallets[identity].contains(_wallet)) {
return identity;
}
return address(0);
}

/**
Expand All @@ -191,7 +228,7 @@ contract IdFactory is IIdFactory, Ownable {
* @dev See {IdFactory-getWallets}.
*/
function getWallets(address _identity) external view override returns (address[] memory) {
return _wallets[_identity];
return _wallets[_identity].values();
}

/**
Expand All @@ -201,6 +238,13 @@ contract IdFactory is IIdFactory, Ownable {
return _tokenAddress[_identity];
}

/**
* @dev See {Nonces-nonces}.
*/
function nonces(address owner) public view override(IIdFactory, Nonces) returns (uint256) {
return super.nonces(owner);
}

/**
* @dev See {IdFactory-isTokenFactory}.
*/
Expand All @@ -226,6 +270,25 @@ contract IdFactory is IIdFactory, Ownable {
return addr;
}

function _linkWallet(address _wallet, address _identity) private {
address boundIdentity = _userIdentity[_wallet];
if (boundIdentity != address(0)) {
require(boundIdentity == _identity, Errors.WalletBoundToAnotherIdentity(_wallet, boundIdentity));
}
require(!_wallets[_identity].contains(_wallet), Errors.WalletAlreadyLinkedToIdentity(_wallet));
require(_tokenIdentity[_wallet] == address(0), Errors.TokenAlreadyLinked(_wallet));
require(_wallets[_identity].length() < _MAX_WALLETS_PER_IDENTITY, Errors.MaxWalletsPerIdentityExceeded());

_userIdentity[_wallet] = _identity;
_wallets[_identity].add(_wallet);
emit WalletLinked(_wallet, _identity);
}

function _unlinkWallet(address _wallet, address _identity) private {
_wallets[_identity].remove(_wallet);
emit WalletUnlinked(_wallet, _identity);
}

// function used to deploy an identity using CREATE2
function _deployIdentity(string memory _salt, address _wallet) private returns (address) {
bytes memory _code = type(IdentityProxy).creationCode;
Expand All @@ -234,4 +297,18 @@ contract IdFactory is IIdFactory, Ownable {
return _deploy(_salt, bytecode);
}

function _verifyWalletSignature(
address wallet,
address identity,
uint256 nonce,
uint256 expiry,
bytes calldata signature
) private view {
bytes32 structHash = keccak256(abi.encode(_LINK_WALLET_TYPEHASH, wallet, identity, nonce, expiry));

bytes32 digest = _hashTypedDataV4(structHash);
(address signer, ECDSA.RecoverError error,) = ECDSA.tryRecover(digest, signature);
require(error == ECDSA.RecoverError.NoError && signer == wallet, Errors.InvalidSignature());
}

}
2 changes: 1 addition & 1 deletion contracts/interface/IIdentityUtilities.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.27;
pragma solidity 0.8.30;

/// @title IIdentityUtilities
/// @notice Interface for a schema registry that maps topic IDs to structured metadata schemas
Expand Down
Loading