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: 52/103
Findings: 4
Award: $160.44
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Lirios
Also found by: 0xcm, 0xsomeone, HE1M, Jeiwan, Koolex, bin2chen, c7e7eff, cergyk, dragotanqueray, evan, ladboy233, synackrst, unforgiven, wallstreetvilkas
33.2422 USDC - $33.24
https://github.com/code-423n4/2023-01-astaria/blob/main/src/ClearingHouse.sol#L114 https://github.com/code-423n4/2023-01-astaria/blob/main/src/ClearingHouse.sol#L172
In ClearingHouse.sol, _execute
settles auction for collateralId
by paying currentOfferPrice
amount of paymentToken
in the contract’s balance. However, paymentToken
can be arbitrarily specified via the public safeTransferFrom
method, and there is no check to ensure that it is using the expected payment token.
Anyone can settle auction using an arbitrary ERC20 token, which will be used to pay debt and liquidator payment, and the corresponding collateral token will be burned. The future, legitimate settlement attempt will fail due to invalid collateral state.
currentOfferPrice
safeTransferFrom(from, to, identifier, amount)
where identifier
is the ERC20 token’s address and the other arguments can be anything.Manual analysis
Include a check to ensure that it is using the expected paymentToken
. In addition, implement access controls for safeTransferFrom
.
#0 - c4-judge
2023-01-24T07:48:08Z
Picodes marked the issue as duplicate of #564
#1 - c4-judge
2023-02-15T07:30:05Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-02-23T21:03:28Z
Picodes changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-02-24T10:37:08Z
This previously downgraded issue has been upgraded by Picodes
#4 - c4-judge
2023-02-24T10:39:33Z
Picodes marked the issue as not a duplicate
#5 - c4-judge
2023-02-24T10:40:34Z
Picodes marked the issue as duplicate of #521
🌟 Selected for report: rbserver
Also found by: 0Kage, 0xcm, bin2chen, caventa, csanuragjain, synackrst, unforgiven
39.0944 USDC - $39.09
https://github.com/AstariaXYZ/astaria-gpl/blob/4b49fe993d9b807fe68b3421ee7f2fe91267c9ef/src/ERC4626-Cloned.sol#L27 https://github.com/AstariaXYZ/astaria-gpl/blob/4b49fe993d9b807fe68b3421ee7f2fe91267c9ef/src/ERC4626-Cloned.sol#L43
The following require
statement In deposit
of ERC4626-Cloned.sol
require(shares > minDepositAmount(), "VALUE_TOO_SMALL");
is ensuring shares
is greater than minDepositAmount()
, even though it should be ensuring assets > minDepositAmount()
(like how it’s done in the mint
method).
An unexpected amount of assets
can be deposited; or a valid amount of assets
cannot be deposited.
See the attached links.
Manual analysis
Fix the require statement with the right invariant.
#0 - c4-judge
2023-01-26T16:22:25Z
Picodes marked the issue as duplicate of #486
#1 - c4-judge
2023-02-21T22:14:00Z
Picodes marked the issue as satisfactory
🌟 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
initialize()
of AstariaRouterFunction initalize()
of AstariaRouter.sol lacks in documentation for the following arguments:
_WITHDRAW_IMPL
_CLEARING_HOUSE_IMPL
_BEACON_PROXY_IMPL
AstariaRouter has multiple payable public methods: mint()
, deposit()
, withdraw()
, redeem()
, and pullToken()
. ETH accidentally sent to this contract via these methods cannot be recovered unless the contract is selfdestructed or upgraded.
liquidatorNFTClaim
of CollateralToken.solThe following variables are not used and hence can be removed.
address tokenContract = underlying.tokenContract; uint256 tokenId = underlying.tokenId;
In multiple locations in PublicVault.sol, require
statements lack in descriptive error information.
require(s.allowList[receiver]); // in mint()
require(s.allowList[receiver]); // in deposit()
Add an error description in the second argument, or create a custom error type such as error NotInAllowList()
and throw it with revert
.
In multiple locations in VaultImplementation.sol, require
statements lack in descriptive error messages.
require(msg.sender == owner()); //owner is "strategist"
Add an error description in the second argument, or create a custom error type and throw it with revert
.
RouterStorage
of IAstariaRouter.solThe field ERC20 WETH
in the RouterStorage
struct is not being used anywhere in the code. This can be removed to reduce the storage space.
minDepositAmount()
in ERC4626-Cloned.solThe following snippet
require(assets > minDepositAmount(), "VALUE_TOO_SMALL");
in the deposit
and mint
functions implicitly assumes that minDepositAmount
is non-inclusive. Clarify this in the documentation.
deposit
of PublicVault.soluint256 assets = totalAssets();
is not used and hence can be removed.
#0 - c4-judge
2023-01-26T14:18:33Z
Picodes marked the issue as grade-b
🌟 Selected for report: c3phas
Also found by: 0x1f8b, 0xAgro, 0xSmartContract, 0xackermann, 0xkato, Aymen0909, Bnke0x0, CloudX, IllIllI, PaludoX0, Rageur, Rahoz, RaymondFam, ReyAdmirado, Rolezn, SadBase, SaeedAlipoor01988, caventa, chaduke, chrisdior4, fatherOfBlocks, fs0c, kaden, nogo, pfapostol, shark, synackrst
36.79 USDC - $36.79
function getImpl(uint8 implType) external view returns (address impl) { impl = _loadRouterSlot().implementations[implType]; if (impl == address(0)) { revert("unsupported/impl"); } }
When impl contract is not defined, the reverting transaction would consume more gas because it’s not using custom errors;
balanceOfBatch
in ClearingHouse.solReplace
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory output) { output = new uint256[](accounts.length); for (uint256 i; i < accounts.length; ) { output[i] = type(uint256).max; unchecked { ++i; } } }
with
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) public view returns (uint256[] memory output) { assembly { // Set the length of the output array let len := mload(accounts) mstore(output, len) // Set every bit of the output array to 1 let i := 0 for { lt(i, len) } { mstore8(add(output, add(i, 32)), 255) i := add(i, 32) } } }
!= 0
instead of > 0
in _execute
of ClearingHouse.solThe following snippet
if (ERC20(paymentToken).balanceOf(address(this)) > 0) { ERC20(paymentToken).safeTransfer( ASTARIA_ROUTER.COLLATERAL_TOKEN().ownerOf(collateralId), ERC20(paymentToken).balanceOf(address(this)) ); }
can be replaced with
if (ERC20(paymentToken).balanceOf(address(this)) != 0) { ERC20(paymentToken).safeTransfer( ASTARIA_ROUTER.COLLATERAL_TOKEN().ownerOf(collateralId), ERC20(paymentToken).balanceOf(address(this)) ); }
_paymentAH
of LienToken.solIn the following snippet in _paymentAH()
of LienToken.sol, the variable remaining
is unnecessarily initialized with a default value.
uint256 remaining = 0; if (owing > payment.safeCastTo88()) { remaining = owing - payment; } else { payment = owing; }
This can be rewritten in the following manner to reduce the gas cost.
uint256 remaining; if (owing > payment.safeCastTo88()) { remaining = owing - payment; } else { payment = owing; }
_getPayee
of LienToken.solfunction _getPayee( LienStorage storage s, uint256 lienId ) internal view returns (address) { return s.lienMeta[lienId].payee != address(0) ? s.lienMeta[lienId].payee : ownerOf(lienId); }
should be rewritten in the following manner to reduce the gas cost:
function _getPayee(LienStorage storage s, uint256 lienId) internal view returns (address) { address payee = s.lienMeta[lienId].payee; if (payee != address(0)) { return payee; } else { return ownerOf(lienId); } }
!= 0
instead of > 0
in processEpoch
of PublicVault.solThe following snippet
if (s.withdrawReserve > 0) { revert InvalidState(InvalidStates.WITHDRAW_RESERVE_NOT_ZERO); }
can be rewritten in the following manner to reduce the gas cost.
if (s.withdrawReserve != 0) { revert InvalidState(InvalidStates.WITHDRAW_RESERVE_NOT_ZERO); }
liquidationWithdrawRatio
in the else statement in processEpoch
of PublicVault.solThe following snippet
s.liquidationWithdrawRatio = 0; // check if there are LPs withdrawing this epoch if ((address(currentWithdrawProxy) != address(0))) { uint256 proxySupply = currentWithdrawProxy.totalSupply(); s.liquidationWithdrawRatio = proxySupply .mulDivDown(1e18, totalSupply()) .safeCastTo88(); currentWithdrawProxy.setWithdrawRatio(s.liquidationWithdrawRatio); uint256 expected = currentWithdrawProxy.getExpected(); unchecked { if (totalAssets() > expected) { s.withdrawReserve = (totalAssets() - expected) .mulWadDown(s.liquidationWithdrawRatio) .safeCastTo88(); } else { s.withdrawReserve = 0; } } _setYIntercept( s, s.yIntercept - totalAssets().mulDivDown(s.liquidationWithdrawRatio, 1e18) ); // burn the tokens of the LPs withdrawing _burn(address(this), proxySupply); }
should be rewritten in the following manner to save the gas cost (fewer sstore
when currentWithdrawProxy is set)
// check if there are LPs withdrawing this epoch if ((address(currentWithdrawProxy) != address(0))) { uint256 proxySupply = currentWithdrawProxy.totalSupply(); s.liquidationWithdrawRatio = proxySupply .mulDivDown(1e18, totalSupply()) .safeCastTo88(); currentWithdrawProxy.setWithdrawRatio(s.liquidationWithdrawRatio); uint256 expected = currentWithdrawProxy.getExpected(); unchecked { if (totalAssets() > expected) { s.withdrawReserve = (totalAssets() - expected) .mulWadDown(s.liquidationWithdrawRatio) .safeCastTo88(); } else { s.withdrawReserve = 0; } } _setYIntercept( s, s.yIntercept - totalAssets().mulDivDown(s.liquidationWithdrawRatio, 1e18) ); // burn the tokens of the LPs withdrawing _burn(address(this), proxySupply); } else { s.liquidationWithdrawRatio = 0; }
!= 0
instead of > 0
in _beforeCommitToLien
of PublicVault.solThe following snippet
if (s.withdrawReserve > uint256(0)) { transferWithdrawReserve(); }
can be rewritten in the following manner to reduce the gas cost
if (s.withdrawReserve != uint256(0)) { transferWithdrawReserve(); }
mulDivDown
in totalAssets()
of PublicVault.solThe following snippet
return uint256(s.slope).mulDivDown(delta_t, 1) + uint256(s.yIntercept);
can be replaced with
return uint256(s.slope) * delta_t + uint256(s.yIntercept);
i++
and i--
can be rewritten as ++i
and --i
respectively to reduce the gas cost in the following snippets in PublicVault.sol:
s.epochData[epoch].liensOpenForEpoch--; // in _decreaseEpochLienCount
s.epochData[epoch].liensOpenForEpoch++; // in _increaseOpenLiens
They can also be safely wrapped in unchecked
to disable the overflow/underflow checks.
This can be applied to the following snippet as well:
unchecked { s.currentEpoch++; } // in processEpoch
i++
and i--
can be rewritten as ++i
and --i
respectively to reduce the gas cost in the following snippets in VaultImplementation.sol:
s.strategistNonce++; // in incrementNonce()
They can also be safely wrapped in unchecked
to disable the overflow/underflow checks.
settleAuction
of CollateralToken.sol_settleAuction(s, collateralId);
does not do much inside and can be replaced with the following to reduce a function call.
#0 - c4-judge
2023-01-25T23:37:49Z
Picodes marked the issue as grade-b