From 745c8c3fc9f7bd97c5c614d7d62b5d312b59e5e5 Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Thu, 24 Oct 2024 11:08:16 +0200 Subject: [PATCH 1/2] feat: helper contract for people to swap and deposit in one transaction without encoded data from a supported collateral asset of the protocol --- contracts/helpers/ClearSwapper.sol | 137 +++++++++++++++++++++++++++++ lib/forge-std | 2 +- lib/prb-math | 2 +- lib/utils | 2 +- scripts/SetRate.s.sol | 28 ++++++ 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 contracts/helpers/ClearSwapper.sol create mode 100644 scripts/SetRate.s.sol diff --git a/contracts/helpers/ClearSwapper.sol b/contracts/helpers/ClearSwapper.sol new file mode 100644 index 00000000..22165222 --- /dev/null +++ b/contracts/helpers/ClearSwapper.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.23; + +import { SafeCast } from "oz/utils/math/SafeCast.sol"; +import { IERC20 } from "oz/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; + +import { ITransmuter } from "interfaces/ITransmuter.sol"; +import { IERC4626 } from "interfaces/external/IERC4626.sol"; +import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; + +import "../utils/Constants.sol"; +import "../utils/Errors.sol"; + +struct StableContracts { + address transmuter; + address savings; +} + +/// @title ClearSwapper +/// @author Angle Labs, Inc. +/// @dev Helper contract for people to permissionlessly swap and deposit into Angle savings solution in one transaction +contract ClearSwapper is AccessControl { + using SafeCast for uint256; + using SafeERC20 for IERC20; + + mapping(address => StableContracts) public stableContracts; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + constructor(address _accessControlManager) { + accessControlManager = IAccessControlManager(_accessControlManager); + } + + function swap( + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 minAmountOut, + address to, + uint256 deadline, + address stablecoin + ) public returns (uint256 amountOut) { + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + StableContracts memory stableData = stableContracts[stablecoin]; + if (tokenOut == stableData.savings) { + _changeAllowance(tokenIn, stableData.transmuter, amountIn); + amountOut = ITransmuter(stableData.transmuter).swapExactInput( + amountIn, + 0, + tokenIn, + stablecoin, + address(this), + deadline + ); + _changeAllowance(stablecoin, stableData.savings, amountOut); + amountOut = IERC4626(stableData.savings).deposit(amountOut, to); + } else if (tokenIn == stableData.savings) { + amountOut = IERC4626(stableData.savings).redeem(amountIn, address(this), address(this)); + amountOut = ITransmuter(stableData.transmuter).swapExactInput( + amountOut, + 0, + stablecoin, + tokenOut, + to, + deadline + ); + } + if (amountOut < minAmountOut) revert TooSmallAmountOut(); + } + + function swapExactOutput( + address tokenIn, + address tokenOut, + uint256 amountOut, + uint256 maxAmountIn, + address to, + uint256 deadline, + address stablecoin + ) external returns (uint256 amountIn) { + amountIn = quoteOut(tokenIn, tokenOut, amountOut, stablecoin); + if (amountIn > maxAmountIn) revert TooBigAmountIn(); + swap(tokenIn, tokenOut, amountIn, amountOut, to, deadline, stablecoin); + } + + function quoteIn( + address tokenIn, + address tokenOut, + uint256 amountIn, + address stablecoin + ) external view returns (uint256 amountOut) { + StableContracts memory stableData = stableContracts[stablecoin]; + if (tokenOut == stableData.savings) { + amountOut = ITransmuter(stableData.transmuter).quoteIn(amountIn, tokenIn, stablecoin); + amountOut = IERC4626(stableData.savings).previewDeposit(amountOut); + } else if (tokenIn == stableData.savings) { + amountOut = IERC4626(stableData.savings).previewRedeem(amountIn); + amountOut = ITransmuter(stableData.transmuter).quoteIn(amountOut, stablecoin, tokenOut); + } + } + + function quoteOut( + address tokenIn, + address tokenOut, + uint256 amountOut, + address stablecoin + ) external view returns (uint256 amountIn) { + StableContracts memory stableData = stableContracts[stablecoin]; + if (tokenOut == stableData.savings) { + amountIn = IERC4626(stableData.savings).previewMint(amountOut); + amountIn = ITransmuter(stableData.transmuter).quoteOut(amountIn, tokenIn, stablecoin); + } else if (tokenIn == stableData.savings) { + amountIn = ITransmuter(stableData.transmuter).quoteOut(amountOut, stablecoin, tokenOut); + amountIn = IERC4626(stableData.savings).previewWithdraw(amountIn); + } + } + + function setStableContracts(address stablecoin, address transmuter, address savings) external onlyGuardian { + StableContracts storage stableData = stableContracts[stablecoin]; + stableData.transmuter = transmuter; + stableData.savings = savings; + } + + function _changeAllowance(IERC20 token, address spender, uint256 amount) internal { + uint256 currentAllowance = token.allowance(address(this), spender); + // In case `currentAllowance < type(uint256).max / 2` and we want to increase it: + // Do nothing (to handle tokens that need reapprovals to 0 and save gas) + if (currentAllowance < amount && currentAllowance < type(uint256).max / 2) { + token.safeIncreaseAllowance(spender, amount - currentAllowance); + } else if (currentAllowance > amount) { + token.safeDecreaseAllowance(spender, currentAllowance - amount); + } + } +} diff --git a/lib/forge-std b/lib/forge-std index bf660614..978ac6fa 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit bf6606142994b1e47e2882ce0cd477c020d77623 +Subproject commit 978ac6fadb62f5f0b723c996f64be52eddba6801 diff --git a/lib/prb-math b/lib/prb-math index 1edf08dd..9dc06519 160000 --- a/lib/prb-math +++ b/lib/prb-math @@ -1 +1 @@ -Subproject commit 1edf08dd73eb1ace0042459ba719b8ea4a55c0e0 +Subproject commit 9dc06519f3b9f1659fec7d396da634fe690f660c diff --git a/lib/utils b/lib/utils index addaee4e..41388c6e 160000 --- a/lib/utils +++ b/lib/utils @@ -1 +1 @@ -Subproject commit addaee4e6971172d44ca6739f3ef54d635734fc8 +Subproject commit 41388c6ea9c45b05b92155356d6e700802fdb566 diff --git a/scripts/SetRate.s.sol b/scripts/SetRate.s.sol new file mode 100644 index 00000000..da642f05 --- /dev/null +++ b/scripts/SetRate.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { Utils } from "./utils/Utils.s.sol"; +import { console } from "forge-std/console.sol"; +import { stdJson } from "forge-std/StdJson.sol"; +import "stringutils/strings.sol"; +import "./Constants.s.sol"; + +import { Savings } from "contracts/savings/Savings.sol"; + +contract SetRate is Utils { + function run() external { + uint256 deployerPrivateKey = vm.envUint("KEEPER_PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + console.log("Address: %s", deployer); + console.log(deployer.balance); + vm.startBroadcast(deployerPrivateKey); + + // stUSD: 0x0022228a2cc5E7eF0274A7Baa600d44da5aB5776 + // stEUR: 0x004626A008B1aCdC4c74ab51644093b155e59A23 + + Savings savings = Savings(0x004626A008B1aCdC4c74ab51644093b155e59A23); + savings.setRate(1996917742275172864); + + vm.stopBroadcast(); + } +} From 7fa3f171e4c81bc4933adb4a5835ac6f2c155964 Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Thu, 24 Oct 2024 13:25:44 +0200 Subject: [PATCH 2/2] feat: add helpers --- contracts/helpers/ClearSwapper.sol | 70 +++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/contracts/helpers/ClearSwapper.sol b/contracts/helpers/ClearSwapper.sol index 22165222..a8f3acde 100644 --- a/contracts/helpers/ClearSwapper.sol +++ b/contracts/helpers/ClearSwapper.sol @@ -72,6 +72,54 @@ contract ClearSwapper is AccessControl { if (amountOut < minAmountOut) revert TooSmallAmountOut(); } + function deposit( + address tokenIn, + uint256 amountIn, + uint256 minAmountOut, + address to, + uint256 deadline, + address stablecoin + ) external returns (uint256) { + StableContracts memory stableData = stableContracts[stablecoin]; + return swap(tokenIn, stableData.savings, amountIn, minAmountOut, to, deadline, stablecoin); + } + + function redeem( + address tokenOut, + uint256 amountIn, + uint256 minAmountOut, + address to, + uint256 deadline, + address stablecoin + ) external returns (uint256) { + StableContracts memory stableData = stableContracts[stablecoin]; + return swap(stableData.savings, tokenOut, amountIn, minAmountOut, to, deadline, stablecoin); + } + + function withdraw( + address tokenOut, + uint256 amountOut, + uint256 maxAmountIn, + address to, + uint256 deadline, + address stablecoin + ) external returns (uint256) { + StableContracts memory stableData = stableContracts[stablecoin]; + return swapExactOutput(stableData.savings, tokenOut, amountOut, maxAmountIn, to, deadline, stablecoin); + } + + function mint( + address tokenIn, + uint256 amountOut, + uint256 maxAmountIn, + address to, + uint256 deadline, + address stablecoin + ) external returns (uint256) { + StableContracts memory stableData = stableContracts[stablecoin]; + return swapExactOutput(tokenIn, stableData.savings, amountOut, maxAmountIn, to, deadline, stablecoin); + } + function swapExactOutput( address tokenIn, address tokenOut, @@ -80,7 +128,7 @@ contract ClearSwapper is AccessControl { address to, uint256 deadline, address stablecoin - ) external returns (uint256 amountIn) { + ) public returns (uint256 amountIn) { amountIn = quoteOut(tokenIn, tokenOut, amountOut, stablecoin); if (amountIn > maxAmountIn) revert TooBigAmountIn(); swap(tokenIn, tokenOut, amountIn, amountOut, to, deadline, stablecoin); @@ -118,6 +166,26 @@ contract ClearSwapper is AccessControl { } } + function previewDeposit(address tokenIn, uint256 amountIn, address stablecoin) external view returns (uint256) { + StableContracts memory stableData = stableContracts[stablecoin]; + return quoteIn(tokenIn, stableData.savings, amountIn, stablecoin); + } + + function previewWithdraw(address tokenOut, uint256 amountIn, address stablecoin) external view returns (uint256) { + StableContracts memory stableData = stableContracts[stablecoin]; + return quoteIn(stableData.savings, tokenOut, amountIn, stablecoin); + } + + function previewMint(address tokenIn, uint256 amountOut, address stablecoin) external view returns (uint256) { + StableContracts memory stableData = stableContracts[stablecoin]; + return quoteOut(tokenIn, stableData.savings, amountOut, stablecoin); + } + + function previewRedeem(address tokenOut, uint256 amountOut, address stablecoin) external view returns (uint256) { + StableContracts memory stableData = stableContracts[stablecoin]; + return quoteOut(stableData.savings, tokenOut, amountOut, stablecoin); + } + function setStableContracts(address stablecoin, address transmuter, address savings) external onlyGuardian { StableContracts storage stableData = stableContracts[stablecoin]; stableData.transmuter = transmuter;