Platform: Code4rena
Start Date: 05/05/2022
Pot Size: $125,000 DAI
Total HM: 17
Participants: 62
Period: 14 days
Judge: leastwood
Total Solo HM: 15
Id: 120
League: ETH
Rank: 22/62
Findings: 2
Award: $309.80
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0xDjango, 0xNazgul, 0xkatana, 0xsomeone, AuditsAreUS, BouSalman, BowTiedWardens, Cityscape, Funen, GimelSec, Hawkeye, JC, MaratCerby, MiloTruck, Picodes, Ruhum, TerrierLover, WatchPug, Waze, bobirichman, catchup, cccz, cryptphi, csanuragjain, delfin454000, ellahi, fatherOfBlocks, hake, horsefacts, hyh, jayjonah8, joestakey, kebabsec, kenta, mics, oyc_109, robee, samruna, shenwilly, sikorico, simon135, throttle, tintin
206.5302 DAI - $206.53
Some fee parameters of functions are not checked for invalid values. Validate the parameters:
AlchemicTokenV2.setFlashFee (newFee) AlchemicTokenV2Base.setFlashFee (newFee)
Some fee parameters of functions are not checked for invalid values. Validate the parameters:
AlchemicTokenV2.constructor (_flashFee)
You use safeApprove of openZeppelin although it's deprecated. (see https://github.com/OpenZeppelin/openzeppelin-contracts/blob/566a774222707e424896c0c390a84dc3c13bdcb2/contracts/token/ERC20/utils/SafeERC20.sol#L38) You should change it to increase/decrease Allowance as OpenZeppilin says.
Deprecated safeApprove in EthAssetManager.sol line 671: SafeERC20.safeApprove(address(metaPool), address(convexBooster), amount); Deprecated safeApprove in AlchemistV2.sol line 478: TokenUtils.safeApprove(ITokenAdapter(adapter).underlyingToken(), adapter, type(uint256).max); Deprecated safeApprove in AlchemistV2.sol line 477: TokenUtils.safeApprove(yieldToken, adapter, type(uint256).max); Deprecated safeApprove in TokenUtils.sol line 83: abi.encodeWithSelector(IERC20Minimal.approve.selector, spender, value) Deprecated safeApprove in ThreePoolAssetManager.sol line 781: SafeERC20.safeApprove(address(tokens[i]), address(threePool), 0);
external / public functions parameters should be validated to make sure the address is not 0. Otherwise if not given the right input it can mistakenly lead to loss of user funds.
TransmuterBuffer.sol.setAlchemist _alchemist TransmuterBuffer.sol.setTransmuter underlyingToken TransmuterV2.sol.initialize _syntheticToken CrossChainCanonicalBase.sol.setSwapFees bridgeTokenAddress AlchemicTokenV1.sol.setCeiling minter
The project is compiled with different versions of solidity, which is not recommended because it can lead to undefined behaviors.
WETH address is hardcoded but it may differ on other chains, e.g. Polygon, so make sure to check this before deploying and update if necessary You should consider injecting WETH address via the constructor. (previous issue: https://github.com/code-423n4/2021-10-ambire-findings/issues/54)
Hardcoded weth address in AutoleverageCurveFactoryethpool.sol
You should use safe math for solidity version <8 since there is no default over/under flow check it suchversions of solidity.
The contract IERC20TokenReceiver.sol doesn't use safe math and is of solidity version < 8 The contract IStaticAToken.sol doesn't use safe math and is of solidity version < 8 The contract IYearnVaultV2.sol doesn't use safe math and is of solidity version < 8 The contract IConvexBooster.sol doesn't use safe math and is of solidity version < 8 The contract IWethGateway.sol doesn't use safe math and is of solidity version < 8
owner param should be validated to make sure the owner address is not address(0). Otherwise if not given the right input all only owner accessible functions will be unaccessible.
AlchemistV2.sol.poke owner TransmuterV2.sol.deposit owner gALCX.sol.transferOwnership _owner AlchemistV2.sol.withdrawFrom owner AlchemicTokenV1.sol.burnFrom owner
Most contracts use an init pattern (instead of a constructor) to initialize contract parameters. Unless these are enforced to be atomic with contact deployment via deployment script or factory contracts, they are susceptible to front-running race conditions where an attacker/griefer can front-run (cannot access control because admin roles are not initialized) to initially with their own (malicious) parameters upon detecting (if an event is emitted) which the contract deployer has to redeploy wasting gas and risking other transactions from interacting with the attacker-initialized contract.
Many init functions do not have an explicit event emission which makes monitoring such scenarios harder. All of them have re-init checks; while many are explicit some (those in auction contracts) have implicit reinit checks in initAccessControls() which is better if converted to an explicit check in the main init function itself. (details credit to: https://github.com/code-423n4/2021-09-sushimiso-findings/issues/64) The vulnerable initialization functions in the codebase are:
TransmuterBuffer.sol, initialize, 84 AlchemistV2.sol, constructor, 110 TransmuterBuffer.sol, constructor, 78 TransmuterV2.sol, initialize, 143 TransmuterV2.sol, constructor, 141
Users can mistakenly think that the return value is the named return, but it is actually the actualreturn statement that comes after. To know that the user needs to read the code and is confusing. Furthermore, removing either the actual return or the named return will save gas.
EthAssetManager.sol, burnMetaPoolTokens ThreePoolAssetManager.sol, mintMetaPoolTokens AlchemistV2.sol, getRepayLimitInfo AlchemistV2.sol, positions AlchemistV2.sol, accounts
The following contracts have a function that allows them an admin to change it to a different address. If the admin accidentally uses an invalid address for which they do not have the private key, then the system gets locked. It is important to have two steps admin change where the first is announcing a pending new admin and the new address should then claim its ownership. A similar issue was reported in a previous contest and was assigned a severity of medium: code-423n4/2021-06-realitycards-findings#105
gALCX.sol ILendingPoolAddressesProvider.sol
The following functions are missing reentrancy modifier although some other pulbic/external functions does use reentrancy modifer. Even though I did not find a way to exploit it, it seems like those functions should have the nonReentrant modifier as the other functions have it as well..
CrossChainCanonicalBase.sol, recoverERC20 is missing a reentrancy modifier
Those are functions and parameters pairs that the function doesn't use the parameter. In case those functions are external/public this is even worst since the user is required to put value that never used and can misslead him and waste its time.
AutoleverageCurveFactoryethpool.sol: function _maybeConvertCurveOutput parameter amountOut isn't used. (_maybeConvertCurveOutput is internal) AlchemistV2.sol: function _usub parameter x isn't used. (_usub is internal) ThreePoolAssetManager.sol: function onERC20Received parameter value isn't used. (onERC20Received is external) AutoleverageCurveMetapool.sol: function _maybeConvertCurveOutput parameter amountOut isn't used. (_maybeConvertCurveOutput is internal) EthAssetManager.sol: function onERC20Received parameter value isn't used. (onERC20Received is external)
Use of tx.origin for authorization may be abused by a MITM malicious contract forwarding calls from the legitimate user who interacts with it. Use msg.sender instead.
WETHGateway.sol, 87: if (tx.origin == msg.sender) { AutoleverageBase.sol, 86: if (!(tx.origin == msg.sender || whitelist.isWhitelisted(msg.sender))) revert Unauthorized(msg.sender); AlchemistV2.sol, 1704: if (tx.origin == msg.sender) {
The following functions are missing commenting as describe below:
ThreePoolAssetManager.sol, onERC20Received (external), parameters token, value not commented gALCX.sol, unstake (external), @return is missing EthAssetManager.sol, onERC20Received (external), parameters token, value not commented
Open TODOs can hint at programming or architectural errors that still need to be fixed. These files has open TODOs:
Open TODO in IStableMetaPool.sol line 5 : /// @dev TODO
Transferring tokens to the zero address is usually prohibited to accidentally avoid "burning" tokens by sending them to an unrecoverable zero address.
https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-full/ThreePoolAssetManager.sol#L731 https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-full/ThreePoolAssetManager.sol#L1014 https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-hardhat/CrossChainCanonicalBase.sol#L96 https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-full/AlchemistV2.sol#L565 https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-full/AlchemistV2.sol#L602
Those are functions and parameters pairs that the function doesn't use the parameter. In case those functions are external/public this is even worst since the user is required to put value that never used and can misslead him and waste its time.
AutoleverageCurveFactoryethpool.sol: function _maybeConvertCurveOutput parameter amountOut isn't used. (_maybeConvertCurveOutput is internal) AlchemistV2.sol: function _usub parameter x isn't used. (_usub is internal) ThreePoolAssetManager.sol: function onERC20Received parameter value isn't used. (onERC20Received is external) AutoleverageCurveMetapool.sol: function _maybeConvertCurveOutput parameter amountOut isn't used. (_maybeConvertCurveOutput is internal) EthAssetManager.sol: function onERC20Received parameter value isn't used. (onERC20Received is external)
Use of tx.origin for authorization may be abused by a MITM malicious contract forwarding calls from the legitimate user who interacts with it. Use msg.sender instead.
WETHGateway.sol, 87: if (tx.origin == msg.sender) { AutoleverageBase.sol, 86: if (!(tx.origin == msg.sender || whitelist.isWhitelisted(msg.sender))) revert Unauthorized(msg.sender); AlchemistV2.sol, 1704: if (tx.origin == msg.sender) {
The following functions are missing commenting as describe below:
ThreePoolAssetManager.sol, onERC20Received (external), parameters token, value not commented gALCX.sol, unstake (external), @return is missing EthAssetManager.sol, onERC20Received (external), parameters token, value not commented
To give more trust to users: functions that set key/critical variables should be put behind a timelock.
https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-full/StakingPools.sol#L123 https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-hardhat/CrossChainCanonicalBase.sol#L163 https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-full/EthAssetManager.sol#L352 https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-full/AlchemicTokenV2Base.sol#L141 https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-full/AlchemicTokenV1.sol#L110
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0x4non, 0xDjango, 0xNazgul, 0xf15ers, 0xkatana, 0xsomeone, AlleyCat, BowTiedWardens, Cityscape, Fitraldys, Funen, GimelSec, Hawkeye, JC, MaratCerby, MiloTruck, Randyyy, TerrierLover, Tomio, UnusualTurtle, WatchPug, Waze, _Adam, augustg, bobirichman, catchup, csanuragjain, ellahi, fatherOfBlocks, hake, hansfriese, horsefacts, ignacio, joestakey, kenta, mics, oyc_109, robee, samruna, sashik_eth, sikorico, simon135, throttle
103.2651 DAI - $103.27
In the following files there are state variables that could be set immutable to save gas.
_mintingLimiter in AlchemistV2.sol token in TransmuterConduit.sol whitelist in AlchemistV2.sol conversionFactor in TransmuterV2.sol underlyingToken in TransmuterV2.sol
Unused state variables are gas consuming at deployment (since they are located in storage) and are a bad code practice. Removing those variables will decrease deployment gas cost and improve code quality. This is a full list of all the unused storage variables we found in your code base.
FuseTokenAdapterV1.sol, version TransmuterBuffer.sol, version VesperAdapterV1.sol, version AutoleverageCurveFactoryethpool.sol, wethAddress RETHAdapterV1.sol, version
Unused local variables are gas consuming, since the initial value assignment costs gas. And are a bad code practice. Removing those variables will decrease the gas cost and improve code quality. This is a full list of all the unused storage variables we found in your code base.
EthAssetManager.sol, reclaimEth, balance EthAssetManager.sol, _mintMetaPoolTokens, minimumMintAmount gALCX.sol, reApprove, success TransmuterBuffer.sol, _alchemistWithdraw, lastAccruedWeight
Caching the array length is more gas efficient. This is because access to a local variable in solidity is more efficient than query storage / calldata / memory. We recommend to change from:
for (uint256 i=0; i<array.length; i++) { ... }
to:
uint len = array.length for (uint256 i=0; i<len; i++) { ... }
TransmuterBuffer.sol, registeredUnderlyings, 242 TransmuterBuffer.sol, tokens.weighting, 479 AlchemistV2.sol, values.depositedTokens, 1282 TransmuterBuffer.sol, registeredUnderlyings, 382 CrossChainCanonicalBase.sol, bridgeTokensArray, 141
Prefix increments are cheaper than postfix increments.
Further more, using unchecked {++x} is even more gas efficient, and the gas saving accumulates every iteration and can make a real change
There is no risk of overflow caused by increamenting the iteration index in for loops (the ++i
in for (uint256 i = 0; i < numIterations; ++i)
).
But increments perform overflow checks that are not necessary in this case.
These functions use not using prefix increments (++x
) or not using the unchecked keyword:
change to prefix increment and unchecked: TransmuterBuffer.sol, j, 382 change to prefix increment and unchecked: TransmuterBuffer.sol, i, 186 change to prefix increment and unchecked: ThreePoolAssetManager.sol, i, 250 change to prefix increment and unchecked: TransmuterBuffer.sol, i, 242 change to prefix increment and unchecked: ThreePoolAssetManager.sol, i, 353
In for loops you initialize the index to start from 0, but it already initialized to 0 in default and this assignment cost gas. It is more clear and gas efficient to declare without assigning 0 and will have the same meaning:
TransmuterBuffer.sol, 186 AlchemistV2.sol, 1355 EthAssetManager.sol, 567 TransmuterBuffer.sol, 272 ThreePoolAssetManager.sol, 902
Reading a storage variable is gas costly (SLOAD). In cases of multiple read of a storage variable in the same scope, caching the first read (i.e saving as a local variable) can save gas and decrease the overall gas uses. The following is a list of functions and the storage variables that you read twice:
ThreePoolAssetManager.sol: rewardReceiver is read twice in claimRewards EthAssetManager.sol: rewardReceiver is read twice in claimRewards TransmuterBuffer.sol: alchemist is read twice in setAlchemist EthAssetManager.sol: rewardReceiver is read twice in _claimRewards ThreePoolAssetManager.sol: rewardReceiver is read twice in _claimRewards
Unnecessary default assignments, you can just declare and it will save gas and have the same meaning.
FuseTokenAdapterV1.sol (L#36) : uint256 private constant NO_ERROR = 0;
Use bytes32 instead of string to save gas whenever possible. String is a dynamic data structure and therefore is more gas consuming then bytes32.
AlchemistV2.sol (L54), string public constant override version = "2.2.6"; RETHAdapterV1.sol (L32), string public override version = "1.0.0"; FuseTokenAdapterV1.sol (L29), string public override version = "1.0.0"; TransmuterBuffer.sol (L37), string public constant override version = "2.2.0"; VesperAdapterV1.sol (L30), string public override version = "1.0.0";
The following require messages are of length more than 32 and we think are short enough to short them into exactly 32 characters such that it will be placed in one slot of memory and the require function will cost less gas. The list:
Solidity file: StakingPools.sol, In line 183, Require message length to shorten: 37, The message: StakingPools: weights length mismatch Solidity file: AlchemicTokenV1.sol, In line 81, Require message length to shorten: 40, The message: AlUSD: Alchemist's ceiling was breached. Solidity file: StakingPools.sol, In line 131, Require message length to shorten: 37, The message: StakingPools: only pending governance Solidity file: StakingPools.sol, In line 160, Require message length to shorten: 38, The message: StakingPools: token already has a pool
Using != 0 is slightly cheaper than > 0. (see https://github.com/code-423n4/2021-12-maple-findings/issues/75 for similar issue)
AlchemistV2.sol, 678: change 'amount > 0' to 'amount != 0' AlchemistV2.sol, 466: change 'blocks > 0' to 'blocks != 0' AlchemistV2.sol, 1103: change 'amount > 0' to 'amount != 0' gALCX.sol, 75: change 'balance > 0' to 'balance != 0' AlchemistV2.sol, 707: change 'amount > 0' to 'amount != 0'
address ThreePoolAssetManager.sol.sweep - unnecessary casting address(token) int256 LiquidityMath.sol.addDelta - unnecessary casting int256(y) MetaPoolAsset EthAssetManager.sol.metaPoolAssetReserves - unnecessary casting MetaPoolAsset(asset) ThreePoolAsset ThreePoolAssetManager.sol.reclaimThreePoolAsset - unnecessary casting ThreePoolAsset(asset) ThreePoolAsset ThreePoolAssetManager.sol._mintThreePoolTokens - unnecessary casting ThreePoolAsset(asset)
You can inline the following functions instead of writing a specific function to save gas. (see https://github.com/code-423n4/2021-11-nested-findings/issues/167 for a similar issue.)
EthAssetManager.sol, min, { return x > y ? y : x; } CrossChainCanonicalBase.sol, _isFeeExempt, { return feeExempt[targetAddress]; } LiquidityMath.sol, deNormalizeValue, { return (input * (10**decimals)) / (10**18); } AlchemistV2.sol, _normalizeDebtTokensToUnderlying, { return amount / _underlyingTokens[underlyingToken].conversionFactor; } AlchemistV2.sol, _normalizeUnderlyingTokensToDebt, { return amount * _underlyingTokens[underlyingToken].conversionFactor; }
The following functions are used exactly once. Therefore you can inline them and save gas and improve code clearness.
FixedPointMath.sol, encodeRaw Pool.sol, getRewardRate Pool.sol, length Mutex.sol, _freeLock TransmuterV2.sol, _normalizeDebtTokensToUnderlying
You calculate the power of 10 every time you use it instead of caching it once as a constant variable and using it instead. Fix the following code lines:
AlchemistV2.sol, 113 : You should cache the used power of 10 as constant state variable since it's used several times (2): return _convertSharesToYieldTokens(yieldToken, 10**_yieldTokens[yieldToken].decimals);
ThreePoolAssetManager.sol, 778 : You should cache the used power of 10 as constant state variable since it's used several times (3): normalizedTotal += amounts[i] * 10**missingDecimals;
ThreePoolAssetManager.sol, 828 : You should cache the used power of 10 as constant state variable since it's used several times (3): uint256 normalizedAmount = amount * 10**missingDecimals;
LiquidityMath.sol, 45 : You should cache the used power of 10 as constant state variable since it's used several times (2): return (input * (10decimals)) / (1018);
AlchemistV2.sol, 1647 : You should cache the used power of 10 as constant state variable since it's used several times (2): return amount * adapter.price() / 10**yieldTokenParams.decimals;
Using newer compiler versions and the optimizer gives gas optimizations and additional safety checks are available for free.
The advantages of versions 0.8.* over <0.8.0 are:
1. Safemath by default from 0.8.0 (can be more gas efficient than library based safemath.) 2. Low level inliner : from 0.8.2, leads to cheaper runtime gas. Especially relevant when the contract has small functions. For example, OpenZeppelin libraries typically have a lot of small helper functions and if they are not inlined, they cost an additional 20 to 40 gas because of 2 extra jump instructions and additional stack operations needed for function calls. 3. Optimizer improvements in packed structs: Before 0.8.3, storing packed structs, in some cases used an additional storage read operation. After EIP-2929, if the slot was already cold, this means unnecessary stack operations and extra deploy time costs. However, if the slot was already warm, this means additional cost of 100 gas alongside the same unnecessary stack operations and extra deploy time costs. 4. Custom errors from 0.8.4, leads to cheaper deploy time cost and run time cost. Note: the run time cost is only relevant when the revert condition is met. In short, replace revert strings by custom errors.
IEthStableMetaPool.sol IAlchemicToken.sol IAlchemistV2AdminActions.sol IConvexBooster.sol IConvexRewards.sol
We recommend not to cache msg.sender since calling it is 2 gas while reading a variable is more.
https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-hardhat/gALCX.sol#L27 https://github.com/code-423n4/2022-05-alchemix/tree/main/contracts-full/gALCX.sol#L27