Astaria contest - cccz'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: 5/103

Findings: 6

Award: $4,450.66

🌟 Selected for report: 4

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: Jeiwan

Also found by: cccz, chaduke, obront, rvierdiiev

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-477

Awards

286.0131 USDC - $286.01

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L107-L231

Vulnerability details

Impact

Vault A can call buyoutLien to buy out Vault B's lien tokens, which calls LienToken.buyoutLien

 function buyoutLien(
   ILienToken.Stack[] calldata stack,
   uint8 position,
   IAstariaRouter.Commitment calldata incomingTerms
 )
   external
   whenNotPaused
   returns (ILienToken.Stack[] memory, ILienToken.Stack memory)
 {
...
   return
     lienToken.buyoutLien(
       ILienToken.LienActionBuyout({
         position: position,
         encumber: ILienToken.LienActionEncumber({
           amount: owed,
           receiver: recipient(),
           lien: ROUTER().validateCommitment({
             commitment: incomingTerms,
             timeToSecondEpochEnd: _timeToSecondEndIfPublic()
           }),
           stack: stack

In LienToken.buyoutLien, it will burn Vault B's lien token and mint a new lien token for Vault A

 function _replaceStackAtPositionWithNewLien(
   LienStorage storage s,
   ILienToken.Stack[] calldata stack,
   uint256 position,
   Stack memory newLien,
   uint256 oldLienId
 ) internal returns (ILienToken.Stack[] memory newStack) {
   newStack = stack;
   newStack[position] = newLien;
   _burn(oldLienId);                        // @ audit: burn Vault B's lien token
   delete s.lienMeta[oldLienId];
 }
...
   newLienId = uint256(keccak256(abi.encode(params.lien)));
   Point memory point = Point({
     lienId: newLienId,
     amount: params.amount.safeCastTo88(),
     last: block.timestamp.safeCastTo40(),
     end: (block.timestamp + params.lien.details.duration).safeCastTo40()
   });
   _mint(params.receiver, newLienId); // @ audit: mint a new lien token for Vault A
   return (newLienId, Stack({lien: params.lien, point: point}));
 }

And, when Vault B is a public vault, the handleBuyoutLien function of Vault B will be called to update the slope(and yIntercept/last) However, when Vault A is a public vault, it does not update Vault A's slope(and yIntercept/last)

   if (_isPublicVault(s, payee)) {
     IPublicVault(payee).handleBuyoutLien(
       IPublicVault.BuyoutLienParams({
         lienSlope: calculateSlope(params.encumber.stack[params.position]),
         lienEnd: params.encumber.stack[params.position].point.end,
         increaseYIntercept: buyout -
           params.encumber.stack[params.position].point.amount
       })
     );
   }
...
 function handleBuyoutLien(BuyoutLienParams calldata params)
   public
   onlyLienToken
 {
   VaultData storage s = _loadStorageSlot();

   unchecked {
     uint48 newSlope = s.slope - params.lienSlope.safeCastTo48();    // @audit: update the slope 
     _setSlope(s, newSlope);
     s.yIntercept += params.increaseYIntercept.safeCastTo88();
     s.last = block.timestamp.safeCastTo40();
   }

   _decreaseEpochLienCount(s, getLienEpoch(params.lienEnd.safeCastTo64())); 
   emit YInterceptChanged(s.yIntercept);
 }

Unlike buyoutLien, when the public vault minted lien tokens via commitToLien, the slope(and yIntercept/last) is updated in _afterCommitToLien

  function _afterCommitToLien(
    uint40 lienEnd,
    uint256 lienId,
    uint256 lienSlope
  ) internal virtual override {
    VaultData storage s = _loadStorageSlot();

    // increment slope for the new lien
    _accrue(s);
    unchecked {
      uint48 newSlope = s.slope + lienSlope.safeCastTo48();
      _setSlope(s, newSlope);
    }

    uint64 epoch = getLienEpoch(lienEnd);

    _increaseOpenLiens(s, epoch);
    emit LienOpen(lienId, epoch);
  }

The slope(and yIntercept/last) of a public vault is determined by the lien tokens held by the vault, and when the slope(and yIntercept/last) is set incorrectly, it can cause the totalAssets of the vault to be undervalued, thus making the vault's shares incorrectly calculated

  function _totalAssets(VaultData storage s) internal view returns (uint256) {
    uint256 delta_t = block.timestamp - s.last;
    return uint256(s.slope).mulDivDown(delta_t, 1) + uint256(s.yIntercept);
  }

Consider the following case. Public Vault B holds a lien token Public Vault A buys out B's lien token to refinance it At this point, the valuation of Public Vault A should include the interest of borrower contained in the lien token. However, since the slope(and yIntercept/last) of Public Vault A is not updated, the valuation of Public Vault A (totalAssets) is undervalued, so that the user receives more shares when depositing

  function convertToShares(
    uint256 assets
  ) public view virtual returns (uint256) {
    uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero.

    return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets());
  }

Proof of Concept

https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L107-L231 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/PublicVault.sol#L439-L457 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/PublicVault.sol#L489-L493

Tools Used

None

In LienToken.buyoutLien, when the caller is a public vault, update the slope(and yIntercept/last) of the public vault

#0 - c4-judge

2023-01-24T18:00:49Z

Picodes marked the issue as duplicate of #366

#1 - c4-judge

2023-02-15T08:52:30Z

Picodes marked the issue as satisfactory

#2 - c4-judge

2023-02-23T21:11:58Z

Picodes changed the severity to QA (Quality Assurance)

#3 - c4-judge

2023-02-24T10:36:18Z

This previously downgraded issue has been upgraded by Picodes

#4 - c4-judge

2023-02-24T10:42:07Z

Picodes marked the issue as not a duplicate

#5 - c4-judge

2023-02-24T10:42:24Z

Picodes marked the issue as duplicate of #477

Findings Information

🌟 Selected for report: cccz

Labels

bug
3 (High Risk)
satisfactory
selected for report
sponsor confirmed
H-14

Awards

2833.5398 USDC - $2,833.54

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L360-L375

Vulnerability details

Impact

In LienToken.transferFrom, transferring lien tokens to the public vault is prohibited because variables such as liensOpenForEpoch are not updated when the public vault receives a lien token, which would prevent the borrower from repaying the loan in that lien token.

  function transferFrom(
    address from,
    address to,
    uint256 id
  ) public override(ERC721, IERC721) {
    LienStorage storage s = _loadLienStorageSlot();
    if (_isPublicVault(s, to)) {
      revert InvalidState(InvalidStates.PUBLIC_VAULT_RECIPIENT);
    }
    if (s.lienMeta[id].atLiquidation) {
      revert InvalidState(InvalidStates.COLLATERAL_AUCTION);
    }
    delete s.lienMeta[id].payee;
    emit PayeeChanged(id, address(0));
    super.transferFrom(from, to, id);
  }

However, public vaults are created using the ClonesWithImmutableArgs.clone function, which uses the create opcode, which allows the address of the public vault to be predicted before it is created.

https://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed

            assembly {
                instance := create(0, ptr, creationSize)
            }

This allows a malicious private vault to transfer lien tokens to the predicted public vault address in advance, and then call AstariaRouter.newPublicVault to create the public vault, which has a liensOpenForEpoch of 0. When the borrower repays the loan via LienToken.makePayment, decreaseEpochLienCount fails due to overflow in _payment, resulting in the liquidation of the borrower's collateral

    } else {
      amount = stack.point.amount;
      if (isPublicVault) {
        // since the openLiens count is only positive when there are liens that haven't been paid off
        // that should be liquidated, this lien should not be counted anymore
        IPublicVault(lienOwner).decreaseEpochLienCount(
          IPublicVault(lienOwner).getLienEpoch(end)
        );
      }

Consider the following scenario where private vault A provides a loan of 1 ETH to the borrower, who deposits NFT worth 2 ETH and borrows 1 ETH. Private Vault A creates Public Vault B using the account alice and predicts the address of Public Vault B before it is created and transfers the lien tokens to it. The borrower calls LienToken.makePayment to repay the loan, but fails due to overflow. The borrower is unable to repay the loan, and when the loan expires, the NFTs used as collateral are auctioned

Proof of Concept

https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L360-L375 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L835-L847 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/AstariaRouter.sol#L731-L742 https://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed

Tools Used

None

In LienToken.transferFrom, require to.code.length >0, thus preventing the transfer of lien tokens to uncreated public vaults

  function transferFrom(
    address from,
    address to,
    uint256 id
  ) public override(ERC721, IERC721) {
    LienStorage storage s = _loadLienStorageSlot();
    if (_isPublicVault(s, to)) {
      revert InvalidState(InvalidStates.PUBLIC_VAULT_RECIPIENT);
    }
+  require(to.code.length > 0);
    if (s.lienMeta[id].atLiquidation) {
      revert InvalidState(InvalidStates.COLLATERAL_AUCTION);
    }
    delete s.lienMeta[id].payee;
    emit PayeeChanged(id, address(0));
    super.transferFrom(from, to, id);
  }

#0 - androolloyd

2023-01-24T11:42:12Z

the mitigation seems like it now blocks transfers to eoas.

#1 - c4-sponsor

2023-01-27T03:30:54Z

SantiagoGregory marked the issue as sponsor confirmed

#2 - androolloyd

2023-02-03T18:08:31Z

@androolloyd

#3 - c4-judge

2023-02-19T16:15:05Z

Picodes marked the issue as satisfactory

#4 - Picodes

2023-02-19T16:15:42Z

Indeed the mitigation may have unintended consequences.

Findings Information

🌟 Selected for report: cccz

Also found by: 0xbepresent, Jeiwan, chaduke

Labels

bug
3 (High Risk)
judge review requested
primary issue
satisfactory
selected for report
sponsor confirmed
H-16

Awards

516.4126 USDC - $516.41

External Links

Lines of code

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

Vulnerability details

Impact

Vault A can call buyoutLien to buy out Vault B's lien tokens, which calls LienToken.buyoutLien

  function buyoutLien(
    ILienToken.Stack[] calldata stack,
    uint8 position,
    IAstariaRouter.Commitment calldata incomingTerms
  )
    external
    whenNotPaused
    returns (ILienToken.Stack[] memory, ILienToken.Stack memory)
  {
...
    return
      lienToken.buyoutLien(
        ILienToken.LienActionBuyout({
          position: position,
          encumber: ILienToken.LienActionEncumber({
            amount: owed,
            receiver: recipient(),
            lien: ROUTER().validateCommitment({
              commitment: incomingTerms,
              timeToSecondEpochEnd: _timeToSecondEndIfPublic()
            }),
            stack: stack

In LienToken.buyoutLien, it will burn Vault B's lien token and mint a new lien token for Vault A

  function _replaceStackAtPositionWithNewLien(
    LienStorage storage s,
    ILienToken.Stack[] calldata stack,
    uint256 position,
    Stack memory newLien,
    uint256 oldLienId
  ) internal returns (ILienToken.Stack[] memory newStack) {
    newStack = stack;
    newStack[position] = newLien;
    _burn(oldLienId);                        // @ audit: burn Vault B's lien token
    delete s.lienMeta[oldLienId];
  }
...
    newLienId = uint256(keccak256(abi.encode(params.lien)));
    Point memory point = Point({
      lienId: newLienId,
      amount: params.amount.safeCastTo88(),
      last: block.timestamp.safeCastTo40(),
      end: (block.timestamp + params.lien.details.duration).safeCastTo40()
    });
    _mint(params.receiver, newLienId); // @ audit: mint a new lien token for Vault A
    return (newLienId, Stack({lien: params.lien, point: point}));
  }

And, when Vault B is a public vault, the handleBuyoutLien function of Vault B will be called to decrease liensOpenForEpoch However, when Vault A is a public vault, it does not increase the liensOpenForEpoch of Vault A

    if (_isPublicVault(s, payee)) {
      IPublicVault(payee).handleBuyoutLien(
        IPublicVault.BuyoutLienParams({
          lienSlope: calculateSlope(params.encumber.stack[params.position]),
          lienEnd: params.encumber.stack[params.position].point.end,
          increaseYIntercept: buyout -
            params.encumber.stack[params.position].point.amount
        })
      );
    }
...
  function handleBuyoutLien(BuyoutLienParams calldata params)
    public
    onlyLienToken
  {
    VaultData storage s = _loadStorageSlot();

    unchecked {
      uint48 newSlope = s.slope - params.lienSlope.safeCastTo48();
      _setSlope(s, newSlope);
      s.yIntercept += params.increaseYIntercept.safeCastTo88();
      s.last = block.timestamp.safeCastTo40();
    }

    _decreaseEpochLienCount(s, getLienEpoch(params.lienEnd.safeCastTo64()));  // @audit: decrease liensOpenForEpoch 
    emit YInterceptChanged(s.yIntercept);
  }

Since the liensOpenForEpoch of the public vault decreases when the lien token is repaid, and since the liensOpenForEpoch of public vault A is not increased, then when that lien token is repaid, _payment will fail due to overflow when decreasing the liensOpenForEpoch.

    } else {
      amount = stack.point.amount;
      if (isPublicVault) {
        // since the openLiens count is only positive when there are liens that haven't been paid off
        // that should be liquidated, this lien should not be counted anymore
        IPublicVault(lienOwner).decreaseEpochLienCount(  //  @audit: overflow here
          IPublicVault(lienOwner).getLienEpoch(end)
        );
      }

Consider the following case. Public Vault B holds a lien token and B.liensOpenForEpoch == 1 Public Vault A buys out B's lien token for refinancing, B.liensOpenForEpoch == 0, A.liensOpenForEpoch == 0 borrower wants to repay the loan, in the _payment function, the decreaseEpochLienCount function of Vault A will be called, A.liensOpenForEpoch-- will trigger an overflow, resulting in borrower not being able to repay the loan, and borrower's collateral will be auctioned off, but in the call to updateVaultAfterLiquidation function will also fail in decreaseEpochLienCount due to the overflow

  function updateVaultAfterLiquidation(
    uint256 maxAuctionWindow,
    AfterLiquidationParams calldata params
  ) public onlyLienToken returns (address withdrawProxyIfNearBoundary) {
    VaultData storage s = _loadStorageSlot();

    _accrue(s);
    unchecked {
      _setSlope(s, s.slope - params.lienSlope.safeCastTo48());
    }

    if (s.currentEpoch != 0) {
      transferWithdrawReserve();
    }
    uint64 lienEpoch = getLienEpoch(params.lienEnd);
    _decreaseEpochLienCount(s, lienEpoch); //  @audit: overflow here

As a result, the borrower cannot repay the loan and the borrower's collateral cannot be auctioned off, thus causing the depositor of the public vault to suffer a loss

Proof of Concept

https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/VaultImplementation.sol#L313-L351 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L835-L843 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/PublicVault.sol#L640-L655

Tools Used

None

In LienToken.buyoutLien, when the caller is a public vault, increase the decreaseEpochLienCount of the public vault

#0 - c4-judge

2023-01-24T18:02:15Z

Picodes marked the issue as primary issue

#1 - c4-sponsor

2023-01-27T03:31:52Z

SantiagoGregory marked the issue as sponsor confirmed

#2 - androolloyd

2023-02-03T18:11:15Z

#3 - c4-sponsor

2023-02-03T18:11:20Z

androolloyd requested judge review

#4 - c4-judge

2023-02-18T17:21:47Z

Picodes marked the issue as selected for report

#5 - c4-judge

2023-02-18T17:22:58Z

Picodes marked issue #476 as primary and marked this issue as a duplicate of 476

#6 - c4-judge

2023-02-18T17:22:59Z

Picodes marked the issue as not selected for report

#7 - c4-judge

2023-02-23T21:14:11Z

Picodes marked the issue as satisfactory

#8 - c4-judge

2023-02-23T21:14:23Z

Picodes marked the issue as selected for report

Findings Information

🌟 Selected for report: rbserver

Also found by: Jeiwan, adriro, cccz, chaduke, rvierdiiev, unforgiven

Labels

bug
2 (Med Risk)
satisfactory
duplicate-488

Awards

49.6437 USDC - $49.64

External Links

Lines of code

https://github.com/AstariaXYZ/astaria-gpl/blob/4b49fe993d9b807fe68b3421ee7f2fe91267c9ef/src/ERC4626RouterBase.sol#L15-L25

Vulnerability details

Impact

ERC4626RouterBase.mint approves shares of asset tokens to the vault, but shares represents the number of vault tokens minted by vault.mint, not the number of asset tokens required, since normally it takes more than 1 asset token to mint 1 vault token, this will result in a failure in vault.mint due to insufficient number of approved asset tokens.

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(); } }

Proof of Concept

https://github.com/AstariaXYZ/astaria-gpl/blob/4b49fe993d9b807fe68b3421ee7f2fe91267c9ef/src/ERC4626RouterBase.sol#L15-L25

Tools Used

None

Change to

  function mint(
    IERC4626 vault,
    address to,
    uint256 shares,
    uint256 maxAmountIn
  ) public payable virtual override returns (uint256 amountIn) {
+    ERC20(vault.asset()).safeApprove(address(vault), maxAmountIn);
-   ERC20(vault.asset()).safeApprove(address(vault), shares);
    if ((amountIn = vault.mint(shares, to)) > maxAmountIn) {
      revert MaxAmountError();
    }
+  ERC20(vault.asset()).safeApprove(address(vault), 0);
  }

#0 - c4-judge

2023-01-26T17:58:03Z

Picodes marked the issue as duplicate of #118

#1 - c4-judge

2023-02-19T11:03:31Z

Picodes marked the issue as satisfactory

#2 - c4-judge

2023-02-24T08:53:50Z

Picodes changed the severity to QA (Quality Assurance)

#3 - c4-judge

2023-02-24T08:56:00Z

This previously downgraded issue has been upgraded by Picodes

#4 - c4-judge

2023-02-24T09:52:48Z

Picodes marked the issue as not a duplicate

#5 - c4-judge

2023-02-24T09:53:05Z

Picodes marked the issue as duplicate of #488

Findings Information

🌟 Selected for report: cccz

Also found by: KIntern_NA, cccz

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
M-21

Awards

382.5279 USDC - $382.53

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L849-L850

Vulnerability details

Impact

When the borrower calls LienToken.makePayment to repay the loan, safeTransferFrom is used to send tokens to the recipient of the vault, which in the case of a private vault is the owner of the private vault.

    s.TRANSFER_PROXY.tokenTransferFrom(stack.lien.token, payer, payee, amount);
...
  function tokenTransferFrom(
    address token,
    address from,
    address to,
    uint256 amount
  ) external requiresAuth {
    ERC20(token).safeTransferFrom(from, to, amount);
  }
...
  function recipient() public view returns (address) {
    if (IMPL_TYPE() == uint8(IAstariaRouter.ImplementationType.PublicVault)) {
      return address(this);
    } else {
      return owner();
    }
  }

If the token for the loan is an ERC777 token, a malicious private vault owner can refuse to receive repayment in the callback, which results in the borrower not being able to repay the loan and the borrower's collateral being auctioned off when the loan expires.

Proof of Concept

https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L849-L850 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/TransferProxy.sol#L28-L35 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L900-L909 https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/VaultImplementation.sol#L366-L372

Tools Used

None

For private vaults, when the borrower repays, sending tokens to the vault, and the private vault owner claims it later

#0 - c4-judge

2023-01-24T07:38:47Z

Picodes marked the issue as duplicate of #248

#1 - c4-judge

2023-02-24T09:39:53Z

Picodes marked the issue as not a duplicate

#2 - c4-judge

2023-02-24T09:40:17Z

Picodes marked the issue as duplicate of #607

#3 - c4-judge

2023-02-24T09:40:57Z

Picodes marked the issue as partial-25

#4 - c4-judge

2023-02-24T09:45:13Z

Picodes marked the issue as full credit

#5 - c4-judge

2023-02-24T09:45:19Z

Picodes marked the issue as not a duplicate

#6 - c4-judge

2023-02-24T09:45:26Z

Picodes marked the issue as satisfactory

#7 - c4-judge

2023-02-24T09:45:45Z

Picodes marked the issue as duplicate of #51

#8 - c4-judge

2023-02-24T09:47:20Z

Picodes marked the issue as not a duplicate

#9 - Picodes

2023-02-24T09:47:35Z

Flagging as duplicate of this issue all findings related to the lack of ERC777 support

#10 - c4-judge

2023-02-24T09:47:57Z

Picodes marked the issue as primary issue

#11 - c4-judge

2023-02-24T09:48:08Z

Picodes marked the issue as selected for report

Findings Information

🌟 Selected for report: cccz

Also found by: KIntern_NA, cccz

Labels

bug
2 (Med Risk)
satisfactory
sponsor acknowledged
duplicate-247

Awards

382.5279 USDC - $382.53

External Links

Lines of code

https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L181-L186

Vulnerability details

Impact

When LienToken.buyoutLien is called to buy out the lien token, it sends the token to the recipient of the vault using safeTransferFrom, where recipient is the owner of the private vault in the case of a private vault.

    s.TRANSFER_PROXY.tokenTransferFrom(
      params.encumber.stack[params.position].lien.token,
      msg.sender,
      payee,
      buyout
    );
...
function tokenTransferFrom(
  address token,
  address from,
  address to,
  uint256 amount
) external requiresAuth {
  ERC20(token).safeTransferFrom(from, to, amount);
}
...
function recipient() public view returns (address) {
  if (IMPL_TYPE() == uint8(IAstariaRouter.ImplementationType.PublicVault)) {
    return address(this);
  } else {
    return owner();
  }
}

If the loan token is an ERC777 token, a malicious private vault owner can refuse to receive the token in the callback, which results in the lien token not being bought out and prevents the borrower from receiving a higher quality loan

Proof of Concept

https://github.com/code-423n4/2023-01-astaria/blob/1bfc58b42109b839528ab1c21dc9803d663df898/src/LienToken.sol#L181-L186

Tools Used

None

For private vaults, sending the tokens to the vault at the time of buyout, after which the private vault owner claims it

#0 - c4-judge

2023-01-24T07:38:39Z

Picodes marked the issue as primary issue

#1 - c4-sponsor

2023-02-02T11:40:04Z

androolloyd marked the issue as sponsor acknowledged

#2 - androolloyd

2023-02-02T11:41:07Z

erc777 issues can be quite problematic, the procotol isn't designed to work with 777, fee on transfer tokens, rebasing tokens

#3 - c4-judge

2023-02-21T22:04:50Z

Picodes marked the issue as satisfactory

#4 - c4-judge

2023-02-24T09:43:38Z

Picodes marked the issue as duplicate of #51

#5 - c4-judge

2023-02-24T09:47:22Z

Picodes marked the issue as not a duplicate

#6 - c4-judge

2023-02-24T09:48:13Z

Picodes marked the issue as duplicate of #247

Findings Information

🌟 Selected for report: cccz

Also found by: 0Kage

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
M-22

Awards

382.5279 USDC - $382.53

External Links

Lines of code

https://github.com/AstariaXYZ/astaria-gpl/blob/4b49fe993d9b807fe68b3421ee7f2fe91267c9ef/src/ERC4626RouterBase.sol#L41-L52

Vulnerability details

Impact

ERC4626RouterBase.withdraw will approve an amount of vault tokens to the vault, but the amount represents the number of asset tokens taken out by vault.withdraw, not the required number of vault tokens, and since it normally requires less than 1 vault token to take out 1 asset token, it will prevent ERC4626RouterBase.withdraw from using all approved vault tokens.

  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();
    }
  }

and since safeApprove cannot approve a non-zero value to a non-zero value, the second call to ERC4626RouterBase.withdraw will fails in safeApprove.

    function safeApprove(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        // safeApprove should only be called when setting an initial allowance,
        // or when resetting it to zero. To increase and decrease it, use
        // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
        require(
            (value == 0) || (token.allowance(address(this), spender) == 0),
            "SafeERC20: approve from non-zero to non-zero allowance"
        );
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
    }

Proof of Concept

https://github.com/AstariaXYZ/astaria-gpl/blob/4b49fe993d9b807fe68b3421ee7f2fe91267c9ef/src/ERC4626RouterBase.sol#L41-L52

Tools Used

None

Change to

  function withdraw(
    IERC4626 vault,
    address to,
    uint256 amount,
-   uint256 maxSharesOut
+  uint256 maxSharesIn
  ) public payable virtual override returns (uint256 sharesOut) {
+   ERC20(address(vault)).safeApprove(address(vault), maxSharesIn);
+   if ((sharesIn = vault.withdraw(amount, to, msg.sender)) > maxSharesIn) {
-   ERC20(address(vault)).safeApprove(address(vault), amount);
-   if ((sharesOut = vault.withdraw(amount, to, msg.sender)) > maxSharesOut) {
      revert MaxSharesError();
    }
+   ERC20(address(vault)).safeApprove(address(vault), 0);

  }

#0 - c4-judge

2023-01-26T17:58:30Z

Picodes marked the issue as duplicate of #467

#1 - c4-judge

2023-02-22T08:48:05Z

Picodes marked the issue as selected for report

#2 - c4-judge

2023-02-22T08:48:09Z

Picodes marked the issue as satisfactory

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