diff --git a/script/SolidlyDeploy.s.sol b/script/SolidlyDeploy.s.sol new file mode 100644 index 0000000..8b052aa --- /dev/null +++ b/script/SolidlyDeploy.s.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.19 <=0.9.0; + +import { Script } from "forge-std/Script.sol"; +import { console2 } from "forge-std/console2.sol"; + +import "./Network.sol"; + +import { Registry } from "src/Registry.sol"; + +import { SolidlyWrapper } from "../src/solidly/SolidlyWrapper.sol"; + +/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting +contract SolidlyDeploy is Script { + bytes32 public constant SALT = keccak256("ultrasecr.eth"); + + struct Fork { + string name; + address factory; + address weth; + address usdc; + } + + mapping(Network network => Fork fork) public forks; + + Registry internal registry = Registry(0xa348320114210b8F4eaF1b0795aa8F70803a93EA); + + constructor() { + forks[BASE] = Fork({ + name: "AerodromeWrapper", + factory: 0x420DD381b31aEf6683db6B902084cB0FFECe40Da, + weth: 0x4200000000000000000000000000000000000006, + usdc: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 + }); + forks[OPTIMISM] = Fork({ + name: "VelodromeWrapper", + factory: 0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a, + weth: 0x4200000000000000000000000000000000000006, + usdc: 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85 + }); + } + + function run() public { + console2.log("Deploying as %s", msg.sender); + + Network network = currentNetwork(); + + Fork memory fork = forks[network]; + require(fork.weth != address(0), "Fork not supported"); + + bytes memory paramsBytes = abi.encode(fork.factory, fork.weth, fork.usdc); + + string memory key = fork.name; + + if (keccak256(registry.get(key)) != keccak256(paramsBytes)) { + console2.log("Updating registry"); + vm.broadcast(); + registry.set(key, paramsBytes); + } + + (address _factory, address _weth, address _usdc) = abi.decode(registry.get(key), (address, address, address)); + console2.log("Factory: %s", _factory); + console2.log("WETH: %s", _weth); + console2.log("USDC: %s", _usdc); + + vm.broadcast(); + SolidlyWrapper wrapper = new SolidlyWrapper{ salt: SALT }(key, registry); + console2.log("SolidlyWrapper deployed at: %s", address(wrapper)); + } +} diff --git a/src/solidly/SolidlyWrapper.sol b/src/solidly/SolidlyWrapper.sol new file mode 100644 index 0000000..78530ae --- /dev/null +++ b/src/solidly/SolidlyWrapper.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +// Thanks to sunnyRK, yashnaman & ultrasecr.eth +pragma solidity ^0.8.19; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +import { Registry } from "src/Registry.sol"; + +import { IPoolFactory } from "./interfaces/IPoolFactory.sol"; +import { IPoolCallee } from "./interfaces/IPoolCallee.sol"; +import { IPool } from "./interfaces/IPool.sol"; + +import { BaseWrapper, IERC7399, IERC20 } from "../BaseWrapper.sol"; + +/// @dev Solidly Flash Lender that uses Solidly Pools as source of liquidity. +/// Solidly allows pushing repayments, so we override `_repayTo`. +contract SolidlyWrapper is BaseWrapper, IPoolCallee { + using { canLoan, balance } for IPool; + + uint256 private constant WAD = 1e18; + + error Unauthorized(); + error UnknownPool(); + error UnsupportedCurrency(address asset); + + // CONSTANTS + IPoolFactory public immutable factory; + + // DEFAULT ASSETS + address public immutable weth; + address public immutable usdc; + + /// @param reg Registry storing constructor parameters + constructor(string memory name, Registry reg) { + // @param factory_ Solidly SolidlyFactory address + // @param weth_ Weth contract used in Solidly Pairs + // @param usdc_ usdc contract used in Solidly Pairs + (factory, weth, usdc) = abi.decode(reg.getSafe(name), (IPoolFactory, address, address)); + } + + /** + * @dev Get the Solidly Pool that will be used as the source of a loan. The opposite asset will be WETH, except for + * WETH that will be usdc. + * @param asset The loan currency. + * @param amount The amount of assets to borrow. + * @return pool The Solidly Pool that will be used as the source of the flash loan. + */ + function cheapestPool(address asset, uint256 amount) public view returns (IPool pool, uint256 fee, bool stable) { + address assetOther = asset == weth ? usdc : weth; + IPool sPool = _pool(asset, assetOther, true); + IPool vPool = _pool(asset, assetOther, false); + + uint256 sFee = address(sPool) != address(0) ? factory.getFee(sPool, true) : type(uint256).max; + uint256 vFee = address(vPool) != address(0) ? factory.getFee(vPool, false) : type(uint256).max; + + if (sFee < vFee) { + if (sPool.canLoan(asset, amount)) return (sPool, sFee, true); + if (vPool.canLoan(asset, amount)) return (vPool, vFee, false); + } else { + if (vPool.canLoan(asset, amount)) return (vPool, vFee, false); + if (sPool.canLoan(asset, amount)) return (sPool, sFee, true); + } + } + + /// @inheritdoc IERC7399 + function maxFlashLoan(address asset) external view returns (uint256) { + return _maxFlashLoan(asset); + } + + function _feeAmount(uint256 amount, uint256 fee) internal pure returns (uint256) { + uint256 feeWAD = fee * 1e14; + uint256 derivedFee = Math.mulDiv(WAD, WAD, WAD - feeWAD, Math.Rounding.Ceil) - WAD; + return Math.mulDiv(amount, derivedFee, WAD, Math.Rounding.Ceil); + } + + /// @inheritdoc IERC7399 + function flashFee(address asset, uint256 amount) external view returns (uint256) { + (IPool pool, uint256 fee,) = cheapestPool(asset, amount); + if (address(pool) == address(0)) revert UnsupportedCurrency(asset); + + return _feeAmount(amount, fee); + } + + /// @inheritdoc IPoolCallee + function hook(address sender, uint256 amount0, uint256 amount1, bytes calldata params) external override { + (address asset0, address asset1, uint256 fee, bool stable, bytes memory data) = + abi.decode(params, (address, address, uint256, bool, bytes)); + + IPool pool = _pool(asset0, asset1, stable); + if (msg.sender != address(pool)) revert UnknownPool(); + if (sender != address(this)) revert Unauthorized(); + + (address asset, uint256 amount) = amount0 > 0 ? (asset0, amount0) : (asset1, amount1); + + _bridgeToCallback(asset, amount, _feeAmount(amount, fee), data); + } + + function _flashLoan(address asset, uint256 amount, bytes memory data) internal override { + (IPool pool, uint256 fee, bool stable) = cheapestPool(asset, amount); + if (address(pool) == address(0)) revert UnsupportedCurrency(asset); + + (address asset0, address asset1) = pool.tokens(); + uint256 amount0 = asset == asset0 ? amount : 0; + uint256 amount1 = asset == asset1 ? amount : 0; + bytes memory params = abi.encode(asset0, asset1, fee, stable, data); + + pool.swap(amount0, amount1, address(this), params); + } + + function _repayTo() internal view override returns (address) { + return msg.sender; + } + + function _pool(address tokenA, address tokenB, bool stable) internal view returns (IPool pool) { + (tokenA, tokenB) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + pool = factory.getPool(tokenA, tokenB, stable); + } + + function _maxFlashLoan(address asset) internal view returns (uint256 max) { + address assetOther = asset == weth ? usdc : weth; + IPool stable = _pool(asset, assetOther, true); + IPool volatile = _pool(asset, assetOther, false); + + uint256 stableBalance = balance(stable, asset); + uint256 volatileBalance = balance(volatile, asset); + + return stableBalance > volatileBalance ? stableBalance : volatileBalance; + } +} + +function canLoan(IPool pool, address asset, uint256 amount) view returns (bool) { + return balance(pool, asset) >= amount; +} + +function balance(IPool pool, address asset) view returns (uint256) { + return IERC20(asset).balanceOf(address(pool)); +} diff --git a/src/solidly/interfaces/IPool.sol b/src/solidly/interfaces/IPool.sol new file mode 100644 index 0000000..186cd9f --- /dev/null +++ b/src/solidly/interfaces/IPool.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +interface IPool { + struct Observation { + uint256 timestamp; + uint256 reserve0Cumulative; + uint256 reserve1Cumulative; + } + + error BelowMinimumK(); + error DepositsNotEqual(); + error FactoryAlreadySet(); + error InsufficientInputAmount(); + error InsufficientLiquidity(); + error InsufficientLiquidityBurned(); + error InsufficientLiquidityMinted(); + error InsufficientOutputAmount(); + error InvalidTo(); + error IsPaused(); + error K(); + error NotEmergencyCouncil(); + error StringTooLong(string str); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function blockTimestampLast() external view returns (uint256); + function burn(address to) external returns (uint256 amount0, uint256 amount1); + function claimFees() external returns (uint256 claimed0, uint256 claimed1); + function claimable0(address) external view returns (uint256); + function claimable1(address) external view returns (uint256); + function currentCumulativePrices() + external + view + returns (uint256 reserve0Cumulative, uint256 reserve1Cumulative, uint256 blockTimestamp); + function decimals() external view returns (uint8); + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); + function factory() external view returns (address); + function getAmountOut(uint256 amountIn, address tokenIn) external view returns (uint256); + function getK() external returns (uint256); + function getReserves() external view returns (uint256 _reserve0, uint256 _reserve1, uint256 _blockTimestampLast); + function increaseAllowance(address spender, uint256 addedValue) external returns (bool); + function index0() external view returns (uint256); + function index1() external view returns (uint256); + function initialize(address _token0, address _token1, bool _stable) external; + function lastObservation() external view returns (Observation memory); + function metadata() + external + view + returns (uint256 dec0, uint256 dec1, uint256 r0, uint256 r1, bool st, address t0, address t1); + function mint(address to) external returns (uint256 liquidity); + function name() external view returns (string memory); + function nonces(address owner) external view returns (uint256); + function observationLength() external view returns (uint256); + function observations(uint256) + external + view + returns (uint256 timestamp, uint256 reserve0Cumulative, uint256 reserve1Cumulative); + function periodSize() external view returns (uint256); + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) + external; + function poolFees() external view returns (address); + function prices(address tokenIn, uint256 amountIn, uint256 points) external view returns (uint256[] memory); + function quote(address tokenIn, uint256 amountIn, uint256 granularity) external view returns (uint256 amountOut); + function reserve0() external view returns (uint256); + function reserve0CumulativeLast() external view returns (uint256); + function reserve1() external view returns (uint256); + function reserve1CumulativeLast() external view returns (uint256); + function sample( + address tokenIn, + uint256 amountIn, + uint256 points, + uint256 window + ) + external + view + returns (uint256[] memory); + function setName(string memory __name) external; + function setSymbol(string memory __symbol) external; + function skim(address to) external; + function stable() external view returns (bool); + function supplyIndex0(address) external view returns (uint256); + function supplyIndex1(address) external view returns (uint256); + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes memory data) external; + function symbol() external view returns (string memory); + function sync() external; + function token0() external view returns (address); + function token1() external view returns (address); + function tokens() external view returns (address, address); + function totalSupply() external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} diff --git a/src/solidly/interfaces/IPoolCallee.sol b/src/solidly/interfaces/IPoolCallee.sol new file mode 100644 index 0000000..2e6c24a --- /dev/null +++ b/src/solidly/interfaces/IPoolCallee.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IPoolCallee { + function hook(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external; +} diff --git a/src/solidly/interfaces/IPoolFactory.sol b/src/solidly/interfaces/IPoolFactory.sol new file mode 100644 index 0000000..2aa6f3a --- /dev/null +++ b/src/solidly/interfaces/IPoolFactory.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +import { IPool } from "./IPool.sol"; + +interface IPoolFactory { + error FeeInvalid(); + error FeeTooHigh(); + error InvalidPool(); + error NotFeeManager(); + error NotPauser(); + error NotVoter(); + error PoolAlreadyExists(); + error SameAddress(); + error ZeroAddress(); + error ZeroFee(); + + function MAX_FEE() external view returns (uint256); + function ZERO_FEE_INDICATOR() external view returns (uint256); + function allPools(uint256) external view returns (address); + function allPoolsLength() external view returns (uint256); + function createPool(address tokenA, address tokenB, bool stable) external returns (address pool); + function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool); + function customFee(address) external view returns (uint256); + function feeManager() external view returns (address); + function getFee(IPool pool, bool _stable) external view returns (uint256); + function getPool(address tokenA, address tokenB, uint24 fee) external view returns (IPool); + function getPool(address tokenA, address tokenB, bool stable) external view returns (IPool); + function implementation() external view returns (address); + function isPaused() external view returns (bool); + function isPool(IPool pool) external view returns (bool); + function stableFee() external view returns (uint256); + function volatileFee() external view returns (uint256); + function voter() external view returns (address); +} diff --git a/test/SolidlyWrapper.t.sol b/test/SolidlyWrapper.t.sol new file mode 100644 index 0000000..73a18ee --- /dev/null +++ b/test/SolidlyWrapper.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.19 <0.9.0; + +import { Test } from "forge-std/Test.sol"; +import { console2 } from "forge-std/console2.sol"; +import { StdCheats } from "forge-std/StdCheats.sol"; + +import { IERC20Metadata as IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { Registry } from "src/Registry.sol"; + +import { MockBorrower } from "./MockBorrower.sol"; +import { SolidlyWrapper } from "../src/solidly/SolidlyWrapper.sol"; +import { IPoolFactory } from "../src/solidly/interfaces/IPoolFactory.sol"; +import { Arrays } from "src/utils/Arrays.sol"; + +/// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: +/// https://book.getfoundry.sh/forge/writing-tests +contract SolidlyWrapperTest is Test { + using Arrays for *; + + SolidlyWrapper internal wrapper; + MockBorrower internal borrower; + address internal usdc; + address internal reth; + address internal weth; + address internal cbeth; + IPoolFactory internal factory; + + /// @dev A function invoked before each test case is run. + function setUp() public virtual { + vm.createSelectFork({ urlOrAlias: "base", blockNumber: 12_118_407 }); + factory = IPoolFactory(0x420DD381b31aEf6683db6B902084cB0FFECe40Da); + usdc = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + reth = 0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c; + weth = 0x4200000000000000000000000000000000000006; + cbeth = 0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22; + + Registry registry = new Registry(address(this).toArray(), address(this).toArray()); + registry.set("AerodromeWrapper", abi.encode(factory, weth, usdc)); + wrapper = new SolidlyWrapper("AerodromeWrapper", registry); + borrower = new MockBorrower(wrapper); + } + + /// @dev Basic test. Run it with `forge test -vvv` to see the console log. + function test_flashFee() external { + console2.log("test_flashFee"); + assertEqDecimal(wrapper.flashFee(usdc, 100e6), 0.050026e6, 6, "Fee not exact USDC"); + assertEqDecimal(wrapper.flashFee(reth, 10e18), 0.03009027081243732e18, 18, "Fee not exact RETH"); + assertEqDecimal(wrapper.flashFee(weth, 0.1e18), 0.000050025012506254e18, 18, "Fee not exact IWETH9 1"); + assertEqDecimal(wrapper.flashFee(weth, 10e18), 0.03009027081243732e18, 18, "Fee not exact IWETH9 2"); + assertEqDecimal(wrapper.flashFee(cbeth, 1e18), 0.000500250125062532e18, 18, "Fee not exact CBETH"); + } + + function test_maxFlashLoan() external { + console2.log("test_maxFlashLoan"); + assertEqDecimal(wrapper.maxFlashLoan(usdc), 32_739_908.187835e6, 6, "Max flash loan not right"); + assertEqDecimal(wrapper.maxFlashLoan(reth), 229.017266311094211102e18, 18, "Max flash loan not right"); + assertEqDecimal(wrapper.maxFlashLoan(weth), 9253.315045893317165385e18, 18, "Max flash loan not right"); + assertEqDecimal(wrapper.maxFlashLoan(cbeth), 1902.400249022382199415e18, 18, "Max flash loan not right"); + } + + function test_flashLoan_USDC() external { + test_flashLoan(usdc, 100e6); + } + + function test_flashLoan_RETH() external { + test_flashLoan(reth, 10e18); + } + + function test_flashLoan_WETH() external { + test_flashLoan(weth, 10 ether); + } + + function test_flashLoan_CBETH() external { + test_flashLoan(cbeth, 1e18); + } + + function test_flashLoan(address token, uint256 loan) internal { + console2.log(string.concat("test_flashLoan: ", IERC20(token).symbol())); + uint256 fee = wrapper.flashFee(token, loan); + deal(address(token), address(borrower), fee); + bytes memory result = borrower.flashBorrow(token, loan); + + // Test the return values passed through the wrapper + (bytes32 callbackReturn) = abi.decode(result, (bytes32)); + assertEq(uint256(callbackReturn), uint256(borrower.ERC3156PP_CALLBACK_SUCCESS()), "Callback failed"); + + // Test the borrower state during the callback + assertEq(borrower.flashInitiator(), address(borrower), "flashInitiator"); + assertEq(address(borrower.flashAsset()), address(token), "flashAsset"); + assertEq(borrower.flashAmount(), loan, "flashAmount"); + // The amount we transferred to pay for fees, plus the amount we borrowed + assertEq(borrower.flashBalance(), loan + fee, "flashBalance"); + assertEq(borrower.flashFee(), fee, "flashFee"); + } + + function test_SolidlyFlashCallback_permissions() public { + vm.expectRevert(SolidlyWrapper.Unauthorized.selector); + vm.prank(0xcDAC0d6c6C59727a65F871236188350531885C43); + wrapper.hook({ sender: address(this), amount0: 0, amount1: 0, params: abi.encode(weth, usdc, 0, false, "") }); + + vm.expectRevert(SolidlyWrapper.UnknownPool.selector); + wrapper.hook({ sender: address(wrapper), amount0: 0, amount1: 0, params: abi.encode(weth, usdc, 0, false, "") }); + } +}