Omniscia WagmiDAO Audit

FamilyContract Manual Review Findings

FamilyContract Manual Review Findings

FCT-01M: Inexistent Enforcement of Accounting Accuracy

Description:

The withdrawUsdc and withdrawWagmi functions do not account for currently unprocessed operations thus potentially disallowing a user from claiming their USDC as the functions claimUsdc and emergencyClaimUsdcAll can fail if they contain backlogged withdrawals.

Example:

FamilyContract.sol
909function emergencyClaimUsdcAll() external nonReentrant whenPaused {
910 require(usdcClaimAmount[msg.sender] > 0, 'there is nothing to claim');
911 require(usdcClaimBlock[msg.sender] < block.number, 'you cannot claim yet');
912
913 uint256 amount = usdcClaimAmount[msg.sender];
914 usdcClaimAmount[msg.sender] = 0;
915
916 uint256 usdcTransferAmount = amount * (1000 - wagmiPermille - treasuryPermille) / 1000;
917 uint256 usdcTreasuryAmount = amount * treasuryPermille / 1000;
918 family.burn(dead, amount);
919 usdc.safeTransfer(treasury, usdcTreasuryAmount);
920 usdc.safeTransfer(msg.sender, usdcTransferAmount);
921
922 emit UsdcClaim(msg.sender, amount);
923}
924
925function withdrawUsdc(uint256 amount) external onlyOwner {
926 require(strategist != address(0), 'strategist not set');
927 usdc.safeTransfer(strategist, amount);
928
929 emit UsdcWithdrawn(amount);
930}
931
932function withdrawWagmi(uint256 amount) external onlyWithdrawer {
933 wagmi.safeTransfer(msg.sender, amount);
934
935 emit WagmiWithdrawn(amount);
936}

Recommendation:

We advise the total of unclaimed USDC to be tracked and its withdrawal to be prohibited in both functions to ensure proper accounting in the overall system and to prohibit the system from being stuck in an intermediate corrupted state whereby FAM tokens have been sent to the dead address but cannot be burned.

Alleviation:

A new variable called totalUsdcClaimAmount was introduced that keeps track of the owed USDC of the system and disallows the withdrawUsdc function from being invoked should the pending USDC not be satisfied. We should note that the introduced require check will never properly execute given that it will either always result in a value greater-than-or-equal-to (>=) zero or throw on underflow, however, it still suffices as a security measure and as such we consider this exhibit dealt with.

FCT-02M: Improper Sanitization of Wagmi Percentage

Description:

The wagmiPermille variable performs a simple upper-bound check and does not enforce the logical constraints the codebase is meant to conform to.

Example:

FamilyContract.sol
880uint256 usdcTransferAmount = amount * (1000 - wagmiPermille - treasuryPermille) / 1000;

Recommendation:

We advise those constraints to be enforced by ensuring that the value of wagmiPermille is non-zero and that the sums of wagmiPerMille + treasuryMille and wagmiPerMille + feePermille do not exceed 1000, their maximum accuracy, as otherwise the contract system becomes inoperable. We advise the sum checks to also be replicated to the setters of the feePermille and treasuryPermille variables as well.

Alleviation:

The WagmiDAO has stated that the sanitizations would be redundant given that the restrictions on each of those values independently guarantee that their sum cannot achieve or exceed the value of 1000. As such, we consider this exhibit null.

FCT-03M: Inexistent Validation of I/O Amounts

TypeSeverityLocation
Logical FaultMediumFamilyContract.sol:L815, L873

Description:

The stake and claimUsdc functions of the system act as its input-output functions that "exchange" an asset for another. However, in doing so, multiple dynamically set fee parameters are enforced that can change between a transaction's submission and a transaction's execution within the blockchain network.

Example:

