Platform: Code4rena
Start Date: 10/03/2022
Pot Size: $75,000 USDT
Total HM: 25
Participants: 54
Period: 7 days
Judge: pauliax
Total Solo HM: 10
Id: 97
League: ETH
Rank: 11/54
Findings: 4
Award: $1,567.26
π Selected for report: 1
π Solo Findings: 0
π Selected for report: minhquanym
1260.6939 USDT - $1,260.69
The getAmountToTransfer
function of LiquidityPool
updates incentivePool[tokenAddress]
by adding some fee to it but the formula is wrong and the value of incentivePool[tokenAddress]
will be divided by BASE_DIVISOR
(10000000000) each time.
After just a few time, the value of incentivePool[tokenAddress]
will become zero and that amount of tokenAddress
token will be locked in contract.
Line 319-322
incentivePool[tokenAddress] = (incentivePool[tokenAddress] + (amount * (transferFeePerc - tokenManager.getTokensInfo(tokenAddress).equilibriumFee))) / BASE_DIVISOR;
Let x = incentivePool[tokenAddress]
, y = amount
, z = transferFeePerc
and t = tokenManager.getTokensInfo(tokenAddress).equilibriumFee
. Then that be written as
x = (x + (y * (z - t))) / BASE_DIVISOR; x = x / BASE_DIVISOR + (y * (z - t)) / BASE_DIVISOR;
Fix the bug by change line 319-322 to:
incentivePool[tokenAddress] += (amount * (transferFeePerc - tokenManager.getTokensInfo(tokenAddress).equilibriumFee)) / BASE_DIVISOR;
#0 - pauliax
2022-04-11T12:31:08Z
Great find, the wrong order of arithmetic operations deserves a severity of high as it would have serious negative consequences.
99.257 USDT - $99.26
depositErc20
function of LiquidityPool
assumes that amount
of tokenAddress
token is transfer to itself and emit an event with value amount + rewardAmount
after that.tokenAddress
is a transfer-on-fee token or a deflationary/rebasing token, causing the received amount to be less than the accounted amount.executor
see that event and call sendFundsToUser
.10000
X from chain Polygon to Avalanche.amount = 10000
. But because X take 10% fee, the actual amount of token X LiquidityPool
received is amount * 0.9 = 9000
.LiquidityPool
token X balance is increased by 9000
. (1)
amount + rewardAmount = 10000 + rewardAmount
. Let's just assume rewardAmount = 0
for now, so amount
emitted is 10000
.Executor
will listen to that Deposit
event and call sendFundsToUser
with the amount = 10000
from emitted event.9000
token X, because transfer X on Avalanche also take 10% fee.LiquidityPool
token X balance decreased 10000
. (2)
(1) and (2)
, LiquidityPool
have lost 1000
token X.uint256 balanceBefore = IERC20Upgradeable(tokenAddress).balanceOf(address(this)); SafeERC20Upgradeable.safeTransferFrom(IERC20Upgradeable(tokenAddress), sender, address(this), amount); uint256 receivedAmount = IERC20Upgradeable(tokenAddress).balanceOf(address(this)) - balanceBefore; emit Deposit(sender, tokenAddress, receiver, toChainId, receivedAmount + rewardAmount, rewardAmount, tag);
#0 - pauliax
2022-04-26T10:50:15Z
π Selected for report: hickuphh3
Also found by: 0v3rf10w, 0x1f8b, 0xDjango, 0xNazgul, 0xngndev, 0xwags, Cantor_Dust, CertoraInc, Dravee, IllIllI, PPrieditis, Ruhum, TerrierLover, WatchPug, XDms, benk10, berndartmueller, bitbopper, catchup, cmichel, cryptphi, csanuragjain, danb, defsec, gzeon, hagrid, hubble, jayjonah8, kenta, kyliek, minhquanym, rfa, robee, saian, samruna, throttle, ye0lde, z3s
147.7473 USDT - $147.75
executors
array when call function removeExecutor
Function removeExecutor
in ExecutorManager.sol
did not remove executorAddress
from state executors
array.
Public view function getAllExecutors
can return address that is no longer executor
Keep a mapping from executorAddress
to index in executors
list
Given an index, we can remove an element from a array by swapping the element to the end of the array and pop it out of array.
Example code
address[] internal someArray; mapping(address => uint) internal indexMapping; function removeFromArray(address _address) public { require(indexMapping[_address] > 0); uint index = indexMapping[_address] - 1; someArray[index] = someArray[someArray.length - 1]; someArray.pop(); }
More than that, we can use this indexMapping
to replace executorStatus
by viewing false
in boolean as 0
in uint
Token fee of already added token can be change through function addSupportedToken
and this function not emit event FeeChanged
Check if token have already been added.
require(!tokensInfo[tokenAddress].supportedToken, βToken already addedβ)
In LiquidityPool
, all 4 function setTrustedForwarder
, setLiquidityProviders
, setBaseGas
, setExecutorManager
require onlyOwner but 2 of them have public
visibility and 2 of them have external
https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityPool.sol#L107 https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityPool.sol#L113 https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityPool.sol#L119 https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityPool.sol#L127
If owner is EOA, should change all of them to external
to save gas
LiquidityPool
has a const BASE_DIVISOR = 10000000000
which is used for better accuracy.
Should use BASE_DIVISOR
in getRewardAmount
function
Replace 10000000000
with BASE_DIVISOR
ifEnabled
can be simplify.Function ifEnabled
in WhitelistPeriodManger
:
https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/WhitelistPeriodManager.sol#L260-L262
function ifEnabled(bool _cond) private view returns (bool) { return !areWhiteListRestrictionsEnabled || (areWhiteListRestrictionsEnabled && _cond); }
It actually just check if areWhiteListRestrictionsEnabled
is false
or _cond
is true
.
https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/WhitelistPeriodManager.sol#L260-L262
Let x = areWhiteListRestrictionsEnabled
and y = _cond
, then we can use truth table to prove that.
ββ!x | (x & y) = !x | y
Use this link and type in the above formula to generate truth table. https://trutabgen.com/
Change the code to
function ifEnabled(bool _cond) private view returns (bool) { return (!areWhiteListRestrictionsEnabled || _cond); }
π Selected for report: Dravee
Also found by: 0v3rf10w, 0x1f8b, 0xDjango, 0xNazgul, 0xngndev, 0xwags, Cantor_Dust, CertoraInc, IllIllI, Jujic, Kenshin, Kiep, PPrieditis, TerrierLover, Tomio, WatchPug, antonttc, benk10, berndartmueller, bitbopper, csanuragjain, defsec, gzeon, hagrid, hickuphh3, kenta, minhquanym, oyc_109, pedroais, peritoflores, rfa, robee, saian, samruna, sirhashalot, throttle, wuwe1, z3s
59.5592 USDT - $59.56
At each iteration of the loop, length is read from memory. We can cache the length and save gas per iteration. Solidity 0.8.0 check safe math in every operation. Use uncheck to increase index can save gas.
Optimize
uint256 length = tokenConfig.length; for (uint256 index = 0; index < length;) { depositConfig[toChainId[index]][tokenAddresses[index]].min = tokenConfig[index].min; depositConfig[toChainId[index]][tokenAddresses[index]].max = tokenConfig[index].max; uncheck { ++index; } }
Function getDepositConfig
of TokenManger
return a struct in memory cost lots of gas.
Cache depositConfig after call getDepositConfig
can save gas.
https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityPool.sol#L157-L158 https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityPool.sol#L248-L249 https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityPool.sol#L273-L274
Optimize
DepositConfig config = tokenManager.getDepositConfig(toChainId, tokenAddress); require(config.min <= amount && config.max >= amount, "Deposit amount not in Cap limit" );
#0 - pauliax
2022-05-09T08:08:34Z
I think 1 is not valid because it is a memory variable, not storage.