diff --git a/README.md b/README.md index 6cafa8c..a09eb41 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,10 @@ Deployments and upgrades can through the `SwapFactory` contract for indexing pur TenderSwap can also be deployed standlone following the same pattern as the `SwapFactory` contract. +`Factory::deploy` requires an implementation address. Each pool will have its own implementation contract where +`constants` or `immutables` can be specified for each pool. While this adds operational overhead and complexity for +potential upgrades, it significantly improves the gas cost of functions that use these parameters. + ### Format Format the contracts: diff --git a/foundry.toml b/foundry.toml index eef7765..ef63b8c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,11 +2,11 @@ [profile.default] bytecode_hash = "none" -fuzz = { runs = 10_000 } +fuzz = { runs = 1_000 } gas_reports = ["*"] libs = ["lib"] # optimizer = true (default) -optimizer_runs = 200 +optimizer_runs = 100 fs_permissions = [{ access = "read-write", path = "./" }] solc = "0.8.20" diff --git a/script/Swap_Deploy.s.sol b/script/Swap_Deploy.s.sol index 10171f1..6b56dc5 100644 --- a/script/Swap_Deploy.s.sol +++ b/script/Swap_Deploy.s.sol @@ -5,7 +5,7 @@ import { Script, console2 } from "forge-std/Script.sol"; import { ERC20 } from "solmate/tokens/ERC20.sol"; import { TenderSwap, ConstructorConfig } from "@tenderize/swap/Swap.sol"; import { SwapFactory } from "@tenderize/swap/Factory.sol"; -import { SD59x18 } from "@prb/math/SD59x18.sol"; +import { UD60x18 } from "@prb/math/UD60x18.sol"; import { ERC1967Proxy } from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; address constant FACTORY = address(0); @@ -19,14 +19,15 @@ contract Swap_Deploy is Script { // Start broadcasting with private key from `.env` file uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address underlying = vm.envAddress("UNDERLYING"); - SD59x18 BASE_FEE = SD59x18.wrap(vm.envInt("BASE_FEE")); - SD59x18 K = SD59x18.wrap(vm.envInt("K")); + UD60x18 BASE_FEE = UD60x18.wrap(vm.envUint("BASE_FEE")); + UD60x18 K = UD60x18.wrap(vm.envUint("K")); ConstructorConfig cfg = ConstructorConfig({ UNDERLYING: ERC20(underlying), BASE_FEE: BASE_FEE, K: K }); function run() public { vm.startBroadcast(deployerPrivateKey); - (address proxy, address implementation) = SwapFactory(FACTORY).deploy(cfg); + address implementation = address(new TenderSwap{ salt: bytes32(uint256(1)) }(cfg)); + (address proxy) = SwapFactory(FACTORY).deploy(implementation); console2.log("Deployment for ", underlying); console2.log("TenderSwap deployed at: ", proxy); console2.log("Implementation deployed at: ", implementation); diff --git a/src/Factory.sol b/src/Factory.sol index be50192..22b862e 100644 --- a/src/Factory.sol +++ b/src/Factory.sol @@ -11,7 +11,6 @@ pragma solidity ^0.8.20; -import { Owned } from "solmate/auth/Owned.sol"; import { ERC1967Proxy } from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { TenderSwap, ConstructorConfig } from "@tenderize/swap/Swap.sol"; @@ -22,8 +21,10 @@ import { UUPSUpgradeable } from "openzeppelin-contracts-upgradeable/proxy/utils/ // Used for subgraph indexing and atomic deployments contract SwapFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable { + error UNDERLYING_MISMATCH(); + event SwapDeployed(address underlying, address swap, address implementation); - event SwapUpgraded(address underlying, address swap, address implementation); + event SwapUpgraded(address underlying, address swap, address implementation, uint256 version); mapping(address pool => uint256 v) public version; @@ -36,10 +37,8 @@ contract SwapFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable { _disableInitializers(); } - function deploy(ConstructorConfig memory cfg) external onlyOwner returns (address proxy, address implementation) { + function deploy(address implementation) external onlyOwner returns (address proxy) { uint256 v = 1; - // Deploy the implementation - implementation = address(new TenderSwap{ salt: bytes32(v) }(cfg)); // deploy the contract proxy = address( new ERC1967Proxy{ salt: bytes32("tenderswap") }(implementation, abi.encodeWithSelector(TenderSwap.initialize.selector)) @@ -47,19 +46,20 @@ contract SwapFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable { TenderSwap(proxy).transferOwnership(owner()); version[proxy] = v; - emit SwapDeployed(address(cfg.UNDERLYING), proxy, implementation); + emit SwapDeployed(address(TenderSwap(proxy).UNDERLYING()), proxy, implementation); } - function upgrade(ConstructorConfig memory cfg, address swapProxy) external onlyOwner returns (address implementation) { - if (TenderSwap(swapProxy).UNDERLYING() != cfg.UNDERLYING) { - revert("SwapFactory: UNDERLYING_MISMATCH"); + function upgrade(address newImplementation, address swapProxy) external onlyOwner returns (address implementation) { + address underlying = address(TenderSwap(swapProxy).UNDERLYING()); + if (underlying != address(TenderSwap(newImplementation).UNDERLYING())) { + revert UNDERLYING_MISMATCH(); } uint256 v = ++version[swapProxy]; - implementation = address(new TenderSwap{ salt: bytes32(v) }(cfg)); - TenderSwap(swapProxy).upgradeTo(implementation); + + emit SwapUpgraded(underlying, swapProxy, implementation, v); } ///@dev required by the OZ UUPS module diff --git a/src/Swap.sol b/src/Swap.sol index 6266d73..61d30a8 100644 --- a/src/Swap.sol +++ b/src/Swap.sol @@ -9,8 +9,7 @@ // // Copyright (c) Tenderize Labs Ltd -import { SD59x18, ZERO as ZERO_SD59, UNIT, unwrap, sd } from "@prb/math/SD59x18.sol"; -import { UD60x18, ZERO as ZERO_UD60, UNIT as UNIT_60x18, ud } from "@prb/math/UD60x18.sol"; +import { UD60x18, ZERO as ZERO_UD60x18, UNIT as UNIT_60x18, ud } from "@prb/math/UD60x18.sol"; import { ERC20 } from "solmate/tokens/ERC20.sol"; import { ERC721 } from "solmate/tokens/ERC721.sol"; import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; @@ -47,15 +46,15 @@ error ErrorCalculateLPShares(); struct ConstructorConfig { ERC20 UNDERLYING; - SD59x18 BASE_FEE; - SD59x18 K; + UD60x18 BASE_FEE; + UD60x18 K; } struct SwapParams { - SD59x18 u; - SD59x18 U; - SD59x18 s; - SD59x18 S; + UD60x18 u; + UD60x18 U; + UD60x18 s; + UD60x18 S; } abstract contract SwapStorage { @@ -68,7 +67,7 @@ abstract contract SwapStorage { // total amount of liabilities owed to LPs uint256 liabilities; // sum of token supplies that have outstanding unlocks - SD59x18 S; + UD60x18 S; // Recovery amount, if `recovery` > 0 enable recovery mode uint256 recovery; // treasury share of rewards pending withdrawal @@ -78,7 +77,7 @@ abstract contract SwapStorage { // amount unlocking per asset mapping(address asset => uint256 unlocking) unlockingForAsset; // last supply of a tenderizer when seen, tracked because they are rebasing tokens - mapping(address asset => SD59x18 lastSupply) lastSupplyForAsset; + mapping(address asset => UD60x18 lastSupply) lastSupplyForAsset; // relayer fees mapping(address relayer => uint256 reward) relayerRewards; } @@ -106,8 +105,8 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS event RelayerRewardsClaimed(address indexed relayer, uint256 rewards); ERC20 public immutable UNDERLYING; - SD59x18 public immutable BASE_FEE; - SD59x18 public immutable K; + UD60x18 public immutable BASE_FEE; + UD60x18 public immutable K; // Minimum cut of the fee for LPs when an unlock is bought UD60x18 public constant MIN_LP_CUT = UD60x18.wrap(0.05e18); @@ -131,7 +130,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS _disableInitializers(); } - function lpToken() public view returns (ERC20) { + function lpToken() external view returns (ERC20) { Data storage $ = _loadStorageSlot(); return ERC20($.lpToken); } @@ -140,7 +139,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS * @notice Amount of liabilities outstanding to liquidity providers. * Liabilities represent all the deposits from liquidity providers and their earned fees. */ - function liabilities() public view returns (uint256) { + function liabilities() external view returns (uint256) { Data storage $ = _loadStorageSlot(); return $.liabilities; } @@ -159,7 +158,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS */ function utilisation() public view returns (UD60x18 r) { Data storage $ = _loadStorageSlot(); - if ($.liabilities == 0) return ZERO_UD60; + if ($.liabilities == 0) return ZERO_UD60x18; r = _utilisation($.unlocking, $.liabilities); } @@ -244,7 +243,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS * @notice Claim outstanding rewards for a relayer. * @return relayerReward Amount of tokens claimed */ - function claimRelayerRewards() public returns (uint256 relayerReward) { + function claimRelayerRewards() external returns (uint256 relayerReward) { Data storage $ = _loadStorageSlot(); relayerReward = $.relayerRewards[msg.sender]; @@ -256,7 +255,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS emit RelayerRewardsClaimed(msg.sender, relayerReward); } - function claimTreasuryRewards() public onlyOwner returns (uint256 treasuryReward) { + function claimTreasuryRewards() external onlyOwner returns (uint256 treasuryReward) { Data storage $ = _loadStorageSlot(); treasuryReward = $.treasuryRewards; @@ -284,12 +283,12 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS * @return out Amount of output tokens * @return fee Amount of fees paid */ - function quote(address asset, uint256 amount) public view returns (uint256 out, uint256 fee) { + function quote(address asset, uint256 amount) external view returns (uint256 out, uint256 fee) { Data storage $ = _loadStorageSlot(); - SD59x18 U = sd(int256($.unlocking)); - SD59x18 u = sd(int256($.unlockingForAsset[asset])); - (SD59x18 s, SD59x18 S) = _checkSupply(asset); + UD60x18 U = ud($.unlocking); + UD60x18 u = ud($.unlockingForAsset[asset]); + (UD60x18 s, UD60x18 S) = _checkSupply(asset); SwapParams memory p = SwapParams({ U: U, u: u, S: S, s: s }); return _quote(amount, p); @@ -310,10 +309,10 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS Data storage $ = _loadStorageSlot(); - SD59x18 U = sd(int256($.unlocking)); - SD59x18 u = sd(int256($.unlockingForAsset[asset])); - SD59x18 x = sd(int256(amount)); - (SD59x18 s, SD59x18 S) = _checkSupply(asset); + UD60x18 U = ud($.unlocking); + UD60x18 u = ud($.unlockingForAsset[asset]); + UD60x18 x = ud(amount); + (UD60x18 s, UD60x18 S) = _checkSupply(asset); SwapParams memory p = SwapParams({ U: U, u: u, S: S, s: s }); @@ -402,7 +401,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS // - Update S if unlockingForAsset is now zero if (ufa == 0) { $.S = $.S.sub($.lastSupplyForAsset[tenderizer]); - $.lastSupplyForAsset[tenderizer] = ZERO_SD59; + $.lastSupplyForAsset[tenderizer] = ZERO_UD60x18; } // - Update unlockingForAsset $.unlockingForAsset[tenderizer] = ufa; @@ -481,7 +480,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS // - Update S if unlockingForAsset is now zero if (ufa == 0) { $.S = $.S.sub($.lastSupplyForAsset[tenderizer]); - $.lastSupplyForAsset[tenderizer] = ZERO_SD59; + $.lastSupplyForAsset[tenderizer] = ZERO_UD60x18; } // - Update unlockingForAsset $.unlockingForAsset[tenderizer] = ufa; @@ -506,31 +505,41 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS function _quote(uint256 amount, SwapParams memory p) internal view returns (uint256 out, uint256 fee) { Data storage $ = _loadStorageSlot(); - SD59x18 x = sd(int256(amount)); - SD59x18 L = sd(int256($.liabilities)); - SD59x18 nom; - SD59x18 denom; + UD60x18 x = ud((amount)); + UD60x18 L = ud(($.liabilities)); + UD60x18 nom; + UD60x18 denom; + + // (((u + x)*k - U + u)*((U + x)/L)**k + (-k*u + U - u)*(U/L)**k)*(S + U)/(k*(1 + k)*(s + u)) + + // in this formula (-k*u + U -u) can be rewritten as U-(k+1)*u + // if U < (k+1)*u then we must do (k+1)*u - U and subtract that from the first part of the sum in the nominator + // else we use the initial formula { - SD59x18 sumA = p.u.add(x); + UD60x18 sumA = p.u.add(x); sumA = sumA.mul(K).sub(p.U).add(p.u); sumA = sumA.mul(p.U.add(x).div(L).pow(K)); - SD59x18 sumB = p.U.sub(p.u).sub(K.mul(p.u)).mul(p.U.div(L).pow(K)); - - nom = sumA.add(sumB).mul(p.S.add(p.U)); + UD60x18 negator = K.add(UNIT_60x18).mul(p.u); + if (p.U < negator) { + UD60x18 sumB = negator.sub(p.U).mul(p.U.div(L).pow(K)); + nom = sumA.sub(sumB).mul(p.S.add(p.U)); + } else { + UD60x18 sumB = p.U.sub(negator).mul(p.U.div(L).pow(K)); + nom = sumA.add(sumB).mul(p.S.add(p.U)); + } - denom = K.mul(UNIT.add(K)).mul(p.s.add(p.u)); + denom = K.mul(UNIT_60x18.add(K)).mul(p.s.add(p.u)); } - SD59x18 baseFee = BASE_FEE.mul(x); - fee = uint256(baseFee.add(nom.div(denom)).unwrap()); + UD60x18 baseFee = BASE_FEE.mul(x); + fee = baseFee.add(nom.div(denom)).unwrap(); fee = fee >= amount ? amount : fee; unchecked { out = amount - fee; } } - // (((u + x)*k - U + u)*((U + x)/L)**k + (-k*u + U - u)*(U/L)**k)*(S + U)/(k*(1 + k)*(s + u)) /** * @notice checks if an asset is a valid tenderizer for `UNDERLYING` @@ -570,13 +579,13 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS * @notice Since the LSTs to be exchanged are aTokens, and thus have a rebasing supply, * we need to update the supplies upon a swap to correctly determine the spread of the asset. */ - function _checkSupply(address tenderizer) internal view returns (SD59x18 s, SD59x18 S) { + function _checkSupply(address tenderizer) internal view returns (UD60x18 s, UD60x18 S) { Data storage $ = _loadStorageSlot(); S = $.S; - s = sd(int256(Tenderizer(tenderizer).totalSupply())); - SD59x18 oldSupply = $.lastSupplyForAsset[tenderizer]; + s = ud(Tenderizer(tenderizer).totalSupply()); + UD60x18 oldSupply = $.lastSupplyForAsset[tenderizer]; if (oldSupply.lt(s)) { S = S.add(s.sub(oldSupply)); diff --git a/test/Swap.t.sol b/test/Swap.t.sol index 96dea8e..edd8cd2 100644 --- a/test/Swap.t.sol +++ b/test/Swap.t.sol @@ -23,7 +23,6 @@ import { Tenderizer, TenderizerImmutableArgs } from "@tenderize/stake/tenderizer import { TenderSwap, ConstructorConfig, _encodeTokenId, _decodeTokenId } from "@tenderize/swap/Swap.sol"; import { LPToken } from "@tenderize/swap/LPToken.sol"; -import { SD59x18, ZERO, UNIT, unwrap, sd } from "@prb/math/SD59x18.sol"; import { UD60x18, ud, UNIT as UNIT_60x18 } from "@prb/math/UD60x18.sol"; import { SwapHarness } from "./Swap.harness.sol"; @@ -74,7 +73,7 @@ contract TenderSwapTest is Test { address(tToken1), abi.encodeWithSelector(TenderizerImmutableArgs.asset.selector), abi.encode(address(underlying)) ); - ConstructorConfig memory cfg = ConstructorConfig({ UNDERLYING: underlying, BASE_FEE: sd(0.0005e18), K: sd(3e18) }); + ConstructorConfig memory cfg = ConstructorConfig({ UNDERLYING: underlying, BASE_FEE: ud(0.0005e18), K: ud(3e18) }); swap = new SwapHarness(cfg); address proxy = address(new ERC1967Proxy(address(swap), "")); swap = SwapHarness(proxy); @@ -279,7 +278,7 @@ contract TenderSwapTest is Test { (uint256 out, uint256 fee) = swap.swap(address(tToken0), amount, 0); console.log("out %s", out); console.log("fee %s", fee); - // just assert the call doesnt fail for now + assertTrue(fee <= out); } function testFuzz_swap_multiple(uint256 liquidity) public {