diff --git a/contracts/buyback/Buyback.sol b/contracts/buyback/Buyback.sol index 57b46f23..150363c2 100644 --- a/contracts/buyback/Buyback.sol +++ b/contracts/buyback/Buyback.sol @@ -35,8 +35,8 @@ contract Buyback is address public constant SWAP_NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /* ============ State Variables ============ */ - // 1Inch router whitelist - mapping(address => bool) public oneInchRouterWhitelist; + // swap router whitelist + mapping(address => bool) public routerWhitelist; // swap input token whitelist mapping(address => bool) public tokenInWhitelist; // swap output token @@ -48,8 +48,15 @@ contract Buyback is /* ============ Events ============ */ event BoughtBack(address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut); + event BoughtBack( + address indexed pair, + address indexed tokenIn, + address indexed tokenOut, + uint256 amountIn, + uint256 amountOut + ); event ReceiverChanged(address indexed receiver); - event OneInchRouterChanged(address indexed oneInchRouter, bool added); + event RouterChanged(address indexed router, bool added); event TokenInChanged(address indexed token, bool added); event EmergencyWithdraw(address token, uint256 amount); @@ -99,7 +106,7 @@ contract Buyback is _grantRole(PAUSER, _pauser); _grantRole(BOT, _bot); - oneInchRouterWhitelist[_1InchRouter] = true; + routerWhitelist[_1InchRouter] = true; tokenOut = _tokenOut; receiver = _receiver; } @@ -115,7 +122,7 @@ contract Buyback is address _1inchRouter, bytes calldata _data ) external override onlyRole(BOT) nonReentrant whenNotPaused { - require(oneInchRouterWhitelist[_1inchRouter], "Invalid 1Inch router"); + require(routerWhitelist[_1inchRouter], "router not whitelisted"); require(bytes4(_data[0:4]) == SWAP_FUNCTION_SELECTOR, "Invalid 1Inch function selector"); (, SwapDescription memory swapDesc, ) = abi.decode(_data[4:], (address, SwapDescription, bytes)); @@ -147,6 +154,50 @@ contract Buyback is emit BoughtBack(address(swapDesc.srcToken), address(swapDesc.dstToken), swapDesc.amount, amountOut); } + /// @dev buy back tokens using router + /// @param _router The address of the router. + /// @param _tokenIn The address of the input token. + /// @param _tokenOut The address of the output token. + /// @param _amountIn The amount to sell. + /// @param _amountOutMin The minimum amount to receive. + /// @param _swapData The swap data. + function buyback( + address _router, + address _tokenIn, + address _tokenOut, + uint256 _amountIn, + uint256 _amountOutMin, + bytes calldata _swapData + ) external onlyRole(BOT) nonReentrant whenNotPaused { + require(tokenInWhitelist[_tokenIn], "token not whitelisted"); + require(tokenOut == _tokenOut, "token not whitelisted"); + require(routerWhitelist[_router], "router not whitelisted"); + + uint256 beforeTokenIn = _getTokenBalance(_tokenIn, address(this)); + uint256 beforeTokenOut = _getTokenBalance(_tokenOut, address(this)); + + bool isNativeTokenIn = (_tokenIn == SWAP_NATIVE_TOKEN_ADDRESS); + if (!isNativeTokenIn) { + IERC20(_tokenIn).safeApprove(_router, _amountIn); + } + (bool success, ) = _router.call{ value: isNativeTokenIn ? _amountIn : 0 }(_swapData); + require(success, "swap failed"); + + if (!isNativeTokenIn) { + IERC20(_tokenIn).safeApprove(_router, 0); + } + + uint256 actualAmountIn = beforeTokenIn - _getTokenBalance(_tokenIn, address(this)); + uint256 actualAmountOut = _getTokenBalance(_tokenOut, address(this)) - beforeTokenOut; + + require(actualAmountIn <= _amountIn, "exceed amount in"); + require(actualAmountOut >= _amountOutMin, "not enough profit"); + + IERC20(_tokenOut).safeTransfer(receiver, actualAmountOut); + + emit BoughtBack(_router, _tokenIn, _tokenOut, actualAmountIn, actualAmountOut); + } + /** * @dev change receiver * @param _receiver - Address of the receiver @@ -159,27 +210,14 @@ contract Buyback is emit ReceiverChanged(_receiver); } - /** - * @dev add 1Inch router to whitelist - * @param _1InchRouter - Address of the 1Inch router - */ - function add1InchRouterWhitelist(address _1InchRouter) external onlyRole(MANAGER) { - require(_1InchRouter != address(0), "Invalid 1Inch router"); - require(!oneInchRouterWhitelist[_1InchRouter], "Already whitelisted"); - - oneInchRouterWhitelist[_1InchRouter] = true; - emit OneInchRouterChanged(_1InchRouter, true); - } - - /** - * @dev remove 1Inch router from whitelist - * @param _1InchRouter - Address of the 1Inch router - */ - function remove1InchRouterWhitelist(address _1InchRouter) external onlyRole(MANAGER) { - require(oneInchRouterWhitelist[_1InchRouter], "1Inch router is not in whitelist"); - - delete oneInchRouterWhitelist[_1InchRouter]; - emit OneInchRouterChanged(_1InchRouter, false); + /// @dev sets the router whitelist. + /// @param _router The address of the router. + /// @param status The status of the router. + function setRouterWhitelist(address _router, bool status) external onlyRole(MANAGER) { + require(_router != address(0), "Invalid router address"); + require(routerWhitelist[_router] != status, "whitelist same status"); + routerWhitelist[_router] = status; + emit RouterChanged(_router, status); } /** @@ -239,4 +277,12 @@ contract Buyback is // /* ============ Internal Functions ============ */ function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} + + function _getTokenBalance(address _token, address account) internal view returns (uint256) { + if (_token == SWAP_NATIVE_TOKEN_ADDRESS) { + return account.balance; + } else { + return IERC20(_token).balanceOf(account); + } + } } diff --git a/contracts/buyback/interfaces/IBuyback.sol b/contracts/buyback/interfaces/IBuyback.sol index a19408c5..d0862c1f 100644 --- a/contracts/buyback/interfaces/IBuyback.sol +++ b/contracts/buyback/interfaces/IBuyback.sol @@ -16,4 +16,13 @@ interface IBuyback { } function buyback(address _1inchRouter, bytes calldata _data) external; + + function buyback( + address router, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOutMin, + bytes calldata swapData + ) external; } diff --git a/contracts/dao/ListaAutoBuyback.sol b/contracts/dao/ListaAutoBuyback.sol index 207e62f0..99cc1e36 100644 --- a/contracts/dao/ListaAutoBuyback.sol +++ b/contracts/dao/ListaAutoBuyback.sol @@ -15,18 +15,37 @@ import "../buyback/library/RevertReasonParser.sol"; * @dev result of swap will be sent to receiver address to distribute to users */ contract ListaAutoBuyback is Initializable, AccessControlUpgradeable { - using SafeERC20 for IERC20; + struct SwapDescription { + address srcToken; + address dstToken; + address payable srcReceiver; + address payable dstReceiver; + uint256 amount; + uint256 minReturnAmount; + uint256 flags; + } + event BoughtBack(address indexed tokenIn, uint256 amountIn, uint256 amountOut); + event BoughtBack( + address indexed pair, + address indexed tokenIn, + address indexed tokenOut, + uint256 amountIn, + uint256 amountOut + ); event ReceiverChanged(address indexed receiver); - event RouterChanged(address indexed router, bool added); + event TokenWhitelistChanged(address indexed token, bool added); + event AdminTransfer(address token, uint256 amount); + bytes32 public constant BOT = keccak256("BOT"); bytes4 public constant SWAP_FUNCTION_SELECTOR = bytes4(keccak256("swap(address,(address,address,address,address,uint256,uint256,uint256),bytes)")); + address public constant SWAP_NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; // The offset of the dstReceiver in the call data // 4 bytes for the function selector + 32 bytes for executor + 32 bytes for srcToken + @@ -38,15 +57,19 @@ contract ListaAutoBuyback is Initializable, AccessControlUpgradeable { address public defaultReceiver; - mapping(address => bool) public oneInchRouterWhitelist; + mapping(address => bool) public routerWhitelist; mapping(uint256 => uint256) public dailyBought; + mapping(address => bool) public tokenWhitelist; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } + receive() external payable {} + function initialize( address _admin, address _bot, @@ -62,7 +85,7 @@ contract ListaAutoBuyback is Initializable, AccessControlUpgradeable { _setupRole(BOT, _bot); defaultReceiver = _initReceiver; - oneInchRouterWhitelist[_initRouter] = true; + routerWhitelist[_initRouter] = true; } /** @@ -77,8 +100,16 @@ contract ListaAutoBuyback is Initializable, AccessControlUpgradeable { onlyRole(BOT) { require(_amountIn > 0, "amountIn is zero"); - require(oneInchRouterWhitelist[_1inchRouter], "router not whitelisted"); + require(routerWhitelist[_1inchRouter], "router not whitelisted"); require(_getFunctionSelector(_data) == SWAP_FUNCTION_SELECTOR, "invalid function selector of _data"); + require(tokenWhitelist[_tokenIn], "token in not whitelisted"); + (, SwapDescription memory swapDesc, ) = abi.decode(_data[4:], (address, SwapDescription, bytes)); + + require(_tokenIn == swapDesc.srcToken, "Invalid swap input token"); + require(tokenWhitelist[swapDesc.dstToken], "token out not whitelisted"); + require(address(swapDesc.dstReceiver) == defaultReceiver, "Invalid receiver"); + require(swapDesc.amount > 0, "Invalid swap input amount"); + require(_extractDstReceiver(_data) == defaultReceiver, "invalid dst receiver of _data"); require(IERC20(_tokenIn).balanceOf(address(this)) >= _amountIn, "insufficient balance"); // Approves the 1inch router contract to spend the specified amount of _tokenIn @@ -92,14 +123,64 @@ contract ListaAutoBuyback is Initializable, AccessControlUpgradeable { } (uint256 amountOut,) = abi.decode(result, (uint256, uint256)); + require(amountOut >= swapDesc.minReturnAmount, "insufficient output amount"); uint256 today = block.timestamp / DAY * DAY; dailyBought[today] = dailyBought[today] + amountOut; emit BoughtBack(_tokenIn, _amountIn, amountOut); } + /// @dev buy back tokens using router + /// @param _router The address of the router. + /// @param _tokenIn The address of the input token. + /// @param _tokenOut The address of the output token. + /// @param _amountIn The amount to sell. + /// @param _amountOutMin The minimum amount to receive. + /// @param _swapData The swap data. + function buyback( + address _router, + address _tokenIn, + address _tokenOut, + uint256 _amountIn, + uint256 _amountOutMin, + bytes calldata _swapData + ) external onlyRole(BOT) { + require(tokenWhitelist[_tokenIn], "token not whitelisted"); + require(tokenWhitelist[_tokenOut], "token not whitelisted"); + require(routerWhitelist[_router], "router not whitelisted"); + + uint256 beforeTokenIn = _getTokenBalance(_tokenIn, address(this)); + uint256 beforeTokenOut = _getTokenBalance(_tokenOut, address(this)); + + bool isNativeTokenIn = (_tokenIn == SWAP_NATIVE_TOKEN_ADDRESS); + if (!isNativeTokenIn) { + IERC20(_tokenIn).safeApprove(_router, _amountIn); + } + (bool success, ) = _router.call{value: isNativeTokenIn ? _amountIn : 0}(_swapData); + require(success, "swap failed"); + if (!isNativeTokenIn) { + IERC20(_tokenIn).safeApprove(_router, 0); + } + + uint256 actualAmountIn = beforeTokenIn - _getTokenBalance(_tokenIn, address(this)); + uint256 actualAmountOut = _getTokenBalance(_tokenOut, address(this)) - beforeTokenOut; + + require(actualAmountIn <= _amountIn, "exceed amount in"); + require(actualAmountOut >= _amountOutMin, "not enough profit"); + + IERC20(_tokenOut).safeTransfer(defaultReceiver, actualAmountOut); + + emit BoughtBack(_router, _tokenIn, _tokenOut, actualAmountIn, actualAmountOut); + } + function adminTransfer(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { - IERC20(_token).safeTransfer(msg.sender, _amount); + if (_token == SWAP_NATIVE_TOKEN_ADDRESS) { + (bool success, ) = payable(msg.sender).call{ value: _amount }(""); + require(success, "Withdraw failed"); + } else { + IERC20(_token).safeTransfer(msg.sender, _amount); + } + emit AdminTransfer(_token, _amount); } function changeDefaultReceiver(address _receiver) external onlyRole(DEFAULT_ADMIN_ROLE) { @@ -110,20 +191,27 @@ contract ListaAutoBuyback is Initializable, AccessControlUpgradeable { emit ReceiverChanged(defaultReceiver); } - function add1InchRouterWhitelist(address _router) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(!oneInchRouterWhitelist[_router], "router already whitelisted"); - - oneInchRouterWhitelist[_router] = true; - emit RouterChanged(_router, true); + /// @dev sets the router whitelist. + /// @param _router The address of the router. + /// @param status The status of the router. + function setRouterWhitelist(address _router, bool status) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_router != address(0), "Invalid router address"); + require(routerWhitelist[_router] != status, "whitelist same status"); + routerWhitelist[_router] = status; + emit RouterChanged(_router, status); } - function remove1InchRouterWhitelist(address _router) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(oneInchRouterWhitelist[_router], "router not whitelisted"); - - delete oneInchRouterWhitelist[_router]; - emit RouterChanged(_router, false); + /// @dev sets the token whitelist. + /// @param token The address of the token. + /// @param status The status of the token. + function setTokenWhitelist(address token, bool status) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(token != address(0), "Invalid token"); + require(tokenWhitelist[token] != status, "whitelist same status"); + tokenWhitelist[token] = status; + emit TokenWhitelistChanged(token, status); } + function _getFunctionSelector(bytes calldata _data) private pure returns (bytes4) { return bytes4(_data[0:4]); } @@ -135,4 +223,12 @@ contract ListaAutoBuyback is Initializable, AccessControlUpgradeable { dstReceiver := calldataload(add(_data.offset, SWAP_DST_RECEIVER_OFFSET)) } } + + function _getTokenBalance(address _token, address account) internal view returns (uint256) { + if (_token == SWAP_NATIVE_TOKEN_ADDRESS) { + return account.balance; + } else { + return IERC20(_token).balanceOf(account); + } + } } diff --git a/scripts/foundry/buyback/deploy_buyback_impl.sol b/scripts/foundry/buyback/deploy_buyback_impl.sol new file mode 100644 index 00000000..ecf5f572 --- /dev/null +++ b/scripts/foundry/buyback/deploy_buyback_impl.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.8.10; + +import { Script, console } from "forge-std/Script.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { Buyback } from "../../../contracts/buyback/Buyback.sol"; + +contract BuybackDeploy is Script { + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + console.log("Deployer: ", deployer); + vm.startBroadcast(deployerPrivateKey); + + // Deploy implementation + Buyback impl = new Buyback(); + console.log("Implementation: ", address(impl)); + + vm.stopBroadcast(); + } +} diff --git a/scripts/foundry/dao/deploy_listaAutoBuyback_impl.sol b/scripts/foundry/dao/deploy_listaAutoBuyback_impl.sol new file mode 100644 index 00000000..d6f7bcb9 --- /dev/null +++ b/scripts/foundry/dao/deploy_listaAutoBuyback_impl.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.8.10; + +import { Script, console } from "forge-std/Script.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { ListaAutoBuyback } from "../../../contracts/dao/ListaAutoBuyback.sol"; + +contract ListaAutoBuybackDeploy is Script { + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + console.log("Deployer: ", deployer); + vm.startBroadcast(deployerPrivateKey); + + // Deploy implementation + ListaAutoBuyback impl = new ListaAutoBuyback(); + console.log("Implementation: ", address(impl)); + + vm.stopBroadcast(); + } +} diff --git a/test/buyback/Buyback.t.sol b/test/buyback/Buyback.t.sol index e490ce16..a4ec5d1c 100644 --- a/test/buyback/Buyback.t.sol +++ b/test/buyback/Buyback.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../../contracts/buyback/Buyback.sol"; +import "../interfaces/IPancakeRouter.sol"; contract BuybackTest is Test { /** @@ -22,6 +23,8 @@ contract BuybackTest is Test { address tokenIn = 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5; // lisUSD address oneInchNativeToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; address tokenOut = 0x55d398326f99059fF775485246999027B3197955; // USDT + address pancakeRouter = 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4; + address WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c; // WBNB Buyback buyback; address buybackImpl; @@ -125,7 +128,7 @@ contract BuybackTest is Test { function test_invalid_1Inch_parameters() public { vm.startPrank(bot); - vm.expectRevert("Invalid 1Inch router"); + vm.expectRevert("router not whitelisted"); buyback.buyback(bot, "0x"); vm.expectRevert("Invalid 1Inch function selector"); @@ -171,27 +174,27 @@ contract BuybackTest is Test { // test change oneInch router whitelist function test_change_oneInch_router_whitelist() public { vm.startPrank(manager); - vm.expectRevert("Invalid 1Inch router"); - buyback.add1InchRouterWhitelist(address(0)); + vm.expectRevert("Invalid router address"); + buyback.setRouterWhitelist(address(0), true); - vm.expectRevert("Already whitelisted"); - buyback.add1InchRouterWhitelist(oneInchRouter); + vm.expectRevert("whitelist same status"); + buyback.setRouterWhitelist(oneInchRouter, true); address oneInchRouter2 = makeAddr("1InchRouter2"); - buyback.add1InchRouterWhitelist(oneInchRouter2); - assertTrue(buyback.oneInchRouterWhitelist(oneInchRouter2)); + buyback.setRouterWhitelist(oneInchRouter2, true); + assertTrue(buyback.routerWhitelist(oneInchRouter2)); - buyback.remove1InchRouterWhitelist(oneInchRouter2); - assertFalse(buyback.oneInchRouterWhitelist(oneInchRouter2)); + buyback.setRouterWhitelist(oneInchRouter2, false); + assertFalse(buyback.routerWhitelist(oneInchRouter2)); vm.stopPrank(); // only manager can change oneInch router whitelist vm.startPrank(admin); vm.expectRevert(); - buyback.add1InchRouterWhitelist(oneInchRouter2); + buyback.setRouterWhitelist(oneInchRouter2, false); vm.expectRevert(); - buyback.remove1InchRouterWhitelist(oneInchRouter); + buyback.setRouterWhitelist(oneInchRouter, true); vm.stopPrank(); } @@ -301,4 +304,58 @@ contract BuybackTest is Test { } return result; } + + function test_swapRouterNotInWhiteList() public { + vm.startPrank(bot); + vm.expectRevert("router not whitelisted"); + buyback.buyback(pancakeRouter, tokenIn, tokenOut, 1 ether, 0, ""); + vm.stopPrank(); + } + + function test_swapTokenNotInWhiteList() public { + vm.startPrank(bot); + vm.expectRevert("token not whitelisted"); + buyback.buyback(oneInchRouter, address(0), tokenOut, 1 ether, 0, ""); + + vm.expectRevert("token not whitelisted"); + buyback.buyback(oneInchRouter, tokenIn, tokenIn, 1 ether, 0, ""); + vm.stopPrank(); + } + + function test_swapPancakeRouterERC20() public { + vm.startPrank(manager); + buyback.setRouterWhitelist(pancakeRouter, true); + vm.stopPrank(); + + IPancakeRouter.ExactInputParams memory params = IPancakeRouter.ExactInputParams({ + path: abi.encodePacked(tokenIn, uint24(500), tokenOut), + recipient: buyback.receiver(), + amountIn: 1 ether, + amountOutMinimum: 0 + }); + bytes memory data = abi.encodeWithSelector(IPancakeRouter.exactInput.selector, params); + + vm.startPrank(bot); + buyback.buyback(pancakeRouter, tokenIn, tokenOut, 1 ether, 0, data); + vm.stopPrank(); + } + + function test_swapPancakeRouterNative() public { + vm.deal(address(buyback), 1 ether); + vm.startPrank(manager); + buyback.setRouterWhitelist(pancakeRouter, true); + vm.stopPrank(); + + IPancakeRouter.ExactInputParams memory params = IPancakeRouter.ExactInputParams({ + path: abi.encodePacked(WBNB, uint24(500), tokenOut), + recipient: buyback.receiver(), + amountIn: 1 ether, + amountOutMinimum: 0 + }); + bytes memory data = abi.encodeWithSelector(IPancakeRouter.exactInput.selector, params); + + vm.startPrank(bot); + buyback.buyback(pancakeRouter, buyback.SWAP_NATIVE_TOKEN_ADDRESS(), tokenOut, 1 ether, 0, data); + vm.stopPrank(); + } } diff --git a/test/dao/ListaAutoBuyback.t.sol b/test/dao/ListaAutoBuyback.t.sol index 2590ae04..08f65fbd 100644 --- a/test/dao/ListaAutoBuyback.t.sol +++ b/test/dao/ListaAutoBuyback.t.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.so import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import "../../contracts/dao/ListaAutoBuyback.sol"; +import "../interfaces/IPancakeRouter.sol"; contract ListaAutoBuyBackTest is Test { address admin = address(0x1A11AA); @@ -15,17 +16,20 @@ contract ListaAutoBuyBackTest is Test { address defaultReceiver = 0x78Ab74C7EC3592B5298CB912f31bD8Fb80A57DC0; address proxyAdminOwner = 0x8d388136d578dCD791D081c6042284CED6d9B0c6; address oneInchRouter = 0x111111125421cA6dc452d289314280a0f8842A65; + address pancakeRouter = 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4; + address WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c; // WBNB + address USDT = 0x55d398326f99059fF775485246999027B3197955; + address lisUSD = 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5; + address lista = 0xFceB31A79F71AC9CBDCF853519c1b12D379EdC46; uint256 mainnet; ListaAutoBuyback listaAutoBuyBack; - IERC20 lisUSD; function setUp() public { mainnet = vm.createSelectFork("https://bsc-dataseed.binance.org"); - lisUSD = IERC20(0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5); ListaAutoBuyback autoPaybackImpl = new ListaAutoBuyback(); TransparentUpgradeableProxy listaAutoBuyBackProxy = new TransparentUpgradeableProxy( @@ -36,13 +40,13 @@ contract ListaAutoBuyBackTest is Test { admin, bot, defaultReceiver, oneInchRouter ) ); - listaAutoBuyBack = ListaAutoBuyback(address(listaAutoBuyBackProxy)); + listaAutoBuyBack = ListaAutoBuyback(payable(address(listaAutoBuyBackProxy))); assertEq(defaultReceiver, listaAutoBuyBack.defaultReceiver()); } function test_autoBuyBack_setUp() public { - assertEq(true, listaAutoBuyBack.oneInchRouterWhitelist(oneInchRouter)); + assertEq(true, listaAutoBuyBack.routerWhitelist(oneInchRouter)); } function test_autoBuyBack_buyback_acl() public { @@ -50,7 +54,7 @@ contract ListaAutoBuyBackTest is Test { vm.startPrank(manager); vm.expectRevert("AccessControl: account 0x00000000000000000000000000000000002a11aa is missing role 0x902cbe3a02736af9827fb6a90bada39e955c0941e08f0c63b3a662a7b17a4e2b"); - listaAutoBuyBack.buyback(address(lisUSD), 100e18, oneInchRouter, data); + listaAutoBuyBack.buyback(lisUSD, 100e18, oneInchRouter, data); vm.stopPrank(); } @@ -63,7 +67,7 @@ contract ListaAutoBuyBackTest is Test { vm.startPrank(manager); vm.expectRevert("amountIn is zero"); - listaAutoBuyBack.buyback(address(lisUSD), 0, oneInchRouter, data); + listaAutoBuyBack.buyback(lisUSD, 0, oneInchRouter, data); vm.stopPrank(); } @@ -72,7 +76,7 @@ contract ListaAutoBuyBackTest is Test { vm.startPrank(bot); vm.expectRevert("amountIn is zero"); - listaAutoBuyBack.buyback(address(lisUSD), 0, oneInchRouter, data); + listaAutoBuyBack.buyback(lisUSD, 0, oneInchRouter, data); vm.stopPrank(); } @@ -81,7 +85,7 @@ contract ListaAutoBuyBackTest is Test { vm.startPrank(bot); vm.expectRevert("router not whitelisted"); - listaAutoBuyBack.buyback(address(lisUSD), 1e18, proxyAdminOwner, data); + listaAutoBuyBack.buyback(lisUSD, 1e18, proxyAdminOwner, data); vm.stopPrank(); } @@ -89,23 +93,30 @@ contract ListaAutoBuyBackTest is Test { bytes memory data = hex"07ed2379000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd090000000000000000000000000782b6d8c4551b9760e74c0545a9bcd90bdc41e5000000000000000000000000fceb31a79f71ac9cbdcf853519c1b12d379edc46000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd0900000000000000000000000078ab74c7ec3592b5298cb912f31bd8fb80a57dc0000000000000000000000000000000000000000000000000016345785d8a000000000000000000000000000000000000000000000000000003f06640a9221e160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001c30000000000000000000000000000000000000000000000000001a500017700a007e5c0d20000000000000000000000000000000000000000000000000001530000f051200520451b19ad0bb00ed35ef391086a692cfc74b20782b6d8c4551b9760e74c0545a9bcd90bdc41e500449908fc8b0000000000000000000000000782b6d8c4551b9760e74c0545a9bcd90bdc41e500000000000000000000000055d398326f99059ff775485246999027b319795500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd090000000000000000000000000000000000000000000000000000000066d57bb802a000000000000000000000000000000000000000000000000003f06640a9221e16ee63c1e5812c533e2c2b4fd1172b5a0a0178805be1526a15a755d398326f99059ff775485246999027b3197955111111125421ca6dc452d289314280a0f8842a650020d6bdbf78fceb31a79f71ac9cbdcf853519c1b12d379edc46111111125421ca6dc452d289314280a0f8842a65000000000000000000000000000000000000000000000000000000000098dd6ed1"; vm.startPrank(admin); listaAutoBuyBack.changeDefaultReceiver(address(0x1A11AA)); + listaAutoBuyBack.setTokenWhitelist(lisUSD, true); + listaAutoBuyBack.setTokenWhitelist(lista, true); vm.stopPrank(); + assertEq(address(0x1A11AA), listaAutoBuyBack.defaultReceiver()); vm.startPrank(bot); - vm.expectRevert("invalid dst receiver of _data"); - listaAutoBuyBack.buyback(address(lisUSD), 1e18, oneInchRouter, data); + vm.expectRevert("Invalid receiver"); + listaAutoBuyBack.buyback(lisUSD, 1e18, oneInchRouter, data); vm.stopPrank(); } function test_autoBuyBack_buyback_invalid_balance() public { - deal(address(lisUSD), address(listaAutoBuyBack), 1e17); + deal(lisUSD, address(listaAutoBuyBack), 1e17); + vm.startPrank(admin); + listaAutoBuyBack.setTokenWhitelist(lisUSD, true); + listaAutoBuyBack.setTokenWhitelist(lista, true); + vm.stopPrank(); bytes memory data = hex"07ed2379000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd090000000000000000000000000782b6d8c4551b9760e74c0545a9bcd90bdc41e5000000000000000000000000fceb31a79f71ac9cbdcf853519c1b12d379edc46000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd0900000000000000000000000078ab74c7ec3592b5298cb912f31bd8fb80a57dc0000000000000000000000000000000000000000000000000016345785d8a000000000000000000000000000000000000000000000000000003f06640a9221e160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001c30000000000000000000000000000000000000000000000000001a500017700a007e5c0d20000000000000000000000000000000000000000000000000001530000f051200520451b19ad0bb00ed35ef391086a692cfc74b20782b6d8c4551b9760e74c0545a9bcd90bdc41e500449908fc8b0000000000000000000000000782b6d8c4551b9760e74c0545a9bcd90bdc41e500000000000000000000000055d398326f99059ff775485246999027b319795500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd090000000000000000000000000000000000000000000000000000000066d57bb802a000000000000000000000000000000000000000000000000003f06640a9221e16ee63c1e5812c533e2c2b4fd1172b5a0a0178805be1526a15a755d398326f99059ff775485246999027b3197955111111125421ca6dc452d289314280a0f8842a650020d6bdbf78fceb31a79f71ac9cbdcf853519c1b12d379edc46111111125421ca6dc452d289314280a0f8842a65000000000000000000000000000000000000000000000000000000000098dd6ed1"; vm.startPrank(bot); vm.expectRevert("insufficient balance"); - listaAutoBuyBack.buyback(address(lisUSD), 1e18, oneInchRouter, data); + listaAutoBuyBack.buyback(lisUSD, 1e18, oneInchRouter, data); vm.stopPrank(); } @@ -114,25 +125,25 @@ contract ListaAutoBuyBackTest is Test { vm.startPrank(bot); vm.expectRevert("invalid function selector of _data"); - listaAutoBuyBack.buyback(address(lisUSD), 1e18, oneInchRouter, data); + listaAutoBuyBack.buyback(lisUSD, 1e18, oneInchRouter, data); vm.stopPrank(); } function test_autoBuyBack_add1InchRouterWhitelist() public { - assertEq(false, listaAutoBuyBack.oneInchRouterWhitelist(address(0x1A11FF))); + assertEq(false, listaAutoBuyBack.routerWhitelist(address(0x1A11FF))); vm.startPrank(admin); - listaAutoBuyBack.add1InchRouterWhitelist(address(0x1A11FF)); + listaAutoBuyBack.setRouterWhitelist(address(0x1A11FF), true); vm.stopPrank(); - assertEq(true, listaAutoBuyBack.oneInchRouterWhitelist(address(0x1A11FF))); + assertEq(true, listaAutoBuyBack.routerWhitelist(address(0x1A11FF))); } function test_autoBuyBack_add1InchRouterWhitelist_already() public { test_autoBuyBack_add1InchRouterWhitelist(); vm.startPrank(admin); - vm.expectRevert("router already whitelisted"); - listaAutoBuyBack.add1InchRouterWhitelist(address(0x1A11FF)); + vm.expectRevert("whitelist same status"); + listaAutoBuyBack.setRouterWhitelist(address(0x1A11FF), true); vm.stopPrank(); } @@ -140,8 +151,78 @@ contract ListaAutoBuyBackTest is Test { test_autoBuyBack_add1InchRouterWhitelist(); vm.startPrank(admin); - listaAutoBuyBack.remove1InchRouterWhitelist(address(0x1A11FF)); + listaAutoBuyBack.setRouterWhitelist(address(0x1A11FF), false); + vm.stopPrank(); + assertEq(false, listaAutoBuyBack.routerWhitelist(address(0x1A11FF))); + } + + function test_swapRouterNotInWhiteList() public { + vm.startPrank(admin); + listaAutoBuyBack.setTokenWhitelist(lisUSD, true); + listaAutoBuyBack.setTokenWhitelist(USDT, true); + vm.stopPrank(); + + vm.startPrank(bot); + vm.expectRevert("router not whitelisted"); + listaAutoBuyBack.buyback(pancakeRouter, lisUSD, USDT, 1 ether, 0, ""); + vm.stopPrank(); + } + + function test_swapTokenNotInWhiteList() public { + vm.startPrank(admin); + listaAutoBuyBack.setTokenWhitelist(lisUSD, true); + listaAutoBuyBack.setTokenWhitelist(USDT, true); + vm.stopPrank(); + + vm.startPrank(bot); + vm.expectRevert("token not whitelisted"); + listaAutoBuyBack.buyback(oneInchRouter, address(0), USDT, 1 ether, 0, ""); + + vm.expectRevert("token not whitelisted"); + listaAutoBuyBack.buyback(oneInchRouter, lisUSD, address(0), 1 ether, 0, ""); + vm.stopPrank(); + } + + function test_swapPancakeRouterERC20() public { + deal(lisUSD, address(listaAutoBuyBack), 1 ether); + vm.startPrank(admin); + listaAutoBuyBack.setRouterWhitelist(pancakeRouter, true); + listaAutoBuyBack.setTokenWhitelist(lisUSD, true); + listaAutoBuyBack.setTokenWhitelist(USDT, true); + vm.stopPrank(); + + IPancakeRouter.ExactInputParams memory params = IPancakeRouter.ExactInputParams({ + path: abi.encodePacked(lisUSD, uint24(500), USDT), + recipient: listaAutoBuyBack.defaultReceiver(), + amountIn: 1 ether, + amountOutMinimum: 0 + }); + bytes memory data = abi.encodeWithSelector(IPancakeRouter.exactInput.selector, params); + + vm.startPrank(bot); + listaAutoBuyBack.buyback(pancakeRouter, lisUSD, USDT, 1 ether, 0, data); + vm.stopPrank(); + } + + function test_swapPancakeRouterNative() public { + vm.deal(address(listaAutoBuyBack), 1 ether); + vm.startPrank(admin); + listaAutoBuyBack.setRouterWhitelist(pancakeRouter, true); + listaAutoBuyBack.setTokenWhitelist(lisUSD, true); + listaAutoBuyBack.setTokenWhitelist(USDT, true); + listaAutoBuyBack.setTokenWhitelist(listaAutoBuyBack.SWAP_NATIVE_TOKEN_ADDRESS(), true); + vm.stopPrank(); + + IPancakeRouter.ExactInputParams memory params = IPancakeRouter.ExactInputParams({ + path: abi.encodePacked(WBNB, uint24(500), USDT), + recipient: listaAutoBuyBack.defaultReceiver(), + amountIn: 1 ether, + amountOutMinimum: 0 + }); + bytes memory data = abi.encodeWithSelector(IPancakeRouter.exactInput.selector, params); + + vm.startPrank(bot); + listaAutoBuyBack.buyback(pancakeRouter, listaAutoBuyBack.SWAP_NATIVE_TOKEN_ADDRESS(), USDT, 1 ether, 0, data); vm.stopPrank(); - assertEq(false, listaAutoBuyBack.oneInchRouterWhitelist(address(0x1A11FF))); } } diff --git a/test/interfaces/IPancakeRouter.sol b/test/interfaces/IPancakeRouter.sol new file mode 100644 index 00000000..fc4fcdc8 --- /dev/null +++ b/test/interfaces/IPancakeRouter.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +interface IPancakeRouter { + struct ExactInputParams { + bytes path; + address recipient; + uint256 amountIn; + uint256 amountOutMinimum; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, + /// and swap the entire amount, enabling contracts to send tokens before calling this function. + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata + /// @return amountOut The amount of the received token + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); +} diff --git a/test/mock/MockStaking.sol b/test/mock/MockStaking.sol index 4269202e..6f8d6c4a 100644 --- a/test/mock/MockStaking.sol +++ b/test/mock/MockStaking.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.10; -import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MockStaking { struct Pool { diff --git a/test/veListaRewardsCourierV2.t.sol b/test/veListaRewardsCourierV2.t.sol index 74bcb553..675e6794 100644 --- a/test/veListaRewardsCourierV2.t.sol +++ b/test/veListaRewardsCourierV2.t.sol @@ -108,7 +108,7 @@ contract VeListaRewardsCourierV2Test is Test { vm.warp(rewardWeekTimestamp + 1 weeks); vm.prank(bot); veListaRewardsCourierV2.deliverRewards(); - assertEq(veListaRewardsCourierV2.rewardsDeliveredForWeek(rewardWeek - 1), true); + assertEq(veListaRewardsCourierV2.rewardsDeliveredForWeek(veLista.getCurrentWeek() - 1), true); // @dev deliver once again, it should fail vm.prank(bot);