-
Notifications
You must be signed in to change notification settings - Fork 5
feat: autoSwapper for WUSDN to SDEX #921
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
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
e79a186
feat: autoSwapper for WUSDN to SDEX
samooyo c3c68f6
test: add tests for the autoswapper
samooyo 082c509
feat: create interface
samooyo b4d6268
perf: remove internal function
samooyo 1619c27
perf: reuse variable
samooyo 7b7b128
perf: reimplemented quote function
samooyo c2d71d8
style: solidity guideline
samooyo f4d1262
style: solidity guidelines
samooyo a27dcd1
test: remove log
samooyo ed61b97
feat: view function for slippage
samooyo 2583d54
docs: natspec
samooyo 8d41e8f
docs: typo
samooyo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity >=0.8.0; | ||
|
||
/// @notice Interface for the AutoSwapperWusdnSdex contract that provides automated token swapping functionality. | ||
interface IAutoSwapperWusdnSdex { | ||
/** | ||
* @notice Emitted when the swap slippage percentage is updated. | ||
* @param newSwapSlippage The new swap slippage (in basis points). | ||
*/ | ||
event SwapSlippageUpdated(uint256 newSwapSlippage); | ||
|
||
/// @notice Emitted when a swap fails. | ||
event FailedSwap(); | ||
|
||
/// @notice Thrown when a swap fails. | ||
error AutoSwapperSwapFailed(); | ||
|
||
/// @notice Thrown when slippage configuration is invalid. | ||
error AutoSwapperInvalidSwapSlippage(); | ||
|
||
/// @notice Thrown when the caller is not authorized to perform the operation. | ||
error AutoSwapperInvalidCaller(); | ||
|
||
/// @notice Swap WUSDN to SDEX. | ||
function swapWusdnToSdex() external; | ||
|
||
/** | ||
* @notice Admin function to send the contract token balance to a specified address. | ||
* @param token The address of the token to send. | ||
* @param to The recipient address. | ||
* @param amount The amount of tokens to send. | ||
*/ | ||
function sweep(address token, address to, uint256 amount) external; | ||
|
||
/** | ||
* @notice Get the current swap slippage setting (in basis points) | ||
* @return Current slippage tolerance | ||
*/ | ||
function getSwapSlippage() external view returns (uint256); | ||
|
||
/** | ||
* @notice Updates the allowed slippage percentage for swaps. | ||
* @param swapSlippage The new slippage value (in basis points). | ||
*/ | ||
function updateSwapSlippage(uint256 swapSlippage) external; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity 0.8.26; | ||
|
||
import { Ownable, Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; | ||
import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; | ||
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; | ||
import { ISmardexPair } from "@smardex-dex-contracts/contracts/ethereum/core/v2/interfaces/ISmardexPair.sol"; | ||
import { ISmardexSwapCallback } from | ||
"@smardex-dex-contracts/contracts/ethereum/core/v2/interfaces/ISmardexSwapCallback.sol"; | ||
import { SmardexLibrary } from "@smardex-dex-contracts/contracts/ethereum/core/v2/libraries/SmardexLibrary.sol"; | ||
import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; | ||
|
||
import { IFeeCollectorCallback } from "./../interfaces/UsdnProtocol/IFeeCollectorCallback.sol"; | ||
import { IAutoSwapperWusdnSdex } from "./../interfaces/Utils/IAutoSwapperWusdnSdex.sol"; | ||
|
||
/** | ||
* @title SDEX buy-back and burn AutoSwapper | ||
* @notice Automates protocol fee conversion from WUSDN to SDEX via Smardex. | ||
*/ | ||
contract AutoSwapperWusdnSdex is | ||
Ownable2Step, | ||
IAutoSwapperWusdnSdex, | ||
IFeeCollectorCallback, | ||
ERC165, | ||
ISmardexSwapCallback | ||
{ | ||
using SafeERC20 for IERC20; | ||
|
||
/// @notice Decimal points for basis points (bps). | ||
uint16 internal constant BPS_DIVISOR = 10_000; | ||
|
||
/// @notice Wrapped USDN token address. | ||
IERC20 internal constant WUSDN = IERC20(0x99999999999999Cc837C997B882957daFdCb1Af9); | ||
|
||
/// @notice SmarDex pair address for WUSDN/SDEX swaps. | ||
ISmardexPair internal constant SMARDEX_WUSDN_SDEX_PAIR = ISmardexPair(0x11443f5B134c37903705e64129BEFc20e35a3725); | ||
|
||
/// @notice Fee rates for LP on the SmarDex pair. | ||
uint128 internal immutable FEE_LP; | ||
|
||
/// @notice Fee rates for the pool on the SmarDex pair. | ||
uint128 internal immutable FEE_POOL; | ||
|
||
/// @notice Allowed slippage for swaps (in basis points). | ||
uint256 internal _swapSlippage = 100; // 1% | ||
|
||
constructor() Ownable(msg.sender) { | ||
(FEE_LP, FEE_POOL) = SMARDEX_WUSDN_SDEX_PAIR.getPairFees(); | ||
} | ||
|
||
/// @inheritdoc IFeeCollectorCallback | ||
function feeCollectorCallback(uint256) external { | ||
try this.swapWusdnToSdex() { } | ||
catch { | ||
emit FailedSwap(); | ||
} | ||
} | ||
|
||
/// @inheritdoc IAutoSwapperWusdnSdex | ||
function swapWusdnToSdex() external { | ||
uint256 wusdnAmount = WUSDN.balanceOf(address(this)); | ||
|
||
uint256 quoteAmountSdexOut = _quoteAmountOut(wusdnAmount); | ||
uint256 minSdexAmount = FixedPointMathLib.mulDiv(quoteAmountSdexOut, BPS_DIVISOR - _swapSlippage, BPS_DIVISOR); | ||
(int256 amountSdexOut,) = SMARDEX_WUSDN_SDEX_PAIR.swap(address(0xdead), false, int256(wusdnAmount), ""); | ||
|
||
if (uint256(-amountSdexOut) < minSdexAmount) { | ||
revert AutoSwapperSwapFailed(); | ||
} | ||
} | ||
|
||
/// @inheritdoc ISmardexSwapCallback | ||
function smardexSwapCallback(int256, int256 amountWusdnIn, bytes calldata) external { | ||
if (msg.sender != address(SMARDEX_WUSDN_SDEX_PAIR)) { | ||
revert AutoSwapperInvalidCaller(); | ||
} | ||
WUSDN.safeTransfer(msg.sender, uint256(amountWusdnIn)); | ||
} | ||
|
||
/// @inheritdoc IAutoSwapperWusdnSdex | ||
function sweep(address token, address to, uint256 amount) external onlyOwner { | ||
IERC20(token).safeTransfer(to, amount); | ||
} | ||
|
||
/// @inheritdoc IAutoSwapperWusdnSdex | ||
function getSwapSlippage() external view returns (uint256) { | ||
return _swapSlippage; | ||
} | ||
|
||
/// @inheritdoc IAutoSwapperWusdnSdex | ||
function updateSwapSlippage(uint256 newSwapSlippage) external onlyOwner { | ||
if (newSwapSlippage == 0) { | ||
revert AutoSwapperInvalidSwapSlippage(); | ||
} | ||
_swapSlippage = newSwapSlippage; | ||
emit SwapSlippageUpdated(newSwapSlippage); | ||
} | ||
|
||
/// @inheritdoc ERC165 | ||
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) { | ||
if (interfaceId == type(IFeeCollectorCallback).interfaceId) { | ||
return true; | ||
} | ||
return super.supportsInterface(interfaceId); | ||
} | ||
|
||
/** | ||
* @notice Calculates the amount of SDEX that can be obtained from a given amount of WUSDN. | ||
* @param amountIn The amount of WUSDN to swap | ||
* @return amountOut_ The amount of SDEX that can be obtained | ||
*/ | ||
function _quoteAmountOut(uint256 amountIn) internal view returns (uint256 amountOut_) { | ||
(uint256 fictiveReserveSdex, uint256 fictiveReserveWusdn) = SMARDEX_WUSDN_SDEX_PAIR.getFictiveReserves(); | ||
(uint256 reservesSdex, uint256 reservesWusdn) = SMARDEX_WUSDN_SDEX_PAIR.getReserves(); | ||
(uint256 priceAvSdex, uint256 priceAvWusdn, uint256 priceAvTimestamp) = | ||
SMARDEX_WUSDN_SDEX_PAIR.getPriceAverage(); | ||
|
||
(priceAvWusdn, priceAvSdex) = SmardexLibrary.getUpdatedPriceAverage( | ||
fictiveReserveWusdn, fictiveReserveSdex, priceAvTimestamp, priceAvWusdn, priceAvSdex, block.timestamp | ||
); | ||
|
||
(fictiveReserveWusdn, fictiveReserveSdex) = | ||
SmardexLibrary.computeFictiveReserves(reservesWusdn, reservesSdex, fictiveReserveWusdn, fictiveReserveSdex); | ||
|
||
(amountOut_,,,,) = SmardexLibrary.applyKConstRuleOut( | ||
SmardexLibrary.GetAmountParameters({ | ||
amount: amountIn, | ||
reserveIn: reservesWusdn, | ||
reserveOut: reservesSdex, | ||
fictiveReserveIn: fictiveReserveWusdn, | ||
fictiveReserveOut: fictiveReserveSdex, | ||
priceAverageIn: priceAvWusdn, | ||
priceAverageOut: priceAvSdex, | ||
feesLP: FEE_LP, | ||
feesPool: FEE_POOL | ||
}) | ||
); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.8.26; | ||
|
||
import { Test } from "forge-std/Test.sol"; | ||
|
||
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; | ||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
|
||
import { SDEX as SDEX_ADDR } from "../../utils/Constants.sol"; | ||
|
||
import { IAutoSwapperWusdnSdex } from "../../../src/interfaces/Utils/IAutoSwapperWusdnSdex.sol"; | ||
import { AutoSwapperWusdnSdex } from "../../../src/utils/AutoSwapperWusdnSdex.sol"; | ||
|
||
/** | ||
* @custom:feature The `AutoSwapperWusdnSdex` contract | ||
* @custom:background Given a `AutoSwapperWusdnSdex` contract and a forked mainnet | ||
*/ | ||
contract TestForkAutoSwapperWusdnSdex is Test { | ||
AutoSwapperWusdnSdex public autoSwapper; | ||
address constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD; | ||
IERC20 constant WUSDN = IERC20(0x99999999999999Cc837C997B882957daFdCb1Af9); | ||
IERC20 constant SDEX = IERC20(SDEX_ADDR); | ||
uint256 constant AMOUNT_TO_SWAP = 2000 ether; | ||
|
||
function setUp() public { | ||
vm.createSelectFork("mainnet"); | ||
|
||
autoSwapper = new AutoSwapperWusdnSdex(); | ||
|
||
deal(address(WUSDN), address(this), AMOUNT_TO_SWAP); | ||
} | ||
|
||
/** | ||
* @custom:scenario Test the AutoSwapper's swap execution via the callback function | ||
* @custom:when `feeCollectorCallback` is called | ||
* @custom:then It should perform the swap | ||
* @custom:and the SDEX balance of the burn address should increase | ||
* @custom:and the WUSDN and SDEX balances of the contract should be zero | ||
*/ | ||
function test_ForkFeeCollectorCallback() public { | ||
uint256 initialBurnAddressBalance = SDEX.balanceOf(BURN_ADDRESS); | ||
|
||
WUSDN.transfer(address(autoSwapper), AMOUNT_TO_SWAP); | ||
autoSwapper.feeCollectorCallback(1); | ||
|
||
assertEq(WUSDN.balanceOf(address(autoSwapper)), 0, "WUSDN balance not zero"); | ||
assertEq(SDEX.balanceOf(address(autoSwapper)), 0, "SDEX balance not zero"); | ||
assertGt( | ||
SDEX.balanceOf(BURN_ADDRESS), initialBurnAddressBalance, "Swap did not increase burn address SDEX balance" | ||
); | ||
} | ||
|
||
/** | ||
* @custom:scenario Test the `Ownable` access control of the AutoSwapper | ||
* @custom:when the `sweep` and `updateSwapSlippage` functions are called | ||
* @custom:then It should revert with the `OwnableUnauthorizedAccount` error | ||
*/ | ||
function test_ForkAdmin() public { | ||
address user = vm.addr(1); | ||
vm.startPrank(user); | ||
|
||
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user)); | ||
autoSwapper.sweep(address(0), address(0), 1); | ||
|
||
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user)); | ||
autoSwapper.updateSwapSlippage(1); | ||
|
||
vm.stopPrank(); | ||
} | ||
|
||
/** | ||
* @custom:scenario Test the external function calls of the AutoSwapper | ||
* @custom:when the `smardexSwapCallback` function is called | ||
* @custom:then it should revert with the `AutoSwapperInvalidCaller` error | ||
*/ | ||
function test_ForkInvalidCaller() public { | ||
address user = vm.addr(1); | ||
vm.startPrank(user); | ||
|
||
vm.expectRevert(IAutoSwapperWusdnSdex.AutoSwapperInvalidCaller.selector); | ||
autoSwapper.smardexSwapCallback(1, 1, ""); | ||
|
||
vm.stopPrank(); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.