Biconomy Hyphen 2.0 contest - hickuphh3's results

Next-Gen Multichain Relayer Protocol.

General Information

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

Biconomy

Findings Distribution

Researcher Performance

Rank: 4/54

Findings: 8

Award: $5,783.05

🌟 Selected for report: 3

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: minhquanym

Also found by: WatchPug, cmichel, hickuphh3

Labels

bug
duplicate
3 (High Risk)
sponsor confirmed

Awards

1260.6939 USDT - $1,260.69

External Links

Lines of code

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L317-L322

Vulnerability details

Impact

In the scenario where the transfer fee exceeds the equilibrium fee, the excess gets credited to the incentive pool. The incentive pool fee added is

incentivePool[tokenAddress] =
  (incentivePool[tokenAddress] +
    (amount * (transferFeePerc - tokenManager.getTokensInfo(tokenAddress).equilibriumFee))) /
  BASE_DIVISOR;

Due to an incorrect bracket placement, the current incentive pool amount is divided by BASE_DIVISOR each time.

Proof of Concept

Add the following lines after L316 in the "Current liquidity < Provided liquidity and pool remains in deficit state after deposit” test case.

// after L316
let currentIncentivePoolAmountAcc = await liquidityPool.incentivePool(tokenAddress);
let currentlpFeeAcc = await liquidityProviders.getTotalLPFeeByToken(tokenAddress);
let currentGasFeeAcc = await liquidityPool.gasFeeAccumulatedByToken(tokenAddress);
let currentReceiverBal = await token.balanceOf(receiver);
expect(currentIncentivePoolAmountAcc).to.be.gt(0);
let withdrawAmount = BigNumber.from(minTokenCap).add(2);
tx = await sendFundsToUser(tokenAddress, withdrawAmount.toString(), receiver, "0");
await tx.wait();
let accountedIncentivePoolFee = (await liquidityPool.incentivePool(tokenAddress)).sub(currentIncentivePoolAmountAcc);
let accountedLpFee = (await liquidityProviders.getTotalLPFeeByToken(tokenAddress)).sub(currentlpFeeAcc);
let accountedGasFee = (await liquidityPool.gasFeeAccumulatedByToken(tokenAddress)).sub(currentGasFeeAcc);
let totalFees = withdrawAmount.sub((await token.balanceOf(receiver)).sub(currentReceiverBal));
expect(totalFees).to.be.eq(accountedIncentivePoolFee.add(accountedLpFee).add(accountedGasFee));

Apart from the incorrect bracket placement, there is a potential loss of 1 wei in certain instances due to precision errors. To ensure every wei is accounted for, calculate the total transfer fee first. The incentive fee is the difference between that and the lpFee.

uint256 transferFeeAmount = (amount * transferFeePerc) / BASE_DIVISOR;
uint256 lpFee;
uint256 equilibriumFee = tokenManager.getTokensInfo(tokenAddress).equilibriumFee;
if (transferFeePerc > equilibriumFee) {
  lpFee = amount * equilibriumFee / BASE_DIVISOR;
  incentivePool[tokenAddress] += transferFeeAmount - lpFee;
} else {
  // avoid recalculation
  lpFee = transferFeeAmount;
}

#0 - pauliax

2022-04-28T20:55:03Z

#38

Findings Information

🌟 Selected for report: defsec

Also found by: Ruhum, catchup, danb, hickuphh3, peritoflores

Labels

bug
duplicate
2 (Med Risk)

Awards

80.3981 USDT - $80.40

External Links

Lines of code

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/token/TokenManager.sol#L49-L52

Vulnerability details

Impact

There isn’t a cap placed on the maximum equilibriumFee and maxFee values that can be set. Values that are greater than BASE_DIVISOR will result in subtraction overflow in sendFundsToUser(), thus preventing transfer transactions from execution.

Proof of Concept

In LiquidityPool.tests.ts, let equilibriumFee = 10_000_000_001; and run the test file. All failing tests should have reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block).

