Blueberry audit result
— Audit result, Sherlock — 7 min read

Blueberry is a defi yield farming protocol. It allows users to deposit collateral into Banks
and withdraw debt tokens from it to farm yield (e.g. on Ichi).
The code was reviewed as part of the Sherlock contest.
I ranked 6th out of 284 participants in this audit. I found 2 high and 1 medium vulnerabilities I present my findings here.
High: Fail to accrue interests on multiple token positions
Summary
In BlueBerryBank.sol
the functions borrow
, repay
, lend
, or withdrawLend
call poke(token)
to trigger interest accrual on concerned token, but fail to do so for other token debts of the concerned position. This could lead to wrong calculation of position's debt and whether the position is liquidatable.
Vulnerability Detail
Whether a position is liquidatable or not is checked at the end of the execute
function, the execution should revert if the position is liquidatable.
The calculation of whether a position is liquidatable takes into account all the different debt tokens within the position. However, the debt accrual has been triggered only for one of these tokens, the one concerned by the executed action. For other tokens, the value of bank.totalDebt
will be lower than what it should be. This results in the debt value of the position being lower than what it should be and a position seen as not liquidatable while it should be liquidatable.
Impact
Users may be able to operate on their position leading them in a virtually liquidatable state while not reverting as interests were not applied. This will worsen the debt situation of the bank and lead to overall more liquidatable positions.
Code Snippet
execute checking isLiquidatable without triggering interests:
https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L607
actions only poke one token (here for borrow):
https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L709-L715
bank.totalDebt is used to calculate a position's debt while looping over every tokens:
https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L451-L475
The position's debt is used to calculate the risk:
https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L477-L495
The risk is used to calculate whether a debt is liquidatable:
https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L497-L505
Tool used
Manual Review
Recommendation
Review how token interests are triggered. Probably need to accrue interests on every debt token of a position at the beginning of execute.
High: Liquidate pays out full colateral for one token repaid
Summary
In BlueBerryBank.sol
, the liquidate
function allows any user to repay the debt of a liquidatable position for a specific token and send the collateral of the position to the liquidator. However, how it is implemented means that for a position with two debt tokens, the liquidator can fully repay the debt of only one of the tokens and receive the full collateral while they should receive only part of it.
Vulnerability Detail
Liquidate will check if a debt is liquidatable (line 517) which checks the liquidation criteria using the collateral and the debt value of every debt tokens in the position (getDebtValue line 451). However, it then only consider the debt share of a single token for repaying the debt and checking which portion of collateral to send to the liquidator (line 522 - 531).
Impact
If a user borrowed 300 USDC with 100 ICHI collateral and 3 ICHI with 1 ICHI collateral, and becomes liquidatable (e.g. through the fall of ICHI price), anyone can repay the 3 ICHI debt and receive the full 101 ICHI collateral as reward.
Code Snippet
Liquidate function:
https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L511-L572
getDebtValue, showing how it takes into account every debt token of the position:
https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L451-L475
Tool used
Manual Review
Testing:
bank.test.ts
1import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';2import chai, { expect } from 'chai';3import { BigNumber, constants, utils } from 'ethers';4import { ethers, upgrades } from 'hardhat';5import {6 BlueBerryBank,7 CoreOracle,8 IchiVaultSpell,9 IWETH,10 SoftVault,11 MockOracle,12 IchiLpOracle,13 WERC20,14 WIchiFarm,15 ProtocolConfig,16 MockIchiVault,17 ERC20,18 MockIchiV2,19 MockIchiFarm,20 HardVault21} from '../typechain-types';22import { ADDRESS, CONTRACT_NAMES } from '../constant';23import SpellABI from '../abi/IchiVaultSpell.json';24
25import { solidity } from 'ethereum-waffle'26import { near } from './assertions/near'27import { roughlyNear } from './assertions/roughlyNear'28import { Protocol, setupProtocol } from './setup-test';29
30chai.use(solidity)31chai.use(near)32chai.use(roughlyNear)33
34const CUSDC = ADDRESS.bUSDC;35const WETH = ADDRESS.WETH;36const USDC = ADDRESS.USDC;37const ICHI = ADDRESS.ICHI;38const ICHIV1 = ADDRESS.ICHI_FARM;39const ICHI_VAULT_PID = 0; // ICHI/USDC Vault PoolId40
41describe('Bank', () => {42 let admin: SignerWithAddress;43 let alice: SignerWithAddress;44 let treasury: SignerWithAddress;45
46 let usdc: ERC20;47 let ichi: MockIchiV2;48 let ichiV1: ERC20;49 let weth: IWETH;50 let werc20: WERC20;51 let mockOracle: MockOracle;52 let ichiOracle: IchiLpOracle;53 let oracle: CoreOracle;54 let spell: IchiVaultSpell;55 let wichi: WIchiFarm;56 let bank: BlueBerryBank;57 let config: ProtocolConfig;58 let usdcSoftVault: SoftVault;59 let ichiSoftVault: SoftVault;60 let hardVault: HardVault;61 let ichiFarm: MockIchiFarm;62 let ichiVault: MockIchiVault;63 let protocol: Protocol;64
65 before(async () => {66 [admin, alice, treasury] = await ethers.getSigners();67 usdc = <ERC20>await ethers.getContractAt("ERC20", USDC);68 ichi = <MockIchiV2>await ethers.getContractAt("MockIchiV2", ICHI);69 ichiV1 = <ERC20>await ethers.getContractAt("ERC20", ICHIV1);70 weth = <IWETH>await ethers.getContractAt(CONTRACT_NAMES.IWETH, WETH);71
72 protocol = await setupProtocol();73 config = protocol.config;74 bank = protocol.bank;75 spell = protocol.spell;76 ichiFarm = protocol.ichiFarm;77 ichiVault = protocol.ichi_USDC_ICHI_Vault;78 wichi = protocol.wichi;79 werc20 = protocol.werc20;80 oracle = protocol.oracle;81 mockOracle = protocol.mockOracle;82 usdcSoftVault = protocol.usdcSoftVault;83 ichiSoftVault = protocol.ichiSoftVault;84 hardVault = protocol.hardVault;85 })86
87 beforeEach(async () => {88 })89
90 describe("Liquidation", () => {91 const depositAmount = utils.parseUnits('100', 18); // worth of $40092 const borrowAmount = utils.parseUnits('300', 6);93 const iface = new ethers.utils.Interface(SpellABI);94
95 beforeEach(async () => {96 await usdc.approve(bank.address, ethers.constants.MaxUint256);97 await ichi.approve(bank.address, ethers.constants.MaxUint256);98 await bank.execute(99 0,100 spell.address,101 iface.encodeFunctionData("openPosition", [102 0,103 ICHI,104 USDC,105 depositAmount,106 borrowAmount // 3x107 ])108 )109
110 await bank.execute(111 1,112 spell.address,113 iface.encodeFunctionData("openPosition", [114 0,115 ICHI,116 ICHI,117 utils.parseUnits('1', 18),118 utils.parseUnits('3', 18), // 3x119 ])120 )121 })122 it.only("should be able to liquidate the position => (OV - PV)/CV = LT", async () => {123 await ichiVault.rebalance(-260400, -260200, -260800, -260600, 0);124
125 const positionInfoBefore = await bank.getPositionInfo(1);126 console.log(positionInfoBefore)127
128 console.log('===ICHI token dumped from $5 to $1===');129 await mockOracle.setPrice(130 [ICHI],131 [132 BigNumber.from(10).pow(17).mul(10), // $0.5133 ]134 );135
136 expect(await bank.isLiquidatable(1)).to.be.true;137
138 const ichiBalanceBefore = await ichi.balanceOf(alice.address)139 const aliceIchiSoftVaultBalanceBefore = await ichiSoftVault.balanceOf(alice.address)140
141 await ichi.connect(alice).approve(bank.address, ethers.constants.MaxUint256)142 await expect(143 bank.connect(alice).liquidate(1, ICHI, utils.parseUnits('3', 18))144 ).to.be.emit(bank, "Liquidate");145 146 const positionInfo = await bank.getPositionInfo(1);147 console.log(positionInfo)148
149 const ichiBalanceAfter = await ichi.balanceOf(alice.address)150 const aliceIchiSoftVaultBalanceAfter = await ichiSoftVault.balanceOf(alice.address)151
152 expect(positionInfo.collateralSize).to.be.lt(utils.parseUnits('1', 2)) // position is almost 0 (due to fees / rounding it's not 0)153 expect(positionInfoBefore.collateralSize.sub(positionInfo.collateralSize)).to.be.roughlyNear(positionInfoBefore.collateralSize) // position is almost 0154
155 expect((ichiBalanceBefore.sub(ichiBalanceAfter).toString())).to.equal(utils.parseUnits('3', 18)) // alice repaid 3156 expect(aliceIchiSoftVaultBalanceAfter.sub(aliceIchiSoftVaultBalanceBefore)).to.be.roughlyNear(positionInfoBefore.underlyingVaultShare) // alice received roughly all the share157 })158 })159})
Recommendation
One fix is not to allow partial liquidation and only allow for liquidations if the liquidator pays out the debt for every token in the position. This comes with the challenge that positions with unpopular debt tokens are hard to liquidate, and that users may not be willing to use type(uint256).max
as repaid amount for fear of front-running transactions that may accrue additional debt, so there will always be dust amount of debts.
The other solution is to calculate the value of debt repaid compared to the total debt and only transfer that share of collateral to the liquidator.
Medium: Bank totalLend accounting wrong
Summary
In BlueBerryBank.sol
, the value of bank.totalLend
is probably wrong after liquidations.
Vulnerability Detail
The value of bank.totalLend
should track the total amount lent to the bank, but liquidations do not update this value when they probably should. This value is not currently used anywhere in the code so it is hard to tell if this is definitely wrong or can be abused. However if spells are later added that rely on the correct calculation of this value for critical accounting, it will become a problem.
Impact
Currently low impact as this value is unused by other parts of the code. If the protocol intends to add spells or other functionalities that rely on this value, the impact may be higher.
Code Snippet
lend function increases bank.totalLend, pos.underlyingAmount, and deposits token to the vault :
https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L620-L662
withdrawLend function decreases bank.totalLend, pos.underlyingAmount, and removes token from the vault:
https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L669-L704
liquidate function decreases pos.underlyingAmount, and transfer vault tokens to the liquidator (they no longer are held by the bank), but does not decrease bank.totalLend:
https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L511-L572
Tool used
Manual Review
Recommendation
Update this value correctly or remove it altogether.