Alchemix contest - robee's results

A protocol for self-repaying loans with no liquidation risk.

General Information

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

Alchemix

Findings Distribution

Researcher Performance

Rank: 22/62

Findings: 2

Award: $309.80

🌟 Selected for report: 0

🚀 Solo Findings: 0

Missing fee parameter validation

Some fee parameters of functions are not checked for invalid values. Validate the parameters:

Code instances:

AlchemicTokenV2.setFlashFee (newFee) AlchemicTokenV2Base.setFlashFee (newFee)

Does not validate the input fee parameter

Some fee parameters of functions are not checked for invalid values. Validate the parameters:

Code instance:

AlchemicTokenV2.constructor (_flashFee)

safeApprove of openZeppelin is deprecated

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.

Code instances:

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);

Not verified input

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.

Code instances:

TransmuterBuffer.sol.setAlchemist _alchemist TransmuterBuffer.sol.setTransmuter underlyingToken TransmuterV2.sol.initialize _syntheticToken CrossChainCanonicalBase.sol.setSwapFees bridgeTokenAddress AlchemicTokenV1.sol.setCeiling minter

Solidity compiler versions mismatch

The project is compiled with different versions of solidity, which is not recommended because it can lead to undefined behaviors.

Code instance:

Hardcoded WETH

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)

Code instance:

Hardcoded weth address in AutoleverageCurveFactoryethpool.sol

Use safe math for solidity version <8

You should use safe math for solidity version <8 since there is no default over/under flow check it suchversions of solidity.

Code instances:

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

Not verified owner

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.

Code instances:

AlchemistV2.sol.poke owner TransmuterV2.sol.deposit owner gALCX.sol.transferOwnership _owner AlchemistV2.sol.withdrawFrom owner AlchemicTokenV1.sol.burnFrom owner

Init frontrun

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:

Code instances:

TransmuterBuffer.sol, initialize, 84 AlchemistV2.sol, constructor, 110 TransmuterBuffer.sol, constructor, 78 TransmuterV2.sol, initialize, 143 TransmuterV2.sol, constructor, 141

Named return issue

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.

Code instances:

EthAssetManager.sol, burnMetaPoolTokens ThreePoolAssetManager.sol, mintMetaPoolTokens AlchemistV2.sol, getRepayLimitInfo AlchemistV2.sol, positions AlchemistV2.sol, accounts

Two Steps Verification before Transferring Ownership

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

Code instances:

gALCX.sol ILendingPoolAddressesProvider.sol

Missing non reentrancy modifier

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..

Code instance:

CrossChainCanonicalBase.sol, recoverERC20 is missing a reentrancy modifier

Never used parameters

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.

Code instances:

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)

Dangerous usage of tx.origin

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.

Code instances:

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) {

Missing commenting

The following functions are missing commenting as describe below:

Code instances:

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

Open TODOs can hint at programming or architectural errors that still need to be fixed. These files has open TODOs:

Code instance:

Open TODO in IStableMetaPool.sol line 5 : /// @dev TODO

Check transfer receiver is not 0 to avoid burned money

Transferring tokens to the zero address is usually prohibited to accidentally avoid "burning" tokens by sending them to an unrecoverable zero address.

Code instances:

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

Never used parameters

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.

Code instances:

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)

Dangerous usage of tx.origin

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.

Code instances:

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) {

Missing commenting

The following functions are missing commenting as describe below:

Code instances:

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

Add a timelock

To give more trust to users: functions that set key/critical variables should be put behind a timelock.

Code instances:

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

State variables that could be set immutable

In the following files there are state variables that could be set immutable to save gas.

Code instances:

_mintingLimiter in AlchemistV2.sol token in TransmuterConduit.sol whitelist in AlchemistV2.sol conversionFactor in TransmuterV2.sol underlyingToken in TransmuterV2.sol

Unused state variables

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.

Code instances:

FuseTokenAdapterV1.sol, version TransmuterBuffer.sol, version VesperAdapterV1.sol, version AutoleverageCurveFactoryethpool.sol, wethAddress RETHAdapterV1.sol, version

Unused declared local variables

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.

Code instances:

EthAssetManager.sol, reclaimEth, balance EthAssetManager.sol, _mintMetaPoolTokens, minimumMintAmount gALCX.sol, reApprove, success TransmuterBuffer.sol, _alchemistWithdraw, lastAccruedWeight

Caching array length can save gas

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++) { ... }

Code instances:

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

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:

Code instances:

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

Unnecessary index init

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:

Code instances:

TransmuterBuffer.sol, 186 AlchemistV2.sol, 1355 EthAssetManager.sol, 567 TransmuterBuffer.sol, 272 ThreePoolAssetManager.sol, 902

Storage double reading. Could save SLOAD

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:

Code instances:

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 assignment

Unnecessary default assignments, you can just declare and it will save gas and have the same meaning.

Code instance:

FuseTokenAdapterV1.sol (L#36) : uint256 private constant NO_ERROR = 0;

Use bytes32 instead of string to save gas whenever possible

Use bytes32 instead of string to save gas whenever possible. String is a dynamic data structure and therefore is more gas consuming then bytes32.

Code instances:

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";

Short the following require messages

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:

Code instances:

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

Use != 0 instead of > 0

Using != 0 is slightly cheaper than > 0. (see https://github.com/code-423n4/2021-12-maple-findings/issues/75 for similar issue)

Code instances:

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'

Unnecessary cast

Code instances:

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)

Consider inline the following functions to save gas

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.)

Code instances

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; }

Inline one time use functions

The following functions are used exactly once. Therefore you can inline them and save gas and improve code clearness.

Code instances:

FixedPointMath.sol, encodeRaw Pool.sol, getRewardRate Pool.sol, length Mutex.sol, _freeLock TransmuterV2.sol, _normalizeDebtTokensToUnderlying

Cache powers of 10 used several times

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:

Code instances:

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;

Upgrade pragma to at least 0.8.4

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.

Code instances:

IEthStableMetaPool.sol IAlchemicToken.sol IAlchemistV2AdminActions.sol IConvexBooster.sol IConvexRewards.sol

Do not cache msg.sender

We recommend not to cache msg.sender since calling it is 2 gas while reading a variable is more.

Code instances:

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
AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter