diff --git a/.gitignore b/.gitignore index 85198aa..89815be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Compiler files cache/ out/ +lib/ # Ignores development broadcast logs !/broadcast diff --git a/audit/H-01.md b/audit/H-01.md new file mode 100644 index 0000000..a5942f4 --- /dev/null +++ b/audit/H-01.md @@ -0,0 +1,118 @@ +# Blocked User Can Call `SoulboundProfileNFT::mintProfile` Again + +## Summary + +Due to missing blocked profile handling, any blocked profile can call `SoulboundProfileNFT::mintProfile` again to mint and take part on the system. The purpose of blocking a profile no longer valid, since they could mint a new profile and continue interacting with existing `LikeRegistry` and `MultiSig`. + +## Vulnerability Details + +When `SoulboundProfileNFT::blockProfile` is called by the owner, it will burn the profile token, delete onchain variable related to the burnt tokenId and delete the profileToToken of the address. + +```solidity +function blockProfile(address blockAddress) external onlyOwner { + uint256 tokenId = profileToToken[blockAddress]; + require(tokenId != 0, "No profile found"); + + _burn(tokenId); + delete profileToToken[blockAddress]; + delete _profiles[tokenId]; + + emit ProfileBurned(blockAddress, tokenId); +} +``` + +So after the `SoulboundNFT::blockProfile` being called, no one can call `LikeRegistry::likeUser` to the blocked address. + +```solidity +function likeUser(address liked) external payable { + [...] + + require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT"); + require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT"); + + [...] +} +``` + +The `LikeRegistry::likeUser` require both the caller to have existing profile tokens. So blocked user unable to like or being liked. + +But the `SoulboundProfileNFT::mintProfile` doesn't have any check if the user or profile is blocked or not. So if someone has already blocked, they can call the mint function again. + +```solidity +function mintProfile(string memory name, uint8 age, string memory profileImage) external { + require(profileToToken[msg.sender] == 0, "Profile already exists"); + + uint256 tokenId = ++_nextTokenId; + _safeMint(msg.sender, tokenId); + + // Store metadata on-chain + _profiles[tokenId] = Profile(name, age, profileImage); + profileToToken[msg.sender] = tokenId; + + emit ProfileMinted(msg.sender, tokenId, name, age, profileImage); +} +``` + +There is no check if sender has its profile blocked or not previously. + +## POC + +```solidity +function testBlockedProfileMintAgain() public { + vm.prank(user); + soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage"); + + uint256 tokenId = soulboundNFT.profileToToken(user); + assertEq(tokenId, 1, "Token should exist before blocking"); + + vm.prank(owner); + soulboundNFT.blockProfile(user); + + uint256 newTokenId = soulboundNFT.profileToToken(user); + assertEq(newTokenId, 0, "Token should be removed after blocking"); + + vm.prank(user); + vm.expectRevert(); + // blocked profile should not be able to mint from the same wallet address again + soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage"); +} +``` + +## Impact + +- Any blocked profile can mint again and continue their activity at this protocol +- Blocked profile can mint again and mint a profile from another wallet, then make the other profile to match with his blocked account, and withdraw all of their own fund. + +## Recommendations + +Add new storage variable to check if a wallet address is blocked or not. + +```diff +--- a/src/SoulboundProfileNFT.sol ++++ b/src/SoulboundProfileNFT.sol +@@ -20,6 +20,7 @@ contract SoulboundProfileNFT is ERC721, Ownable { + + mapping(address => uint256) public profileToToken; // Maps user to their profile NFT + mapping(uint256 => Profile) private _profiles; // Stores profile metadata ++ mapping(address => bool) public profileBlocked; + + event ProfileMinted(address indexed user, uint256 tokenId, string name, uint8 age, string profileImage); + event ProfileBurned(address indexed user, uint256 tokenId); +@@ -29,6 +30,7 @@ contract SoulboundProfileNFT is ERC721, Ownable { + /// @notice Mint a soulbound NFT representing the user's profile. + function mintProfile(string memory name, uint8 age, string memory profileImage) external { + require(profileToToken[msg.sender] == 0, "Profile already exists"); ++ require(profileBlocked[msg.sender] == false, "Profile is blocked"); + + uint256 tokenId = ++_nextTokenId; + _safeMint(msg.sender, tokenId); +@@ -61,6 +63,7 @@ contract SoulboundProfileNFT is ERC721, Ownable { + _burn(tokenId); + delete profileToToken[blockAddress]; + delete _profiles[tokenId]; ++ profileBlocked[blockAddress] = true; + + emit ProfileBurned(blockAddress, tokenId); + } +``` + diff --git a/audit/H-02.md b/audit/H-02.md new file mode 100644 index 0000000..b797f61 --- /dev/null +++ b/audit/H-02.md @@ -0,0 +1,174 @@ +# H-02. User Fund Sent to LikeRegistry::likeUser is Never Accounted and Make `LikeRegistry::matchRewards` Always Return Zero + +## Summary + +Due to miscalculation in the `LikeRegistry::matchRewards`, any deployed MultisigWallet will contain 0 fund, and also fees for the protocol is always 0 as well. User funds sent through `LikeRegistry::likeUser` is never being accounted to the user. It leads to the created multisigWallet has no fund in it, and user lose their fund. + +## Vulnerability Detail + +```solidity +function matchRewards(address from, address to) internal { + uint256 matchUserOne = userBalances[from]; + uint256 matchUserTwo = userBalances[to]; + userBalances[from] = 0; + userBalances[to] = 0; + + uint256 totalRewards = matchUserOne + matchUserTwo; + uint256 matchingFees = (totalRewards * FIXEDFEE) / 100; + uint256 rewards = totalRewards - matchingFees; + totalFees += matchingFees; + + // Deploy a MultiSig contract for the matched users + MultiSigWallet multiSigWallet = new MultiSigWallet(from, to); + + // Send ETH to the deployed multisig wallet + (bool success,) = payable(address(multiSigWallet)).call{value: rewards}(""); + require(success, "Transfer failed"); +} +``` + +As we can see at the above snippet, `matchUserOne` and `matchUserTwo` are the main variables for this calculation. Its retrieved from the `userBalances` variable. But `userBalances` variable value actually never increased. As we can see in the below `LikeRegistry::likeUser` code: + +```solidity +function likeUser(address liked) external payable { + [...] + + likes[msg.sender][liked] = true; + emit Liked(msg.sender, liked); + +userBalances[msg.sender] += msg.value; + + // Check if mutual like + if (likes[liked][msg.sender]) { + matches[msg.sender].push(liked); + matches[liked].push(msg.sender); + emit Matched(msg.sender, liked); + address multiSigWallet = matchRewards(liked, msg.sender); + } +} +``` + +The `LikeRegistry::likeUser` require user to send fund greater than or equal to 1 ether, but these 1 ether never being accounted to the sender address. + +## POC + +We modify the `src/LikeRegistry.sol` file a bit to return the multisigWallet address from `likeUser` and `matchRewards`. + +`LikeRegistry::likeUser` + +```diff +- function likeUser(address liked) external payable { ++ function likeUser(address liked) external payable returns (address) { + require(msg.value >= 1 ether, "Must send at least 1 ETH"); + require(!likes[msg.sender][liked], "Already liked"); + require(msg.sender != liked, "Cannot like yourself"); +@@ -38,16 +38,23 @@ contract LikeRegistry is Ownable { + likes[msg.sender][liked] = true; + emit Liked(msg.sender, liked); + + // Check if mutual like + if (likes[liked][msg.sender]) { + matches[msg.sender].push(liked); + matches[liked].push(msg.sender); + emit Matched(msg.sender, liked); +- matchRewards(liked, msg.sender); ++ address multiSigWallet = matchRewards(liked, msg.sender); ++ return multiSigWallet; + } ++ ++ return address(0); + } +``` + +`LikeRegistry::matchRewards` + +```solidity +- function matchRewards(address from, address to) internal { ++ function matchRewards(address from, address to) internal returns (address) { + uint256 matchUserOne = userBalances[from]; + uint256 matchUserTwo = userBalances[to]; + userBalances[from] = 0; +@@ -64,6 +71,8 @@ contract LikeRegistry is Ownable { + // Send ETH to the deployed multisig wallet + (bool success,) = payable(address(multiSigWallet)).call{value: rewards}(""); + require(success, "Transfer failed"); ++ ++ return address(multiSigWallet); + } +``` + +`test/testLikeRegistry.t.sol` + +```solidity +function testMatchMiscalculation() public { + uint256 likeAmount = 1 ether; + uint256 FIXEDFEE = 10; + uint256 fees = ((likeAmount + likeAmount) * FIXEDFEE) / 100; + uint multiSigFund = likeAmount + likeAmount - fees; + + // user mint profile + vm.prank(user); + soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage"); + uint256 userTokenId = soulboundNFT.profileToToken(user); + assertEq(userTokenId, 1, "Token ID should be 1"); + + // user2 mint profile + vm.prank(user2); + soulboundNFT.mintProfile("Bob", 27, "ipfs://profileImage"); + uint256 user2TokenId = soulboundNFT.profileToToken(user2); + assertEq(user2TokenId, 2, "Token ID should be 2"); + + // likeRegistry has 0 ether + assertEq(address(likeRegistry).balance, 0); + + // user likes user2 with 1 eth + vm.prank(user); + likeRegistry.likeUser{value: likeAmount}(user2); + + // user2 likes uer with 1 eth and match + vm.prank(user2); + address multiSigAddress = likeRegistry.likeUser{value: likeAmount}(user); + assert(multiSigAddress != address(0)); + + // get user matches + vm.prank(user); + address[] memory userMatches = likeRegistry.getMatches(); + assertEq(userMatches.length, 1); + assertEq(userMatches[0], address(user2)); + + assertEq(address(multiSigAddress).balance, multiSigFund); // giving a like for each user 1 ether should give the wallet 1.8 ether + assertEq(address(likeRegistry).balance, fees); // likeRegistry should have balance equal to `fees`, and send the rest of fund to the MultisigWallet +} +``` + +## Impact + +- The user sending fund to `LikeRegistry::likeUser` has its fund never accounted to, which make a financial losses to the user +- The new created MultisigWallet has no fund in it +- Protocol unable to claim the protocol fee with `LikeRegistry::withdrawFees` despite the protocol balance increases + +## Recommendations + +Account user fund when user called `LikeRegistry::likeUser` to a storage variable. + +```diff + function likeUser(address liked) external payable { + require(msg.value >= 1 ether, "Must send at least 1 ETH"); + require(!likes[msg.sender][liked], "Already liked"); + require(msg.sender != liked, "Cannot like yourself"); +@@ -38,16 +38,23 @@ contract LikeRegistry is Ownable { + likes[msg.sender][liked] = true; + emit Liked(msg.sender, liked); + ++ userBalances[msg.sender] += msg.value; ++ + // Check if mutual like + if (likes[liked][msg.sender]) { + matches[msg.sender].push(liked); + matches[liked].push(msg.sender); + emit Matched(msg.sender, liked); + matchRewards(liked, msg.sender); + } + } +``` + diff --git a/audit/H-03.md b/audit/H-03.md new file mode 100644 index 0000000..77bc252 --- /dev/null +++ b/audit/H-03.md @@ -0,0 +1,74 @@ +# Fund Sent Directly to `LikeRegistry` Will Not be Able to Withdraw by Anyone + +## Summary + +Due to missing handling of received funds which transferred directly to the contract address, it can lead to the funds unable to withdraw. Both user and the contract owner are unable to withdraw the fund being sent directly to the contract lead to a loss of fund. + +## Vulnerability Details + +When a user send fund directly to `LikeRegistry` contract, it will do nothing and doen't account the fund to the user who send it. + +```solidity + /// @notice Allows the contract to receive ETH + receive() external payable {} +``` + +While we see there is a storage variable designed to store and account the user funds called `userBalances`. + +```solidity +contract LikeRegistry is Ownable { + [...] + + mapping(address => mapping(address => bool)) public likes; + mapping(address => address[]) public matches; + mapping(address => uint256) public userBalances; + + [...] +``` + +## POC + +```solidity +function testSendMoneyToLikeRegistry() public { + uint256 sendAmount = 1 ether; + uint256 initialLikeRegistryFund = address(likeRegistry).balance; + + // user send money directly to like registry contract + vm.prank(user); + (bool success,) = payable(address(likeRegistry)).call{value: sendAmount}(""); + uint256 likeRegistryFundAfterFunded = address(likeRegistry).balance; + + assertEq(success, true); + assertEq(initialLikeRegistryFund, 0); + assertEq(likeRegistryFundAfterFunded, sendAmount); + + // check userBalances + uint256 userUserBalance = likeRegistry.userBalances(user); + assertEq(userUserBalance, sendAmount); // sending money directly to contract should adjust userBalance to reflect the address fund + + // owner try to withdraw money + vm.prank(owner); + vm.expectRevert(); + likeRegistry.withdrawFees(); // the contract will assume that there is no fees to withdraw + + // try to withdraw money + vm.prank(user); + vm.expectRevert(); + likeRegistry.withdrawFees(); // there is no withdraw function for user +} +``` + +## Impact + +- Loss of fund because no one can withdraw it + +## Recommendations + +Account the fund to address by storing it in the `userBalances` variable + +```diff +- receive() external payable {} ++ receive() external payable { ++ userBalances[msg.sender] += msg.value; ++ } +``` diff --git a/audit/H-04.md b/audit/H-04.md new file mode 100644 index 0000000..da1842a --- /dev/null +++ b/audit/H-04.md @@ -0,0 +1,55 @@ +# Fund Locked in MultiSig when Match with a Blocked Profile + +## Summary + +A profile can be blocked by the owner through `SoulboundProfileNFT::blockProfile`. But the fact that the profile is being blocked, doesn't change anything on the MultiSIgWallet. Someone who match up with a blocked profile now in the risk of their fund will never be able to withdraw. There is no other way to withdraw fund from MultiSigWallet other than both of the owner approve the transaction. + +## Vulnerability Details + +When user being blocked by owner through `SoulBoundProfileNFT::blockProfile`, it doesn't change any state of the `MultiSigWallet`. Transaction execution from `MultiSigWallet::executeTransaction` require both of party to sign through `MultiSigWallet::approveTransaction`. + +```solidity +function executeTransaction(uint256 _txId) external onlyOwners { + require(_txId < transactions.length, "Invalid transaction ID"); + Transaction storage txn = transactions[_txId]; + require(!txn.executed, "Transaction already executed"); + require(txn.approvedByOwner1 && txn.approvedByOwner2, "Not enough approvals"); + + [...] + } +``` + +## POC + +```solidity +function testUserInRelationshipWithBlockedProfileUnableToWithdraw() public { + uint256 initialMultiSigWalletBalance = address(multiSigWallet).balance; + uint256 initialUserBalance = address(user).balance; + uint256 withdrawBalance = initialMultiSigWalletBalance / 2; + + vm.startPrank(user); + // submit tx + multiSigWallet.submitTransaction(address(user), withdrawBalance); + + // approve tx + multiSigWallet.approveTransaction(0); + + // execute + vm.expectRevert(); + multiSigWallet.executeTransaction(0); + vm.stopPrank(); + + (address to, uint256 value, bool approvedByOwner1, bool approvedByOwner2, bool executed) = multiSigWallet.transactions(0); + assertEq(executed, false); // unable to execute because need both of approval, despite the other user is already blocked + assertEq(address(user).balance, initialUserBalance + withdrawBalance); // user should be able to withdraw their fund from multiSigWallet from a blocked profile +} +``` + +## Impact + +- Lost of funds because funds are locked in the MultiSigWallet and require approval from the blocked profile which might rejects all the transaction requests that benefit other user + +## Recommendations + +Add a function to let user withdraw funds to their wallet if their match up is blocked. + diff --git a/audit/template.md b/audit/template.md new file mode 100644 index 0000000..7a334de --- /dev/null +++ b/audit/template.md @@ -0,0 +1,12 @@ +# Title + +## Summary + +## Vulnerability Details + +## POC + +## Impact + +## Recommendations + diff --git a/foundry.toml b/foundry.toml index 4d6c384..5a6fb78 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,6 +6,6 @@ libs = ["lib"] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options remapping = [ - "@openzeppelin//lib/openzeppelin-contracts/", + "@openzeppelin/=/lib/openzeppelin-contracts/", "forge-std/=/lib/forge-std/src/", ] diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..3b20d60 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..acd4ff7 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit acd4ff74de833399287ed6b31b4debf6b2b35527 diff --git a/src/LikeRegistry.sol b/src/LikeRegistry.sol index 5efd567..66e3fca 100644 --- a/src/LikeRegistry.sol +++ b/src/LikeRegistry.sol @@ -28,7 +28,7 @@ contract LikeRegistry is Ownable { profileNFT = SoulboundProfileNFT(_profileNFT); } - function likeUser(address liked) external payable { + function likeUser(address liked) external payable returns (address) { require(msg.value >= 1 ether, "Must send at least 1 ETH"); require(!likes[msg.sender][liked], "Already liked"); require(msg.sender != liked, "Cannot like yourself"); @@ -38,16 +38,23 @@ contract LikeRegistry is Ownable { likes[msg.sender][liked] = true; emit Liked(msg.sender, liked); + userBalances[msg.sender] += msg.value; + // Check if mutual like if (likes[liked][msg.sender]) { matches[msg.sender].push(liked); matches[liked].push(msg.sender); emit Matched(msg.sender, liked); - matchRewards(liked, msg.sender); + address multiSigWallet = matchRewards(liked, msg.sender); + return multiSigWallet; } + + return address(0); } - function matchRewards(address from, address to) internal { + function matchRewards(address from, address to) internal returns (address) { + // @audit: will always be zero + // @audit: critical uint256 matchUserOne = userBalances[from]; uint256 matchUserTwo = userBalances[to]; userBalances[from] = 0; @@ -64,6 +71,8 @@ contract LikeRegistry is Ownable { // Send ETH to the deployed multisig wallet (bool success,) = payable(address(multiSigWallet)).call{value: rewards}(""); require(success, "Transfer failed"); + + return address(multiSigWallet); } function getMatches() external view returns (address[] memory) { @@ -79,6 +88,10 @@ contract LikeRegistry is Ownable { require(success, "Transfer failed"); } + // @audit: if user A send money to this contract, user A unable to retrieve it + // @audit: critical /// @notice Allows the contract to receive ETH - receive() external payable {} + receive() external payable { + userBalances[msg.sender] += msg.value; + } } diff --git a/src/MultiSig.sol b/src/MultiSig.sol index 8214cd0..627c764 100644 --- a/src/MultiSig.sol +++ b/src/MultiSig.sol @@ -37,6 +37,7 @@ contract MultiSigWallet { owner2 = _owner2; } + // @audit: there is a risk of the fund will locked here forever /// @notice Submit a transaction for approval function submitTransaction(address _to, uint256 _value) external onlyOwners { if (_to == address(0)) revert InvalidRecipient(); diff --git a/src/SoulboundProfileNFT.sol b/src/SoulboundProfileNFT.sol index b8fbf60..6efdb4d 100644 --- a/src/SoulboundProfileNFT.sol +++ b/src/SoulboundProfileNFT.sol @@ -59,6 +59,8 @@ contract SoulboundProfileNFT is ERC721, Ownable { require(tokenId != 0, "No profile found"); _burn(tokenId); + // @audit: blocked profile can mint a new profile + // @audit-finding: high delete profileToToken[blockAddress]; delete _profiles[tokenId]; @@ -71,6 +73,8 @@ contract SoulboundProfileNFT is ERC721, Ownable { revert SoulboundTokenCannotBeTransferred(); } + // @audit: what happen with `safeTransferFrom(address, address, uint256) without bytes` + // @audit-comment: no its reverted function safeTransferFrom(address, address, uint256, bytes memory) public pure override { // Soulbound token cannot be transferred revert SoulboundTokenCannotBeTransferred(); diff --git a/test/testLikeRegistry.t.sol b/test/testLikeRegistry.t.sol new file mode 100644 index 0000000..4ebba51 --- /dev/null +++ b/test/testLikeRegistry.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../src/SoulboundProfileNFT.sol"; +import "../src/LikeRegistry.sol"; +import {MultiSigWallet} from "../src/MultiSig.sol"; + +contract LikeRegistryTest is Test { + SoulboundProfileNFT soulboundNFT; + LikeRegistry likeRegistry; + address user = address(0x123); + address user2 = address(0x456); + address owner = address(this); // Test contract acts as the owner + + function setUp() public { + soulboundNFT = new SoulboundProfileNFT(); + likeRegistry = new LikeRegistry(address(soulboundNFT)); + + vm.deal(user, 10 ether); + vm.deal(user2, 10 ether); + } + + function testGetMatches() public { + vm.prank(user); + address[] memory matches = likeRegistry.getMatches(); + address[] memory empty; + assertEq(matches, empty); + } + + function testMatchMiscalculation() public { + uint256 likeAmount = 1 ether; + uint256 FIXEDFEE = 10; + uint256 fees = ((likeAmount + likeAmount) * FIXEDFEE) / 100; + uint multiSigFund = likeAmount + likeAmount - fees; + + // user mint profile + vm.prank(user); + soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage"); + uint256 userTokenId = soulboundNFT.profileToToken(user); + assertEq(userTokenId, 1, "Token ID should be 1"); + + // user2 mint profile + vm.prank(user2); + soulboundNFT.mintProfile("Bob", 27, "ipfs://profileImage"); + uint256 user2TokenId = soulboundNFT.profileToToken(user2); + assertEq(user2TokenId, 2, "Token ID should be 2"); + + // likeRegistry has 0 ether + assertEq(address(likeRegistry).balance, 0); + + // user likes user2 with 1 eth + vm.prank(user); + likeRegistry.likeUser{value: likeAmount}(user2); + + // user2 likes uer with 1 eth and match + vm.prank(user2); + address multiSigAddress = likeRegistry.likeUser{value: likeAmount}(user); + assert(multiSigAddress != address(0)); + + // get user matches + vm.prank(user); + address[] memory userMatches = likeRegistry.getMatches(); + assertEq(userMatches.length, 1); + assertEq(userMatches[0], address(user2)); + + assertEq(address(multiSigAddress).balance, multiSigFund); // giving a like for each user 1 ether should give the wallet 1.8 ether + assertEq(address(likeRegistry).balance, fees); // likeRegistry should have balance equal to `fees`, and send the rest of fund to the MultisigWallet + } + + function testSendMoneyToLikeRegistry() public { + uint256 sendAmount = 1 ether; + uint256 initialLikeRegistryFund = address(likeRegistry).balance; + + // user send money directly to like registry contract + vm.prank(user); + (bool success,) = payable(address(likeRegistry)).call{value: sendAmount}(""); + uint256 likeRegistryFundAfterFunded = address(likeRegistry).balance; + + assertEq(success, true); + assertEq(initialLikeRegistryFund, 0); + assertEq(likeRegistryFundAfterFunded, sendAmount); + + // check userBalances + uint256 userUserBalance = likeRegistry.userBalances(user); + assertEq(userUserBalance, sendAmount); // sending money directly to contract should adjust userBalance to reflect the address fund + + // owner try to withdraw money + vm.prank(owner); + vm.expectRevert(); + likeRegistry.withdrawFees(); // the contract will assume that there is no fees to withdraw + + // try to withdraw money + vm.prank(user); + vm.expectRevert(); + likeRegistry.withdrawFees(); // there is no withdraw function for user + } +} + diff --git a/test/testMultiSig.t.sol b/test/testMultiSig.t.sol new file mode 100644 index 0000000..beb2f5e --- /dev/null +++ b/test/testMultiSig.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {MultiSigWallet} from "../src/MultiSig.sol"; + +contract MultiSigWalletTest is Test { + MultiSigWallet multiSigWallet; + address user = address(0x123); + address user2 = address(0x456); + + function setUp() public { + multiSigWallet = new MultiSigWallet(user, user2); + + vm.deal(address(multiSigWallet), 10 ether); + vm.deal(user, 10 ether); + vm.deal(user2, 10 ether); + } + + function testSetup() public { + address owner1 = multiSigWallet.owner1(); + address owner2 = multiSigWallet.owner2(); + + assertEq(owner1, user); + assertEq(owner2, user2); + assertEq(address(multiSigWallet).balance, 10 ether); + } + + function testUserInRelationshipWithBlockedProfileUnableToWithdraw() public { + uint256 initialMultiSigWalletBalance = address(multiSigWallet).balance; + uint256 initialUserBalance = address(user).balance; + uint256 withdrawBalance = initialMultiSigWalletBalance / 2; + + vm.startPrank(user); + // submit tx + multiSigWallet.submitTransaction(address(user), withdrawBalance); + + // approve tx + multiSigWallet.approveTransaction(0); + + // execute + vm.expectRevert(); + multiSigWallet.executeTransaction(0); + vm.stopPrank(); + + (address to, uint256 value, bool approvedByOwner1, bool approvedByOwner2, bool executed) = multiSigWallet.transactions(0); + assertEq(executed, false); // unable to execute because need both of approval, despite the other user is already blocked + assertEq(address(user).balance, initialUserBalance + withdrawBalance); // user should be able to withdraw their fund from multiSigWallet from a blocked profile + } +} + diff --git a/test/testSoulboundProfileNFT.t.sol b/test/testSoulboundProfileNFT.t.sol index 79e53da..8c62b55 100644 --- a/test/testSoulboundProfileNFT.t.sol +++ b/test/testSoulboundProfileNFT.t.sol @@ -94,6 +94,27 @@ contract SoulboundProfileNFTTest is Test { assertEq(newTokenId, 0, "Token should be removed after blocking"); } + // audit + function testBlockedProfileMintAgain() public { + vm.prank(user); + soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage"); + + uint256 tokenId = soulboundNFT.profileToToken(user); + assertEq(tokenId, 1, "Token should exist before blocking"); + + vm.prank(owner); + soulboundNFT.blockProfile(user); + + uint256 newTokenId = soulboundNFT.profileToToken(user); + assertEq(newTokenId, 0, "Token should be removed after blocking"); + + vm.prank(user); + vm.expectRevert(); + // blocked profile should not be able to mint from the same wallet address again + soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage"); + } + // end of audit + function testNonOwnerCannotBlockProfile() public { vm.prank(user); soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");