Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ artifacts
coverage.json
cache
typechain-types

out
2 changes: 1 addition & 1 deletion contracts/IdentityUtilities.sol
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ contract IdentityUtilities is
bytes memory signature,
bytes memory data
) internal view returns (bool) {
if (issuer == address(0)) return false;
if (issuer == address(0) || issuer.code.length == 0) return false;
try
IClaimIssuer(issuer).isClaimValid(
IIdentity(identity),
Expand Down
35 changes: 34 additions & 1 deletion contracts/factory/IIdFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pragma solidity ^0.8.27;

interface IIdFactory {
/// events

// event emitted whenever a single contract is deployed by the factory
event Deployed(address indexed _addr);

Expand Down Expand Up @@ -95,6 +94,34 @@ interface IIdFactory {
*/
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 signature provided by the wallet
* @param nonce replay protection nonce, must match the current nonce for the wallet
* @param expiry expiry timestamp for the signature
* requires the wallet to sign a message binding wallet, identity, nonce, expiry, contract and chain id
* requires the wallet to hold a MANAGEMENT key on the identity
* requires msg.sender to be the identity contract
* wallet cannot be address 0
* signature must not be expired
* nonce must match the current wallet nonce (replay protection)
*/
function linkWalletWithSignature(
address wallet,
bytes calldata signature,
uint256 nonce,
uint256 expiry
) external;

/**
* @dev function used to unlink a wallet from an identity via 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 unlinkWalletWithSignature(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 @@ -148,6 +175,12 @@ interface IIdFactory {
*/
function isSaltTaken(string calldata _salt) external view returns (bool);

/**
* @dev getter for the current nonce of a wallet, used for signature replay protection
* @param wallet the address of the wallet
*/
function walletNonce(address wallet) external view returns (uint256);

/**
* @dev getter for the implementation authority used by this factory.
*/
Expand Down
116 changes: 115 additions & 1 deletion contracts/factory/IdFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@
pragma solidity ^0.8.27;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

import { IdentityProxy } from "../proxy/IdentityProxy.sol";
import { IIdFactory } from "./IIdFactory.sol";
import { IERC734 } from "../interface/IERC734.sol";
import { IIdentity } from "../interface/IIdentity.sol";
import { Errors } from "../libraries/Errors.sol";
import { KeyPurposes } from "../libraries/KeyPurposes.sol";
import { KeyTypes } from "../libraries/KeyTypes.sol";

contract IdFactory is IIdFactory, Ownable {
uint256 private constant _MAX_WALLETS_PER_IDENTITY = 101;

// address of the _implementationAuthority contract making the link to the implementation contract
address public immutable implementationAuthority;

Expand All @@ -32,6 +37,9 @@ contract IdFactory is IIdFactory, Ownable {
// token linked to an ONCHAINID
mapping(address => address) private _tokenAddress;

// nonce per wallet for signature replay protection
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inherits OpenZeppelin Nonces instead of custom implementation

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapping(address => uint256) private _walletNonces;

// setting
constructor(address implementationAuthorityAddress) Ownable(msg.sender) {
require(
Expand Down Expand Up @@ -186,7 +194,7 @@ contract IdFactory is IIdFactory, Ownable {
);
address identity = _userIdentity[msg.sender];
require(
_wallets[identity].length < 101,
_wallets[identity].length < _MAX_WALLETS_PER_IDENTITY,
Errors.MaxWalletsPerIdentityExceeded()
);
_userIdentity[_newWallet] = identity;
Expand Down Expand Up @@ -220,6 +228,74 @@ contract IdFactory is IIdFactory, Ownable {
emit WalletUnlinked(_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));
require(nonce == _walletNonces[wallet], Errors.InvalidNonce(nonce));

address identity = msg.sender;
_verifyWalletSignature(wallet, identity, nonce, expiry, signature);

// require the wallet is a MANAGEMENT key on the identity
bytes32 key = keccak256(abi.encode(wallet));
require(
IIdentity(identity).keyHasPurpose(key, KeyPurposes.MANAGEMENT),
Errors.KeyDoesNotHavePurpose(key, KeyPurposes.MANAGEMENT)
);

// Check if wallet is already linked
require(
_userIdentity[wallet] == address(0),
Errors.WalletAlreadyLinkedToIdentity(wallet)
);
require(
_tokenIdentity[wallet] == address(0),
Errors.TokenAlreadyLinked(wallet)
);

// Check max wallets per identity
require(
_wallets[identity].length < _MAX_WALLETS_PER_IDENTITY,
Errors.MaxWalletsPerIdentityExceeded()
);

_walletNonces[wallet]++;
_userIdentity[wallet] = identity;
_wallets[identity].push(wallet);
emit WalletLinked(wallet, identity);
}

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

address identity = _userIdentity[wallet];
delete _userIdentity[wallet];
uint256 length = _wallets[identity].length;
for (uint256 i = 0; i < length; i++) {
if (_wallets[identity][i] == wallet) {
_wallets[identity][i] = _wallets[identity][length - 1];
_wallets[identity].pop();
break;
}
}
emit WalletUnlinked(wallet, identity);
}

/**
* @dev See {IdFactory-getIdentity}.
*/
Expand Down Expand Up @@ -260,6 +336,15 @@ contract IdFactory is IIdFactory, Ownable {
return _tokenAddress[_identity];
}

/**
* @dev See {IIdFactory-walletNonce}.
*/
function walletNonce(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove (OZ Nonces inherits)

address wallet
) external view override returns (uint256) {
return _walletNonces[wallet];
}

/**
* @dev See {IdFactory-isTokenFactory}.
*/
Expand Down Expand Up @@ -303,4 +388,33 @@ contract IdFactory is IIdFactory, Ownable {
bytes memory bytecode = abi.encodePacked(_code, _constructData);
return _deploy(_salt, bytecode);
}

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

bytes32 digest = MessageHashUtils.toEthSignedMessageHash(structHash);
(address signer, ECDSA.RecoverError error, ) = ECDSA.tryRecover(
digest,
signature
);
require(
error == ECDSA.RecoverError.NoError && signer == wallet,
Errors.InvalidSignature()
);
}
}
46 changes: 6 additions & 40 deletions contracts/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ library Errors {
/// @notice Reverts if the factory is already registered
error AlreadyAFactory(address factory);

/// @notice Reverts when the recovered signer does not match the wallet being registered
error InvalidSignature();

/// @notice Reverts if the function is called on the sender address
error CannotBeCalledOnSenderAddress();

Expand All @@ -32,9 +35,6 @@ library Errors {
/// @notice Reverts if the only linked wallet tries to unlink
error OnlyLinkedWalletCanUnlink();

/// @notice Reverts if the account is not authorized to call the function
error OwnableUnauthorizedAccount(address account); // TODO: OZ

/// @notice Reverts if the salt is taken
error SaltTaken(string salt);

Expand All @@ -50,6 +50,9 @@ library Errors {
/// @notice Reverts if the wallet is not linked to an identity
error WalletNotLinkedToIdentity(address wallet);

/// @notice Reverts if the nonce does not match the expected wallet nonce
error InvalidNonce(uint256 nonce);

/* ----- Gateway ----- */

/// @notice The maximum number of signers was reached at deployment.
Expand Down Expand Up @@ -84,26 +87,6 @@ library Errors {
/// @notice The initialization failed.
error InitializationFailed();

/* ----- Verifier ----- */

/// @notice The claim topic already exists.
error ClaimTopicAlreadyExists(uint256 claimTopic);

/// @notice The maximum number of claim topics is exceeded.
error MaxClaimTopicsExceeded();

/// @notice The maximum number of trusted issuers is exceeded.
error MaxTrustedIssuersExceeded();

/// @notice The trusted issuer already exists.
error TrustedIssuerAlreadyExists(address trustedIssuer);

/// @notice The trusted claim topics cannot be empty.
error TrustedClaimTopicsCannotBeEmpty();

/// @notice The trusted issuer does not exist.
error NotATrustedIssuer(address trustedIssuer);

/* ----- ClaimIssuer ----- */

/// @notice The claim already exists.
Expand Down Expand Up @@ -147,23 +130,6 @@ library Errors {
/// @notice The claim is invalid.
error InvalidClaim();

/* ----- IdentityUtilities ----- */

/// @notice 0 is not a valid topic.
error EmptyTopic();

/// @notice 0 is not a valid Format.
error EmptyFormat();

/// @notice Name cannot be left empty.
error EmptyName();

/// @notice Use update function for existing topics.
error TopicAlreadyExists(uint256 topic);

/// @notice Topic is not registered yet.
error TopicNotFound(uint256 topic);

/* ----- ClaimIssuerFactory ----- */

/// @notice The claim issuer already exists.
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default [
"artifacts/**",
"cache/**",
"coverage/**",
"dependencies/**",
"**/coverage/**",
"**/node_modules/**",
"**/coverage/lcov-report/**",
Expand Down
Loading
Loading