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: 14/54
Findings: 5
Award: $1,006.59
🌟 Selected for report: 0
🚀 Solo Findings: 0
https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L170 https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityProviders.sol#L325
Since the protocol is supposed to be able to bridge all kinds of tokens it's important to keep in mind that some tokens take fees on transfer and some might in the future (e.g. USDT). If a user deposits tokens that either have a fee-on-transfer or are rebasing tokens, the internal accounting of the protocol will be messed up. For example, the protocol will think that it has more tokens than it actually has. That will result in a loss of funds for the users.
See the linked code blocks where the user deposits funds. The contract doesn't verify how many tokens it received.
To support these kinds of tokens, the protocol has to verify the actual amount of tokens that were transferred:
uint oldBalance = ERC20(_token).balanceOf(address(liquidityPool)); SafeERC20Upgradeable.safeTransferFrom(IERC20Upgradeable(_token), _msgSender(), address(liquidityPool), _amount); uint newBalance = ERC20(_token).balanceOf(address(liquidityPool)); uint actualAmount = newBalance - oldBalance;
none
On incoming deposits, check how many tokens the contract received and use that value for internal accounting.
#0 - ankurdubey521
2022-03-30T15:48:00Z
Duplicate of #39
#1 - pauliax
2022-04-26T10:51:58Z
99.257 USDT - $99.26
https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityFarming.sol#L180
The LiquidityFarming contract has a function, reclaimTokens()
, that allows the owner to withdraw all of the contract's funds. In some contracts, such a function is used as a last resort to save user funds in case of an emergency. But, the LiquidityFarming contract is both pausable and upgradable. It doesn't need to be able to withdraw all the funds. Instead, it poses a new security risk. In case the owner's keys are compromised all the funds can be stolen.
It doesn't seem to serve any purpose or affect the contract's usability. I'd recommend removing it.
https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityFarming.sol#L180
none
remove the reclaimTokens()
function.
#0 - ankurdubey521
2022-03-30T11:29:36Z
I agree this is an issue, but in the current iteration of Hyphen it is still a centralized system, therefore there is an implicit trust in the contract owners and executors. A decentralized version of the Hyphen bridge is in the works and will fix these issues.
#1 - pauliax
2022-04-26T11:09:01Z
560.3084 USDT - $560.31
https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L149-L173 https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L242-L261
The LiquidityPool
contract doesn't verify that the user transfers to a different chain. They are able to make a transfer within the same chain. I've tried to come up with a situation where an attacker would be able to drain the pool's funds through the reward system. But, the fees seem to be always larger than the reward amount.
Still, it isn't in anybody's interest to allow transfers within the same chain. It causes losses for the user (fees > reward) and the executor (gas costs).
See linked code blocks. There is no check for the chainID. Well, there can't be because the contract doesn't know what chain it's on.
none
Add a chainID
state variable and assign it a value in initalize()
. On every deposit verify that toChainID != chainID
.
#0 - pauliax
2022-04-28T20:35:45Z
#87
80.3981 USDT - $80.40
https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/token/TokenManager.sol#L44-L54 https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/token/TokenManager.sol#L97-L98 https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L352
There are no validity checks verifying that the maxFee
of a token is actually larger than the equilibriumFee
. If the maxFee
is lower, transfers of that token will always fail because of an underflow. The executor will lose their gas on these failed transactions.
Here the following line will cause an underflow:
uint256 denominator = equilibriumFee * providedLiquidity + (maxFee - equilibriumFee) * resultingLiquidity;
none
add a check that maxFee >= equilibriumFee
#0 - pauliax
2022-04-30T17:26:42Z
🌟 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
167.3608 USDT - $167.36
Two issues with rounding when withdrawing liquidity and fees:
// LiquidityProvers.test.ts describe("Fee Calculation and Extraction", async function () { this.beforeEach(async () => { await token.connect(owner).approve(liquidityProviders.address, await token.balanceOf(owner.address)); await token.connect(charlie).approve(liquidityProviders.address, await token.balanceOf(charlie.address)); await liquidityProviders.addTokenLiquidity(token.address, 100); await liquidityProviders.connect(charlie).addTokenLiquidity(token.address, 300); }); it.only("Should allow extraction of fee in ERC20", async function () { await liquidityProviders.addLpFeeTesting(token.address, 10); await liquidityProviders.increaseTokenLiquidity(1, 100); await liquidityProviders.addLpFeeTesting(token.address, 10); await expect(() => liquidityProviders.connect(charlie).removeLiquidity(2, 300)).to.changeTokenBalances( token, [liquidityPool, charlie], [-313, 313] ); await expect(() => liquidityProviders.removeLiquidity(1, 199)).to.changeTokenBalances( token, [liquidityPool, owner], [-206, 206] ); const result = await lpToken.tokenMetadata(1); console.log(result); expect(result.suppliedLiquidity.toString()).to.not.equal("0"); expect(result.shares.toString()).to.not.equal("0"); }); });
// LiquidityProvers.test.ts describe("Fee Calculation and Extraction", async function () { this.beforeEach(async () => { await token.connect(owner).approve(liquidityProviders.address, await token.balanceOf(owner.address)); await token.connect(charlie).approve(liquidityProviders.address, await token.balanceOf(charlie.address)); await liquidityProviders.addTokenLiquidity(token.address, 100); await liquidityProviders.connect(charlie).addTokenLiquidity(token.address, 300); }); it.only("Should allow extraction of fee in ERC20", async function () { await liquidityProviders.addLpFeeTesting(token.address, 10); await liquidityProviders.increaseTokenLiquidity(1, 100); await liquidityProviders.addLpFeeTesting(token.address, 10); const charlieRewards = await liquidityProviders.getFeeAccumulatedOnNft(2); const aliceRewards = await liquidityProviders.getFeeAccumulatedOnNft(1); expect(charlieRewards.add(aliceRewards).toString()).to.equal("20"); }); });
Instead of fully removing the executor from the array, the removeExecutor()
function only sets the executorStatus
map to false
. If somebody retrieves all the executors using getAllExecutors()
they won't receive a correct representation of the executors.
To properly remove it from the array use the following code:
uint executorsLength = executors.length; for (uint i; i < executorsLength; i++) { if (executors[i] == executorAddress) { executors[i] = executors[executorsLength - 1]; executors.pop(); } }