FamilyContract.sol
873function claimUsdc(uint256 amountOutMin) external nonReentrant whenNotPaused {
874 require(usdcClaimAmount[msg.sender] > 0, 'there is nothing to claim');
875 require(usdcClaimBlock[msg.sender] < block.number, 'you cannnot claim yet');
876
877 uint256 amount = usdcClaimAmount[msg.sender];
878 usdcClaimAmount[msg.sender] = 0;
879
880 uint256 usdcTransferAmount = amount * (1000 - wagmiPermille - treasuryPermille) / 1000;
881 uint256 usdcTreasuryAmount = amount * treasuryPermille / 1000;
882 uint256 wagmiTransferAmount = wagmi.balanceOf(address(this)) * amount / family.totalSupply();
883 family.burn(dead, amount);
884 usdc.safeTransfer(treasury, usdcTreasuryAmount);
885 usdc.safeTransfer(msg.sender, usdcTransferAmount);
886 wagmi.approve(address(wagmiRouter), wagmiTransferAmount);
887 wagmiRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
888 wagmiTransferAmount,
889 amountOutMin,
890 swapPathReverse,
891 msg.sender,
892 block.timestamp
893 );
894
895 emit UsdcClaim(msg.sender, amount);
896}

Recommendation:

We advise a similar paradigm to Uniswap V2 to be applied whereby the minimum amount of FAM and USDC tokens are enforced with a require check on each respective function.

Alleviation:

A slippage-style argument was introduced to both stake and claimUsdc ensuring that a minimum amount is actually extracted from the system and alleviating this exhibit.

FCT-04M: Overly Centralized Single Points of Failure

Description:

The FamilyContract provides its owner with full access to the underlying USDC and GMI held, rendering it fully prone to a compromised owner.

Example:

FamilyContract.sol
925function withdrawUsdc(uint256 amount) external onlyOwner {
926 require(strategist != address(0), 'strategist not set');
927 usdc.safeTransfer(strategist, amount);
928
929 emit UsdcWithdrawn(amount);
930}
931
932function withdrawWagmi(uint256 amount) external onlyWithdrawer {
933 wagmi.safeTransfer(msg.sender, amount);
934
935 emit WagmiWithdrawn(amount);
936}

Recommendation:

We advise this trait of the system to be revised as it currently acts as a Single-Point-of-Failure (SPoF) with significant consequences to the system's integrity.

Alleviation:

The WagmiDAO team opted to retain the current behaviour in place as it is integral to the business strategy of the Family token. In detail, the WagmiDAO team has stated that withdrawing USDC will be noticed by the community and will only be performed to audited strategies with possibility to return the USDC solely back to the Family contract. In light of those details, we consider this exhibit adequately responded to.

FCT-05M: Potential Arbitrage Opportunity

Description:

The adjustment of the wagmiPermille value can cause arbitrage opportunities to arise in the same block whereby a user detects such a transaction and by means of MEV performs a sandwich attack whereby a stake operation is performed before the adjustment, the adjustment occurs and a claimUsdc operation is performed after the adjustment. We should note that even though block number thresholds are imposed on both functions to avoid arbitrage opportunities atomically, it is still possible to perform one "input" and one "output" operation in a single block by using multiple accounts.

Example:

