Omniscia WagmiDAO Audit
FamilyContract Manual Review Findings
FamilyContract Manual Review Findings
FCT-01M: Inexistent Enforcement of Accounting Accuracy
Type | Severity | Location |
---|---|---|
Logical Fault | Major | FamilyContract.sol:L925-L930, L932-L936 |
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:
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
Type | Severity | Location |
---|---|---|
Input Sanitization | Medium | FamilyContract.sol:L764, L784, L834 |
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:
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
Type | Severity | Location |
---|---|---|
Logical Fault | Medium | FamilyContract.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:
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.timestamp893 );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
Type | Severity | Location |
---|---|---|
Logical Fault | Medium | FamilyContract.sol:L925-L930, L932-L936 |
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:
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
Type | Severity | Location |
---|---|---|
Language Specific | Medium | FamilyContract.sol:L763-L768, L834, L880 |
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:
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.timestamp842 );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.timestamp893 );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
Type | Severity | Location |
---|---|---|
Input Sanitization | Minor | FamilyContract.sol:L798, L810 |
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:
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.