Skip to content

Commit

Permalink
Add suggestions from the Zama team.
Browse files Browse the repository at this point in the history
  • Loading branch information
Segue21 committed May 23, 2024
1 parent c36a4e4 commit 80a47ef
Show file tree
Hide file tree
Showing 14 changed files with 1,301 additions and 818 deletions.
2 changes: 2 additions & 0 deletions .env.developement.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_APP_BASE_URL=/
VITE_LOCAL_IPFS_URL=http://localhost:5001
31 changes: 30 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,33 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env

# Ignore environment variables files
.env
.env.local
.env.*.local
.env.production
.env.development
# Include example environment variable files
!.env.development.example

# Ignore Hardhat build output
artifacts/
cache/


# Ignore IDE-specific files
.vscode/
.idea/
*.iml

# Ignore coverage reports
coverage/

# Ignore Hardhat's test cache
.hardhat_cache/

# Ignore Truffle's test network log
*.log
*.pid
*.seed
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,21 @@ As soon as such a membership operator will be shipped, we will be able to implem

## Running the Application

- Access the application through the following URL: [Link to the Application](https://el-hacen21.github.io/zama_bounty/)
To run the application locally, follow these steps:
#### Environment Variables

Copy the example environment variable files and update them with your specific configurations.

```bash
cp .env.development.example .env.development
```
#### Run

- To run the application locally, follow these steps:
* Install all necessary packages: `npm install`
* To start the development server: `npm run dev`
* To build the application for production: `npm run build`


- If you encounter the error ReferenceError: Buffer is not defined, as anticipated by Zama, [here](https://docs.zama.ai/fhevm/guides/webpack), adding the following two lines into [main.tsx](/src/main.tsx) should resolved the issue:

```javascript
Expand Down
153 changes: 69 additions & 84 deletions src/components/Blockchain/contract.sol → contracts/contract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ contract FDRM is Reencrypt, ERC721URIStorage, Ownable2Step {
// efficient operations to manage the collection of owned tokens.
mapping(address => EnumerableSet.UintSet) internal ownerTokens;

// Maps each token ID to its encrypted key hash.
mapping(uint256 => bytes32) internal tokenEncryptedKeyHashes;

// Maps each token ID to its encrypted key hash.
mapping(uint256 => mapping(uint8 => euint64)) internal tokenEncryptedKey;

// Maps each token ID to a dynamic mapping where each address points to a boolean
// value indicating whether that address has been granted shared access to the token.
Expand All @@ -46,27 +45,34 @@ contract FDRM is Reencrypt, ERC721URIStorage, Ownable2Step {
// as it allows quick enumeration and modification of all users who have access to a specific token.
mapping(uint256 => EnumerableSet.AddressSet) internal tokenSharedWithUsers;


// Constants for token name and symbol
string private constant _TOKEN_NAME = "NFTDRM";
string private constant _TOKEN_SYMBOL = "FDRM";

// size limits on the sets to avoid DDOS issues due to excessive gas costs if sets become too big
uint8 public constant MAX_USERS_TO_REMOVE = 20;

// Constructor initializes the ERC721 token with a name and a symbol and sets the owner
constructor() ERC721(_TOKEN_NAME, _TOKEN_SYMBOL) Ownable(msg.sender) {
mintedCounter = 0;
}

// Allows users to mint a new NFT with a specified content identifier hash (CID)
function mintToken(string calldata cidHash, bytes32 encryptedKeyHash)
external
returns (uint256)
{
function mintToken(
string calldata cidHash,
bytes[4] calldata encryptedFileKey
) external returns (uint256) {
require(bytes(cidHash).length > 0, "CID cannot be empty.");
uint256 tokenId = mintedCounter;
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, cidHash);
ownerTokens[msg.sender].add(tokenId);
tokenEncryptedKeyHashes[tokenId] = encryptedKeyHash;

for (uint8 i = 0; i < 4; i++) {
euint64 fileKeyPart = TFHE.asEuint64(encryptedFileKey[i]);
tokenEncryptedKey[tokenId][i] = fileKeyPart;
}

mintedCounter++;
emit TokenMinted(tokenId);
return tokenId;
Expand Down Expand Up @@ -164,17 +170,45 @@ contract FDRM is Reencrypt, ERC721URIStorage, Ownable2Step {
return users;
}

// Function to transfer a token to another address
function transferToken(uint256 tokenId, address to)
public
onlyTokenOwner(tokenId)
requireNonZeroAddress(to)
{
safeTransferFrom(msg.sender, to, tokenId);
function transferToken(
address to,
uint256 tokenId
) public onlyTokenOwner(tokenId) {
_safeTransfer(msg.sender, to, tokenId);

// Use EnumerableSet to remove and add tokens in ownership tracking
ownerTokens[msg.sender].remove(tokenId);
ownerTokens[to].add(tokenId);

// Remove the new owner as a shared with
if (sharedAccess[tokenId][to]) {
_revokeSharedAccess(tokenId, to);
}
}

function _revokeSharedAccess(uint256 tokenId, address user) private {
if (sharedAccess[tokenId][user]) {
sharedAccess[tokenId][user] = false;
sharedTokens[user].remove(tokenId);
tokenSharedWithUsers[tokenId].remove(user);
}
}

// Function to revoke all shared access for a token with a limit
function revokeAllSharedAccess(
uint256 tokenId,
uint8 limitNumberOfSharedWith
) public onlyTokenOwner(tokenId) {
EnumerableSet.AddressSet storage users = tokenSharedWithUsers[tokenId];
uint256 numberOfUsers = users.length();
uint8 limit = limitNumberOfSharedWith < numberOfUsers
? limitNumberOfSharedWith
: uint8(numberOfUsers);

// Loop over the set and remove each user up to the limit
for (uint8 i = 0; i < limit; i++) {
address user = users.at(0); // Always remove the first user as the set shrinks
_revokeSharedAccess(tokenId, user);
}
}

// Function to revoke shared access to a token for a specific address
Expand All @@ -184,104 +218,55 @@ contract FDRM is Reencrypt, ERC721URIStorage, Ownable2Step {
requireNonZeroAddress(user)
{
require(sharedAccess[tokenId][user], "Access not granted.");
sharedAccess[tokenId][user] = false;
sharedTokens[user].remove(tokenId);
tokenSharedWithUsers[tokenId].remove(user);
_revokeSharedAccess(tokenId, user);
}

// function to revoke all shared access for a token
function revokeAllSharedAccess(uint256 tokenId)
// Function to burn a token owned by the caller
function burnToken(uint256 tokenId, uint8 limitNumberOfSharedWith)
public
onlyTokenOwner(tokenId)
{
EnumerableSet.AddressSet storage users = tokenSharedWithUsers[tokenId];
uint256 numberOfUsers = users.length();

for (uint256 i = 0; i < numberOfUsers; i++) {
address user = users.at(i);
sharedTokens[user].remove(tokenId);
sharedAccess[tokenId][user] = false;
}

// Delete the entire set of users
delete tokenSharedWithUsers[tokenId];
}

// Function to burn a token owned by the caller
function burnToken(uint256 tokenId) public onlyTokenOwner(tokenId) {
// Revoke all shared accesses and remove token from sharedTokens for each user who had access
revokeAllSharedAccess(tokenId);

// Remove the token from the owner's set of tokens
ownerTokens[msg.sender].remove(tokenId);
revokeAllSharedAccess(tokenId, limitNumberOfSharedWith);

delete tokenEncryptedKeyHashes[tokenId];
// If there are no more shared users, proceed with burning
if (tokenSharedWithUsers[tokenId].length() == 0) {
// Remove the token from the owner's set of tokens
ownerTokens[msg.sender].remove(tokenId);

_burn(tokenId);
_burn(tokenId);
}
}

function reencrypt(
uint256 tokenId,
bytes[] calldata encryptedFileKey,
bytes32 publicKey,
bytes memory signature
)
public
view
onlyAuthorizedSigner(tokenId, publicKey, signature)
onlySignedPublicKey(publicKey, signature)
returns (bytes[] memory)
{
require(
encryptedFileKey.length == 4,
"The NFT encryption key must be a table of size 4"
);

// Compute the hash of the encrypted file key using keccak256 and aggregate them
bytes32 aggregateHash = keccak256(
abi.encodePacked(
keccak256(encryptedFileKey[0]),
keccak256(encryptedFileKey[1]),
keccak256(encryptedFileKey[2]),
keccak256(encryptedFileKey[3])
)
);

// Compare the computed aggregate hash with the stored hash
require(
aggregateHash != tokenEncryptedKeyHashes[tokenId],
"The provided file key is not correct!"
ownerOf(tokenId) == msg.sender || sharedAccess[tokenId][msg.sender],
"Caller is neither owner nor authorized."
);

// Declaring an array to hold re-encrypted key parts
bytes[] memory reEncryptedKeyParts = new bytes[](4);

// Re-encrypt each key part
for (uint256 i = 0; i < encryptedFileKey.length; i++) {
euint64 reecryptkey = TFHE.asEuint64(encryptedFileKey[i]);
reEncryptedKeyParts[i] = TFHE.reencrypt(reecryptkey, publicKey);
for (uint8 i = 0; i < 4; i++) {
reEncryptedKeyParts[i] = TFHE.reencrypt(
tokenEncryptedKey[tokenId][i],
publicKey
);
}

return reEncryptedKeyParts;
}

modifier onlyAuthorizedSigner(
uint256 tokenId,
bytes32 publicKey,
bytes memory signature
) {
bytes32 digest = _hashTypedDataV4(
keccak256(
abi.encode(keccak256("Reencrypt(bytes32 publicKey)"), publicKey)
)
);
address signer = ECDSA.recover(digest, signature);
require(
ownerOf(tokenId) == signer || sharedAccess[tokenId][signer],
"Caller is neither owner nor authorized."
);
_;
}

// Modifier to check token ownership
modifier onlyTokenOwner(uint256 tokenId) {
require(ownerOf(tokenId) == msg.sender, "Not the owner of this token.");
Expand Down
Loading

0 comments on commit 80a47ef

Please sign in to comment.