Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
26 changes: 25 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,31 @@ 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 expiry expiry timestamp for the signature
* requires the wallet to sign an EIP-712 typed message binding wallet, identity, and expiry
* 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
*/
function linkWalletWithSignature(
address wallet,
bytes calldata signature,
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
119 changes: 105 additions & 14 deletions contracts/factory/IdFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@
pragma solidity ^0.8.27;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.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 {
contract IdFactory is IIdFactory, Ownable, EIP712 {
uint256 private constant _MAX_WALLETS_PER_IDENTITY = 101;

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

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

Expand All @@ -33,7 +41,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()
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 All @@ -207,17 +215,61 @@ contract IdFactory is IIdFactory, Ownable {
_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);
_unlinkWallet(_oldWallet, _userIdentity[_oldWallet]);
}

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

address identity = msg.sender;
_verifyWalletSignature(wallet, identity, 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()
);

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

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

/**
Expand Down Expand Up @@ -290,6 +342,19 @@ contract IdFactory is IIdFactory, Ownable {
return addr;
}

function _unlinkWallet(address _wallet, address _identity) private {
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);
}

// function used to deploy an identity using CREATE2
function _deployIdentity(
string memory _salt,
Expand All @@ -303,4 +368,30 @@ contract IdFactory is IIdFactory, Ownable {
bytes memory bytecode = abi.encodePacked(_code, _constructData);
return _deploy(_salt, bytecode);
}

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

bytes32 digest = _hashTypedDataV4(structHash);
(address signer, ECDSA.RecoverError error, ) = ECDSA.tryRecover(
digest,
signature
);
require(
error == ECDSA.RecoverError.NoError && signer == wallet,
Errors.InvalidSignature()
);
}
}
43 changes: 3 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 Down Expand Up @@ -84,26 +84,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 +127,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