Platform: Code4rena
Start Date: 16/01/2024
Pot Size: $80,000 USDC
Total HM: 37
Participants: 178
Period: 14 days
Judge: Picodes
Total Solo HM: 4
Id: 320
League: ETH
Rank: 88/178
Findings: 2
Award: $73.76
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: neocrao
Also found by: 00xSEV, 0x11singh99, 0x3b, 0xAlix2, 0xRobocop, 0xSmartContractSamurai, 0xanmol, AgileJune, Drynooo, HALITUS, Imp, J4X, KHOROAMU, Kalyan-Singh, MSaptarshi, RootKit0xCE, The-Seraphs, agadzhalov, aman, ayden, cu5t0mpeo, erosjohn, ewah, jasonxiale, jesjupyter, juancito, klau5, memforvik, okolicodes, parrotAudits0, rudolph, t0x1c, wangxx2026, zhaojohnson
1.6255 USDC - $1.63
existing protocol tests did not catch this bug, despite efforts...good idea to modify the tests accordingly...
It seems that this bug can never lead to any issues due to the proportionality factor remaining consistent between reclaimedA
and reclaimedB
regarding the values for reserve0
and reserve1
, which means that neither of the reserves can ever become less than PoolUtils.DUST
without the other reserve also becoming less than PoolUtils.DUST
at the same time, i.e. it's impossible to have a situation where reserve1 < PoolUtils.DUST
but reserve0 >= PoolUtils.DUST
.
(correct me if I'm wrong, but I checked).
This means that IF reserve1 < PoolUtils.DUST
ever happens, due to the bug this will be missed but due to above proportionality factor this reserve0 < PoolUtils.DUST
will also happen and thankfully there's a check to prevent this one, so it will revert. (So it's basically accidental mitigation against any side-effects of this bug...)
However, it's still a bug, and it needs to be fixed, the damage/impact on the protocol could have been serious if it wasnt for the accidental mitigation logic that prevents this (rather obvious) bug from doing anything...
Description:
The bug is in the require
statement of the removeLiquidity
function, where the condition (reserves.reserve0 >= PoolUtils.DUST)
is duplicated, and condition (reserves.reserve1 >= PoolUtils.DUST)
is skipped completely, effectively disregarding the value of reserves.reserve1
. This oversight could have led to the function proceeding with liquidity removal and token transfers even when reserve1
falls below an acceptable level, however, it seems the way the logic in the function was implemented the potentially bad effects of this bug was side-stepped by pure luck. See impact section for more clarity on this.
The code snippet containing the bug: L187: https://github.com/code-423n4/2024-01-salty/blob/ce719503b43ebf086f5147c0f6efc3c0890bf0f9/src/pools/Pools.sol#L186-L187
require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve0 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal");
Severity:
Definitely cannot be low severity due to the potential serious impact on entire protocol stability IF the function's logic didnt accidentally neutralize this bug. Therefore I deem the bug at least medium severity, but not high due to the fact the bug is neutralized accidentally.
However, since the bug wasnt picked up by the protocol's tests, exactly due to this accidental neutralization of the bug by the function's logic, it should serve as a good lesson to both devs and smart contract security auditoors. Exactly what that lesson is, I'm not sure, but successful tests are never a guarantee, as is the case here...
This (pretty obvious) bug was completely missed by all the tests, purely because it was impossible to detect it due to how the function was implemented, and by pure luck this implementation also accidentally neutralized the bug...
My PoC proves the bug, but since I struggled to setup a working test for locally and since I couldn't modify the contract on sepolia testnet for existing tests, I had to copy the buggy line of code from the removeLiquidity()
function and add it to one of the existing protocol tests, in order to demonstrate the bug, what happens.
This should suffice as a proof of concept that demonstrates the bug.
Also, the existing protocol tests were not able to detect this bug due to the accidental bug neutralizing logic that was implemented in the function. Good lesson in here!
The accidental bug neutralizing logic:
reclaimedA = ( reserves.reserve0 * liquidityToRemove ) / totalLiquidity; reclaimedB = ( reserves.reserve1 * liquidityToRemove ) / totalLiquidity; reserves.reserve0 -= uint128(reclaimedA); reserves.reserve1 -= uint128(reclaimedB);
Proof of Concept (PoC):
Existing test function's modification:
Pools2.t.sol::testMaximumLiquidityRemoval()
// A unit test to check for proper handling of maximum liquidity removal in removeLiquidity. function testMaximumLiquidityRemoval() public { vm.startPrank(address(collateralAndLiquidity)); IERC20 tokenA = new TestERC20( "TEST", 18 ); IERC20 tokenB = new TestERC20( "TEST", 18 ); vm.stopPrank(); vm.prank(address(dao)); poolsConfig.whitelistPool( pools, tokenA, tokenB); vm.startPrank(address(collateralAndLiquidity)); tokenA.approve(address(pools), type(uint256).max); tokenB.approve(address(pools), type(uint256).max); pools.addLiquidity(tokenA, tokenB, 100 ether, 100 ether, 0, 0); uint256 totalShares = 200 ether; pools.removeLiquidity(tokenA, tokenB, 200 ether * ( 100 ether - 100 ) / ( 100 ether ), 0, 0, totalShares); (uint256 reservesA, uint256 reservesB) = pools.getPoolReserves(tokenA, tokenB); // Ensure that DUST remains // assertEq( reservesA, PoolUtils.DUST ); /// @audit commented out for PoC // assertEq( reservesB, PoolUtils.DUST ); /// @audit commented out for PoC + /// @audit added for PoC testing purposes: + reservesB = 0; /// @audit invalid value should trigger revert, but bug avoids revert + //require((reservesA >= PoolUtils.DUST) && (reservesA >= PoolUtils.DUST), "Insufficient reserves after liquidity removal"); /// @audit bug avoids revert + require((reservesA >= PoolUtils.DUST) && (reservesB >= PoolUtils.DUST), "Insufficient reserves after liquidity removal"); /// @audit bug fixed: reverts as intended/expected // Ensure that liquidity can be added back in pools.addLiquidity(tokenA, tokenB, 100 ether, 100 ether, 0, 0); }
forge test command used:
COVERAGE="no" NETWORK="sep" forge test --contracts src/pools/tests/Pools2.t.sol --mt testMaximumLiquidityRemoval -vvvvv --rpc-url https://eth-sepolia.g.alchemy.com/v2/gnLVzN-JjRB_L1mwqUL_LFNcyNBrpzRI
Case (bug not fixed):
require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve0 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal");
Using for test:
require((reservesA >= PoolUtils.DUST) && (reservesA >= PoolUtils.DUST), "Insufficient reserves after liquidity removal");
Test results:
emit LiquidityRemoved(tokenA: TestERC20: [0x3a8D553652AdC9BdBB8c7212580C13c0C33a81eB], tokenB: TestERC20: [0x64f7F003bcDf4fc7625ff5e9312aa2e8b4bdd39F], reclaimedA: 99999999999999999900 [9.999e19], reclaimedB: 99999999999999999900 [9.999e19], removedLiquidity: 199999999999999999800 [1.999e20]) │ └─ ← 99999999999999999900 [9.999e19], 99999999999999999900 [9.999e19] ├─ [1178] 0xce360D2cDCA292aD704223B145926336bf7a34Fb::getPoolReserves(TestERC20: [0x3a8D553652AdC9BdBB8c7212580C13c0C33a81eB], TestERC20: [0x64f7F003bcDf4fc7625ff5e9312aa2e8b4bdd39F]) [staticcall] │ └─ ← 100, 100 ├─ [18550] 0xce360D2cDCA292aD704223B145926336bf7a34Fb::addLiquidity(TestERC20: [0x3a8D553652AdC9BdBB8c7212580C13c0C33a81eB], TestERC20: [0x64f7F003bcDf4fc7625ff5e9312aa2e8b4bdd39F], 100000000000000000000 [1e20], 100000000000000000000 [1e20], 0, 0) │ ├─ [3420] TestERC20::transferFrom(0x9Ecf36320532d700440323A16b1A86251a49B7e2, 0xce360D2cDCA292aD704223B145926336bf7a34Fb, 100000000000000000000 [1e20]) │ │ ├─ emit Transfer(from: 0x9Ecf36320532d700440323A16b1A86251a49B7e2, to: 0xce360D2cDCA292aD704223B145926336bf7a34Fb, value: 100000000000000000000 [1e20]) │ │ └─ ← true │ ├─ [3420] TestERC20::transferFrom(0x9Ecf36320532d700440323A16b1A86251a49B7e2, 0xce360D2cDCA292aD704223B145926336bf7a34Fb, 100000000000000000000 [1e20]) │ │ ├─ emit Transfer(from: 0x9Ecf36320532d700440323A16b1A86251a49B7e2, to: 0xce360D2cDCA292aD704223B145926336bf7a34Fb, value: 100000000000000000000 [1e20]) │ │ └─ ← true │ ├─ emit LiquidityAdded(tokenA: TestERC20: [0x3a8D553652AdC9BdBB8c7212580C13c0C33a81eB], tokenB: TestERC20: [0x64f7F003bcDf4fc7625ff5e9312aa2e8b4bdd39F], addedAmountA: 100000000000000000000 [1e20], addedAmountB: 100000000000000000000 [1e20], addedLiquidity: 0) │ └─ ← 100000000000000000000 [1e20], 100000000000000000000 [1e20], 0 └─ ← () Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 119.45s Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Case (bug fixed):
require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve1 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal");
Using for test:
require((reservesA >= PoolUtils.DUST) && (reservesB >= PoolUtils.DUST), "Insufficient reserves after liquidity removal");
Test results:
emit LiquidityRemoved(tokenA: TestERC20: [0x3a8D553652AdC9BdBB8c7212580C13c0C33a81eB], tokenB: TestERC20: [0x64f7F003bcDf4fc7625ff5e9312aa2e8b4bdd39F], reclaimedA: 99999999999999999900 [9.999e19], reclaimedB: 99999999999999999900 [9.999e19], removedLiquidity: 199999999999999999800 [1.999e20]) │ └─ ← 99999999999999999900 [9.999e19], 99999999999999999900 [9.999e19] ├─ [1178] 0xce360D2cDCA292aD704223B145926336bf7a34Fb::getPoolReserves(TestERC20: [0x3a8D553652AdC9BdBB8c7212580C13c0C33a81eB], TestERC20: [0x64f7F003bcDf4fc7625ff5e9312aa2e8b4bdd39F]) [staticcall] │ └─ ← 100, 100 └─ ← revert: Insufficient reserves after liquidity removal Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 111.65s Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests) Failing tests: Encountered 1 failing test in src/pools/tests/Pools2.t.sol:TestPools2 [FAIL. Reason: revert: Insufficient reserves after liquidity removal] testMaximumLiquidityRemoval() (gas: 2098967) Encountered a total of 1 failing tests, 0 tests succeeded
VSC. Manual. Loads of frustration & persistence.
Fix:
- require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve0 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal"); + require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve1 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal");
Existing non-modified removeLiquidity()
function with the bug:
function removeLiquidity( IERC20 tokenA, IERC20 tokenB, uint256 liquidityToRemove, uint256 minReclaimedA, uint256 minReclaimedB, uint256 totalLiquidity ) external nonReentrant returns (uint256 reclaimedA, uint256 reclaimedB) { require( msg.sender == address(collateralAndLiquidity), "Pools.removeLiquidity is only callable from the CollateralAndLiquidity contract" ); require( liquidityToRemove > 0, "The amount of liquidityToRemove cannot be zero" ); (bytes32 poolID, bool flipped) = PoolUtils._poolIDAndFlipped(tokenA, tokenB); // Determine how much liquidity is being withdrawn and round down in favor of the protocol PoolReserves storage reserves = _poolReserves[poolID]; reclaimedA = ( reserves.reserve0 * liquidityToRemove ) / totalLiquidity; reclaimedB = ( reserves.reserve1 * liquidityToRemove ) / totalLiquidity; reserves.reserve0 -= uint128(reclaimedA); reserves.reserve1 -= uint128(reclaimedB); // Make sure that removing liquidity doesn't drive either of the reserves below DUST. // This is to ensure that ratios remain relatively constant even after a maximum withdrawal. require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve0 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal"); // Switch reclaimed amounts back to the order that was specified in the call arguments so they make sense to the caller if (flipped) (reclaimedA,reclaimedB) = (reclaimedB,reclaimedA); require( (reclaimedA >= minReclaimedA) && (reclaimedB >= minReclaimedB), "Insufficient underlying tokens returned" ); // Send the reclaimed tokens to the user tokenA.safeTransfer( msg.sender, reclaimedA ); tokenB.safeTransfer( msg.sender, reclaimedB ); emit LiquidityRemoved(tokenA, tokenB, reclaimedA, reclaimedB, liquidityToRemove); }
Invalid Validation
#0 - c4-judge
2024-02-07T14:22:20Z
Picodes marked the issue as duplicate of #647
#1 - c4-judge
2024-02-19T15:46:26Z
Picodes marked the issue as satisfactory
#2 - Picodes
2024-02-19T15:47:00Z
The issue has been found but the description of the impact is flawed.
🌟 Selected for report: juancito
Also found by: 0x3b, 0xBinChook, 0xCiphky, 0xHelium, 0xMango, 0xOmer, 0xRobocop, 0xSmartContractSamurai, 0xWaitress, 0xbepresent, 0xpiken, 7ashraf, Arz, Audinarey, Banditx0x, Bauchibred, Draiakoo, IceBear, J4X, Jorgect, Kaysoft, KingNFT, Rhaydden, Rolezn, The-Seraphs, Tigerfrake, Topmark, Tripathi, Udsen, ZanyBonzy, a3yip6, b0g0, chaduke, codeslide, csanuragjain, dharma09, djxploit, eta, ether_sky, grearlake, inzinko, jasonxiale, jesjupyter, josephdara, lanrebayode77, linmiaomiao, lsaudit, niroh, oakcobalt, peanuts, pina, sivanesh_808, slvDev, t0x1c, vnavascues, wangxx2026
72.1303 USDC - $72.13
Proposals::proposeSendSALT()
- L202: Whenever salt balance of DAO.sol is 0 < DAO salt balance < 20
, maxSendable
will round down to zero, therefore 0 salt will be sent even when salt balance is 19.99
.Whenever exchangeConfig.salt().balanceOf( address(exchangeConfig.dao()) )
is > 0 but < 20
, maxSendable
will round down to zero, and therefore user passed parameter amount
(which should be > 0) will trigger a revert in require( amount <= maxSendable, "Cannot send more than 5% of the DAO SALT balance" );
, therefore wont be possible to send salt to anyone whenever DAO has salt tokens > 0 but < 20
.
Impact:
PoC:
L202:
maxSendable = balance * 5 / 100
Will round down to zero for balance * 5 / 100 < 1
:
balance * 5 / 100 < 1
balance < 100/5
balance < 20
So for DAO salt balance < 20
, e.g. 19.99, maxSendable
will round down to zero due to how solidity/evm treats non-integer numbers, especially anything less than 1.
Recommendation:
Might be OK if DAO salt balance will never be less than 20, otherwise:
DAO::_executeApproval()
- L170: ( exchangeConfig.salt().balanceOf(address(this)) >= ballot.number1 )
should use >
instead of >=
because of the related check in Proposals::proposeSendSALT()
at L203: require( amount <= maxSendable
.https://github.com/code-423n4/2024-01-salty/blob/ce719503b43ebf086f5147c0f6efc3c0890bf0f9/src/dao/Proposals.sol#L203 https://github.com/code-423n4/2024-01-salty/blob/ce719503b43ebf086f5147c0f6efc3c0890bf0f9/src/dao/DAO.sol#L170
_executeApproval()
L170:
if ( exchangeConfig.salt().balanceOf(address(this)) >= ballot.number1 )
Should be >
due to check in Proposals::proposeSendSALT()
and in line with this comment too: "Only one sendSALT Ballot can be open at a time and the sending limit is 5% of the current SALT balance of the DAO."
Impact:
Proposals::proposeSendSALT()
at L203.Recommendation:
So, L170 should ideally be <= salt.balanceOf(address(this)) * 5 / 100;
as follows:
if ( exchangeConfig.salt().balanceOf(address(this)) * 5 / 100 >= ballot.number1 )
ArbitrageSearch::_rightMoreProfitable()
- L68: potential rounding down to zero risk in uint256 amountOut = (reservesA1 * midpoint) / (reservesA0 + midpoint);
Chances of this happening is small, but not impossible, should be some extreme market or other external event which causes the initial reserves of 1000 to drop to updated reserves of 20, and with midpoint of 50 would cause amountOut
to be rounded down to zero, effectively causing user to lose their funds during the swap/trade.
IMPACT:
PoC:
uint256 amountOut = (reservesA1 * midpoint) / (reservesA0 + midpoint);
For rounding down to zero we need the following condition to be true:
reservesA1 * midpoint < reservesA0 + midpoint
So:
= reservesA1 < (reservesA0 + midpoint) / midpoint
= reservesA1 < (reservesA0 / midpoint) + 1
Then when we apply this specific example:
reservesA0 = 1000
, reservesA1 = 20
, and midpoint = 50
, the condition for rounding down to zero is met. However, such extreme reductions in reserves from 1000 to 20 are unrealistic in typical AMM scenarios and may indicate abnormal behavior, a potential issue, or an attack on the system.
Completing the above equation:
= 20 < 21
So if we go back to the original equation for amountOut
:
amountOut = (reservesA1 * midpoint) / (reservesA0 + midpoint)
= (20 * 50) / (1000 + 50)
= 0.952380952381
which will be rounded down to:
= 0
SigningTools::_verifySignature()
- L26: Recommend to use OpenZeppelin's latest ECDSA over ecrecover()
to enhance signature security and mitigate malleability risks.(Although it may not pose any risk for this specific implementation due to the fact that the signature is expected to be reused, the recommendation remains valid.)
L26:
address recoveredAddress = ecrecover(messageHash, v, r, s);
RECOMMENDATION:
When it comes to smart contract signatures, consider swapping out ecrecover()
for OpenZeppelin's ECDSA. Why? Well, certain signature schemes, like ECDSA, can get tricky with signature malleability, allowing the creation of distinct valid signatures for the same message and private key. OpenZeppelin's ECDSA library steps in as an advanced alternative, reducing the risk of encountering multiple valid signatures tied to a single message and key. In a nutshell, making this switch enhances signature security in smart contracts by addressing malleability challenges.
It's noteworthy that the missing check for a returned address(0)
is not considered a concern, given the subsequent boolean "check" on L28 that effectively covers this scenario.
SigningTools::_verifySignature()
- L13: The constant 65
in the require statement require(signature.length == 65, "Invalid signature length");
is a magic number.Consider replacing it with a named constant or providing a comment explaining why the length is expected to be 65
.
CoreChainlinkFeed::latestChainlinkPrice()
- L32: Replace deprecated Chainlink functions like latestRoundData()
with current recommended methods for accurate oracle data retrieval in smart contracts.Using outdated Chainlink functions like latestRoundData()
might serve up a bit of stale or wonky data, messing with the mojo of the contracts and price oracle functionality.
Reference: https://github.com/code-423n4/2022-04-backd-findings/issues/17
Good effort by the dev to try implement mitigations against the issues caused by latestRoundData()
:
https://github.com/code-423n4/2024-01-salty/blob/ce719503b43ebf086f5147c0f6efc3c0890bf0f9/src/price_feed/CoreChainlinkFeed.sol#L26-L60
Recommendation:
Despite the dev's efforts to address concerns with the deprecated latestRoundData
method, opting for the recommended, up-to-date Chainlink practices is advisable. This ensures a proactive and reliable approach, aligning with current standards and reducing reliance on outdated functionality for long-term stability.
PriceAggregator::setPriceFeed()
- L47: Risk of 35-day reliance on a single price feed if two feeds fail simultaneously, as setPriceFeed()
can only be called once every 35 days for performance review.Potential problem if 2 pricefeeds go down at similar time and become unreliable for longer than acceptable, then stuck with just 1 pricefeed for at least 35 days, unless emergency action is implemented for such a case:
Comment on L11:
"setPriceFeed can only be called once every 35 days by default (to allow time to review performance of the most recently upgraded PriceFeed before setting another)."
DAO::_executeSetContract()
- L133: Missing input validation check for zero hash: if for whatever reason nameHash = 0x
then event SetContract()
will still be emitted.Due to missing input validation if nameHash = 0x
then the if
block will be skipped and successfully emit event SetContract(ballot.ballotName, ballot.address1)
, which would be unexpected and potentially misleading, besides the fact that the function won't revert as it should and could potentially cause problems down the line.
Chances of nameHash = 0x
happening is probably minimal but unless gas optimization is a valid excuse, the relevant input validation check should be implemented just to play it safe.
Recommendation:
function _executeSetContract( Ballot memory ballot ) internal { bytes32 nameHash = keccak256(bytes( ballot.ballotName ) ); + require(nameHash != 0x, "Invalid/zero hash");
private
modifier, which can be viewed by anyone.Blockchain data, even information designated as private
within smart contracts, is susceptible to visibility by individuals skilled in querying the blockchain's state or analyzing transaction history. Private variables, despite their designation, are not immune to public scrutiny.
Recommendation:
To address this potential issue, it is recommended to either store sensitive data off-chain or encrypt it before being stored on-chain. Extra care should be taken to prevent on-chain data from inadvertently exposing private information.
InitialDistribution::distributionApproved()
- L53: If for whatever reason salt.balanceOf(address(this))
gets skewed from expected 100 * MILLION_ETHER
, will cause DoS of token distribution & permanent freezing of funds.Due to the fact that salt tokens dont exist in public before the airdrop & salt token distribution, it would be near impossible for an attacker to send non-existent salt tokens to this contract in order to DoS the distribution functionality and permanently freeze the salt token funds in there.
However, there's likely no guarantee that the salt.balanceOf(address(this))
won't get slightly skewed anyway due to external factors, so it would be wise to implement guaranteed mitigation against this potential scenario...
Therefore this is at least a QA and at most a medium severity issue.
IF there was any salt tokens already in circulation before this distribution function was called, this would be a high/critical severity vulnerability...
L53:
require( salt.balanceOf(address(this)) == 100 * MILLION_ETHER, "SALT has already been sent from the contract" );
IMPACT:
salt.balanceOf(address(this))
was somehow skewed before token distribution, which is highly unlikely but not impossible. To make it impossible, the recommended "fix" should be implemented.RECOMMENDATION:
==
to >=
to ensure the funds can never become stuck...- require( salt.balanceOf(address(this)) == 100 * MILLION_ETHER, "SALT has already been sent from the contract" ); + require( salt.balanceOf(address(this)) >= 100 * MILLION_ETHER, "SALT has already been sent from the contract" );
And then just modify/update the implemented logic where necessary to account for the above change, so that the intended logic behind the error message remains respected.
ManagedWallet::receive()
- L65: Contract intended to receive ETH via receive()
but seems to be no way to ever get any ETH out again, which is problematic unless its deliberate intention of protocol team?.05 ether isnt a small amount, currently it's around $125, so when this is sent to this contract 10 times, it's already $1250 of ETH funds stuck forever in the contract...
I checked and didn't see any methods/functions to withdraw the ETH funds again...
// The confirmation wallet confirms or rejects wallet proposals by sending a specific amount of ETH to this contract receive() external payable { require( msg.sender == confirmationWallet, "Invalid sender" ); // Confirm if .05 or more ether is sent and otherwise reject. // Done this way in case custodial wallets are used as the confirmationWallet - which sometimes won't allow for smart contract calls. if ( msg.value >= .05 ether ) activeTimelock = block.timestamp + TIMELOCK_DURATION; // establish the timelock else activeTimelock = type(uint256).max; // effectively never }
RECOMMENDATION:
via transfer()
:
// Function to allow the owner to withdraw ETH from the contract function withdraw(uint256 amount) external onlyOwner { require(amount > 0, "Withdrawal amount must be greater than 0"); require(address(this).balance >= amount, "Insufficient balance in the contract"); // Transfer ETH to the owner payable(owner).transfer(amount); // Emit event emit Withdrawal(owner, amount); }
OR
via call()
:
function withdraw(uint256 amount) external onlyOwner { require(amount > 0, "Withdrawal amount must be greater than 0"); require(address(this).balance >= amount, "Insufficient balance in the contract"); // Call with explicit gas and value (bool success, ) = payable(owner).call{value: amount, gas: gasleft()}(""); require(success, "Transfer failed"); // Emit event emit Withdrawal(owner, amount); }
Proposals::proposeSendSALT()
& DAO::_executeApproval()
- lack of input validation for amount
aka ballot.number1
parameter, can pass zero value and result in 0 salt safeTransferred.function proposeSendSALT( address wallet, uint256 amount, string calldata description )
function _executeApproval( Ballot memory ballot )
#0 - c4-judge
2024-02-03T14:14:59Z
Picodes marked the issue as grade-a
#1 - c4-sponsor
2024-02-08T07:23:25Z
othernet-global (sponsor) disputed
#2 - othernet-global
2024-02-08T07:23:38Z
This is acceptable and not considered a practical issue.