diff --git a/README.md b/README.md index 63b945a..1feec93 100644 --- a/README.md +++ b/README.md @@ -14,4 +14,7 @@ Below are few assumptions you must follow before implementing your own adapter: - If your integration requires a callback function, implement it in the adapter contract. - Use our libraries [TokenHelper](src/libraries/TokenHelper.sol) to interact with tokens and [CalldataDecoder](src/libraries/CalldataDecoder.sol) to decode calldata whenever possible. - If your integration does not transfer the output tokens directly to the recipient, just leave them inside the adapter contract, don't need to explicitly transfer them. -- The adapter supports native tokens as input and output tokens, through symbolic address `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` (`TokenHelper.NATIVE_ADDRESS`). \ No newline at end of file +- The adapter supports native tokens as input and output tokens, through symbolic address `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` (`TokenHelper.NATIVE_ADDRESS`). +- If your integration do not supports native tokens directly, you can simply ignore it. + +Also don't forget to [ALLOW EDITS FROM MAINTAINERS IN THE PR SETTINGS](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork). \ No newline at end of file diff --git a/src/adapters/wasabi-prop-amm/IPropPool.sol b/src/adapters/wasabi-prop-amm/IPropPool.sol new file mode 100644 index 0000000..8c92ac2 --- /dev/null +++ b/src/adapters/wasabi-prop-amm/IPropPool.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +interface IPropPool { + /// @notice Swap exact input amount of tokenIn for tokenOut + /// @param tokenIn The token to swap from + /// @param amountIn The amount of tokenIn to swap + /// @param minAmountOut The minimum amount of tokenOut to receive + /// @return amountOut The amount of tokenOut received + function swapExactInput(address tokenIn, uint256 amountIn, uint256 minAmountOut) + external + returns (uint256 amountOut); + + /// @notice Get the base token of the pool + /// @return The base token + function getBaseToken() external view returns (address); + + /// @notice Get the quote token of the pool + /// @return The quote token + function getQuoteToken() external view returns (address); +} diff --git a/src/adapters/wasabi-prop-amm/WasabiPropAmmAdapter.sol b/src/adapters/wasabi-prop-amm/WasabiPropAmmAdapter.sol new file mode 100644 index 0000000..bfa00e4 --- /dev/null +++ b/src/adapters/wasabi-prop-amm/WasabiPropAmmAdapter.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import './IPropPool.sol'; + +import '../../libraries/CalldataDecoder.sol'; +import '../../libraries/TokenHelper.sol'; + +contract WasabiPropAmmAdapter { + using TokenHelper for address; + using CalldataDecoder for bytes; + + function executeWasabiPropAmm( + bytes calldata data, + uint256 amountIn, + address tokenIn, + address, + address + ) external payable returns (uint256 amountUnused, uint256 amountOut) { + address pool = data.decodeAddress(0); + + // Approve pool to pull tokenIn from this adapter + tokenIn.forceApprove(pool, amountIn); + + // Execute swap -- pool pulls tokenIn, sends tokenOut back to this contract + amountOut = IPropPool(pool).swapExactInput(tokenIn, amountIn, 1); + + amountUnused = 0; // PropPool exact-input always consumes full amount + } +} diff --git a/test/adapters/wasabi-prop-amm/WasabiPropAmmAdapter.t.sol b/test/adapters/wasabi-prop-amm/WasabiPropAmmAdapter.t.sol new file mode 100644 index 0000000..17a1ed9 --- /dev/null +++ b/test/adapters/wasabi-prop-amm/WasabiPropAmmAdapter.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; + +import 'src/adapters/wasabi-prop-amm/WasabiPropAmmAdapter.sol'; + +interface ITestPropPoolFactory { + function getPropPool(address token) external view returns (address); + function checkRole(uint64 roleId, address account) external view; +} + +interface ITestPropPool { + function getBaseToken() external view returns (address); + function getQuoteToken() external view returns (address); + function getPriceOracle() external view returns (address); +} + +interface ITestPriceOracle { + struct PriceData { + uint256 price; + uint8 precision; + uint16 volatilityPips; + uint256 lastUpdated; + } + + function getUSDPrice(address token) external view returns (PriceData memory); +} + +contract WasabiPropAmmAdapterTest is Test { + using TokenHelper for address; + WasabiPropAmmAdapter adapter; + + address constant FACTORY = 0x851fC799C9F1443A2c1e6B966605A80f8A1b1BF2; + address constant BASE_WETH = 0x4200000000000000000000000000000000000006; + address constant BASE_USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + uint64 constant AUTHORIZED_SWAPPER_ROLE = 100; + + address pool; + address recipient = makeAddr('recipient'); + + string RPC_URL = 'https://mainnet.base.org'; + uint256 BLOCK_NUMBER = 42_026_331; + + function setUp() public { + vm.createSelectFork(RPC_URL, BLOCK_NUMBER); + + adapter = new WasabiPropAmmAdapter(); + pool = ITestPropPoolFactory(FACTORY).getPropPool(BASE_WETH); + + // Mock factory's checkRole to allow the adapter to swap + vm.mockCall( + FACTORY, + abi.encodeWithSelector( + ITestPropPoolFactory.checkRole.selector, AUTHORIZED_SWAPPER_ROLE, address(adapter) + ), + bytes('') + ); + } + + function _warpToFreshOracle() internal { + address oracle = ITestPropPool(pool).getPriceOracle(); + address baseToken = ITestPropPool(pool).getBaseToken(); + ITestPriceOracle.PriceData memory priceData = ITestPriceOracle(oracle).getUSDPrice(baseToken); + vm.warp(priceData.lastUpdated + 1); + } + + function test_executeWasabiPropAmm(uint256 amountIn, bool tokenToUSDC) public { + vm.assume(pool != address(0)); + _warpToFreshOracle(); + + address tokenIn; + address tokenOut; + if (tokenToUSDC) { + tokenIn = BASE_WETH; + tokenOut = BASE_USDC; + amountIn = bound(amountIn, 1e15, 1e18); + } else { + tokenIn = BASE_USDC; + tokenOut = BASE_WETH; + amountIn = bound(amountIn, 1e6, 3000e6); + } + + deal(tokenIn, address(adapter), amountIn); + + bytes memory data = abi.encode(pool); + (uint256 amountUnused, uint256 amountOut) = + adapter.executeWasabiPropAmm(data, amountIn, tokenIn, tokenOut, recipient); + + assertEq(amountUnused, 0); + assertGt(amountOut, 0); + // ERC20 output stays in adapter + assertEq(amountOut, tokenOut.balanceOf(address(adapter))); + // Input token fully consumed + assertEq(tokenIn.balanceOf(address(adapter)), 0); + } +}