Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ via_ir = true
dynamic_test_linking = true
ffi = true
evm_version = 'osaka'
gas_limit = 16_777_215

[fuzz]
runs = 32
Expand Down
42 changes: 42 additions & 0 deletions src/adapters/baseline/BaselineAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import './IBaseline.sol';

import '../../libraries/CalldataDecoder.sol';
import '../../libraries/TokenHelper.sol';

contract BaselineAdapter {
using TokenHelper for address;
using CalldataDecoder for bytes;

function executeBaseline(bytes calldata data, uint256 amountIn, address tokenIn, address, address)
external
payable
returns (uint256 amountUnused, uint256 amountOut)
{
(address relay, address bToken, bool isBuy) = _decodeData(data);

tokenIn.forceApprove(relay, amountIn);

if (isBuy) {
// Buy: reserve -> bToken via solver path
(amountOut,) = IBaseline(relay).buyTokensExactIn(bToken, amountIn, 1);
} else {
// Sell: bToken -> reserve
(amountOut,) = IBaseline(relay).sellTokensExactIn(bToken, amountIn, 1);
}

amountUnused = 0;
}

function _decodeData(bytes calldata data)
internal
pure
returns (address relay, address bToken, bool isBuy)
{
relay = data.decodeAddress(0);
bToken = data.decodeAddress(1);
isBuy = data.decodeBool(2);
}
}
46 changes: 46 additions & 0 deletions src/adapters/baseline/IBaseline.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

interface IBaseline {
/// @notice Buy bTokens with exact reserve input
/// @param bToken The bToken to buy
/// @param amountIn The amount of reserve tokens to spend
/// @param limitAmount The minimum amount of bTokens to receive
/// @return amountOut The amount of bTokens received
/// @return feesReceived The fees collected
function buyTokensExactIn(address bToken, uint256 amountIn, uint256 limitAmount)
external
returns (uint256 amountOut, uint256 feesReceived);

/// @notice Buy exact amount of bTokens
/// @param bToken The bToken to buy
/// @param amountOut The exact amount of bTokens to receive
/// @param limitAmount The maximum amount of reserve tokens to spend
/// @return amountIn The amount of reserve tokens spent
/// @return feesReceived The fees collected
function buyTokensExactOut(address bToken, uint256 amountOut, uint256 limitAmount)
external
payable
returns (uint256 amountIn, uint256 feesReceived);

/// @notice Quote buying tokens with exact reserves input
/// @param bToken The bToken to buy
/// @param reservesIn The amount of reserve tokens to spend
/// @return tokensOut The amount of bTokens that would be received
/// @return feesReceived The fees collected
/// @return slippage The price impact
function quoteBuyExactIn(address bToken, uint256 reservesIn)
external
view
returns (uint256 tokensOut, uint256 feesReceived, uint256 slippage);

/// @notice Sell exact bTokens for reserve
/// @param bToken The bToken to sell
/// @param amountIn The amount of bTokens to sell
/// @param limitAmount The minimum amount of reserve tokens to receive
/// @return amountOut The amount of reserve tokens received
/// @return feesReceived The fees collected
function sellTokensExactIn(address bToken, uint256 amountIn, uint256 limitAmount)
external
returns (uint256 amountOut, uint256 feesReceived);
}
141 changes: 141 additions & 0 deletions test/adapters/baseline/BaselineAdapter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import 'forge-std/Test.sol';

import 'src/adapters/baseline/BaselineAdapter.sol';

contract BaselineAdapterTest is Test {
using TokenHelper for address;

BaselineAdapter adapter;

// Ethereum mainnet addresses
address constant RELAY = 0xc81Fd894C0acE037d133aF4886550aC8133568E8;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant B = 0x9fDbDE76236998Dc2836FE67A9954eDE456A1D63;

address recipient = makeAddr('recipient');

function setUp() public {
vm.createSelectFork(vm.envString('ETH_RPC_URL'));
adapter = new BaselineAdapter();
}

function test_executeBuy() public {
uint256 amountIn = 0.01 ether;
deal(WETH, address(adapter), amountIn);

(uint256 quotedOut,,) = IBaseline(RELAY).quoteBuyExactIn(B, amountIn);
bytes memory data = abi.encode(RELAY, B, true);
(uint256 amountUnused, uint256 amountOut) =
adapter.executeBaseline(data, amountIn, WETH, B, recipient);

assertEq(amountOut, quotedOut);
assertEq(amountUnused, 0);
assertEq(B.balanceOf(address(adapter)), amountOut);
assertEq(WETH.balanceOf(address(adapter)), 0);
}

function test_executeSell() public {
uint256 buyAmount = 0.01 ether;
deal(WETH, address(adapter), buyAmount);

bytes memory buyData = abi.encode(RELAY, B, true);
(, uint256 tb5Amount) = adapter.executeBaseline(buyData, buyAmount, WETH, B, recipient);

bytes memory sellData = abi.encode(RELAY, B, false);
(uint256 amountUnused, uint256 amountOut) =
adapter.executeBaseline(sellData, tb5Amount, B, WETH, recipient);

assertEq(amountUnused, 0);
assertGt(amountOut, 0);
assertEq(WETH.balanceOf(address(adapter)), amountOut);
assertEq(B.balanceOf(address(adapter)), 0);
}

/// @notice Buy then immediately sell — net reserve loss should be bounded by curve + fees
function test_buyThenSell_roundtrip() public {
uint256 amountIn = 0.01 ether;
deal(WETH, address(adapter), amountIn);

bytes memory buyData = abi.encode(RELAY, B, true);
(, uint256 tb5Received) = adapter.executeBaseline(buyData, amountIn, WETH, B, recipient);

bytes memory sellData = abi.encode(RELAY, B, false);
(, uint256 wethReturned) = adapter.executeBaseline(sellData, tb5Received, B, WETH, recipient);

// Roundtrip should lose some reserve to fees/spread but never gain
assertLt(wethReturned, amountIn);
// Sanity: shouldn't lose more than 20% on a small trade
assertGt(wethReturned, (amountIn * 80) / 100);
// No leftover bTokens
assertEq(B.balanceOf(address(adapter)), 0);
}

/// @notice Zero-amount buy reverts
function test_revert_buy_zeroAmountIn() public {
bytes memory data = abi.encode(RELAY, B, true);
vm.expectRevert();
adapter.executeBaseline(data, 0, WETH, B, recipient);
}

/// @notice Zero-amount sell reverts: MakerLib.swapTokens rejects deltaCirc == 0
function test_revert_sell_zeroAmountIn() public {
bytes memory data = abi.encode(RELAY, B, false);
vm.expectRevert();
adapter.executeBaseline(data, 0, B, WETH, recipient);
}

/// @notice Adapter can't pay relay if it doesn't hold the promised input balance
function test_revert_buy_insufficientBalance() public {
uint256 amountIn = 0.01 ether;
deal(WETH, address(adapter), amountIn / 10);

bytes memory data = abi.encode(RELAY, B, true);
vm.expectRevert();
adapter.executeBaseline(data, amountIn, WETH, B, recipient);
}

/// @notice Random non-bToken address should not be swappable
function test_revert_invalidBToken() public {
address fakeToken = makeAddr('fakeBToken');
uint256 amountIn = 0.01 ether;
deal(WETH, address(adapter), amountIn);

bytes memory data = abi.encode(RELAY, fakeToken, true);
vm.expectRevert();
adapter.executeBaseline(data, amountIn, WETH, fakeToken, recipient);
}

/// @notice Adapter produces consistent output across a range of input sizes
function test_fuzz_executeBuy(uint256 amountIn) public {
amountIn = bound(amountIn, 1e13, 1e17);
deal(WETH, address(adapter), amountIn);

(uint256 quotedOut,,) = IBaseline(RELAY).quoteBuyExactIn(B, amountIn);
bytes memory data = abi.encode(RELAY, B, true);
(uint256 amountUnused, uint256 amountOut) =
adapter.executeBaseline(data, amountIn, WETH, B, recipient);

assertEq(amountOut, quotedOut);
assertEq(amountUnused, 0);
assertEq(B.balanceOf(address(adapter)), amountOut);
assertEq(WETH.balanceOf(address(adapter)), 0);
}

/// @notice Back-to-back swaps must not break on leftover allowances (USDT-style reset path)
function test_forceApprove_consecutiveBuys() public {
uint256 amountIn = 0.005 ether;

deal(WETH, address(adapter), amountIn);
bytes memory data = abi.encode(RELAY, B, true);
adapter.executeBaseline(data, amountIn, WETH, B, recipient);

deal(WETH, address(adapter), amountIn);
bytes memory data2 = abi.encode(RELAY, B, true);
(, uint256 amountOut) = adapter.executeBaseline(data2, amountIn, WETH, B, recipient);

assertGt(amountOut, 0);
}
}