Skip to content

Commit

Permalink
feat() Implement feature explained in the article
Browse files Browse the repository at this point in the history
  • Loading branch information
immortal-tofu committed Nov 8, 2023
1 parent c6836e8 commit e5c50b5
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 96 deletions.
4 changes: 4 additions & 0 deletions examples/Identity/AbstractIdentifiedERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
68 changes: 53 additions & 15 deletions examples/Identity/ERC20Rules.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
13 changes: 7 additions & 6 deletions examples/Identity/Identity.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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];
}

Expand All @@ -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;
}

Expand Down
166 changes: 94 additions & 72 deletions test/identity/identifiedERC20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
});
});
4 changes: 1 addition & 3 deletions test/identity/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit e5c50b5

Please sign in to comment.