FamilyContract.sol
815function stake(uint256 amount, uint256 amountOutMin) external nonReentrant whenNotPaused {
816 require(amount > 0, 'amount cannot be zero');
817 require(familyClaimAmount[msg.sender] == 0, 'you have to claim first');
818 require(amount <= maxStakeAmount, 'amount too high');
819 if(lastBlock != block.number) {
820 lastBlockUsdcStaked = amount;
821 lastBlock = block.number;
822 } else {
823 lastBlockUsdcStaked += amount;
824 }
825 require(lastBlockUsdcStaked <= maxStakePerBlock, 'maximum stake per block exceeded');
826
827 usdc.safeTransferFrom(msg.sender, address(this), amount);
828 if(feePermille > 0) {
829 uint256 feeAmount = amount * feePermille / 1000;
830 usdc.safeTransfer(treasury, feeAmount);
831 amount = amount - feeAmount;
832 }
833 family.mint(address(this), amount);
834 uint256 wagmiAmount = amount * wagmiPermille / 1000;
835 usdc.approve(address(wagmiRouter), wagmiAmount);
836 wagmiRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
837 wagmiAmount,
838 amountOutMin,
839 swapPath,
840 address(this),
841 block.timestamp
842 );
843
844 familyClaimAmount[msg.sender] = amount;
845 familyClaimBlock[msg.sender] = block.number;
846
847 emit Stake(msg.sender, amount);
848}
849
850function claimFamily() external nonReentrant whenNotPaused {
851 require(familyClaimAmount[msg.sender] > 0, 'there is nothing to claim');
852 require(familyClaimBlock[msg.sender] < block.number, 'you cannnot claim yet');
853
854 uint256 amount = familyClaimAmount[msg.sender];
855 familyClaimAmount[msg.sender] = 0;
856 family.safeTransfer(msg.sender, amount);
857
858 emit FamilyClaim(msg.sender, amount);
859}
860
861function redeem(uint256 amount) external nonReentrant whenNotPaused {
862 require(amount > 0, 'amount cannot be zero');
863 require(usdcClaimAmount[msg.sender] == 0, 'you have to claim first');
864 require(amount <= maxRedeemAmount, 'amount too high');
865
866 family.safeTransferFrom(msg.sender, dead, amount);
867 usdcClaimAmount[msg.sender] = amount;
868 usdcClaimBlock[msg.sender] = block.number;
869
870 emit Redeem(msg.sender, amount);
871}
872
873function claimUsdc(uint256 amountOutMin) external nonReentrant whenNotPaused {
874 require(usdcClaimAmount[msg.sender] > 0, 'there is nothing to claim');
875 require(usdcClaimBlock[msg.sender] < block.number, 'you cannnot claim yet');
876
877 uint256 amount = usdcClaimAmount[msg.sender];
878 usdcClaimAmount[msg.sender] = 0;
879
880 uint256 usdcTransferAmount = amount * (1000 - wagmiPermille - treasuryPermille) / 1000;
881 uint256 usdcTreasuryAmount = amount * treasuryPermille / 1000;
882 uint256 wagmiTransferAmount = wagmi.balanceOf(address(this)) * amount / family.totalSupply();
883 family.burn(dead, amount);
884 usdc.safeTransfer(treasury, usdcTreasuryAmount);
885 usdc.safeTransfer(msg.sender, usdcTransferAmount);
886 wagmi.approve(address(wagmiRouter), wagmiTransferAmount);
887 wagmiRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
888 wagmiTransferAmount,
889 amountOutMin,
890 swapPathReverse,
891 msg.sender,
892 block.timestamp
893 );
894
895 emit UsdcClaim(msg.sender, amount);
896}

Recommendation:

We advise this trait of the system to be evaluated and if deemed undesirable, the adjustment of wagmiPermille to be reconsidered as it is a sensitive percentage that significantly affects the contract's operation.

Alleviation:

A new block-based check was introduced to the codebase that prevents functions utilizing the wagmiPermille value from being invoked in the same block the value was adjusted in thus disallowing this arbitrage opportunity and alleviating this exhibit.

FCT-06M: Inexistent Validation of Threshold

Description:

The maxStakePerBlock value should at all times be greater-than-or-equal-to (>=) the value of maxStakeAmount as otherwise the latter is rendered redundant.

Example:

FamilyContract.sol
818require(amount <= maxStakeAmount, 'amount too high');
819if(lastBlock != block.number) {
820 lastBlockUsdcStaked = amount;
821 lastBlock = block.number;
822} else {
823 lastBlockUsdcStaked += amount;
824}
825require(lastBlockUsdcStaked <= maxStakePerBlock, 'maximum stake per block exceeded');

Recommendation:

We advise this to be enforced within the code by a corresponding require check in the setter functions of both setMaxStakeAmount and setMaxStakePerBlock.

Alleviation:

The corresponding require check was properly introduced in the maxStakePerBlock setter function.