-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add Wasabi Prop AMM adapter and related interfaces #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
1c1336a
7187a8a
0b9f3b6
a2ef1a5
96ed1a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| // SPDX-License-Identifier: UNLICENSED | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| interface IPropPool { | ||
| function swapExactInput( | ||
| address tokenIn, | ||
| uint256 amountIn, | ||
| uint256 minAmountOut | ||
| ) external returns (uint256 amountOut); | ||
|
|
||
| function getBaseToken() external view returns (address); | ||
| function getQuoteToken() external view returns (address); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| // SPDX-License-Identifier: UNLICENSED | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| interface IWETH { | ||
| function deposit() external payable; | ||
| function withdraw(uint256 amount) external; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,59 @@ | ||||||||||||||||||||||||||
| // SPDX-License-Identifier: UNLICENSED | ||||||||||||||||||||||||||
| pragma solidity ^0.8.0; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import './IPropPool.sol'; | ||||||||||||||||||||||||||
| import './IWETH.sol'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import '../../libraries/CalldataDecoder.sol'; | ||||||||||||||||||||||||||
| import '../../libraries/TokenHelper.sol'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| contract WasabiPropAmmAdapter { | ||||||||||||||||||||||||||
| using TokenHelper for address; | ||||||||||||||||||||||||||
| using CalldataDecoder for bytes; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| error InvalidMsgValue(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| address public immutable WETH; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| constructor(address _weth) { | ||||||||||||||||||||||||||
| WETH = _weth; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| receive() external payable {} | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function executeWasabiPropAmm( | ||||||||||||||||||||||||||
| bytes calldata data, | ||||||||||||||||||||||||||
| uint256 amountIn, | ||||||||||||||||||||||||||
| address tokenIn, | ||||||||||||||||||||||||||
| address tokenOut, | ||||||||||||||||||||||||||
| address recipient | ||||||||||||||||||||||||||
| ) external payable returns (uint256 amountUnused, uint256 amountOut) { | ||||||||||||||||||||||||||
| address pool = data.decodeAddress(0); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Resolve actual ERC20 token address for input (native ETH -> WETH) | ||||||||||||||||||||||||||
| address actualTokenIn = tokenIn.isNative() ? WETH : tokenIn; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Wrap native ETH to WETH if needed | ||||||||||||||||||||||||||
| if (tokenIn.isNative()) { | ||||||||||||||||||||||||||
| if (msg.value != amountIn) revert InvalidMsgValue(); | ||||||||||||||||||||||||||
| IWETH(WETH).deposit{value: amountIn}(); | ||||||||||||||||||||||||||
| } else if (msg.value != 0) { | ||||||||||||||||||||||||||
| revert InvalidMsgValue(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Approve pool to pull tokenIn from this adapter | ||||||||||||||||||||||||||
| actualTokenIn.forceApprove(pool, amountIn); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Execute swap -- pool pulls tokenIn, sends tokenOut back to this contract | ||||||||||||||||||||||||||
| amountOut = IPropPool(pool).swapExactInput(actualTokenIn, amountIn, 1); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Only explicitly transfer if native ETH is requested as output; | ||||||||||||||||||||||||||
| // ERC20 outputs are left in the adapter for the framework to collect | ||||||||||||||||||||||||||
| if (tokenOut.isNative()) { | ||||||||||||||||||||||||||
| IWETH(WETH).withdraw(amountOut); | ||||||||||||||||||||||||||
| TokenHelper.safeTransferNative(recipient, amountOut); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| // Only explicitly transfer if native ETH is requested as output; | |
| // ERC20 outputs are left in the adapter for the framework to collect | |
| if (tokenOut.isNative()) { | |
| IWETH(WETH).withdraw(amountOut); | |
| TokenHelper.safeTransferNative(recipient, amountOut); | |
| // Transfer output to the recipient. If native ETH is requested, unwrap WETH first; | |
| // otherwise, transfer the ERC20 tokenOut directly. | |
| if (tokenOut.isNative()) { | |
| IWETH(WETH).withdraw(amountOut); | |
| TokenHelper.safeTransferNative(recipient, amountOut); | |
| } else { | |
| tokenOut.safeTransfer(recipient, amountOut); |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
executeWasabiPropAmm doesn’t validate that tokenIn/tokenOut match the pool’s pair. If the caller supplies mismatched tokens, the swap may still succeed (outputting the pool’s “other” token) while the framework believes it received tokenOut; additionally, tokenOut.isNative() will attempt to unwrap WETH even if the pool didn’t output WETH. Consider checking IPropPool(pool).getBaseToken()/getQuoteToken() and reverting when (actualTokenIn, actualTokenOut) isn’t exactly the pool’s pair (with native mapped to WETH).
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| // 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'; | ||
|
|
||
| function setUp() public { | ||
| vm.createSelectFork(RPC_URL); | ||
|
|
||
|
WEBthe3rd marked this conversation as resolved.
|
||
| adapter = new WasabiPropAmmAdapter(BASE_WETH); | ||
| 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); | ||
| } | ||
|
|
||
| function test_executeWasabiPropAmm_nativeETHIn(uint256 amountIn) public { | ||
| vm.assume(pool != address(0)); | ||
| _warpToFreshOracle(); | ||
|
|
||
| amountIn = bound(amountIn, 1e15, 1e18); | ||
| deal(address(this), amountIn); | ||
|
|
||
| bytes memory data = abi.encode(pool); | ||
| (uint256 amountUnused, uint256 amountOut) = adapter.executeWasabiPropAmm{value: amountIn}( | ||
| data, | ||
| amountIn, | ||
| TokenHelper.NATIVE_ADDRESS, | ||
| BASE_USDC, | ||
| recipient | ||
| ); | ||
|
|
||
| assertEq(amountUnused, 0); | ||
| assertGt(amountOut, 0); | ||
| // USDC output stays in adapter | ||
| assertEq(amountOut, BASE_USDC.balanceOf(address(adapter))); | ||
| // No remaining ETH or WETH in adapter | ||
| assertEq(address(adapter).balance, 0); | ||
| assertEq(BASE_WETH.balanceOf(address(adapter)), 0); | ||
| } | ||
|
|
||
| function test_executeWasabiPropAmm_revertsOnInvalidMsgValue() public { | ||
| vm.assume(pool != address(0)); | ||
| _warpToFreshOracle(); | ||
|
|
||
| uint256 amountIn = 1e17; | ||
| deal(address(this), amountIn); | ||
|
|
||
| bytes memory data = abi.encode(pool); | ||
|
|
||
| vm.expectRevert(WasabiPropAmmAdapter.InvalidMsgValue.selector); | ||
| adapter.executeWasabiPropAmm{value: amountIn - 1}( | ||
| data, | ||
| amountIn, | ||
| TokenHelper.NATIVE_ADDRESS, | ||
| BASE_USDC, | ||
| recipient | ||
| ); | ||
|
|
||
| deal(BASE_USDC, address(adapter), 1e6); | ||
| vm.expectRevert(WasabiPropAmmAdapter.InvalidMsgValue.selector); | ||
| adapter.executeWasabiPropAmm{value: 1}( | ||
| data, | ||
| 1e6, | ||
| BASE_USDC, | ||
| BASE_WETH, | ||
| recipient | ||
| ); | ||
| } | ||
|
|
||
| function test_executeWasabiPropAmm_nativeETHOut(uint256 amountIn) public { | ||
| vm.assume(pool != address(0)); | ||
| _warpToFreshOracle(); | ||
|
|
||
| amountIn = bound(amountIn, 1e6, 3000e6); | ||
| deal(BASE_USDC, address(adapter), amountIn); | ||
|
|
||
| uint256 recipientBalanceBefore = recipient.balance; | ||
|
|
||
| bytes memory data = abi.encode(pool); | ||
| (uint256 amountUnused, uint256 amountOut) = adapter.executeWasabiPropAmm( | ||
| data, amountIn, BASE_USDC, TokenHelper.NATIVE_ADDRESS, recipient | ||
| ); | ||
|
|
||
| assertEq(amountUnused, 0); | ||
| assertGt(amountOut, 0); | ||
| // Native ETH sent to recipient | ||
| assertEq(amountOut, recipient.balance - recipientBalanceBefore); | ||
| // No remaining tokens in adapter | ||
| assertEq(BASE_USDC.balanceOf(address(adapter)), 0); | ||
| assertEq(BASE_WETH.balanceOf(address(adapter)), 0); | ||
| assertEq(address(adapter).balance, 0); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
swapExactInputis called withminAmountOuthardcoded to1, effectively disabling slippage protection at the adapter layer even though the PropPool API supports it. If the surrounding framework can provide a minimum acceptable output, consider encoding/decoding it fromdata(or adding a parameter) and passing it through.