Ensure the values to be set do not exceed BASE_DIVISOR or a smaller limit since they are gas fees to be deducted too.

require(_equilibriumFee <= BASE_DIVISOR, "Equilibrium Fee cannot exceed 100%");
require(_maxFee <= BASE_DIVISOR, "Max Fee cannot exceed 100%");

#0 - ankurdubey521

2022-03-30T15:44:45Z

Duplicate of #8

Findings Information

🌟 Selected for report: hickuphh3

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

933.8474 USDT - $933.85

External Links

Lines of code

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/WhitelistPeriodManager.sol#L178-L184 https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/WhitelistPeriodManager.sol#L115-L125

Vulnerability details

Impact

The totalLiquidity and totalLiquidityByLp mappings are not updated when an address is removed from the isExcludedAddress mapping. While this affects the enforcement of the cap limits and the getMaxCommunityLpPositon() function, the worst impact this has is that the address cannot have liquidity removed / transferred due to subtraction overflow.

In particular, users can be prevented from withdrawing their staked LP tokens from the liquidity farming contract should it become non-excluded.

Proof of Concept

  • Assume liquidity farming address 0xA is excluded
  • Bob stakes his LP token
  • Liquidity farming contract is no longer to be excluded: setIsExcludedAddressStatus([0xA, false])
  • Bob attempts to withdraw liquidity → reverts because totalLiquidityByLp[USDC][0xA] = 0, resulting in subtraction overflow.
// insert test case in Withdraw test block of LiquidityFarming.tests.ts
it.only('will brick withdrawals by no longer excluding farming contract', async () => {
  await farmingContract.deposit(1, bob.address);
  await wlpm.setIsExcludedAddressStatus([farmingContract.address], [false]);
  await farmingContract.connect(bob).withdraw(1, bob.address);
});

// results in
// Error: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)

The simplest way is to prevent exclusion removals.

function setIsExcludedAddresses(address[] memory _addresses) external onlyOwner {
  for (uint256 i = 0; i < _addresses.length; ++i) {
    isExcludedAddress[_addresses[i]] = true;
    // emit event
    emit AddressExcluded(_addresses[i]);
  }
}

#0 - pauliax

2022-05-02T12:32:24Z

Great find, but I think it should be of Medium severity because it requires an external condition, the owner should stop excluding the contract, and also in case that happens, function setIsExcludedAddresses can be used to exclude this address again so the funds are not stuck forever in this case.

Findings Information

🌟 Selected for report: hickuphh3

Labels

bug
2 (Med Risk)

Awards

2075.2164 USDT - $2,075.22

External Links

Lines of code

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/WhitelistPeriodManager.sol#L178-L184 https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/WhitelistPeriodManager.sol#L83-L99

Vulnerability details

Impact

The totalLiquidity and totalLiquidityByLp mappings are not updated when an address is added to the isExcludedAddress mapping. This affects the enforcement of the cap limits and the getMaxCommunityLpPositon() function, which implicitly assumes that whitelisted addresses will have 0 liquidity, for addresses with non-zero liquidity at the time of addition to the whitelist.

Proof of Concept

  • Assume the following initial conditions:
    • Alice’s address 0xA is the sole USDC liquidity provider
      • totalLiquidity[USDC] = 500
      • totalLiquidity[USDC][0xA] = 500
    • USDC total cap of 500, ie. perTokenTotalCap[USDC] = 500
  • Exclude Alice’s address 0xA: setIsExcludedAddressStatus([0xA, true])
    • totalLiquidity mappings are unchanged
  • The following deviant behaviour is observed:
    • getMaxCommunityLpPositon() returns 500 when it should return 0
    • All non-excluded addresses are unable to provide liquidity when they should have been able to, as Alice’s liquidity should have been excluded.
    // insert test case in WhitelistPeriodManager.test.ts
    describe.only("Test whitelist addition", async () => {
      it('produces deviant behaviour if excluding address with existing liquidity', async () => {
        await wlpm.setCaps([token.address], [500], [500]);
        await liquidityProviders.connect(owner).addTokenLiquidity(token.address, 500);
        await wlpm.setIsExcludedAddressStatus([owner.address], [true]);
        // 1) returns 500 instead of 0
        console.log((await wlpm.getMaxCommunityLpPositon(token.address)).toString());
        // 2) bob (or other non-excluded addresses) will be unable to add liquidity
        await expect(liquidityProviders.connect(bob).addTokenLiquidity(token.address, 1)).to.be.revertedWith('ERR__LIQUIDITY_EXCEEDS_PTTC');
      });
    });

Check that the address to be excluded is not holding any LP token at the time of exclusion.

// in setIsExcludedAddressStatus()
for (uint256 i = 0; i < _addresses.length; ++i) {
  if (_status[i]) {
    require(lpToken.balanceOf(_addresses[i]) == 0, 'address has existing liquidity');
  }
  ...
}

#0 - ankurdubey521

2022-03-30T15:41:07Z

Duplicate of #72

#1 - pauliax

2022-05-02T13:11:22Z

I think it is a different issue than #72, based on the description it deserves a severity of Medium.

Awards

1163.6691 USDT - $1,163.67

Labels

bug
QA (Quality Assurance)

External Links

Codebase Impressions & Summary

Overall, code quality for the Hyphen 2.0 contracts is high. Supporting documentation was adequate in helping to understand the incentive and fee mechanisms for cross-chain transfers.

The contracts in scope have 81.36% statement and 54.91% branch test coverage. Notably, the Liquidity Pool’s permitAndDepositErc20() and permitEIP2612AndDepositErc20() functions that allow users to deposit with signed messages are untested. It will be ideal to write more tests so that better coverage is achieved. Also note that some liquidity farming tests often fail because rewards are continuously accruing, so the actual amount tends to be greater than the expected amount.

One area of concern is the sendFundsToUser() function, where executors are fully trusted to provide the correct information to complete the cross-chain transfer. Any executor that becomes compromised will enable the attacker to fully drain the available liquidity of a pool by submitting multiple malicious transactions.

Low Severity Findings

L01: Conflicting values of BASE_DIVISOR

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L20

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityProviders.sol#L27

Description

BASE_DIVISOR is 10_000_000_000 in LiquidityPool, but 10**18 in LiquidityProviders. This can easily confuse 3rd parties integrating the token bridge.

Rename either variable. I recommend renaming the instance in LiquidityPool to FEE_DIVISOR.

L02: Incorrect comment for description of BASE_DIVISOR

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L20

Description

BASE_DIVISOR is defined as 10_000_000_000; with accompanying description // Basis Points * 100 for better accuracy.

This isn’t accurate as 100% = 10,000 basis points, and 10,000 * 100 = 1_000_000, not 10_000_000_000.

Either update the comment to be

uint256 private constant BASE_DIVISOR = 10_000_000_000; // 100 * (Basis Points ^ 2) for better accuracy

or the BASE_DIVISOR itself to be a different value.

L03: Standardize fee denomination

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L20

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L350

Description

In relation to L02, there are conflicting definitions of the fee denomination. BASE_DIVISOR says that fees are in Basis points * 100, while the comment in getTransferFees() says they are specified in basis points * 10.

Standardize the fee denomination.

L04: Sub-optimal calculations in getAmountToTransfer() results in wei losses

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L317-L322

Description

In the scenario where the transfer fee exceeds the equilibrium fee, the excess gets credited to the incentive pool. Disregarding from the fact that a bracket is incorrectly placed causing a massive loss in incentives (raised in separate issue), there are cases where 1 wei is unaccounted for from precision loss in the calculation.

