diff --git a/examples/Identity/AbstractIdentifiedERC20.sol b/examples/Identity/AbstractIdentifiedERC20.sol index 7fdf4bf6..ee45ce5e 100644 --- a/examples/Identity/AbstractIdentifiedERC20.sol +++ b/examples/Identity/AbstractIdentifiedERC20.sol @@ -16,6 +16,10 @@ abstract contract AbstractIdentifiedERC20 is EIP712WithModifier { rulesContract = ERC20Rules(_rulesAddr); } + function identifiers() public view returns (string[] memory) { + return rulesContract.getIdentifiers(); + } + function balanceOf( address wallet, bytes32 publicKey, diff --git a/examples/Identity/ERC20Rules.sol b/examples/Identity/ERC20Rules.sol index 6524727a..74273f14 100644 --- a/examples/Identity/ERC20Rules.sol +++ b/examples/Identity/ERC20Rules.sol @@ -13,41 +13,79 @@ import "../../lib/TFHE.sol"; contract ERC20Rules { address immutable _this; + string[] public identifiers; + constructor() { _this = address(this); + identifiers = ["country", "blacklist"]; + } + + function getIdentifiers() public view returns (string[] memory) { + return identifiers; } function transfer( Identity identityContract, address from, address to, - euint32 _amount + euint32 amount ) public view returns (euint32) { require(address(this) != _this, "isTransferable must be called with delegatecall"); // Condition 1: 10k limit between two different countries + ebool transferLimitOK = checkLimitTransfer(identityContract, from, to, amount); + + ebool condition = transferLimitOK; + + // Condition 2: Check that noone is blacklisted + // ebool blacklistOK = checkBlacklist(identityContract, from, to); + + // condition = TFHE.and(condition, blacklistOK); + + // Condition 3: Check country to country rules + ebool c2cOK = checkCountryToCountry(identityContract, from, to); + + condition = TFHE.and(condition, c2cOK); + + return TFHE.cmux(condition, amount, TFHE.asEuint32(0)); + } + + function checkLimitTransfer( + Identity identityContract, + address from, + address to, + euint32 amount + ) internal view returns (ebool) { euint8 fromCountry = identityContract.getCountry(from); euint8 toCountry = identityContract.getCountry(to); require(TFHE.isInitialized(fromCountry) && TFHE.isInitialized(toCountry), "You don't have access"); ebool sameCountry = TFHE.eq(fromCountry, toCountry); - ebool amountAbove10k = TFHE.gt(_amount, 10000); - ebool countryCondition = TFHE.cmux( - sameCountry, - TFHE.asEbool(true), - TFHE.cmux(amountAbove10k, TFHE.asEbool(false), TFHE.asEbool(true)) - ); + ebool amountBelow10k = TFHE.le(amount, 10000); - // Condition 2: Check that noone is blacklisted + return TFHE.or(sameCountry, amountBelow10k); + } + + function checkBlacklist(Identity identityContract, address from, address to) internal view returns (ebool) { ebool fromBlacklisted = TFHE.asEbool(identityContract.getIdentifier(from, "blacklist")); ebool toBlacklisted = TFHE.asEbool(identityContract.getIdentifier(to, "blacklist")); - ebool whitelisted = TFHE.not(TFHE.and(toBlacklisted, fromBlacklisted)); + return TFHE.not(TFHE.and(toBlacklisted, fromBlacklisted)); + } + + function checkCountryToCountry(Identity identityContract, address from, address to) internal view returns (ebool) { + // Disallow transfer from country 2 to country 1 + euint16[1] memory c2cRestrictions = [TFHE.shl(TFHE.asEuint16(2), 8) + TFHE.asEuint16(1)]; + + euint8 fromCountry = identityContract.getCountry(from); + euint8 toCountry = identityContract.getCountry(to); + require(TFHE.isInitialized(fromCountry) && TFHE.isInitialized(toCountry), "You don't have access"); + euint16 countryToCountry = TFHE.shl(TFHE.asEuint16(fromCountry), 8) + TFHE.asEuint16(toCountry); + ebool condition = TFHE.asEbool(true); - euint32 amount = TFHE.cmux( - countryCondition, - TFHE.cmux(whitelisted, _amount, TFHE.asEuint32(0)), - TFHE.asEuint32(0) - ); + // Check all countryToCountry restrictions + for (uint i = 0; i < c2cRestrictions.length; i++) { + condition = TFHE.and(condition, TFHE.ne(c2cRestrictions[i], countryToCountry)); + } - return amount; + return condition; } } diff --git a/examples/Identity/Identity.sol b/examples/Identity/Identity.sol index 30b14433..7ef924fd 100644 --- a/examples/Identity/Identity.sol +++ b/examples/Identity/Identity.sol @@ -15,7 +15,7 @@ import "../../abstracts/EIP712WithModifier.sol"; import "../../lib/TFHE.sol"; contract Identity is EIP712WithModifier, Ownable { - // A mapping from did to an identity. + // A mapping from wallet to an identity. mapping(address => UserIdentity) internal identities; struct UserIdentity { @@ -24,7 +24,7 @@ contract Identity is EIP712WithModifier, Ownable { mapping(string => euint32) identifiers; } - mapping(address => mapping(address => mapping(string => bool))) permissions; // user => contracts => identifiers[] + mapping(address => mapping(address => mapping(string => bool))) permissions; // users => contracts => identifiers[] event NewDid(address wallet); event RemoveDid(address wallet); @@ -104,11 +104,11 @@ contract Identity is EIP712WithModifier, Ownable { } // User handling permission permission - function givePermission(string calldata identifier, address allowed) public { + function grantAccess(address allowed, string calldata identifier) public { permissions[msg.sender][allowed][identifier] = true; } - function removePermission(string calldata identifier, address allowed) public { + function revokeAccess(address allowed, string calldata identifier) public { delete permissions[msg.sender][allowed][identifier]; } @@ -118,8 +118,9 @@ contract Identity is EIP712WithModifier, Ownable { return _getCountry(wallet); } - function _getCountry(address wallet) internal view onlyAllowed(wallet, "country") returns (euint8) { - require(TFHE.isInitialized(identities[wallet].country), "This wallet isn't registered"); + function _getCountry( + address wallet + ) internal view onlyExistingWallet(wallet) onlyAllowed(wallet, "country") returns (euint8) { return identities[wallet].country; } diff --git a/test/identity/identifiedERC20.ts b/test/identity/identifiedERC20.ts index 2c2e7587..07153d11 100644 --- a/test/identity/identifiedERC20.ts +++ b/test/identity/identifiedERC20.ts @@ -40,64 +40,68 @@ describe('IdentifiedERC20', function () { await Promise.all([tx1.wait(), tx2.wait(), tx3.wait(), tx4.wait()]); // Give permissions - - const txP1 = await this.identity.connect(this.signers.alice).givePermission('country', this.contractAddress); - const txP2issuer = await this.identity.connect(this.signers.bob).givePermission('issuer', this.contractAddress); - const txP2 = await this.identity.connect(this.signers.bob).givePermission('country', this.contractAddress); - const txP3 = await this.identity.connect(this.signers.carol).givePermission('country', this.contractAddress); - const txP4 = await this.identity.connect(this.signers.dave).givePermission('country', this.contractAddress); - await Promise.all([txP2issuer.wait(), txP1.wait(), txP2.wait(), txP3.wait(), txP4.wait()]); - - const txB1 = await this.identity.connect(this.signers.alice).givePermission('blacklist', this.contractAddress); - const txB2 = await this.identity.connect(this.signers.bob).givePermission('blacklist', this.contractAddress); - const txB3 = await this.identity.connect(this.signers.carol).givePermission('blacklist', this.contractAddress); - const txB4 = await this.identity.connect(this.signers.dave).givePermission('blacklist', this.contractAddress); - await Promise.all([txB1.wait(), txB2.wait(), txB3.wait(), txB4.wait()]); + const list = await this.identifiedErc20.identifiers(); + await list.reduce(async (p, identifier) => { + return p.then(async () => { + const txs = await Promise.all([ + this.identity.connect(this.signers.alice).grantAccess(this.contractAddress, identifier), + this.identity.connect(this.signers.bob).grantAccess(this.contractAddress, identifier), + this.identity.connect(this.signers.carol).grantAccess(this.contractAddress, identifier), + this.identity.connect(this.signers.dave).grantAccess(this.contractAddress, identifier), + ]); + + await Promise.all(txs.map((tx) => tx.wait())); + }); + }, Promise.resolve()); + + const txIssuer = await this.identity.connect(this.signers.bob).grantAccess(this.contractAddress, 'issuer'); + await txIssuer.wait(); + + const amount20k = this.instances.alice.encrypt32(20000); + const amount10k = this.instances.alice.encrypt32(10000); const encryptedAmount = this.instances.alice.encrypt32(100000); const transaction = await this.identifiedErc20.mint(encryptedAmount); await transaction.wait(); - const encryptedTransferAmount = this.instances.alice.encrypt32(20000); - const txT1 = await this.identifiedErc20['transfer(address,bytes)'](this.signers.carol, encryptedTransferAmount); - // Transmit 20000 tokens to dave is possible since alice is admin - const txT2 = await this.identifiedErc20['transfer(address,bytes)'](this.signers.dave, encryptedTransferAmount); + const txT1 = await this.identifiedErc20['transfer(address,bytes)'](this.signers.carol, amount20k); + const txT2 = await this.identifiedErc20['transfer(address,bytes)'](this.signers.dave, amount10k); await Promise.all([txT1.wait(), txT2.wait()]); - // Call the method - const token = this.instances.bob.getTokenSignature(this.contractAddress) || { - signature: '', - publicKey: '', - }; + // // Call the method + // const token = this.instances.bob.getTokenSignature(this.contractAddress) || { + // signature: '', + // publicKey: '', + // }; - const encryptedBalance = await this.identifiedErc20 - .connect(this.signers.bob) - ['balanceOf(address,bytes32,bytes)'](this.signers.carol, token.publicKey, token.signature); + // const encryptedBalance = await this.identifiedErc20 + // .connect(this.signers.bob) + // ['balanceOf(address,bytes32,bytes)'](this.signers.carol, token.publicKey, token.signature); - // Decrypt the balance - const balance = this.instances.bob.decrypt(this.contractAddress, encryptedBalance); - expect(balance).to.equal(20000); + // // Decrypt the balance + // const balance = this.instances.bob.decrypt(this.contractAddress, encryptedBalance); + // expect(balance).to.equal(20000); - const daveBalance = this.identifiedErc20 - .connect(this.signers.bob) - ['balanceOf(address,bytes32,bytes)'](this.signers.dave.address, token.publicKey, token.signature); + // const daveBalance = this.identifiedErc20 + // .connect(this.signers.bob) + // ['balanceOf(address,bytes32,bytes)'](this.signers.dave.address, token.publicKey, token.signature); - expect(daveBalance).to.throw; + // expect(daveBalance).to.throw; - const carolToken = this.instances.carol.getTokenSignature(this.contractAddress) || { - signature: '', - publicKey: '', - }; + // const carolToken = this.instances.carol.getTokenSignature(this.contractAddress) || { + // signature: '', + // publicKey: '', + // }; - // It must throw since Carol is not owner of the identity contract - const bobBalance = this.identifiedErc20 - .connect(this.signers.carol) - ['balanceOf(address,bytes32,bytes)'](this.signers.bob.address, carolToken.publicKey, carolToken.signature); + // // It must throw since Carol is not owner of the identity contract + // const bobBalance = this.identifiedErc20 + // .connect(this.signers.carol) + // ['balanceOf(address,bytes32,bytes)'](this.signers.bob.address, carolToken.publicKey, carolToken.signature); - expect(bobBalance).to.throw; + // expect(bobBalance).to.throw; }); - it('should allow decryption of balance for identity owner', async function () { + it('should prevent transfers', async function () { // Create accounts; const country1 = this.instances.alice.encrypt8(1); const country2 = this.instances.alice.encrypt8(2); @@ -113,58 +117,76 @@ describe('IdentifiedERC20', function () { await Promise.all([tx1.wait(), tx2.wait(), tx3.wait(), tx4.wait()]); // Give permissions - - const txP1 = await this.identity.connect(this.signers.alice).givePermission('country', this.contractAddress); - const txP2issuer = await this.identity.connect(this.signers.bob).givePermission('issuer', this.contractAddress); - const txP2 = await this.identity.connect(this.signers.bob).givePermission('country', this.contractAddress); - const txP3 = await this.identity.connect(this.signers.carol).givePermission('country', this.contractAddress); - const txP4 = await this.identity.connect(this.signers.dave).givePermission('country', this.contractAddress); - await Promise.all([txP2issuer.wait(), txP1.wait(), txP2.wait(), txP3.wait(), txP4.wait()]); - - const txB1 = await this.identity.connect(this.signers.alice).givePermission('blacklist', this.contractAddress); - const txB2 = await this.identity.connect(this.signers.bob).givePermission('blacklist', this.contractAddress); - const txB3 = await this.identity.connect(this.signers.carol).givePermission('blacklist', this.contractAddress); - const txB4 = await this.identity.connect(this.signers.dave).givePermission('blacklist', this.contractAddress); - await Promise.all([txB1.wait(), txB2.wait(), txB3.wait(), txB4.wait()]); + const list = await this.identifiedErc20.identifiers(); + await list.reduce(async (p, identifier) => { + return p.then(async () => { + const txs = await Promise.all([ + this.identity.connect(this.signers.alice).grantAccess(this.contractAddress, identifier), + this.identity.connect(this.signers.bob).grantAccess(this.contractAddress, identifier), + this.identity.connect(this.signers.carol).grantAccess(this.contractAddress, identifier), + this.identity.connect(this.signers.dave).grantAccess(this.contractAddress, identifier), + ]); + + await Promise.all(txs.map((tx) => tx.wait())); + }); + }, Promise.resolve()); const encryptedAmount = this.instances.alice.encrypt32(100000); const transaction = await this.identifiedErc20.mint(encryptedAmount); await transaction.wait(); - const encryptedTransferAmount = this.instances.alice.encrypt32(20000); - const txT1 = await this.identifiedErc20['transfer(address,bytes)'](this.signers.carol, encryptedTransferAmount); + const amount20k = this.instances.alice.encrypt32(20000); + const amount10k = this.instances.alice.encrypt32(10000); + const amount3k = this.instances.alice.encrypt32(3000); + const txT1 = await this.identifiedErc20['transfer(address,bytes)'](this.signers.carol, amount20k); // Transmit 20000 tokens to dave is possible since alice is admin - const txT2 = await this.identifiedErc20['transfer(address,bytes)'](this.signers.dave, encryptedTransferAmount); + const txT2 = await this.identifiedErc20['transfer(address,bytes)'](this.signers.dave, amount10k); await Promise.all([txT1.wait(), txT2.wait()]); - // Carol try to transfer 150000 to dave - - const transfer1 = await this.identifiedErc20 - .connect(this.signers.carol) - ['transfer(address,bytes)'](this.signers.dave, encryptedTransferAmount); - await transfer1.wait(); + const carolToken = this.instances.carol.getTokenSignature(this.contractAddress) || { + signature: '', + publicKey: '', + }; - const token = this.instances.carol.getTokenSignature(this.contractAddress) || { + const daveToken = this.instances.dave.getTokenSignature(this.contractAddress) || { signature: '', publicKey: '', }; + // Check that it's not possible to transfer from country 1 to country 2 an amount above 10k + const transfer1 = await this.identifiedErc20 + .connect(this.signers.carol) + ['transfer(address,bytes)'](this.signers.dave, amount20k); + await transfer1.wait(); + const encryptedBalance = await this.identifiedErc20 .connect(this.signers.carol) - ['balanceOf(bytes32,bytes)'](token.publicKey, token.signature); + ['balanceOf(bytes32,bytes)'](carolToken.publicKey, carolToken.signature); const balance = this.instances.carol.decrypt(this.contractAddress, encryptedBalance); expect(balance).to.be.equal(20000); // The amount didn't move - const encryptedTransferAmount2 = this.instances.alice.encrypt32(3000); + // Check that it's not possible to transfer from country 2 to country 1 const transfer2 = await this.identifiedErc20 - .connect(this.signers.carol) - ['transfer(address,bytes)'](this.signers.dave, encryptedTransferAmount2); + .connect(this.signers.dave) + ['transfer(address,bytes)'](this.signers.carol, amount3k); await transfer2.wait(); const encryptedBalance2 = await this.identifiedErc20 + .connect(this.signers.dave) + ['balanceOf(bytes32,bytes)'](daveToken.publicKey, daveToken.signature); + const balance2 = this.instances.dave.decrypt(this.contractAddress, encryptedBalance2); + expect(balance2).to.be.equal(10000); // The amount didn't move + + // Check that it's possible to transfer from country 1 to country 2 an amount below 10k + const transfer3 = await this.identifiedErc20 + .connect(this.signers.carol) + ['transfer(address,bytes)'](this.signers.dave, amount3k); + await transfer3.wait(); + + const encryptedBalance3 = await this.identifiedErc20 .connect(this.signers.carol) - ['balanceOf(bytes32,bytes)'](token.publicKey, token.signature); - const balance2 = this.instances.carol.decrypt(this.contractAddress, encryptedBalance2); - expect(balance2).to.be.equal(17000); // The amount didn't move + ['balanceOf(bytes32,bytes)'](carolToken.publicKey, carolToken.signature); + const balance3 = this.instances.carol.decrypt(this.contractAddress, encryptedBalance3); + expect(balance3).to.be.equal(17000); // The amount moved }); }); diff --git a/test/identity/identity.ts b/test/identity/identity.ts index dd4396e0..4eed09cc 100644 --- a/test/identity/identity.ts +++ b/test/identity/identity.ts @@ -28,9 +28,7 @@ describe('Identity', function () { const transaction = await this.identity.setIdentifier(this.signers.bob.address, 'birthdate', encryptedBirth); await transaction.wait(); - const allowed = await this.identity - .connect(this.signers.bob) - .givePermission('birthdate', this.signers.carol.address); + const allowed = await this.identity.connect(this.signers.bob).grantAccess(this.signers.carol.address, 'birthdate'); await allowed.wait(); // Carol use this token to access information