Platform: Code4rena
Start Date: 05/01/2023
Pot Size: $90,500 USDC
Total HM: 55
Participants: 103
Period: 14 days
Judge: Picodes
Total Solo HM: 18
Id: 202
League: ETH
Rank: 28/103
Findings: 5
Award: $450.89
🌟 Selected for report: 3
🚀 Solo Findings: 0
69.0905 USDC - $69.09
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L38-L52 https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L129-L133 https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC20-Cloned.sol#L172-L183
For a public vault, calling the AstariaRouter.mint
or PublicVault.mint
function can eventually call the following ERC4626Cloned.mint
function. Because the ERC4626Cloned.previewMint
function returns 10e18
when the public vault's totalSupply()
returns 0, the ERC4626Cloned.mint
function would transfer 10e18
asset tokens from the first liquidity provider to the public vault and mint the specified number of shares in return. Hence, as the first liquidity provider of the public vault, a user who has 10 ether asset tokens can, for example, call the AstariaRouter.mint
function with the shares
and maxAmountIn
inputs both being type(uint256).max
to mint type(uint256).max
shares while only lending 10 ether asset tokens. Afterwards, all other users' AstariaRouter.mint
, PublicVault.mint
, AstariaRouter.deposit
, and PublicVault.deposit
function calls will all revert since the public vault's total supply is already type(uint256).max
, and the ERC20Cloned._mint
function's execution of s._totalSupply += amount
will overflow. As a result, for this public vault, the first liquidity provider is able to block other users from becoming liquidity providers and becomes the only one who can earn yield.
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L38-L52
function mint( uint256 shares, address receiver ) public virtual returns (uint256 assets) { assets = previewMint(shares); // No need to check for rounding error, previewMint rounds up. require(assets > minDepositAmount(), "VALUE_TOO_SMALL"); // Need to transfer before minting or ERC777s could reenter. ERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); afterDeposit(assets, shares); }
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L129-L133
function previewMint(uint256 shares) public view virtual returns (uint256) { uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? 10e18 : shares.mulDivUp(totalAssets(), supply); }
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC20-Cloned.sol#L172-L183
function _mint(address to, uint256 amount) internal virtual { ERC20Data storage s = _loadERC20Slot(); s._totalSupply += amount; // Cannot overflow because the sum of all user // balances can't exceed the max uint256 value. unchecked { s.balanceOf[to] += amount; } emit Transfer(address(0), to, amount); }
Please add the following test in src\test\AstariaTest.t.sol
. This test will pass to demonstrate the described scenario.
function testFirstPublicVaultLiquidityProviderWhoHas10EtherAssetTokensBlocksOthersFromBecomingLP() public { uint256 amountIn = 10 ether; address alice = address(1); vm.deal(alice, amountIn); address bob = address(2); vm.deal(bob, amountIn); address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 14 days }); vm.startPrank(alice); // alice owns 10 ether WETH WETH9.deposit{value: amountIn}(); WETH9.transfer(address(ASTARIA_ROUTER), amountIn); // alice becomes publicVault's first liquidity provider by minting type(uint256).max shares // while only lending 10 ether WETH. ASTARIA_ROUTER.mint( IERC4626(publicVault), alice, type(uint256).max, type(uint256).max ); vm.stopPrank(); vm.startPrank(bob); // bob also owns 10 ether WETH WETH9.deposit{value: amountIn}(); WETH9.transfer(address(ASTARIA_ROUTER), amountIn); // However, bob's various ways of lending WETH to publicVault all fail after alice's minting // so he is unable to become a liquidity provider of publicVault. vm.expectRevert(bytes("VALUE_TOO_SMALL")); ASTARIA_ROUTER.mint( IERC4626(publicVault), bob, amountIn, type(uint256).max ); vm.expectRevert(bytes("")); ASTARIA_ROUTER.mint( IERC4626(publicVault), bob, type(uint256).max, type(uint256).max ); vm.expectRevert(bytes("")); ASTARIA_ROUTER.deposit( IERC4626(publicVault), bob, amountIn, 0 ); vm.stopPrank(); }
VSCode
The ERC4626Cloned.previewMint
function can be updated to return shares
instead of 10e18
when totalSupply()
returns 0. Then, in the ERC4626Cloned.mint
function, for the first minting, shares
can be required to be more than the minimum deposit amount, and some extra shares can be minted to address(0)
for preventing the share price manipulation by the first liquidity provider.
#0 - c4-judge
2023-01-25T17:04:32Z
Picodes marked the issue as duplicate of #588
#1 - c4-judge
2023-01-25T17:04:37Z
Picodes marked the issue as partial-50
#2 - c4-judge
2023-02-19T16:58:07Z
Picodes changed the severity to 3 (High Risk)
#3 - c4-judge
2023-02-24T10:22:48Z
Picodes marked the issue as satisfactory
🌟 Selected for report: rbserver
Also found by: Apocalypto, Jeiwan, evan, jesusrod15, ladboy233, m9800
215.1227 USDC - $215.12
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626RouterBase.sol#L41-L52 https://github.com/code-423n4/2023-01-astaria/blob/main/src/Vault.sol#L70-L73
Calling the AstariaRouter.withdraw
function calls the following ERC4626RouterBase.withdraw
function; however, calling ERC4626RouterBase.withdraw
function for a private vault reverts because the Vault
contract does not have an approve
function. Directly calling the Vault.withdraw
function for a private vault can also revert since the Vault
contract does not have a way to set the allowance for itself to transfer the asset token, which can cause many ERC20 tokens' transferFrom
function calls to revert when deducting the transfer amount from the allowance. Hence, after depositing some of the asset token in a private vault, the strategist can fail to withdraw this asset token from this private vault and lose this deposit.
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626RouterBase.sol#L41-L52
function withdraw( IERC4626 vault, address to, uint256 amount, uint256 maxSharesOut ) public payable virtual override returns (uint256 sharesOut) { ERC20(address(vault)).safeApprove(address(vault), amount); if ((sharesOut = vault.withdraw(amount, to, msg.sender)) > maxSharesOut) { revert MaxSharesError(); } }
https://github.com/code-423n4/2023-01-astaria/blob/main/src/Vault.sol#L70-L73
function withdraw(uint256 amount) external { require(msg.sender == owner()); ERC20(asset()).safeTransferFrom(address(this), msg.sender, amount); }
Please add the following test in src\test\AstariaTest.t.sol
. This test will pass to demonstrate the described scenario.
function testPrivateVaultStrategistIsUnableToWithdraw() public { uint256 amountToLend = 50 ether; vm.deal(strategistOne, amountToLend); address privateVault = _createPrivateVault({ strategist: strategistOne, delegate: strategistTwo }); vm.startPrank(strategistOne); WETH9.deposit{value: amountToLend}(); WETH9.approve(privateVault, amountToLend); // strategistOne deposits 50 ether WETH to privateVault Vault(privateVault).deposit(amountToLend, strategistOne); // calling router's withdraw function for withdrawing assets from privateVault reverts vm.expectRevert(bytes("APPROVE_FAILED")); ASTARIA_ROUTER.withdraw( IERC4626(privateVault), strategistOne, amountToLend, type(uint256).max ); // directly withdrawing various asset amounts from privateVault also fails vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); Vault(privateVault).withdraw(amountToLend); vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); Vault(privateVault).withdraw(1); vm.stopPrank(); }
VSCode
https://github.com/code-423n4/2023-01-astaria/blob/main/src/Vault.sol#L72 can be updated to the following code.
ERC20(asset()).safeTransfer(msg.sender, amount);
#0 - c4-judge
2023-01-24T09:26:20Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2023-01-27T03:15:15Z
SantiagoGregory marked the issue as sponsor confirmed
#2 - androolloyd
2023-02-03T17:41:53Z
@SantiagoGregory
#3 - c4-judge
2023-02-15T07:49:14Z
Picodes marked the issue as satisfactory
#4 - c4-judge
2023-02-15T07:49:33Z
Picodes marked the issue as selected for report
🌟 Selected for report: rbserver
Also found by: Jeiwan, adriro, cccz, chaduke, rvierdiiev, unforgiven
64.5368 USDC - $64.54
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626RouterBase.sol#L15-L25 https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L38-L52 https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L129-L133
For a public vault, calling the AstariaRouter.mint
function calls the following ERC4626RouterBase.mint
function and then the ERC4626Cloned.mint
function below. After user actions like borrowing and repaying after some time, the public vault's share price can become bigger than 1 so ERC4626Cloned.previewMint
function's execution of shares.mulDivUp(totalAssets(), supply)
would return assets
that is bigger than shares
; then, calling the ERC4626Cloned.mint
function would try to transfer this assets
from msg.sender
to the public vault. When the AstariaRouter.mint
function is called, this msg.sender
is the AstariaRouter
contract. However, because the AstariaRouter.mint
function only approves the public vault for transferring shares
of the asset token on behalf of the router, the ERC4626Cloned.mint
function's transfer of assets
of the asset token would fail due to the insufficient asset token allowance. Hence, when the public vault's share price is bigger than 1, users are unable to mint shares from the public vault using the AstariaRouter
contract and cannot utilize the slippage control associated with the maxAmountIn
input.
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626RouterBase.sol#L15-L25
function mint( IERC4626 vault, address to, uint256 shares, uint256 maxAmountIn ) public payable virtual override returns (uint256 amountIn) { ERC20(vault.asset()).safeApprove(address(vault), shares); if ((amountIn = vault.mint(shares, to)) > maxAmountIn) { revert MaxAmountError(); } }
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L38-L52
function mint( uint256 shares, address receiver ) public virtual returns (uint256 assets) { assets = previewMint(shares); // No need to check for rounding error, previewMint rounds up. require(assets > minDepositAmount(), "VALUE_TOO_SMALL"); // Need to transfer before minting or ERC777s could reenter. ERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); afterDeposit(assets, shares); }
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L129-L133
function previewMint(uint256 shares) public view virtual returns (uint256) { uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? 10e18 : shares.mulDivUp(totalAssets(), supply); }
Please add the following test in src\test\AstariaTest.t.sol
. This test will pass to demonstrate the described scenario.
function testUserFailsToMintSharesFromPublicVaultUsingRouterWhenSharePriceIsBiggerThanOne() public { uint256 amountIn = 50 ether; address alice = address(1); address bob = address(2); vm.deal(bob, amountIn); TestNFT nft = new TestNFT(2); _mintNoDepositApproveRouter(address(nft), 5); address tokenContract = address(nft); uint256 tokenId = uint256(0); address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 14 days }); // after alice deposits 50 ether WETH in publicVault, publicVault's share price becomes 1 _lendToVault(Lender({addr: alice, amountToLend: amountIn}), publicVault); // the borrower borrows 10 ether WETH from publicVault (, ILienToken.Stack[] memory stack1) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: standardLienDetails, amount: 10 ether, isFirstLien: true }); uint256 collateralId = tokenContract.computeId(tokenId); // the borrower repays for the lien after 9 days, and publicVault's share price becomes bigger than 1 vm.warp(block.timestamp + 9 days); _repay(stack1, 0, 100 ether, address(this)); vm.startPrank(bob); // bob owns 50 ether WETH WETH9.deposit{value: amountIn}(); WETH9.transfer(address(ASTARIA_ROUTER), amountIn); // bob wants to mint 1 ether shares from publicVault using the router but fails vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); ASTARIA_ROUTER.mint( IERC4626(publicVault), bob, 1 ether, type(uint256).max ); vm.stopPrank(); }
VSCode
In the ERC4626RouterBase.mint
function, the public vault's previewMint
function can be used to get an assetAmount
for the shares
input. https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626RouterBase.sol#L21 can then be updated to the following code.
ERC20(vault.asset()).safeApprove(address(vault), assetAmount);
#0 - c4-judge
2023-01-26T16:22:05Z
Picodes marked the issue as duplicate of #118
#1 - c4-judge
2023-02-19T11:01:32Z
Picodes marked the issue as selected for report
#2 - c4-judge
2023-02-19T11:06:42Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-02-24T08:53:50Z
Picodes changed the severity to QA (Quality Assurance)
#4 - c4-judge
2023-02-24T08:55:59Z
This previously downgraded issue has been upgraded by Picodes
🌟 Selected for report: rbserver
Also found by: 0Kage, 0xcm, bin2chen, caventa, csanuragjain, synackrst, unforgiven
50.8227 USDC - $50.82
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L19-L36 https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L38-L52
The following ERC4626Cloned.deposit
function has require(shares > minDepositAmount(), "VALUE_TOO_SMALL")
as the minimum deposit requirement, and the ERC4626Cloned.mint
function below has require(assets > minDepositAmount(), "VALUE_TOO_SMALL")
as the minimum deposit requirement. For a public vault, when the share price becomes different than 1, these functions' minimum deposit requirements are no longer the same. For example, given S
is the shares
input value for the ERC4626Cloned.mint
function and A
equals ERC4626Cloned.previewMint(S)
, when the share price is bigger than 1 and A
equals minDepositAmount() + 1
, such A
will violate the ERC4626Cloned.deposit
function's minimum deposit requirement but the corresponding S
will not violate the ERC4626Cloned.mint
function's minimum deposit requirement; in this case, the user can just ignore the ERC4626Cloned.deposit
function and call ERC4626Cloned.mint
function to become a liquidity provider. Thus, when the public vault's share price is different than 1, the liquidity provider can call the less restrictive function out of the two so the minimum deposit requirement enforced by one of the two functions is not effective at all; this can result in unexpected deposit amounts and degraded filtering of who can participate as a liquidity provider.
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L19-L36
function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { // Check for rounding error since we round down in previewDeposit. require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); require(shares > minDepositAmount(), "VALUE_TOO_SMALL"); // Need to transfer before minting or ERC777s could reenter. ERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); _mint(receiver, shares); ... }
https://github.com/AstariaXYZ/astaria-gpl/blob/main/src/ERC4626-Cloned.sol#L38-L52
function mint( uint256 shares, address receiver ) public virtual returns (uint256 assets) { assets = previewMint(shares); // No need to check for rounding error, previewMint rounds up. require(assets > minDepositAmount(), "VALUE_TOO_SMALL"); // Need to transfer before minting or ERC777s could reenter. ERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); _mint(receiver, shares); ... }
Please add the following test in src\test\AstariaTest.t.sol
. This test will pass to demonstrate the described scenario.
function testMinimumDepositRequirementForPublicVaultThatIsEnforcedByDepositFunctionCanBeBypassedByMintFunctionOfERC4626ClonedContractWhenSharePriceIsNotOne() public { uint256 budget = 50 ether; address alice = address(1); address bob = address(2); vm.deal(bob, budget); TestNFT nft = new TestNFT(2); _mintNoDepositApproveRouter(address(nft), 5); address tokenContract = address(nft); uint256 tokenId = uint256(0); address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 14 days }); // after alice deposits 50 ether WETH in publicVault, publicVault's share price becomes 1 _lendToVault(Lender({addr: alice, amountToLend: budget}), publicVault); // the borrower borrows 10 ether WETH from publicVault (, ILienToken.Stack[] memory stack1) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: standardLienDetails, amount: 10 ether, isFirstLien: true }); uint256 collateralId = tokenContract.computeId(tokenId); // the borrower repays for the lien after 9 days, and publicVault's share price becomes bigger than 1 vm.warp(block.timestamp + 9 days); _repay(stack1, 0, 100 ether, address(this)); vm.startPrank(bob); // bob owns 50 ether WETH WETH9.deposit{value: budget}(); WETH9.approve(publicVault, budget); uint256 assetsIn = 100 gwei + 1; // for publicVault at this moment, 99265705739 shares are equivalent to (100 gwei + 1) WETH according to previewMint function uint256 sharesIn = IERC4626(publicVault).convertToShares(assetsIn); assertEq(sharesIn, 99265705739); assertEq(IERC4626(publicVault).previewMint(sharesIn), assetsIn); // bob is unable to call publicVault's deposit function for depositing (100 gwei + 1) WETH vm.expectRevert(bytes("VALUE_TOO_SMALL")); IERC4626(publicVault).deposit(assetsIn, bob); // bob is also unable to call publicVault's deposit function for depositing a little more than (100 gwei + 1) WETH vm.expectRevert(bytes("VALUE_TOO_SMALL")); IERC4626(publicVault).deposit(assetsIn + 100, bob); // however, bob is able to call publicVault's mint function for minting 99265705739 shares while transferring (100 gwei + 1) WETH to publicVault IERC4626(publicVault).mint(sharesIn, bob); vm.stopPrank(); }
VSCode
The ERC4626Cloned.deposit
function can be updated to directly compare the assets
input to minDepositAmount()
for the minimum deposit requirement while keeping the ERC4626Cloned.mint
function as is. Alternatively, the ERC4626Cloned.mint
function can be updated to directly compare the shares
input to minDepositAmount()
for the minimum deposit requirement while keeping the ERC4626Cloned.deposit
function as is.
#0 - c4-judge
2023-01-25T15:27:04Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2023-02-02T00:13:08Z
SantiagoGregory marked the issue as sponsor confirmed
#2 - androolloyd
2023-02-03T18:36:15Z
@SantiagoGregory
#3 - c4-judge
2023-02-21T22:13:20Z
Picodes marked the issue as satisfactory
#4 - c4-judge
2023-02-21T22:15:18Z
Picodes marked the issue as selected for report
🌟 Selected for report: ladboy233
Also found by: 0x1f8b, 0xAgro, 0xSmartContract, 0xbepresent, 0xkato, Aymen0909, CodingNameKiki, Cryptor, Deekshith99, Deivitto, HE1M, IllIllI, Kaysoft, Koolex, PaludoX0, Qeew, RaymondFam, Rolezn, Sathish9098, Tointer, a12jmx, arialblack14, ast3ros, ayeslick, bin2chen, btk, caventa, ch0bu, chaduke, chrisdior4, delfin454000, descharre, evan, fatherOfBlocks, georgits, gz627, jasonxiale, joestakey, kaden, lukris02, nicobevi, nogo, oberon, oyc_109, pfapostol, rbserver, sakshamguruji, seeu, shark, simon135, slvDev, synackrst, tnevler, whilom, zaskoh
51.3151 USDC - $51.32
SafeTransferLib
LIBRARY DOES NOT CHECK FOR ERC20 TOKEN CONTRACT'S EXISTENCESolmate's SafeTransferLib
library is used throughout the protocol. As indicated by the following comment in this library, it does not check if the corresponding ERC20 token contract exists or not, which can be problematic. For example, for a public vault, if the asset token becomes non-existent for some reason after some amounts of it are deposited by the liquidity providers, a borrower's borrowing user action will eventually call the VaultImplementation._requestLienAndIssuePayout
function, which further calls ERC20(asset()).safeTransfer(receiver, payout)
. Even though the asset token does not exist anymore, calling ERC20(asset()).safeTransfer(receiver, payout)
will not revert because Solmate's SafeTransferLib
library is used. As a result, the borrower can succeed in sending her or his NFT as the collateral but fail to receive any asset token in return. As a mitigation, please consider using OpenZeppelin's SafeERC20
library, instead of Solmate's, for ERC20 tokens' safe transfers because it does check for ERC20 token contract's existence.
https://github.com/rari-capital/solmate/blob/main/src/utils/SafeTransferLib.sol#L9
/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.
https://github.com/code-423n4/2023-01-astaria/blob/main/src/VaultImplementation.sol#L379-L395
function _requestLienAndIssuePayout( IAstariaRouter.Commitment calldata c, address receiver ) internal returns ( uint256 newLienId, ILienToken.Stack[] memory stack, uint256 slope, uint256 payout ) { ... ERC20(asset()).safeTransfer(receiver, payout); }
( Please note that the following instance is not found in https://gist.github.com/Picodes/d16459938599db69f9ad88c73a2d2990#m-1-centralization-risk-for-trusted-owners. )
The guardian is allowed to call the following AstariaRouter.fileGuardian
function for updating important components used in this protocol. If becoming malicious or compromised, the guardian can call this function to change some of these components to the malicious ones. For example, by calling this function, the guardian is able to change the s.TRANSFER_PROXY
to a malicious contract. Afterwards, when a user starts a user action that eventually calls the AstariaRouter.pullToken
function below, the user's funds can be transferred and lost to such malicious contract. As a mitigation, please consider making the AstariaRouter.fileGuardian
time-delayed, such as making it only callable by a timelock contract, for giving users more time to react when malicious usage of this function is detected.
https://github.com/code-423n4/2023-01-astaria/blob/main/src/AstariaRouter.sol#L359-L391
function fileGuardian(File[] calldata file) external { RouterStorage storage s = _loadRouterSlot(); require(msg.sender == address(s.guardian)); uint256 i; for (; i < file.length; ) { FileType what = file[i].what; bytes memory data = file[i].data; if (what == FileType.Implementation) { (uint8 implType, address addr) = abi.decode(data, (uint8, address)); if (addr == address(0)) revert InvalidFileData(); s.implementations[implType] = addr; } else if (what == FileType.CollateralToken) { address addr = abi.decode(data, (address)); if (addr == address(0)) revert InvalidFileData(); s.COLLATERAL_TOKEN = ICollateralToken(addr); } else if (what == FileType.LienToken) { address addr = abi.decode(data, (address)); if (addr == address(0)) revert InvalidFileData(); s.LIEN_TOKEN = ILienToken(addr); } else if (what == FileType.TransferProxy) { address addr = abi.decode(data, (address)); if (addr == address(0)) revert InvalidFileData(); s.TRANSFER_PROXY = ITransferProxy(addr); } else { revert UnsupportedFile(); } ... } }
https://github.com/code-423n4/2023-01-astaria/blob/main/src/AstariaRouter.sol#L203-L215
function pullToken( address token, uint256 amount, address recipient ) public payable override { RouterStorage storage s = _loadRouterSlot(); s.TRANSFER_PROXY.tokenTransferFrom( address(token), msg.sender, recipient, amount ); }
decimals()
CALL FOR ASSET TOKENS THAT DO NOT IMPLEMENT decimals()
For a public vault, when minting shares or depositing asset tokens by the liquidity providers, the following PublicVault.minDepositAmount
function is eventually called, which further calls ERC20(asset()).decimals()
. According to https://eips.ethereum.org/EIPS/eip-20, decimals()
is optional so it is possible that the strategist wants to create a public vault with an asset token that does not implement decimals()
. However, when this occurs, calling the PublicVault.minDepositAmount
function will revert, and none of such asset token can be lent to this public vault. As a mitigation, to support the usage of such asset token for a public vault, helper functions like BoringCrypto's safeDecimals
can be used instead of directly calling decimals()
in the PublicVault.minDepositAmount
function.
https://github.com/code-423n4/2023-01-astaria/blob/main/src/PublicVault.sol#L96-L108
function minDepositAmount() public view virtual override(ERC4626Cloned) returns (uint256) { if (ERC20(asset()).decimals() == uint8(18)) { return 100 gwei; } else { return 10**(ERC20(asset()).decimals() - 1); } }
_safeMint
FUNCTION CAN BE CALLED INSTEAD OF _mint
FUNCTION FOR MINTING CollateralToken
NFTWhen the CollateralToken.onERC721Received
function is called, _mint(from_, collateralId)
is executed. If the from_
input, which is the address of the owner of the collateral deposited, corresponds to a contract, calling the _mint
function does not check if the receiving contract supports the ERC721 protocol; if not supported, the minted CollateralToken
NFT can be locked and cannot be retrieved. To make sure that the receiving contract supports the ERC721 protocol, please consider calling the _safeMint
function instead of the _mint
function in the CollateralToken.onERC721Received
function.
https://github.com/code-423n4/2023-01-astaria/blob/main/src/CollateralToken.sol#L553-L600
function onERC721Received( address, /* operator_ */ address from_, uint256 tokenId_, bytes calldata // calldata data_ ) external override whenNotPaused returns (bytes4) { ... uint256 collateralId = msg.sender.computeId(tokenId_); ... if (incomingAsset.tokenContract == address(0)) { ... _mint(from_, collateralId); ... } else { revert(); } }
AstariaRouter
CONTRACT HAVE payable
MODIFIERSome functions in the AstariaRouter
contract, such as the followings functions, have the payable
modifier. However, the purposes of these functions do not require users to provide ETH. When users accidentally send ETH when calling these functions, the users will lose these sent ETH amounts to the AstariaRouter
contract. To protect users from losing ETH accidentally, please consider removing the payable
modifiers from these functions.
https://github.com/code-423n4/2023-01-astaria/blob/main/src/AstariaRouter.sol#L123-L137
function mint( IERC4626 vault, address to, uint256 shares, uint256 maxAmountIn ) public payable virtual override validVault(address(vault)) returns (uint256 amountIn) { return super.mint(vault, to, shares, maxAmountIn); }
https://github.com/code-423n4/2023-01-astaria/blob/main/src/AstariaRouter.sol#L139-L153
function deposit( IERC4626 vault, address to, uint256 amount, uint256 minSharesOut ) public payable virtual override validVault(address(vault)) returns (uint256 sharesOut) { return super.deposit(vault, to, amount, minSharesOut); }
https://github.com/code-423n4/2023-01-astaria/blob/main/src/AstariaRouter.sol#L155-L169
function withdraw( IERC4626 vault, address to, uint256 amount, uint256 maxSharesOut ) public payable virtual override validVault(address(vault)) returns (uint256 sharesOut) { return super.withdraw(vault, to, amount, maxSharesOut); }
https://github.com/code-423n4/2023-01-astaria/blob/main/src/AstariaRouter.sol#L171-L185
function redeem( IERC4626 vault, address to, uint256 shares, uint256 minAmountOut ) public payable virtual override validVault(address(vault)) returns (uint256 amountOut) { return super.redeem(vault, to, shares, minAmountOut); }
NatSpec comments provide rich code documentation. The following functions are some examples that miss the @param
and/or @return
comments. Please consider completing the NatSpec comments for functions like these.
src\AstariaRouter.sol 90: address _WITHDRAW_IMPL, 252: function __emergencyPause() external requiresAuth whenNotPaused { 259: function __emergencyUnpause() external requiresAuth whenPaused { 426: function validateCommitment( 552: ) public whenNotPaused returns (address) { 712: function _newVault(
NatSpec comments provide rich code documentation. The following functions are some examples that miss NatSpec comments. Please consider adding NatSpec comments for functions like these.
lib\gpl\src\ERC721.sol 69: function __initERC721(string memory _name, string memory _symbol) internal { src\AstariaRouter.sol 217: function _loadRouterSlot() internal pure returns (RouterStorage storage rs) { 345: function __renounceGuardian() external { src\CollateralToken.sol 80: function initialize( 145: function _loadCollateralSlot() src\TransferProxy.sol 28: function tokenTransferFrom( src\VaultImplementation.sol 190: function init(InitParams calldata params) external virtual {
#0 - c4-judge
2023-01-26T00:12:54Z
Picodes marked the issue as grade-b
#1 - rbserver
2023-02-26T09:10:29Z
Hi @Picodes ,
I was reading through the reports and noticed that #52 reports a centralization risk that affects TRANSFER_PROXY
and is considered as a medium risk. In my QA report, [02] MALICIOUS OR COMPROMISED GUARDIAN CAN STEAL USERS' FUNDS
also reports a centralization risk that can affect TRANSFER_PROXY
, and the corresponding AstariaRouter.fileGuardian
function is not flagged in https://gist.github.com/Picodes/d16459938599db69f9ad88c73a2d2990#m-1-centralization-risk-for-trusted-owners. Hence, I would like to ask if [02] MALICIOUS OR COMPROMISED GUARDIAN CAN STEAL USERS' FUNDS
can be considered as a medium risk as well.
Thanks for your time and work!
#2 - Picodes
2023-02-27T15:28:46Z
Hi @rbserver, thanks for reaching out!
My reasoning for #52 was that the issue is that allowances given to the TRANSFER_PROXY
could be exploited, which is different from issues like this one describing how the admin could break the configuration and steal funds within the system. For the latter, I've kept Low severity.
Please let me know if you disagree with this reasoning!
#3 - rbserver
2023-02-27T21:11:29Z
Hi @Picodes,
Thanks for your reply!
To me, the root cause of both #52 and [02] MALICIOUS OR COMPROMISED GUARDIAN CAN STEAL USERS' FUNDS
is a centralization risk in which #52 requires the admin to be compromised while [02] MALICIOUS OR COMPROMISED GUARDIAN CAN STEAL USERS' FUNDS
requires the guardian to be compromised. If both centralized authorities are not compromised, then both issues won't happen; if both centralized authorities are compromised, then both issues can happen, which all lead to loss of users' funds. While the compromised admin in #52 can utilize a malicious contract, the compromised guardian for [02] MALICIOUS OR COMPROMISED GUARDIAN CAN STEAL USERS' FUNDS
can also change the s.TRANSFER_PROXY
to a malicious contract; when users call functions like ERC4626Router.depositToVault
, ERC4626Router.depositMax
, and ERC4626Router.redeemMax
, which calls the AstariaRouter.pullToken
function, users can unknowingly interact with such malicious contract and lose their funds.
Based on the similarities between #52 and [[02] MALICIOUS OR COMPROMISED GUARDIAN CAN STEAL USERS' FUNDS
] and that https://gist.github.com/Picodes/d16459938599db69f9ad88c73a2d2990#m-1-centralization-risk-for-trusted-owners does flag https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/TransferProxy.sol#L33 which is the affected code in #52, I do find it hard to think that the severities of #52 and [[02] MALICIOUS OR COMPROMISED GUARDIAN CAN STEAL USERS' FUNDS
] are different. Hence, I would still ask if [02] MALICIOUS OR COMPROMISED GUARDIAN CAN STEAL USERS' FUNDS
and #52 can be considered as risks of the same level.
Anyways, I will respect your final judgement. Thanks again for your time and effort!
#4 - Picodes
2023-02-28T10:55:23Z
Hi @rbserver! I thought about this again and discussed this with other judges. Although your arguments were correct, my final decision may disappoint you: I'll downgrade #52 to low, out of coherence among Admin Privilege issues, and considering #52 is not a case of privilege escalation.
#5 - rbserver
2023-02-28T18:57:30Z
Hi @Picodes, I understand. Thanks again for looking into these reports!