lpFee = (amount * tokenManager.getTokensInfo(tokenAddress).equilibriumFee) / BASE_DIVISOR;
// altered for bracket positioning
incentivePool[tokenAddress] +=
  (amount * (transferFeePerc - tokenManager.getTokensInfo(tokenAddress).equilibriumFee)) /
  BASE_DIVISOR;

Proof of Concept

  • amount = 337671308498
  • transferFeePerc = 181480242
  • equilibriumFee = 10000000 (0.1%)

Calculated amounts are

  • lpFee = 337671308
  • incentive = 337671308498 * (181480242 - 10000000) / BASE_DIVISOR = 5790395769

Total fee calculated = 337671308 + 5790395769 = 6128067077

  • transferFeeAmount = 337671308498 * 181480242 / BASE_DIVISOR = 6128067078

We therefore see 1 wei being unaccounted for.

uint256 transferFeeAmount = (amount * transferFeePerc) / BASE_DIVISOR;
uint256 lpFee;
uint256 equilibriumFee = tokenManager.getTokensInfo(tokenAddress).equilibriumFee;
if (transferFeePerc > equilibriumFee) {
  lpFee = amount * equilibriumFee / BASE_DIVISOR;
  incentivePool[tokenAddress] += transferFeeAmount - lpFee;
} else {
...
}

L05: Unbounded iterations for getMaxCommunityLpPositon()

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/WhitelistPeriodManager.sol#L248

Description

The getMaxCommunityLpPosition() iterates through the LP token supply to obtain the maximum community LP position obtained. Because the supply of NFT tokens is uncapped, there will come a point where this function runs out of gas.

Proof of Concept

In the worst case, the limit seems to be at about 1250 NFTs where the (N+1)th LP token has more liquidity than the Nth LP token.

it.only("Tries to get max iterations possible for getMaxCommunityLpPositon()", async () => {
  let MAX_LOOPS = 1250;
  // summation formula for 1 to MAX_LOOPS
  let maxCap = MAX_LOOPS * (MAX_LOOPS + 1) / 2; 
  await wlpm.setCaps([token.address], [maxCap], [maxCap]);
  for (let i = 1; i <= MAX_LOOPS; i++) {
    console.log(`adding ${i}`);
    // worst case: every iteration in getMaxCommunityLpPositon() enters if case
    // by giving next tokenId more liquidity
    await liquidityProviders.connect(owner).addTokenLiquidity(token.address, i);
  }
  console.log('getting max lp position...');
  // Runs out of gas
  // Error: Transaction reverted and Hardhat couldn't infer the reason. Please report this to help us improve Hardhat.
  await wlpm.getMaxCommunityLpPositon(token.address);
});

Have start and end indexes as inputs to cap the number of iterations performed.

L06: Incorrect comment for address to use for withdrawing native token

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityFarming.sol#L175

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityFarming.sol#L186

Description

The comment says to use 0x00 for Ethereum, but the implementation uses NATIVE instead.

use 0x00 for Ethereum → use NATIVE for native token

L07: addSupportedToken() allows zero fees to be set, but changeFee() doesn’t

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/token/TokenManager.sol#L49-L53

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/token/TokenManager.sol#L91-L98

Description

As per the title, the addSupportedToken() allows for zero equilibriumFee or maxFee to be set, but changeFee() doesn’t.

Either include non-zero checks in addSupportedToken() or remove them in changeFee().

L08: Restrict / remove receive() function

Description

In general, it would be good to restrict native fund transfer senders to prevent users from accidentally sending them. 3 contracts have the receive() function:

  1. LiquidityFarming
    • Reward providers (anyone) can send native tokens for liquidity farming, so having it unrestricted is somewhat ideal.
  2. LiquidityPool
  3. LiquidityProviders
    • Native funds are not expected to be received except from payable functions. It would therefore be ideal to remove the function entirely.
  • In LiquidityPool:
receive() external payable {
  require(_msgSender() == address(liquidityProviders), "Only liquidityProviders is allowed");
  emit EthReceived(_msgSender(), msg.value);
}
  • Remove the receive() function in LiquidityProviders

L09: _sendErc20AndGetSentAmount() uses recipient instead of sender balance difference

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityFarming.sol#L109-L117

Description

The function name implies that the sent amount should be returned, but it uses the amount received by the recipient instead.

uint256 recepientBalance = _token.balanceOf(_to);
_token.safeTransfer(_to, _amount);
return _token.balanceOf(_to) - recepientBalance;

If a fee-on-transfer token is the reward token, the amount sent vs received would differ.

Decide which value is to be returned and logged. Either update the function to be _sendErc20AndGetReceivedAmount() or change it to use the contract’s balance difference instead.

uint256 senderBalance = _token.balanceOf(address(this));
_token.safeTransfer(_to, _amount);
return _token.balanceOf(address(this)) - senderBalance;

L10: Deposits don’t work with FoT tokens

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityProviders.sol#L273-L274

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityProviders.sol#L325-L326

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L169-L170

Description

FoT token deposits are not supported because _amount is used for internal accounting, but the actual amount received will be less than it due to the fee.

I gave a low severity rating because of the existence of a token whitelist. Referenced from https://github.com/code-423n4/2021-12-sublime-findings/issues/143

Use the actual token balance change instead of _amount, similar to the _sendErc20AndGetSentAmount.

uint256 currentBalance = _token.balanceOf(address(this));
IERC20Upgradeable(_token).safeTransferFrom(_msgSender(), address(liquidityPool), _amount);
return _token.balanceOf(address(this)) - currentBalance;

L11: Clarify reserve variable descriptions

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityProviders.sol#L42-L44

Description

It is unclear what each variable consists of, because there is:

  • Supplied liquidity (SL) from liquidity providers
  • Available liquidity: SL + net deposits and withdrawals from bridging activity
  • Incentive fees
  • Gas fees
  • LP fees (accumulated equilibrium fees)

Description

It would be best to explictly state what each variable consists of for clarity.

mapping(address => uint256) public totalReserve; // Supplied Liquidity (SL) + LP Fees
mapping(address => uint256) public totalLiquidity; // Supplied Liquidity only
// Available liquidity = SL + net deposits and withdrawals from bridging activity
mapping(address => uint256) public currentLiquidity; // Available Liquidity + All Fees (LP, Incentive, Gas), updated on every in and out transfer

L12: Add constructor initializer in implementation contracts

Description

As per OpenZeppelin’s (OZ) recommendation, “The guidelines are now to make it impossible for anyone to run initialize on an implementation contract, by adding an empty constructor with the initializer modifier. So the implementation contract gets initialized automatically upon deployment.”

Note that this behaviour is also incorporated the OZ Wizard since the UUPS vulnerability discovery: “Additionally, we modified the code generated by the Wizard to include a constructor that automatically initializes the implementation when deployed.”

Add an empty constructor method to the relevant upgradeable contracts.

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {}

L13: Consider having limit on gas fee charged

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L330-L336

Description

There is no limit to the gas fee charged. While it is claimed that “there are no incentives for the executors”, in reality, executors can be indirectly incentivised by inflating the gas price so that they will be credited a higher fee.

The fee can be as high as the bridged amount - transfer fee, leaving nothing for the user. While there is a lot of trust placed on the executor already, it would help to be able to provide a trust-less solution by enforcing a cap on the gas fee.

Limit the maximum gas fee chargeable.

Non-Critical Findings

NC01: Typo errors

NC02: Missing underscore for error

Line Reference

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityFarming.sol#L103

Description

The format seems to be 2 underscores after ERR, but the line reference above only has 1 underscore: ERR_REWARD_TOKEN_IS_ZERO.

ERR_REWARD_TOKEN_IS_ZERO -> ERR__REWARD_TOKEN_IS_ZERO

NC03: Swap comment order

Line Reference

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L351

Description

The variable order in the comment do not correspond to that of the implementation. For readability, I recommend that they do.

uint256 numerator = providedLiquidity * equilibriumFee * maxFee; // L(e) * F(e) * F(max)

NC04: Deep factor not customisable

Reference

https://biconomy.notion.site/Self-Balancing-Cross-Chain-Liquidity-Pools-c19a725673964d5aaec6b16e5c7ce9a5

Description

The fee calculation formula mentions a deep factor d: Value that decides how much deeper the curve looks. Readers may have the impression that this may therefore be a customisable parameter in the contract, but in actual factor, is set to a constant value of 1.

Users / readers should be made aware that the deep factor has been fixed.

#0 - pauliax

2022-05-15T19:58:04Z

"L13: Consider having limit on gas fee charged" could be Medium: https://github.com/code-423n4/2022-03-biconomy-findings/issues/8#issuecomment-1114023886 but is already reported in a separate issue #76

#1 - pauliax

2022-05-15T20:00:23Z

"L10: Deposits don’t work with FoT tokens" should be Medium: https://github.com/code-423n4/2022-03-biconomy-findings/issues/91#issuecomment-1109645407

#2 - pauliax

2022-05-15T20:02:11Z

"L08: Restrict / remove receive() function" should be Medium: #157

#3 - pauliax

2022-05-15T20:05:41Z

"One area of concern is the sendFundsToUser() function, where executors are fully trusted to provide the correct information to complete the cross-chain transfer." could be Medium: https://github.com/code-423n4/2022-03-biconomy-findings/issues/80#issuecomment-1109662505

#4 - pauliax

2022-06-12T07:27:44Z

I think L02, L03, L06, and L11 are non-critical.

Awards

70.7008 USDT - $70.70

Labels

bug
G (Gas Optimization)

External Links

G01: Reward calculation magnification by 10000000000 doesn’t work

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L183-L185

Tools Used

Foundry. Worked like a charm for fuzzing.

Description

If amount < liquidityDifference, the reward amount calculated is

rewardAmount = (amount * incentivePool[tokenAddress] * 10000000000) / liquidityDifference;
rewardAmount = rewardAmount / 10000000000;

The magnification doesn’t work at all because you’re multiplying and dividing by the same amount. It only makes sense if the numerator is represented as a fraction of the denominator, like equilibriumFee being denominated in BASE_DIVISOR.

Proof of Concept

I ported over the logic to a separate contract and fuzz tested it in foundry to see if the magnification worked.

pragma solidity 0.8.0;

contract RoundCalc {
  function calcRewardAmount(uint256 amount, uint256 incentive, uint256 liquidityDifference) public view returns (uint256 rewardAmount) {
    rewardAmount = (amount * incentive * 10000000000) / liquidityDifference;
    rewardAmount = rewardAmount / 10000000000;
  }

  function calcRewardAmountNoMagnification(uint256 amount, uint256 incentive, uint256 liquidityDifference) public view returns (uint256 rewardAmount) {
    rewardAmount = amount * incentive / liquidityDifference;
  }
}

import "ds-test/test.sol";

contract RoundCalcTest is DSTest {
  RoundCalc round;

  function setUp() public {
    round = new RoundCalc();
  }

  function testMagnification(uint256 amount, uint256 incentive, uint256 liquidityDifference) public {
    // reasonably sized caps on incentive and amount (indirectly through liquidityDifference)
    incentive = bound(incentive, 1, 1e30);
    liquidityDifference = bound(liquidityDifference, 1, 1e35);
    // amount < liquidityDifference
    amount = bound(amount, 1, liquidityDifference);
    uint256 rewardAmountMagnified = round.calcRewardAmount(amount, incentive, liquidityDifference);
    uint256 rewardAmount = round.calcRewardAmountNoMagnification(amount, incentive, liquidityDifference);
    assertEq(rewardAmountMagnified, rewardAmount);
  }

  // taken from solmate, thanks @t11s =)
  function bound(
    uint256 x,
    uint256 min,
    uint256 max
  ) internal pure returns (uint256 result) {
    require(max >= min, "MAX_LESS_THAN_MIN");

    uint256 size = max - min;
	
    if (max != type(uint256).max) size++; // Make the max inclusive.
    if (size == 0) return min; // Using max would be equivalent as well.
    // Ensure max is inclusive in cases where x != 0 and max is at uint max.
    if (max == type(uint256).max && x != 0) x--; // Accounted for later.

    if (x < min) x += size * (((min - x) / size) + 1);
    result = min + ((x - min) % size);
	
    // Account for decrementing x to make max inclusive.
    if (max == type(uint256).max && x != 0) result++;
  }
}

Using 100_000 fuzz runs, there was no difference found in the computed values.

Remove the redundant multiplication and division by 10000000000.

rewardAmount = amount * incentivePool[tokenAddress] / liquidityDifference;

G02: Save external call results in memory

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L157-L158

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L248-L249

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L273-L274

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L316

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L318

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L321

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityPool.sol#L348-L349

Description

Multiple external calls are made to fetch values from other contracts. It would be more gas efficient to save the result in memory.


TokenConfig memory depositConfig = tokenManager.getDepositConfig(toChainId, NATIVE);
require(depositConfig.min <= amount && depositConfig.max >= amount, "Deposit amount not in Cap limit");

TokenConfig memory depositConfig = tokenManager.getDepositConfig(toChainId, tokenAddress);
require(depositConfig.min <= amount && depositConfig.max >= amount, "Deposit amount not in Cap limit");

TokenConfig memory transferConfig = tokenManager.getTransferConfig(tokenAddress);
require(transferConfig.min <= amount && transferConfig.max >= amount, "Withdraw amnt not in Cap limits");

uint256 equilibriumFee = tokenManager.getTokensInfo(tokenAddress).equilibriumFee;
// replace tokenManager.getTokensInfo(tokenAddress).equilibriumFee with equilibriumFee subsequently

// alternatively, change getTokensInfo to return (uint256, uint256) instead
ITokenManager.TokenInfo memory config = tokenManager.getTokensInfo(tokenAddress);
uint256 numerator = providedLiquidity * config.equilibriumFee * config.maxFee;
uint256 denominator = config.equilibriumFee * providedLiquidity + (config.maxFee - config.equilibriumFee) * resultingLiquidity;

G03: Duplicate getter methods in LiquidityProviders

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityProviders.sol#L42-L45

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityProviders.sol#L96-L110

Description

Majority of the mappings are declared public, but have duplicate accessor methods. You have situations where a mixture of both ways are being called:

uint256 liquidityPoolBalance = liquidityProviders.getCurrentLiquidity(tokenAddress);
        
currentLiquidity =
  liquidityPoolBalance -
  liquidityProviders.totalLPFees(tokenAddress) -
  gasFeeAccumulatedByToken[tokenAddress] -
  incentivePool[tokenAddress];

Either have the mappings made internal, or remove the accessor methods.

G04: Replace totalSharesMinted[_baseToken] with supply

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityProviders.sol#L181

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityProviders.sol#L183

Description

totalSharesMinted[_baseToken] has already been fetched a couple of lines above. Avoid the duplicate external call.

uint256 supply = totalSharesMinted[_baseToken];
if (supply > 0) {
  return supply / totalReserve[_baseToken];
}
return BASE_DIVISOR;

G05: Only make external calls for non-zero pending rewards

Line References

https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityFarming.sol#L133-L162

Description

Since pending can be 0, checking if (pending != 0) before doing subsequent accounting and transfer of the token will potentially save an external call and the unnecessary gas cost of a 0 token transfer.

pending = ((amount * pool.accTokenPerShare) / ACC_TOKEN_PRECISION) - nft.rewardDebt + nft.unpaidRewards;
if (pending != 0) {
  if (rewardTokens[baseToken] == NATIVE) {
  ...
  }
  ...
}
AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter