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: 21/103
Findings: 3
Award: $673.06
🌟 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
The function safeTransferFrom
is used when an NFT is auctioned, and it is supposed to handle the payments done for the NFT in seaport. The payment is done through a token which is not validated. So, an attacker can use a fake token for the payment.
The function safeTransferFrom
can be called by anyone, so:
Bob (an attacker) calls the function safeTransferFrom
to buy the auctioned NFT.
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/ClearingHouse.sol#L169
He provides the parameter identifier
, so that its last 20 bytes is the address of a malicious contract under his control.
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/ClearingHouse.sol#L172
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/ClearingHouse.sol#L123
So, the paymentToken
is a fake token under control of Bob.
It returns a value larger than currentOfferPrice
, when it is called in the line:
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/ClearingHouse.sol#L132
It does not do anything when called in the following lines: https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/ClearingHouse.sol#L143 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/ClearingHouse.sol#L148 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/ClearingHouse.sol#L161
During the call to the function payDebtViaClearingHouse
, this fake token does not do anything either.
payDebtViaClearingHouse
>> _payDebt
>> _paymentAH
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/ClearingHouse.sol#L153
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L497
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L523
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L643
As a result, all the debt payments are done for the auctioned NFT with a fake token. In other words, it is not validated that the token which is going to be used for payments of the NFT is a valid and accepted one or not.
In summary, the attacker could use a fake token for buying the NFT and paying the debts.
The identifier
parameter should be validated.
#0 - c4-judge
2023-01-24T07:57:49Z
Picodes marked the issue as duplicate of #564
#1 - c4-judge
2023-02-15T07:33:58Z
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:42Z
Picodes marked the issue as not a duplicate
#5 - c4-judge
2023-02-24T10:40:53Z
Picodes marked the issue as duplicate of #521
588.5044 USDC - $588.50
The ClearingHouse
is supposed to be the owner of the collateralized NFT always. The only time that it is not the owner, is during flashAction
in which the NFT will be transferred back to the ClearingHouse
at the end of the transaction. The ownership is tracked by ownerOf(collateralId)
, and the NFT data is tracked by s.idToUnderlying[collateralId]
. In a normal case, if an NFT is going to be released by calling the function releaseToAddress
, these data will be burned and removed, ownerOf(collateralId) = address(0)
and delete s.idToUnderlying[collateralId]
.
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/CollateralToken.sol#L331
But, this is not the case if the NFT is going to be released by a liquidator during an auction.
CollateralToken.sol
to be used as collateral. This transfer triggers the onERC721Received
:collateralId
to this NFT by appending the token contract address and token id.ClearingHouse
contract and transfers this NFT to that contract.So, we will have:
ERC721(tokenContract).ownerOf(tokenId) = the address of the deployed contract ClearingHouse
s.clearingHouse[collateralId] = the address of the deployed contract ClearingHouse
ownerOf(collateralId) = Alice
s.idToUnderlying[collateralId].tokenContract = address of the token contract
Suppose later, Alice is going to be liquidated, and her collateral (her NFT which is locked in the ClearingHouse) is auctioned.
After the liquidation, the liquidator should claim this NFT by calling liquidatorNFTClaim
.
_releaseToAddress
.In this function, the NFT will be transferred to the liquidator. So, we will have:
ERC721(tokenContract).ownerOf(tokenId) = the address of liquidator
s.clearingHouse[collateralId] = the address of the deployed contract ClearingHouse
ownerOf(collateralId) = Alice
s.idToUnderlying[collateralId].tokenContract = address of the token contract
This is the vulnerability, because that NFT is transferred to the liquidator, but the mapping idToUnderlying
is not deleted and the minted collaterId
is not burned. In other words, it is correct to have the following states:
ERC721(tokenContract).ownerOf(tokenId) = the address of liquidator
s.clearingHouse[collateralId] = the address of the deployed contract ClearingHouse
ownerOf(collateralId) = address(0)
s.idToUnderlying[collateralId].tokenContract = address(0)
This can lead to some issues. For example, if the liquidator sells this NFT to another innocent user (Bob):
CollateralToken
, it will be failed. Because s.idToUnderlying[collateralId].tokenContract
is nonzero, and it means that this NFT has already used.
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/CollateralToken.sol#L598ClearingHouse
already deployed for this NFT, he will loss this NFT, and can not take it back. Because, we have still ownerOf(collateralId) = Alice
, so only Alice will be able to release this NFT from the ClearingHouse.
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/CollateralToken.sol#L338The situation can be even worse:
commitToLien
.
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/VaultImplementation.sol#L287CT.ownerOf(collateralId)
is still Alice not address(0). Although, Alice's NFT was sold to the liquidator in the auction, but collateralId
is not burned, so Alice is still owner of this collateralId
.
commitToLien
>> _requestLienAndIssuePayout
>> _validateCommitment
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/VaultImplementation.sol#L235In summary, the issue is that the mapping and burning are done only in the function releaseToAddress
, while the internal function _releaseToAddress
is called during claiming the NFT by the liquidator in the function liquidatorNFTClaim
.
The following modifications should be applied:
// Before function releaseToAddress(uint256 collateralId, address releaseTo) public releaseCheck(collateralId) onlyOwner(collateralId) { CollateralStorage storage s = _loadCollateralSlot(); if (msg.sender != ownerOf(collateralId)) { revert InvalidSender(); } Asset memory underlying = s.idToUnderlying[collateralId]; address tokenContract = underlying.tokenContract; _burn(collateralId); delete s.idToUnderlying[collateralId]; _releaseToAddress(s, underlying, collateralId, releaseTo); } function _releaseToAddress( CollateralStorage storage s, Asset memory underlyingAsset, uint256 collateralId, address releaseTo ) internal { ClearingHouse(s.clearingHouse[collateralId]).transferUnderlying( underlyingAsset.tokenContract, underlyingAsset.tokenId, releaseTo ); emit ReleaseTo( underlyingAsset.tokenContract, underlyingAsset.tokenId, releaseTo ); }
//After function releaseToAddress(uint256 collateralId, address releaseTo) public releaseCheck(collateralId) onlyOwner(collateralId) { CollateralStorage storage s = _loadCollateralSlot(); if (msg.sender != ownerOf(collateralId)) { revert InvalidSender(); } Asset memory underlying = s.idToUnderlying[collateralId]; _releaseToAddress(s, underlying, collateralId, releaseTo); } function _releaseToAddress( CollateralStorage storage s, Asset memory underlyingAsset, uint256 collateralId, address releaseTo ) internal { _burn(collateralId); delete s.idToUnderlying[collateralId]; ClearingHouse(s.clearingHouse[collateralId]).transferUnderlying( underlyingAsset.tokenContract, underlyingAsset.tokenId, releaseTo ); emit ReleaseTo( underlyingAsset.tokenContract, underlyingAsset.tokenId, releaseTo ); }
#0 - c4-sponsor
2023-01-27T03:32:09Z
SantiagoGregory marked the issue as sponsor confirmed
#1 - androolloyd
2023-02-03T18:12:10Z
same underlying issue as this, https://github.com/code-423n4/2023-01-astaria-findings/issues/480
#2 - c4-sponsor
2023-02-03T18:12:15Z
androolloyd requested judge review
#3 - c4-judge
2023-02-15T08:11:53Z
Picodes marked the issue as duplicate of #480
#4 - c4-judge
2023-02-15T08:12:57Z
Picodes marked the issue as selected for report
#5 - c4-judge
2023-02-15T08:13:01Z
Picodes marked issue #480 as primary and marked this issue as a duplicate of 480
#6 - c4-judge
2023-02-15T08:13:35Z
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
The following line does nothing: https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/PublicVault.sol#L262
The following line is redundant as the modifier onlyOwner
does the same job:
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/CollateralToken.sol#L338-L340
https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/CollateralToken.sol#L334
The terms are hashed and singed by the strategist not the borrower, so the following comment should be modified. https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/VaultImplementation.sol#L223
#0 - c4-judge
2023-01-26T14:56:14Z
Picodes marked the issue as grade-b