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: 9/103
Findings: 11
Award: $3,097.92
🌟 Selected for report: 3
🚀 Solo Findings: 1
69.0905 USDC - $69.09
exclusive access to vault,Other users will not be able to mint
In the previewMint method, if the first user mint, no matter how many shares the user passes in, only 10e18 assets are needed
function previewMint(uint256 shares) public view virtual returns (uint256) { uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero. //****@audit when supply ==0, ignore shares, Direct return 10e18 ***// return supply == 0 ? 10e18 : shares.mulDivUp(totalAssets(), supply); }
So that users can mint(shares=type(uint256).max), only need 10e18 asset
Other users will not be able to mint, because totalSupply will overflow
asset 1:1 return
#0 - Picodes
2023-01-23T16:22:58Z
Seems correct but what would be the impact of this aside from the user blocking users to join a vault where he is alone
#1 - c4-judge
2023-01-25T14:28:37Z
Picodes marked the issue as duplicate of #588
#2 - c4-judge
2023-01-25T14:28:42Z
Picodes marked the issue as partial-50
#3 - c4-judge
2023-02-19T16:58:07Z
Picodes changed the severity to 3 (High Risk)
#4 - c4-judge
2023-02-24T10:22:31Z
Picodes marked the issue as satisfactory
🌟 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
vault loss assets
When the auction is successful the NFT is transferred to the bidder and seaport calls ClearingHouse.safeTransferFrom() to trigger the repayment of the debt through the conduit mechanism
ClearingHouse.safeTransferFrom() -> ClearingHouse._execute() In _execute(), it does not check if msg.sender is from seaport, it only cares about the amount of payment received > currentOfferPrice This is fine. It doesn't matter who calls it, as long as the amount is right
But here is a problem, only limit the number of amounts, but did not check whether the token address is legal, the address is a parameter, can be passed arbitrarily
function _execute( address tokenContract, // collateral token sending the fake nft address to, // buyer uint256 encodedMetaData, //retrieve token address from the encoded data uint256 // space to encode whatever is needed, ) internal { address paymentToken = bytes32(encodedMetaData).fromLast20Bytes(); <------From the parameter encodedMetaData, which can be forged arbitrarily uint256 currentOfferPrice = _locateCurrentAmount({ startAmount: s.auctionStack.startAmount, endAmount: s.auctionStack.endAmount, startTime: s.auctionStack.startTime, endTime: s.auctionStack.endTime, roundUp: true //we are a consideration we round up }); uint256 payment = ERC20(paymentToken).balanceOf(address(this));
And this paymentToken address, in the subsequent logic also did not verify whether this token address is the payment token specified when creating Liens
function _execute( address tokenContract, // collateral token sending the fake nft address to, // buyer uint256 encodedMetaData, //retrieve token address from the encoded data uint256 // space to encode whatever is needed, ) internal { ... ASTARIA_ROUTER.LIEN_TOKEN().payDebtViaClearingHouse( paymentToken, <----Inside the payDebtViaClearingHouse() there is still no verification that the paymentToken address is legitimate collateralId, payment - liquidatorPayment, s.auctionStack.stack ); ASTARIA_ROUTER.COLLATERAL_TOKEN().settleAuction(collateralId); <---- settleAuction() can pass ,it ok
To sum up:A malicious user can pass a worthless forged payment token to repay the vault, resulting in vault loss assets
1._execute() add check payment token address
//ClearingHouse.sol function _execute( address tokenContract, // collateral token sending the fake nft address to, // buyer uint256 encodedMetaData, //retrieve token address from the encoded data uint256 // space to encode whatever is needed, ) internal { ... address paymentToken = bytes32(encodedMetaData).fromLast20Bytes(); + require(paymentToken!=address(0) && paymentToken == s.auctionData.paymentToken) ; ... } //LienToken.sol function _stopLiens( ... + auctionData.paymentToken = stack[0].lien.token; s.COLLATERAL_TOKEN.getClearingHouse(collateralId).setAuctionData( auctionData ); }
2.payDebtViaClearingHouse() also verifies the legitimacy of the payment token, currently createLien does not save the payment address, it is recommended to save it in s.lienMeta[id]
//LienToken.sol function _createLien( ... newLienId = uint256(keccak256(abi.encode(params.lien))); + s.lienMeta[newLienId].paymentToken = params.lien.token; ... } //LienToken.sol function _paymentAH( LienStorage storage s, address token, AuctionStack[] memory stack, uint256 position, uint256 payment, address payer ) internal returns (uint256) { ... uint256 lienId = stack[position].lienId; + require(s.lienMeta[lienId].paymentToken == token,"invald payment token");
#0 - c4-judge
2023-01-24T07:44:16Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2023-01-27T03:11:23Z
SantiagoGregory marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-15T07:23:42Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-02-15T07:28:50Z
Picodes marked issue #521 as primary and marked this issue as a duplicate of 521
#4 - c4-judge
2023-02-23T21:03:28Z
Picodes changed the severity to QA (Quality Assurance)
#5 - c4-judge
2023-02-24T10:37:08Z
This previously downgraded issue has been upgraded by Picodes
#6 - c4-judge
2023-02-24T10:39:22Z
Picodes marked the issue as not a duplicate
#7 - c4-judge
2023-02-24T10:40:12Z
Picodes marked the issue as duplicate of #521
980.8407 USDC - $980.84
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/VaultImplementation.sol#L287-L306 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/AstariaRouter.sol#L485-L486
Stealing vault assets
There are currently two ways to create new Liens in the system
Currently, the direct call to VaultImplementation.commitToLien() has a payment token legitimacy check vulnerability
Calling process: VaultImplementation.commitToLien() -> VaultImplementation._requestLienAndIssuePayout() -> AstariaRouter.requestLienPosition() -> AstariaRouter._validateCommitment() Set the payment token in _validateCommitment() The code is as follows:
function _validateCommitment( ..... lien = ILienToken.Lien({ collateralType: nlrType, details: details, strategyRoot: commitment.lienRequest.merkle.root, collateralId: commitment.tokenContract.computeId(commitment.tokenId), vault: commitment.lienRequest.strategy.vault, <----Note that this vault parameter is passed in and can be specified arbitrarily, and can be specified as a malicious contract token: IAstariaVaultBase(commitment.lienRequest.strategy.vault).asset() <-----Specify vault as a malicious contract that can return a malicious payment token like:mock_token });
There is no other place in the system to verify that this token is legitimate
But the malicious user receives the real asset token of the vault
VaultImplementation.commitToLien() -> VaultImplementation._requestLienAndIssuePayout()
function _requestLienAndIssuePayout( ... _validateCommitment(c, receiver); (newLienId, stack, slope) = ROUTER().requestLienPosition(c, recipient()); payout = _handleProtocolFee(c.lienRequest.amount); ERC20(asset()).safeTransfer(receiver, payout); <------use get real asset token }
LienToken.makePayment() is also repaid with the malicious payment token
In summary, a malicious user can borrow vault's asset token, but the malicious invalid payment token is repaid
AstariaRouter.requestLienPosition() add check msg.sender == commitment.lienRequest.strategy.vault to avoid malicious vault parameters
function requestLienPosition( IAstariaRouter.Commitment calldata params, address receiver ) external whenNotPaused validVault(msg.sender) returns ( uint256, ILienToken.Stack[] memory, uint256 ) { + require(msg.sender == params.lienRequest.strategy.vault,"invalid strategy.vault"); RouterStorage storage s = _loadRouterSlot(); return s.LIEN_TOKEN.createLien( ILienToken.LienActionEncumber({ lien: _validateCommitment({ s: s, commitment: params, timeToSecondEpochEnd: IPublicVault(msg.sender).supportsInterface( type(IPublicVault).interfaceId ) ? IPublicVault(msg.sender).timeToSecondEpochEnd() : 0 }), amount: params.lienRequest.amount, stack: params.lienRequest.stack, receiver: receiver }) ); }
#0 - c4-sponsor
2023-01-27T03:11:30Z
SantiagoGregory marked the issue as sponsor confirmed
#1 - c4-judge
2023-02-15T07:39:36Z
Picodes marked the issue as duplicate of #409
#2 - c4-judge
2023-02-24T10:05:46Z
Picodes marked the issue as satisfactory
🌟 Selected for report: csanuragjain
Also found by: 7siech, KIntern_NA, Koolex, bin2chen, cergyk, evan, obront, unforgiven
104.2518 USDC - $104.25
Detailed description of the impact of this finding.
The VaultImplementation.commitToLien() method is external and can be executed by anyone The method will internally verify that the corresponding collateralId is yours or has the corresponding authorization The validation code is as follows:
function _validateCommitment( if ( msg.sender ! = holder && receiver ! = holder && receiver ! = operator && !CT.isApprovedForAll(holder, msg.sender) ) { revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); }
Note that this receiver comes from the parameter, so anyone can pass receiver==holder to skip this authentication, i.e. anyone can createLien for the owner of the collateralId
There are many possible problems that can be caused: For example:
if the collateralId has not been borrowed (only transfer NFT to CollateralToken), then the malicious user can generate a private vault, vault.asset as a worthless token, then set a very short borrowing period policy, and then through the above commitToLien() to borrow money for others Since the period is very short, it will soon expire and enter the auction. Use worthless assets to bid to steal NFT
collateralId already has Lien, then the user can generate a very high interest rate strategy, and then generate Lien for someone else's borrowing through the above commitToLien(), thus earning interest
collateralId borrowed without knowing it
Suggestion:
Only verify msg.sender, do not verify receiver
function _validateCommitment( IAstariaRouter.Commitment calldata params, address receiver ) internal view { ... if ( msg.sender != holder && - receiver != holder && - receiver != operator && + msg.sender != operator && !CT.isApprovedForAll(holder, msg.sender) ) { revert InvalidRequest(InvalidRequestReason.NO_AUTHORITY); }
#0 - c4-judge
2023-01-24T10:22:08Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2023-01-27T03:11:12Z
SantiagoGregory marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-15T07:04:00Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-02-15T07:08:20Z
Picodes marked issue #19 as primary and marked this issue as a duplicate of 19
#4 - c4-judge
2023-02-15T07:18:02Z
Picodes changed the severity to QA (Quality Assurance)
#5 - c4-judge
2023-02-15T07:22:03Z
This previously downgraded issue has been upgraded by Picodes
#6 - c4-judge
2023-02-15T07:35:50Z
Picodes marked the issue as not a duplicate
#7 - c4-judge
2023-02-15T07:35:58Z
Picodes marked the issue as duplicate of #19
382.5279 USDC - $382.53
Illegal liquidationInitialAsk, resulting in insufficient bids to cover the debt
_buyoutLien() will validate against liquidationInitialAsk, but incorrectly uses the old stack for validation
function _buyoutLien( LienStorage storage s, ILienToken.LienActionBuyout calldata params ) internal returns (Stack[] memory newStack, Stack memory newLien) { .... uint256 potentialDebt = 0; for (uint256 i = params.encumber.stack.length; i > 0; ) { uint256 j = i - 1; // should not be able to purchase lien if any lien in the stack is expired (and will be liquidated) if (block.timestamp >= params.encumber.stack[j].point.end) { revert InvalidState(InvalidStates.EXPIRED_LIEN); } potentialDebt += _getOwed( params.encumber.stack[j], params.encumber.stack[j].point.end ); if ( potentialDebt > params.encumber.stack[j].lien.details.liquidationInitialAsk //1.****@audit use old stack ) { revert InvalidState(InvalidStates.INITIAL_ASK_EXCEEDED); } unchecked { --i; } } .... newStack = _replaceStackAtPositionWithNewLien( s, params.encumber.stack, params.position, newLien, params.encumber.stack[params.position].point.lienId //2.****@audit replace newStack );
Replace then verify, using the newStack[] for verification
#0 - c4-judge
2023-01-26T21:06:01Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2023-02-01T22:35:36Z
SantiagoGregory marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-21T09:25:50Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-02-21T09:26:50Z
Picodes marked the issue as selected for report
382.5279 USDC - $382.53
ClearingHouse.safeTransferFrom() to execute successfully even if there is no bid
settleAuction is called at the end of the auction and will check if the status is legal
function settleAuction(uint256 collateralId) public { if ( s.collateralIdToAuction[collateralId] == bytes32(0) && ERC721(s.idToUnderlying[collateralId].tokenContract).ownerOf( s.idToUnderlying[collateralId].tokenId ) != s.clearingHouse[collateralId] ) { revert InvalidCollateralState(InvalidCollateralStates.NO_AUCTION); }
This check seems to be miswritten,The normal logic would be
s.collateralIdToAuction[collateralId] == bytes32(0) || ERC721(s.idToUnderlying[collateralId].tokenContract).ownerOf( s.idToUnderlying[collateralId].tokenId ) == s.clearingHouse[collateralId]
This causes ClearingHouse.safeTransferFrom() to execute successfully even if there is no bid
function settleAuction(uint256 collateralId) public { if ( - s.collateralIdToAuction[collateralId] == bytes32(0) && - ERC721(s.idToUnderlying[collateralId].tokenContract).ownerOf( - s.idToUnderlying[collateralId].tokenId - ) != - s.clearingHouse[collateralId] + s.collateralIdToAuction[collateralId] == bytes32(0) || + ERC721(s.idToUnderlying[collateralId].tokenContract).ownerOf(s.idToUnderlying[collateralId].tokenId + ) == + s.clearingHouse[collateralId] ) { revert InvalidCollateralState(InvalidCollateralStates.NO_AUCTION); }
#0 - c4-judge
2023-01-26T16:04:05Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2023-02-01T21:49:19Z
androolloyd marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-21T22:06:39Z
Picodes marked the issue as selected for report
#3 - c4-judge
2023-02-21T22:06:51Z
Picodes marked the issue as satisfactory
#4 - Picodes
2023-02-21T22:08:13Z
Keeping medium severity despite the lack of clear impact, the lack of clear impact being due to flaws in the flow before these lines
🌟 Selected for report: bin2chen
850.0619 USDC - $850.06
Corrupt multiple key properties of public vault, causing vault not to function properly
When LienToken.makePayment()/buyoutLien()/payDebtViaClearingHouse() If it corresponds to PublicVault, it will make multiple changes to the vault, such as: yIntercept, slope, last, epochData, etc.
If LienToken corresponds to PublicVault, then ownerOf(lienId) = PublicVault address
When the LienToken is a private vault, it is possible to transfer the owner of the LienToken.
As the above seems, if the private vault is transferred to the PublicVault address will result in the wrong modification of the yIntercept, slope, last, epochData, etc.
So we restrict the to in transferFrom to not be a PublicVault address
function transferFrom( address from, address to, uint256 id ) public override(ERC721, IERC721) { if (_isPublicVault(s, to)) { //***@audit when to == PublicVault address , will revert revert InvalidState(InvalidStates.PUBLIC_VAULT_RECIPIENT); }
However, such a restriction does not prevent an attacker from transferring PrivateVault's LienToken to PublicVault Because the address is predictable when the vault contract is created, a malicious user can predict the vault address, front-run, and transfer PrivateVault's LienToken to the predicted PublicVault address before the public vault is created, thus bypassing this restriction
Assumptions:
see:https://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed
The address for an Ethereum contract is deterministically computed from the address of its creator (sender) and how many transactions the creator has sent (nonce). The sender and nonce are RLP encoded and then hashed with Keccak-256.
3.front-run , and transfer LienToken to public vault predict address 4.bob's public vault created success and do some loan 5.alice do makePayment() to Corrupt bob's public vault
The corresponding vault address is stored in s.lienMeta[id].orginOwner when the LienToken is created, this is not modified. Get the vault address from this variable, not from ownerOf(id).
#0 - c4-sponsor
2023-01-31T15:21:57Z
androolloyd marked the issue as sponsor confirmed
#1 - c4-judge
2023-02-21T22:05:21Z
Picodes marked the issue as satisfactory
🌟 Selected for report: rbserver
Also found by: 0Kage, 0xcm, bin2chen, caventa, csanuragjain, synackrst, unforgiven
39.0944 USDC - $39.09
Judge has assessed an item in Issue #598 as M risk. The relevant finding follows:
Low: 1.deposit() check wrong variant
function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) {
#0 - c4-judge
2023-01-26T13:52:57Z
Picodes marked the issue as duplicate of #486
#1 - c4-sponsor
2023-01-31T14:44:14Z
androolloyd marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-21T22:13:22Z
Picodes marked the issue as satisfactory
85.8039 USDC - $85.80
Judge has assessed an item in Issue #598 as M risk. The relevant finding follows:
2.minDepositAmount() When the asset is btc, the minDepositAmount is too large
when asset == btc , minDepositAmount = 0.1 btc , equal 2000 usd suggest:
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);
} }return 10**(ERC20(asset()).decimals() /2);
#0 - c4-judge
2023-01-26T13:52:44Z
Picodes marked the issue as duplicate of #367
#1 - c4-sponsor
2023-01-31T14:43:23Z
androolloyd marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T07:32:18Z
Picodes marked the issue as satisfactory
🌟 Selected for report: rvierdiiev
119.1721 USDC - $119.17
Detailed description of the impact of this finding.
makePayment() will be called when the user needs to repay the money, in two cases.
1. If the repayment amount >= (borrowed amount + interest), then the loan is paid off and the corresponding LienToken is deleted 2. If the repayment amount < (borrowed amount + interest), then the borrowed amount = the remaining amount (borrowed amount + interest - repayment amount)
Case 2, if the repayment amount is not enough when not taking into account a situation, the repayment amount is not even enough interest, according to case 2 of the principal becomes larger, resulting in the subsequent interest will become larger
Example: alice borrowing amount:1000 rate:10%/year borrowing 2 years After 1 year borrowing amount = 1000, interest = 100
At this point the user repays: 50, which will result in alice's borrowing amount becoming 1050
When the two years are up, alice will have to repay a total of: 1050 + 1050*10% = 1155 Together with the 50 already repaid, alice actually repay 1155 + 50 = 1205
Normal if 2 years at most should be to pay back 1000 + 1000 * 10% * 2 = 1200
As above, resulting in an extra repayment : 5 So case 2 is not reasonable
If the repayment amount is less than the current interest rate, directly revert
#0 - c4-judge
2023-01-26T16:39:15Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2023-01-31T15:09:59Z
SantiagoGregory marked the issue as sponsor acknowledged
#2 - SantiagoGregory
2023-01-31T15:10:13Z
This is the intended flow, early payments will compound interest.
#3 - c4-judge
2023-02-21T22:11:06Z
Picodes marked issue #96 as primary and marked this issue as a duplicate of 96
#4 - Picodes
2023-02-21T22:11:47Z
Keeping medium severity because of the mention in the doc stating that interests are non-compounding, so considering this is a broken functionality.
#5 - c4-judge
2023-02-21T22:12:21Z
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
Low: 1.deposit() check wrong variant
function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { - require(shares > minDepositAmount(), "VALUE_TOO_SMALL"); + require(assets > minDepositAmount(), "VALUE_TOO_SMALL");
2.minDepositAmount() When the asset is btc, the minDepositAmount is too large
when asset == btc , minDepositAmount = 0.1 btc , equal 2000 usd suggest:
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); + return 10**(ERC20(asset()).decimals() /2); } }
#0 - c4-judge
2023-01-26T13:52:17Z
Picodes marked the issue as grade-b