Platform: Code4rena
Start Date: 08/05/2023
Pot Size: $90,500 USDC
Total HM: 17
Participants: 102
Period: 7 days
Judge: 0xean
Total Solo HM: 4
Id: 236
League: ETH
Rank: 49/102
Findings: 2
Award: $123.22
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: Team_Rocket
Also found by: 0xkazim, BPZ, Bauchibred, BoltzmannBrain, Brenzee, DeliChainSec, Franfran, Lilyjjo, MohammedRizwan, SaeedAlipoor01988, Yardi256, ast3ros, berlin-101, carlitox477, fs0c, peritoflores, sashik_eth, sces60107, thekmj, volodya, zzykxx
66.5871 USDC - $66.59
https://github.com/code-423n4/2023-05-venus/blob/main/contracts/WhitePaperInterestRateModel.sol#L17
For getting the interest rate related calculations in the VToken
, two contracts may be used:
BaseJumpRateModelV2
WhitePaperInterestRateModel
Both are similar and serve the same purpose, but the BaseJumpRateModelV2
has a more unique feature, it supports non-linear borrow rates thanks to the setting of the kink
variable.
For calculating the interest between two accrueInterest()
, the delta of the last block at which the interest as been accrued last time is taken as a time unit. This is a reasonable choice considering that on the BNB Chain, the block time is consistent with about 3s
per block.
In order to calculate the baseRatePerBlock
and the multiplierPerBlock
from the baseRatePerYear
and multiplierPerYear
respectively, we must use the blocksPerYear
The issue, is that this blocksPerYear
is not calculated correctly.
At the moment, the constant's value is 2102400
.
Let's make the calculation:
This is roughly twice less than the constant that is currently in the code.
So we will admit that the contract assumes a 6s
block time.
Let's run a quick test to see what's the consequence on calculations for a 6s
block time.
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../contracts/WhitePaperInterestRateModel.sol"; import "../contracts/JumpRateModelV2.sol"; contract BlocksTest is Test { WhitePaperInterestRateModel wp; JumpRateModelV2 jr; uint256 baseRatePerYear = 1e18; uint256 multiplierPerYear = 1e18; function setUp() public { wp = new WhitePaperInterestRateModel(baseRatePerYear, multiplierPerYear); } function testRate() public { uint256 cash = 1e18; uint256 borrows = 2e17; uint256 reserves = 1e17; emit log_uint(wp.getBorrowRate(cash, borrows, reserves)); emit log_uint(fixedGetBorrowRate(cash, borrows, reserves)); } function fixedGetBorrowRate( uint256 cash, uint256 borrows, uint256 reserves ) public view returns (uint256) { uint256 ur = wp.utilizationRate(cash, borrows, reserves); /// @audit-ok rate{(bor/supply)/block} = (ur{bor/supply} * val{int/block}) + rate{APR/block} uint256 baseRatePerBlock = baseRatePerYear / 10512000; return ((ur * wp.multiplierPerBlock()) / 1e18) + baseRatePerBlock; } }
output:
[โ ] Compiling... [โ ] Compiling 1 files with 0.8.13 [โ ] Solc 0.8.13 finished in 1.93s Compiler run successful Running 1 test for test/Blocks.t.sol:BlocksTest [PASS] testRate() (gas: 12587) Logs: 562128130620 181610626815 Test result: ok. 1 passed; 0 failed; finished in 849.25ยตs
As you can see, the first one (current implementation) assuming a 6s block time compensate by returning a bigger borrow rate than intented.
Some quite bad consequences are:
Manual inspection
Replace blocksPerYear
by 10512000
in the WhitePaperInterestRateModel
contract
Math
#0 - c4-judge
2023-05-16T09:23:24Z
0xean marked the issue as duplicate of #559
#1 - c4-judge
2023-06-05T14:03:04Z
0xean marked the issue as satisfactory
#2 - c4-judge
2023-06-05T14:38:22Z
0xean changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-06-05T14:38:32Z
0xean changed the severity to 3 (High Risk)
๐ Selected for report: brgltd
Also found by: 0x73696d616f, 0xAce, 0xSmartContract, 0xWaitress, 0xkazim, 0xnev, Aymen0909, BGSecurity, Bauchibred, Cayo, ChrisTina, Franfran, IceBear, Infect3d, Kose, Lilyjjo, PNS, RaymondFam, Sathish9098, Team_Rocket, Udsen, YakuzaKiawe, YoungWolves, berlin-101, bin2chen, btk, codeslide, fatherOfBlocks, frazerch, kodyvim, koxuan, lfzkoala, lukris02, matrix_0wl, nadin, naman1778, sashik_eth, tnevler, volodya, wonjun, yjrwkk
56.6347 USDC - $56.63
Examples of when it is checked:
And when it is not:
Submitted as "Low" because the oracle is OOS.
NO_ERROR
check is not always handledThe NO_ERROR
constant is returned to notify that the execution of the last call, even if didn't reverted, went smoothly. This should be handled.
In the healBorrow()
function of the VToken
contract, the HealBorrow()
event is emitted at the end: https://github.com/code-423n4/2023-05-venus/blob/main/contracts/VToken.sol#L428
But the last element appears to be wrong. It emits repayAmount
, but we probably need actualRepayAmount
, in the case of using fee-on-transfer tokens.
delete
on a dynamic type does not zeroes out all the elementsIn the Shortfall
contract, when creating a new auction, we must empty the old values of the last one.
This is done by cleaning the Auction
struct with multiple tricks.
When it comes to cleaning the array market
field, the delete
keyword is used: https://github.com/code-423n4/2023-05-venus/blob/main/contracts/Shortfall/Shortfall.sol#LL379C13-L379C13
But using the delete
keyword on a dynamic type only resets its length to 0.
Luckily, this doesn't seem to have any bad consequences but it could have got under different circumstances.
In the _liquidateBorrowFresh()
function of the VToken
contract used to liquidate underwater accounts, there is a check to make sure that the liquidator isn't the borrower at the same time: https://github.com/code-423n4/2023-05-venus/blob/main/contracts/VToken.sol#LL1046C48-L1046C48
It seems like this check is useless because it is already done in the seize()
function that is called later on.
#0 - c4-judge
2023-05-18T19:35:25Z
0xean marked the issue as grade-b