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: 6/62
Findings: 2
Award: $6,595.97
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: Ruhum
6389.4401 DAI - $6,389.44
https://github.com/code-423n4/2022-05-alchemix/blob/main/contracts-full/adapters/yearn/YearnTokenAdapter.sol#L13 https://github.com/code-423n4/2022-05-alchemix/blob/main/contracts-full/adapters/yearn/YearnTokenAdapter.sol#L43
YearnTokenAdapter allows slippage of 100% when withdrawing from the vault which will cause a loss of funds.
Here's the documentation straight from the vault contract: https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy#L1025-L1073
It allows the user to specify the maxLoss
as the last parameter. It determines how many shares can be burned to fulfill the withdrawal. Currently, the contract uses 10.000 which is 100%. Meaning there is no slippage check at all. This is bound to cause a loss of funds.
I'd suggest letting the user determine the slippage check themselves instead of hardcoding it.
Current maxLoss
value: https://github.com/code-423n4/2022-05-alchemix/blob/main/contracts-full/adapters/yearn/YearnTokenAdapter.sol#L13
call to Yearn vault's withdraw()
function: https://github.com/code-423n4/2022-05-alchemix/blob/main/contracts-full/adapters/yearn/YearnTokenAdapter.sol#L43
none
Allow the user to specify the slippage value themselves:
function unwrap(uint256 amount, address recipient, uint maxLoss) external override returns (uint256) { TokenUtils.safeTransferFrom(token, msg.sender, address(this), amount); uint256 balanceBefore = TokenUtils.safeBalanceOf(token, address(this)); uint256 amountWithdrawn = IYearnVaultV2(token).withdraw(amount, recipient, maxLoss); uint256 balanceAfter = TokenUtils.safeBalanceOf(token, address(this)); // If the Yearn vault did not burn all of the shares then revert. This is critical in mathematical operations // performed by the system because the system always expects that all of the tokens were unwrapped. In Yearn, // this sometimes does not happen in cases where strategies cannot withdraw all of the requested tokens (an // example strategy where this can occur is with Compound and AAVE where funds may not be accessible because // they were lent out). if (balanceBefore - balanceAfter != amount) { revert IllegalState(); } return amountWithdrawn; }
If you don't want to change the ITokenAdapter interface you can also leave the value blank. The vault will then use the default value (0.01%
)
#0 - 0xfoobar
2022-05-22T21:48:54Z
Sponsor acknowledged
This could be made more configurable by the end user but yearn vaults do not frequently experience high slippage. Slippage is handled upstream in the Alchemist contract. The reason why this slippage is set to 100% is so to permit handling of slippage in the Alchemist for all cases.
#1 - 0xleastwood
2022-06-03T17:34:22Z
Because we can't know how the yearn strategy implements withdrawals, its possible that it might contain custom swap logic which exposes itself to sandwich attacks. However, at face value, the current use of MAXIMUM_SLIPPAGE
allows the contract to successfully unwrap their tokens under poor network conditions, but it makes sense for the user to have more control over this. Downgrading this to medium risk as I believe it is more in line with that.
🌟 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
In the function's comment you state that the function should revert if msg.sender
has exceeded their mintable ceiling. But, there's no mintable ceiling in the contract.
So either the mint()
function is missing a relevant contraint or the comment is not up-to-date.
The AlchemicTokenV2Base and AlchemicTokenV1 contracts have it tho.
Relevant code:
Currently, the admin can set the fee to whatever amount they want. I think it would be reasonable to add constraints her to limit it the possibilities.
Most people tend to limit fees to < 100%
although you could go constrain it even more if you don't expect to go over a certain number.
Relevant code:
In some of your contracts you already use a two-step process to change the admin/governance address. But, you don't do it consistently. Multiple contracts just use OpenZeppelin's Ownable contract which has a direct transfer. I'd encourage you to use the two-step process throughout all the contracts since it's safer.
Relevant code:
The two-step process doesn't allow the zero address being set as the pending address. But, that should be possible. It allows the current governance address to reset the pending address. Otherwise, to revoke the pending address you have to set it to some other value.
Also, when the pending address accepts, it the function should reset the pending address back to the zero address.
So it would look like this:
function setPendingGovernance(address _pendingGovernance) external onlyGovernance { pendingGovernance = _pendingGovernance; emit PendingGovernanceUpdated(_pendingGovernance); } function acceptGovernance() external { require(msg.sender == pendingGovernance, "StakingPools: only pending governance"); address _pendingGovernance = pendingGovernance; governance = _pendingGovernance; pendingGovernance = address(0); emit GovernanceUpdated(_pendingGovernance); }
Relevant code:
The TransmuterConduit contract has a state variable sourceTransmuter
from which the funds should be migrated (according to comment).
But, the distribute()
function pulls the funds from the origin
parameter instead.
Relevant code:
The contract has two state variables: WETH
& whitelist
. WETH
is already immutable but whitelist
isn't.
But, there's no way to update the value of whitelist
.
Incase you want to update the variable, add the necessary function. Otherwise, set the variable to be immutable to save gas.
Relevant code:
Since the functions makes changes to critical parts of the contract's configuration you should emit an event there.
Relevant code:
Since the functions makes changes to critical parts of the contract's configuration you should emit an event there.
Relevant code: