Platform: Code4rena
Start Date: 08/09/2023
Pot Size: $70,000 USDC
Total HM: 8
Participants: 84
Period: 6 days
Judge: gzeon
Total Solo HM: 2
Id: 285
League: ETH
Rank: 14/84
Findings: 2
Award: $907.74
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: bin2chen
Also found by: castle_chain, maanas, merlin, mert_eren, nobody2018
857.3144 USDC - $857.31
Due to the onlyCentrifugeChainOrigin() modifier, the execute() function in Axelar router will always revert as the msg.sender will not be the Axelar Gateway. https://github.com/code-423n4/2023-09-centrifuge/blob/512e7a71ebd9ae76384f837204216f26380c9f91/src/gateway/routers/axelar/Router.sol#L43
modifier onlyCentrifugeChainOrigin(string calldata sourceChain, string calldata sourceAddress) { require(msg.sender == address(axelarGateway), "AxelarRouter/invalid-origin"); ..... more code }
The check is a wrong check as the Axelar Gateway only approves the call and doesn't actually make the call. Hence all cross-chain messages using Axelar router will fail.
From the documentation of Axelar: https://docs.axelar.dev/learn/network/flow
After a relayer sends it out, a Gateway on the destination chain receives the approved message, where its payload is marked as approved by Axelar. The Gateway stores the contract call approval along with a hash of the payload, representing the legitimacy of this cross-chain message. The approved payload can now be executed by anyone at any time.
In the following transaction containing Axelar callContract, it can be seen that at the executed step, the call to the destination contract is made by Axelar Relayer and not Axelar Gateway https://axelarscan.io/gmp/0x7baba0fb9de79efc8eb8f343dd0e3b0b9d253f6a304a7f96f898c0aee8d21fff:285
Documentation
Remove the msg.sender check. The validity of the incoming call is established by calling the validateContractCall() function of the Axelar gateway as implemented inside the execute() function of the router. https://github.com/code-423n4/2023-09-centrifuge/blob/512e7a71ebd9ae76384f837204216f26380c9f91/src/gateway/routers/axelar/Router.sol#L73-L86
function execute( bytes32 commandId, string calldata sourceChain, string calldata sourceAddress, bytes calldata payload ) public onlyCentrifugeChainOrigin(sourceChain, sourceAddress) { bytes32 payloadHash = keccak256(payload); require( axelarGateway.validateContractCall(commandId, sourceChain, sourceAddress, payloadHash), "Router/not-approved-by-gateway" ); gateway.handle(payload); }
Invalid Validation
#0 - c4-pre-sort
2023-09-15T21:59:19Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-09-15T21:59:31Z
raymondfam marked the issue as duplicate of #537
#2 - c4-judge
2023-09-26T16:06:02Z
gzeon-c4 changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-09-26T18:12:20Z
gzeon-c4 marked the issue as satisfactory
50.4324 USDC - $50.43
When depositing, the price calculated for the tranche token is rounded down leading to excess tranche tokens being transferred to the investor.
The price of the tranche token is calculated using the calculateDepositPrice() function. This function in turn calls the _calculatePrice() function which always rounds down after division.
function calculateDepositPrice(address user, address liquidityPool) public view returns (uint256 depositPrice) { LPValues storage lpValues = orderbook[user][liquidityPool]; if (lpValues.maxMint == 0) { return 0; } depositPrice = _calculatePrice(lpValues.maxDeposit, lpValues.maxMint, liquidityPool); }
function _calculatePrice(uint128 currencyAmount, uint128 trancheTokenAmount, address liquidityPool) public view returns (uint256 depositPrice) { (uint8 currencyDecimals, uint8 trancheTokenDecimals) = _getPoolDecimals(liquidityPool); uint256 currencyAmountInPriceDecimals = _toPriceDecimals(currencyAmount, currencyDecimals, liquidityPool); uint256 trancheTokenAmountInPriceDecimals = _toPriceDecimals(trancheTokenAmount, trancheTokenDecimals, liquidityPool); depositPrice = currencyAmountInPriceDecimals.mulDiv( 10 ** PRICE_DECIMALS, trancheTokenAmountInPriceDecimals, MathLib.Rounding.Down ); }
A lowered price for the tranche token would result in more tranche tokens to be transferred to the investor than what was originally minted towards this deposit. Based on the condition of the escrow, this can lead to either this deposit reverting or future deposits reverting.
function handleExecutedCollectInvest( uint64 poolId, bytes16 trancheId, address recipient, uint128 currency, uint128 currencyPayout, uint128 trancheTokensPayout ) public onlyGateway { require(currencyPayout != 0, "InvestmentManager/zero-invest"); address _currency = poolManager.currencyIdToAddress(currency); address liquidityPool = poolManager.getLiquidityPool(poolId, trancheId, _currency); require(liquidityPool != address(0), "InvestmentManager/tranche-does-not-exist"); LPValues storage lpValues = orderbook[recipient][liquidityPool]; lpValues.maxDeposit = lpValues.maxDeposit + currencyPayout; lpValues.maxMint = lpValues.maxMint + trancheTokensPayout; // @audit: fixed number of tranche tokens are minted for this deposit here LiquidityPoolLike(liquidityPool).mint(address(escrow), trancheTokensPayout); // mint to escrow. Recepeint can claim by calling withdraw / redeem _updateLiquidityPoolPrice(liquidityPool, currencyPayout, trancheTokensPayout); }
The following scenario would prove the concept:
The code for the above scenario is attached below. Add this test to test/LiquidityPool.t.sol
and run forge test --match-test testDepositFailsDueToRounding
function testDepositFailsDueToRounding(uint64 poolId, bytes16 trancheId, uint128 currencyId) public { vm.assume(currencyId > 0); uint8 TRANCHE_TOKEN_DECIMALS = 6; uint8 INVESTMENT_CURRENCY_DECIMALS = 9; ERC20 currency = _newErc20("Currency", "CR", INVESTMENT_CURRENCY_DECIMALS); address lPool_ = deployLiquidityPool(poolId, TRANCHE_TOKEN_DECIMALS, "", "", trancheId, currencyId, address(currency)); LiquidityPool lPool = LiquidityPool(lPool_); uint128 _currencyId = poolManager.currencyAddressToId(address(currency)); // two investor invests the same amount and gets their order fulfilled at the same price uint256 investmentAmount = 1000000000; uint128 currencyPayout = 1000000000; uint128 trancheTokenPayout = 8412560094779513; address investor1 = address(1); // investor 1 invests { currency.mint(investor1, investmentAmount); homePools.updateMember(poolId, trancheId, investor1, type(uint64).max); vm.startPrank(investor1); currency.approve(address(investmentManager), investmentAmount); lPool.requestDeposit(investmentAmount, investor1); vm.stopPrank(); homePools.isExecutedCollectInvest( poolId, trancheId, bytes32(bytes20(investor1)), _currencyId, currencyPayout, trancheTokenPayout ); // assert deposit & mint values adjusted assertEq(lPool.maxDeposit(investor1), currencyPayout); assertEq(lPool.maxMint(investor1), trancheTokenPayout); } address investor2 = address(2); { // investor 2 invests currency.mint(investor2, investmentAmount); homePools.updateMember(poolId, trancheId, investor2, type(uint64).max); vm.startPrank(investor2); currency.approve(address(investmentManager), investmentAmount); lPool.requestDeposit(investmentAmount, investor2); vm.stopPrank(); homePools.isExecutedCollectInvest( poolId, trancheId, bytes32(bytes20(investor2)), _currencyId, currencyPayout, trancheTokenPayout ); // assert deposit & mint values adjusted assertEq(lPool.maxDeposit(investor2), currencyPayout); assertEq(lPool.maxMint(investor2), trancheTokenPayout); } // due to rounding issue, investor1 receives more trancheTokenAmount than expected { // investor1 deposits maximum lPool.deposit(currencyPayout, investor1); assertGt(lPool.balanceOf(investor1), trancheTokenPayout); } // since overall trancheTokenPayout + trancheTokenPayout has only been minted, the next investor cannot deposit his maximum { // investor1 deposits maximum vm.expectRevert(); lPool.deposit(currencyPayout, investor2); } }
Foundry
Math
#0 - c4-pre-sort
2023-09-15T07:49:49Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-09-15T07:49:59Z
raymondfam marked the issue as duplicate of #34
#2 - c4-judge
2023-09-25T13:42:13Z
gzeon-c4 changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-09-26T18:11:29Z
gzeon-c4 marked the issue as satisfactory