Skip to content

Commit 5087573

Browse files
authored
feat: autoSwapper for WUSDN to SDEX (#921)
* feat: autoSwapper for WUSDN to SDEX * test: add tests for the autoswapper * feat: create interface * perf: remove internal function * perf: reuse variable * perf: reimplemented quote function * style: solidity guideline * style: solidity guidelines * test: remove log * feat: view function for slippage * docs: natspec * docs: typo
1 parent ec65255 commit 5087573

File tree

5 files changed

+273
-9
lines changed

5 files changed

+273
-9
lines changed

src/interfaces/Utils/IAutoSwapperWstethSdex.sol

+1-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ interface IAutoSwapperWstethSdex {
2121
/// @notice Thrown when the caller is not authorized to perform the operation.
2222
error AutoSwapperInvalidCaller();
2323

24-
/**
25-
* @notice Swap wstETH to SDEX.
26-
* @dev This function can only be called by the contract itself.
27-
*/
24+
/// @notice Swap wstETH to SDEX.
2825
function swapWstethToSdex() external;
2926

3027
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity >=0.8.0;
3+
4+
/// @notice Interface for the AutoSwapperWusdnSdex contract that provides automated token swapping functionality.
5+
interface IAutoSwapperWusdnSdex {
6+
/**
7+
* @notice Emitted when the swap slippage percentage is updated.
8+
* @param newSwapSlippage The new swap slippage (in basis points).
9+
*/
10+
event SwapSlippageUpdated(uint256 newSwapSlippage);
11+
12+
/// @notice Emitted when a swap fails.
13+
event FailedSwap();
14+
15+
/// @notice Thrown when a swap fails.
16+
error AutoSwapperSwapFailed();
17+
18+
/// @notice Thrown when slippage configuration is invalid.
19+
error AutoSwapperInvalidSwapSlippage();
20+
21+
/// @notice Thrown when the caller is not authorized to perform the operation.
22+
error AutoSwapperInvalidCaller();
23+
24+
/// @notice Swap WUSDN to SDEX.
25+
function swapWusdnToSdex() external;
26+
27+
/**
28+
* @notice Admin function to send the contract token balance to a specified address.
29+
* @param token The address of the token to send.
30+
* @param to The recipient address.
31+
* @param amount The amount of tokens to send.
32+
*/
33+
function sweep(address token, address to, uint256 amount) external;
34+
35+
/**
36+
* @notice Get the current swap slippage setting (in basis points)
37+
* @return Current slippage tolerance
38+
*/
39+
function getSwapSlippage() external view returns (uint256);
40+
41+
/**
42+
* @notice Updates the allowed slippage percentage for swaps.
43+
* @param swapSlippage The new slippage value (in basis points).
44+
*/
45+
function updateSwapSlippage(uint256 swapSlippage) external;
46+
}

src/utils/AutoSwapperWstethSdex.sol

+1-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { IFeeCollectorCallback } from "./../interfaces/UsdnProtocol/IFeeCollecto
1717
import { IAutoSwapperWstethSdex } from "./../interfaces/Utils/IAutoSwapperWstethSdex.sol";
1818

1919
/**
20-
* @title SDEX buy-back and burn Autoswapper
20+
* @title SDEX buy-back and burn AutoSwapper
2121
* @notice Automates protocol fee conversion from wstETH to SDEX via Uniswap V3 and Smardex.
2222
*/
2323
contract AutoSwapperWstethSdex is
@@ -31,10 +31,6 @@ contract AutoSwapperWstethSdex is
3131
using SafeERC20 for IERC20;
3232
using SafeERC20 for IWstETH;
3333

34-
/* -------------------------------------------------------------------------- */
35-
/* Constants */
36-
/* -------------------------------------------------------------------------- */
37-
3834
/// @notice Decimal points for basis points (bps).
3935
uint16 internal constant BPS_DIVISOR = 10_000;
4036

src/utils/AutoSwapperWusdnSdex.sol

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity 0.8.26;
3+
4+
import { Ownable, Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol";
5+
import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
7+
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
8+
import { ISmardexPair } from "@smardex-dex-contracts/contracts/ethereum/core/v2/interfaces/ISmardexPair.sol";
9+
import { ISmardexSwapCallback } from
10+
"@smardex-dex-contracts/contracts/ethereum/core/v2/interfaces/ISmardexSwapCallback.sol";
11+
import { SmardexLibrary } from "@smardex-dex-contracts/contracts/ethereum/core/v2/libraries/SmardexLibrary.sol";
12+
import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol";
13+
14+
import { IFeeCollectorCallback } from "./../interfaces/UsdnProtocol/IFeeCollectorCallback.sol";
15+
import { IAutoSwapperWusdnSdex } from "./../interfaces/Utils/IAutoSwapperWusdnSdex.sol";
16+
17+
/**
18+
* @title SDEX buy-back and burn AutoSwapper
19+
* @notice Automates protocol fee conversion from WUSDN to SDEX via Smardex.
20+
*/
21+
contract AutoSwapperWusdnSdex is
22+
Ownable2Step,
23+
IAutoSwapperWusdnSdex,
24+
IFeeCollectorCallback,
25+
ERC165,
26+
ISmardexSwapCallback
27+
{
28+
using SafeERC20 for IERC20;
29+
30+
/// @notice Decimal points for basis points (bps).
31+
uint16 internal constant BPS_DIVISOR = 10_000;
32+
33+
/// @notice Wrapped USDN token address.
34+
IERC20 internal constant WUSDN = IERC20(0x99999999999999Cc837C997B882957daFdCb1Af9);
35+
36+
/// @notice SmarDex pair address for WUSDN/SDEX swaps.
37+
ISmardexPair internal constant SMARDEX_WUSDN_SDEX_PAIR = ISmardexPair(0x11443f5B134c37903705e64129BEFc20e35a3725);
38+
39+
/// @notice Fee rates for LP on the SmarDex pair.
40+
uint128 internal immutable FEE_LP;
41+
42+
/// @notice Fee rates for the pool on the SmarDex pair.
43+
uint128 internal immutable FEE_POOL;
44+
45+
/// @notice Allowed slippage for swaps (in basis points).
46+
uint256 internal _swapSlippage = 100; // 1%
47+
48+
constructor() Ownable(msg.sender) {
49+
(FEE_LP, FEE_POOL) = SMARDEX_WUSDN_SDEX_PAIR.getPairFees();
50+
}
51+
52+
/// @inheritdoc IFeeCollectorCallback
53+
function feeCollectorCallback(uint256) external {
54+
try this.swapWusdnToSdex() { }
55+
catch {
56+
emit FailedSwap();
57+
}
58+
}
59+
60+
/// @inheritdoc IAutoSwapperWusdnSdex
61+
function swapWusdnToSdex() external {
62+
uint256 wusdnAmount = WUSDN.balanceOf(address(this));
63+
64+
uint256 quoteAmountSdexOut = _quoteAmountOut(wusdnAmount);
65+
uint256 minSdexAmount = FixedPointMathLib.mulDiv(quoteAmountSdexOut, BPS_DIVISOR - _swapSlippage, BPS_DIVISOR);
66+
(int256 amountSdexOut,) = SMARDEX_WUSDN_SDEX_PAIR.swap(address(0xdead), false, int256(wusdnAmount), "");
67+
68+
if (uint256(-amountSdexOut) < minSdexAmount) {
69+
revert AutoSwapperSwapFailed();
70+
}
71+
}
72+
73+
/// @inheritdoc ISmardexSwapCallback
74+
function smardexSwapCallback(int256, int256 amountWusdnIn, bytes calldata) external {
75+
if (msg.sender != address(SMARDEX_WUSDN_SDEX_PAIR)) {
76+
revert AutoSwapperInvalidCaller();
77+
}
78+
WUSDN.safeTransfer(msg.sender, uint256(amountWusdnIn));
79+
}
80+
81+
/// @inheritdoc IAutoSwapperWusdnSdex
82+
function sweep(address token, address to, uint256 amount) external onlyOwner {
83+
IERC20(token).safeTransfer(to, amount);
84+
}
85+
86+
/// @inheritdoc IAutoSwapperWusdnSdex
87+
function getSwapSlippage() external view returns (uint256) {
88+
return _swapSlippage;
89+
}
90+
91+
/// @inheritdoc IAutoSwapperWusdnSdex
92+
function updateSwapSlippage(uint256 newSwapSlippage) external onlyOwner {
93+
if (newSwapSlippage == 0) {
94+
revert AutoSwapperInvalidSwapSlippage();
95+
}
96+
_swapSlippage = newSwapSlippage;
97+
emit SwapSlippageUpdated(newSwapSlippage);
98+
}
99+
100+
/// @inheritdoc ERC165
101+
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
102+
if (interfaceId == type(IFeeCollectorCallback).interfaceId) {
103+
return true;
104+
}
105+
return super.supportsInterface(interfaceId);
106+
}
107+
108+
/**
109+
* @notice Calculates the amount of SDEX that can be obtained from a given amount of WUSDN.
110+
* @param amountIn The amount of WUSDN to swap
111+
* @return amountOut_ The amount of SDEX that can be obtained
112+
*/
113+
function _quoteAmountOut(uint256 amountIn) internal view returns (uint256 amountOut_) {
114+
(uint256 fictiveReserveSdex, uint256 fictiveReserveWusdn) = SMARDEX_WUSDN_SDEX_PAIR.getFictiveReserves();
115+
(uint256 reservesSdex, uint256 reservesWusdn) = SMARDEX_WUSDN_SDEX_PAIR.getReserves();
116+
(uint256 priceAvSdex, uint256 priceAvWusdn, uint256 priceAvTimestamp) =
117+
SMARDEX_WUSDN_SDEX_PAIR.getPriceAverage();
118+
119+
(priceAvWusdn, priceAvSdex) = SmardexLibrary.getUpdatedPriceAverage(
120+
fictiveReserveWusdn, fictiveReserveSdex, priceAvTimestamp, priceAvWusdn, priceAvSdex, block.timestamp
121+
);
122+
123+
(fictiveReserveWusdn, fictiveReserveSdex) =
124+
SmardexLibrary.computeFictiveReserves(reservesWusdn, reservesSdex, fictiveReserveWusdn, fictiveReserveSdex);
125+
126+
(amountOut_,,,,) = SmardexLibrary.applyKConstRuleOut(
127+
SmardexLibrary.GetAmountParameters({
128+
amount: amountIn,
129+
reserveIn: reservesWusdn,
130+
reserveOut: reservesSdex,
131+
fictiveReserveIn: fictiveReserveWusdn,
132+
fictiveReserveOut: fictiveReserveSdex,
133+
priceAverageIn: priceAvWusdn,
134+
priceAverageOut: priceAvSdex,
135+
feesLP: FEE_LP,
136+
feesPool: FEE_POOL
137+
})
138+
);
139+
}
140+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity 0.8.26;
3+
4+
import { Test } from "forge-std/Test.sol";
5+
6+
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
7+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
8+
9+
import { SDEX as SDEX_ADDR } from "../../utils/Constants.sol";
10+
11+
import { IAutoSwapperWusdnSdex } from "../../../src/interfaces/Utils/IAutoSwapperWusdnSdex.sol";
12+
import { AutoSwapperWusdnSdex } from "../../../src/utils/AutoSwapperWusdnSdex.sol";
13+
14+
/**
15+
* @custom:feature The `AutoSwapperWusdnSdex` contract
16+
* @custom:background Given a `AutoSwapperWusdnSdex` contract and a forked mainnet
17+
*/
18+
contract TestForkAutoSwapperWusdnSdex is Test {
19+
AutoSwapperWusdnSdex public autoSwapper;
20+
address constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
21+
IERC20 constant WUSDN = IERC20(0x99999999999999Cc837C997B882957daFdCb1Af9);
22+
IERC20 constant SDEX = IERC20(SDEX_ADDR);
23+
uint256 constant AMOUNT_TO_SWAP = 2000 ether;
24+
25+
function setUp() public {
26+
vm.createSelectFork("mainnet");
27+
28+
autoSwapper = new AutoSwapperWusdnSdex();
29+
30+
deal(address(WUSDN), address(this), AMOUNT_TO_SWAP);
31+
}
32+
33+
/**
34+
* @custom:scenario Test the AutoSwapper's swap execution via the callback function
35+
* @custom:when `feeCollectorCallback` is called
36+
* @custom:then It should perform the swap
37+
* @custom:and the SDEX balance of the burn address should increase
38+
* @custom:and the WUSDN and SDEX balances of the contract should be zero
39+
*/
40+
function test_ForkFeeCollectorCallback() public {
41+
uint256 initialBurnAddressBalance = SDEX.balanceOf(BURN_ADDRESS);
42+
43+
WUSDN.transfer(address(autoSwapper), AMOUNT_TO_SWAP);
44+
autoSwapper.feeCollectorCallback(1);
45+
46+
assertEq(WUSDN.balanceOf(address(autoSwapper)), 0, "WUSDN balance not zero");
47+
assertEq(SDEX.balanceOf(address(autoSwapper)), 0, "SDEX balance not zero");
48+
assertGt(
49+
SDEX.balanceOf(BURN_ADDRESS), initialBurnAddressBalance, "Swap did not increase burn address SDEX balance"
50+
);
51+
}
52+
53+
/**
54+
* @custom:scenario Test the `Ownable` access control of the AutoSwapper
55+
* @custom:when the `sweep` and `updateSwapSlippage` functions are called
56+
* @custom:then It should revert with the `OwnableUnauthorizedAccount` error
57+
*/
58+
function test_ForkAdmin() public {
59+
address user = vm.addr(1);
60+
vm.startPrank(user);
61+
62+
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user));
63+
autoSwapper.sweep(address(0), address(0), 1);
64+
65+
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user));
66+
autoSwapper.updateSwapSlippage(1);
67+
68+
vm.stopPrank();
69+
}
70+
71+
/**
72+
* @custom:scenario Test the external function calls of the AutoSwapper
73+
* @custom:when the `smardexSwapCallback` function is called
74+
* @custom:then it should revert with the `AutoSwapperInvalidCaller` error
75+
*/
76+
function test_ForkInvalidCaller() public {
77+
address user = vm.addr(1);
78+
vm.startPrank(user);
79+
80+
vm.expectRevert(IAutoSwapperWusdnSdex.AutoSwapperInvalidCaller.selector);
81+
autoSwapper.smardexSwapCallback(1, 1, "");
82+
83+
vm.stopPrank();
84+
}
85+
}

0 commit comments

Comments
 (0)