diff --git a/foundry.toml b/foundry.toml index 798b799..c7af728 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,7 @@ optimizer = true optimizer_runs = 50 fs_permissions = [{access = "read-write", path = "./"}] -solc_version="0.8.27" +solc_version="0.8.28" evm_version="shanghai" diff --git a/lib/v2-core b/lib/v2-core index 5109cf4..d714851 160000 --- a/lib/v2-core +++ b/lib/v2-core @@ -1 +1 @@ -Subproject commit 5109cf43460783b5d80f8365f8cffe0f360cfa6f +Subproject commit d7148518dfee909014ca3f251e914eab916c615a diff --git a/scripts/deploy-lbtsa.s.sol b/scripts/deploy-lbtsa.s.sol index 29276dc..c2166f3 100644 --- a/scripts/deploy-lbtsa.s.sol +++ b/scripts/deploy-lbtsa.s.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "forge-std/console.sol"; import {Utils} from "./utils.sol"; import "../src/periphery/LyraSettlementUtils.sol"; -import {BaseTSA} from "../src/tokenizedSubaccounts/BaseTSA.sol"; +import {BaseTSA} from "../src/tokenizedSubaccounts/shared/BaseTSA.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {CashAsset} from "v2-core/src/assets/CashAsset.sol"; import {DutchAuction} from "v2-core/src/liquidation/DutchAuction.sol"; @@ -79,7 +79,18 @@ contract DeployLBTSA is Utils { manager: ILiquidatableManager(_getCoreContract("srm")), matching: IMatching(_getMatchingModule("matching")), symbol: vaultTokenName, - name: string.concat(marketName, " Basis Trade") + name: string.concat(marketName, " Basis Trade"), + initialParams: BaseTSA.TSAParams({ + depositCap: 10000000e18, + minDepositValue: 0, + depositScale: 1e18, + // slight withdrawal fee + withdrawScale: 0.998e18, + managementFee: 0, + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) }), LeveragedBasisTSA.LBTSAInitParams({ baseFeed: ISpotFeed(_getMarketAddress(marketName, "spotFeed")), @@ -93,17 +104,6 @@ contract DeployLBTSA is Utils { LeveragedBasisTSA lbtsa = LeveragedBasisTSA(address(proxy)); - lbtsa.setTSAParams( - BaseTSA.TSAParams({ - depositCap: 10000000e18, - minDepositValue: 0, - depositScale: 1e18, - // slight withdrawal fee - withdrawScale: 0.998e18, - managementFee: 0, - feeRecipient: address(0) - }) - ); lbtsa.setLBTSAParams(defaultLbtsaTSAParams); lbtsa.setCollateralManagementParams(defaultCollateralManagementParams); diff --git a/scripts/deploy-prod-tsa.s.sol b/scripts/deploy-prod-tsa.s.sol index a4adf1c..a813a76 100644 --- a/scripts/deploy-prod-tsa.s.sol +++ b/scripts/deploy-prod-tsa.s.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "forge-std/console.sol"; import {Utils} from "./utils.sol"; import "../src/periphery/LyraSettlementUtils.sol"; -import {BaseTSA} from "../src/tokenizedSubaccounts/BaseTSA.sol"; +import {BaseTSA} from "../src/tokenizedSubaccounts/shared/BaseTSA.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {CashAsset} from "v2-core/src/assets/CashAsset.sol"; import {DutchAuction} from "v2-core/src/liquidation/DutchAuction.sol"; @@ -60,7 +60,9 @@ contract DeployTSA is Utils { depositScale: 1e18, withdrawScale: 1e18, managementFee: 0, - feeRecipient: address(0) + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 }); CollateralManagementTSA.CollateralManagementParams public defaultCollateralManagementParams = CollateralManagementTSA @@ -132,7 +134,8 @@ contract DeployTSA is Utils { manager: ILiquidatableManager(_getCoreContract("srm")), matching: IMatching(_getMatchingModule("matching")), symbol: settings.vaultSymbol, - name: settings.vaultName + name: settings.vaultName, + initialParams: defaultBaseTSAParams }), CoveredCallTSA.CCTSAInitParams({ baseFeed: ISpotFeed(_getMarketAddress(settings.depositAssetName, "spotFeed")), @@ -146,7 +149,6 @@ contract DeployTSA is Utils { console.log("proxy: ", address(proxy)); - CoveredCallTSA(address(proxy)).setTSAParams(defaultBaseTSAParams); CoveredCallTSA cctsa = CoveredCallTSA(address(proxy)); cctsa.setCCTSAParams(defaultLrtccTSAParams); cctsa.setCollateralManagementParams(defaultCollateralManagementParams); @@ -179,7 +181,8 @@ contract DeployTSA is Utils { manager: ILiquidatableManager(_getCoreContract("srm")), matching: IMatching(_getMatchingModule("matching")), symbol: settings.vaultSymbol, - name: settings.vaultName + name: settings.vaultName, + initialParams: defaultBaseTSAParams }), PrincipalProtectedTSA.PPTSAInitParams({ baseFeed: ISpotFeed(_getMarketAddress(settings.depositAssetName, "spotFeed")), @@ -198,7 +201,6 @@ contract DeployTSA is Utils { PrincipalProtectedTSA pptsa = PrincipalProtectedTSA(address(proxy)); - pptsa.setTSAParams(defaultBaseTSAParams); pptsa.setPPTSAParams(defaultLrtppTSAParams); pptsa.setCollateralManagementParams(defaultCollateralManagementParams); diff --git a/scripts/deploy-tsa-with-implementation.s.sol b/scripts/deploy-tsa-with-implementation.s.sol index 76ecb1c..016f6b3 100644 --- a/scripts/deploy-tsa-with-implementation.s.sol +++ b/scripts/deploy-tsa-with-implementation.s.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "forge-std/console.sol"; import {Utils} from "./utils.sol"; import "../src/periphery/LyraSettlementUtils.sol"; -import {BaseTSA} from "../src/tokenizedSubaccounts/BaseTSA.sol"; +import {BaseTSA} from "../src/tokenizedSubaccounts/shared/BaseTSA.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {CashAsset} from "v2-core/src/assets/CashAsset.sol"; import {DutchAuction} from "v2-core/src/liquidation/DutchAuction.sol"; @@ -72,7 +72,9 @@ contract DeployTSA is Utils { depositScale: 1e18, withdrawScale: 1e18, managementFee: 0, - feeRecipient: address(0) + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 }); CollateralManagementTSA.CollateralManagementParams @@ -172,7 +174,8 @@ contract DeployTSA is Utils { manager: ILiquidatableManager(_getCoreContract("srm")), matching: IMatching(_getMatchingModule("matching")), symbol: settings.vaultSymbol, - name: settings.vaultName + name: settings.vaultName, + initialParams: defaultBaseTSAParams }), CoveredCallTSA.CCTSAInitParams({ baseFeed: ISpotFeed( @@ -194,7 +197,6 @@ contract DeployTSA is Utils { console.log("proxy: ", address(proxy)); - CoveredCallTSA(address(proxy)).setTSAParams(defaultBaseTSAParams); CoveredCallTSA cctsa = CoveredCallTSA(address(proxy)); cctsa.setCCTSAParams(defaultLrtccTSAParams); cctsa.setCollateralManagementParams(defaultCollateralManagementParams); @@ -241,7 +243,8 @@ contract DeployTSA is Utils { manager: ILiquidatableManager(_getCoreContract("srm")), matching: IMatching(_getMatchingModule("matching")), symbol: settings.vaultSymbol, - name: settings.vaultName + name: settings.vaultName, + initialParams: defaultBaseTSAParams }), PrincipalProtectedTSA.PPTSAInitParams({ baseFeed: ISpotFeed( @@ -268,7 +271,6 @@ contract DeployTSA is Utils { PrincipalProtectedTSA pptsa = PrincipalProtectedTSA(address(proxy)); - pptsa.setTSAParams(defaultBaseTSAParams); pptsa.setPPTSAParams(defaultLrtppTSAParams); pptsa.setCollateralManagementParams(defaultCollateralManagementParams); diff --git a/scripts/deploy-tsa.s.sol b/scripts/deploy-tsa.s.sol index d814047..725e7a9 100644 --- a/scripts/deploy-tsa.s.sol +++ b/scripts/deploy-tsa.s.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "forge-std/console2.sol"; import {Utils} from "./utils.sol"; import "../src/periphery/LyraSettlementUtils.sol"; -import {BaseTSA} from "../src/tokenizedSubaccounts/BaseTSA.sol"; +import {BaseTSA} from "../src/tokenizedSubaccounts/shared/BaseTSA.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {CashAsset} from "v2-core/src/assets/CashAsset.sol"; import {DutchAuction} from "v2-core/src/liquidation/DutchAuction.sol"; @@ -109,7 +109,17 @@ contract DeployTSA is Utils { manager: ILiquidatableManager(_getCoreContract("srm")), matching: IMatching(_getMatchingModule("matching")), symbol: "ETH", - name: "ETH Covered Call" + name: "ETH Covered Call", + initialParams: BaseTSA.TSAParams({ + depositCap: 10000000e18, + minDepositValue: 0.01e18, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0, + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) }), CoveredCallTSA.CCTSAInitParams({ baseFeed: ISpotFeed(_getMarketAddress("ETH", "spotFeed")), @@ -121,16 +131,6 @@ contract DeployTSA is Utils { ) ); - CoveredCallTSA(address(proxy)).setTSAParams( - BaseTSA.TSAParams({ - depositCap: 10000000e18, - minDepositValue: 0.01e18, - depositScale: 1e18, - withdrawScale: 1e18, - managementFee: 0, - feeRecipient: address(0) - }) - ); CoveredCallTSA cctsa = CoveredCallTSA(address(proxy)); cctsa.setCCTSAParams(defaultLrtccTSAParams); cctsa.setCollateralManagementParams(defaultCollateralManagementParams); @@ -192,7 +192,17 @@ contract DeployTSA is Utils { manager: ILiquidatableManager(_getCoreContract("srm")), matching: IMatching(_getMatchingModule("matching")), symbol: "sUSDe", - name: "sUSDe Principal Protected Bull Call Spread" + name: "sUSDe Principal Protected Bull Call Spread", + initialParams: BaseTSA.TSAParams({ + depositCap: 10000000e18, + minDepositValue: 0.01e18, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0, + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) }), PrincipalProtectedTSA.PPTSAInitParams({ baseFeed: ISpotFeed(_getMarketAddress("sUSDe", "spotFeed")), @@ -207,16 +217,6 @@ contract DeployTSA is Utils { ) ); - PrincipalProtectedTSA(address(proxy)).setTSAParams( - BaseTSA.TSAParams({ - depositCap: 10000000e18, - minDepositValue: 0.01e18, - depositScale: 1e18, - withdrawScale: 1e18, - managementFee: 0, - feeRecipient: address(0) - }) - ); PrincipalProtectedTSA pptsa = PrincipalProtectedTSA(address(proxy)); pptsa.setPPTSAParams(defaultLrtppTSAParams); pptsa.setCollateralManagementParams(defaultCollateralManagementParams); diff --git a/scripts/upgrade-lbtsa.s.sol b/scripts/upgrade-lbtsa.s.sol index 453e620..fa59946 100644 --- a/scripts/upgrade-lbtsa.s.sol +++ b/scripts/upgrade-lbtsa.s.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "forge-std/console.sol"; import {Utils} from "./utils.sol"; import "../src/periphery/LyraSettlementUtils.sol"; -import {BaseTSA} from "../src/tokenizedSubaccounts/BaseTSA.sol"; +import {BaseTSA} from "../src/tokenizedSubaccounts/shared/BaseTSA.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {CashAsset} from "v2-core/src/assets/CashAsset.sol"; import {DutchAuction} from "v2-core/src/liquidation/DutchAuction.sol"; @@ -97,7 +97,8 @@ contract UpgradeLBTSA is UtilBase { manager: ILiquidatableManager(_getCoreContract("srm")), matching: IMatching(_getMatchingContract("matching", "matching")), symbol: string.concat("b", marketName), - name: string.concat("Basis traded ", marketName) + name: string.concat("Basis traded ", marketName), + initialParams: defaultLbtsaTSAParams }), LeveragedBasisTSA.LBTSAInitParams({ baseFeed: ISpotFeed(_getV2CoreContract(marketName, "spotFeed")), @@ -110,7 +111,7 @@ contract UpgradeLBTSA is UtilBase { LeveragedBasisTSA(address(proxy)).setSubmitter(0x47E946f9027B0e7E0117afa482AF4C4053C53b40, true); // -// LeveragedBasisTSA(address(proxy)).setLBTSAParams(defaultLbtsaTSAParams); +// LeveragedBasisTSA(address(proxy)).setLBTSAParams(); // LeveragedBasisTSA(address(proxy)).setCollateralManagementParams(defaultCollateralManagementParams); //// proxyAdmin.transferOwnership(0xB176A44D819372A38cee878fB0603AEd4d26C5a5); @@ -137,7 +138,8 @@ contract UpgradeLBTSA is UtilBase { manager: ILiquidatableManager(_getCoreContract("srm")), matching: IMatching(_getMatchingContract("matching", "matching")), symbol: string.concat("b", marketName), - name: string.concat("Basis traded ", marketName) + name: string.concat("Basis traded ", marketName), + initialParams: defaultLbtsaTSAParams }), LeveragedBasisTSA.LBTSAInitParams({ baseFeed: ISpotFeed(_getV2CoreContract(marketName, "spotFeed")), @@ -152,7 +154,7 @@ contract UpgradeLBTSA is UtilBase { // console.log("implementation: ", address(implementation)); -// LeveragedBasisTSA(address(proxy)).setLBTSAParams(defaultLbtsaTSAParams); +// LeveragedBasisTSA(address(proxy)).setLBTSAParams(); // LeveragedBasisTSA(address(proxy)).setCollateralManagementParams(defaultCollateralManagementParams); // proxyAdmin.transferOwnership(0xB176A44D819372A38cee878fB0603AEd4d26C5a5); diff --git a/scripts/upgrade-tsa.s.sol b/scripts/upgrade-tsa.s.sol index dd80b03..b5aeb27 100644 --- a/scripts/upgrade-tsa.s.sol +++ b/scripts/upgrade-tsa.s.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import "forge-std/console2.sol"; import {Utils} from "./utils.sol"; import "../src/periphery/LyraSettlementUtils.sol"; -import {BaseTSA} from "../src/tokenizedSubaccounts/BaseTSA.sol"; +import {BaseTSA} from "../src/tokenizedSubaccounts/shared/BaseTSA.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {CashAsset} from "v2-core/src/assets/CashAsset.sol"; import {DutchAuction} from "v2-core/src/liquidation/DutchAuction.sol"; @@ -114,7 +114,18 @@ contract DeployTSA is Utils { manager: ILiquidatableManager(_getCoreContract("srm")), matching: IMatching(_getMatchingModule("matching")), symbol: tsaName, - name: string.concat(marketName, " Covered Call") + name: string.concat(marketName, " Covered Call"), + initialParams: BaseTSA.TSAParams({ + depositCap: 10000000e18, + minDepositValue: 0.01e18, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0.015e18, + // TODO: Mainnet fee recipient should be different + feeRecipient: address(deployer), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) }), CoveredCallTSA.CCTSAInitParams({ baseFeed: ISpotFeed( @@ -134,17 +145,6 @@ contract DeployTSA is Utils { ) ); - CoveredCallTSA(address(proxy)).setTSAParams( - BaseTSA.TSAParams({ - depositCap: 10000000e18, - minDepositValue: 0.01e18, - depositScale: 1e18, - withdrawScale: 1e18, - managementFee: 0.015e18, - // TODO: Mainnet fee recipient should be different - feeRecipient: address(deployer) - }) - ); CoveredCallTSA cctsa = CoveredCallTSA(address(proxy)); cctsa.setCCTSAParams(defaultLrtccTSAParams); cctsa.setCollateralManagementParams(defaultCollateralManagementParams); @@ -185,7 +185,17 @@ contract DeployTSA is Utils { manager: ILiquidatableManager(_getCoreContract("srm")), matching: IMatching(_getMatchingModule("matching")), symbol: tsaName, - name: string.concat(marketName, "Covered Put Spread") + name: string.concat(marketName, "Covered Put Spread"), + initialParams: BaseTSA.TSAParams({ + depositCap: 100000000e18, + minDepositValue: 0, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0, + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) }), PrincipalProtectedTSA.PPTSAInitParams({ baseFeed: ISpotFeed( @@ -208,16 +218,6 @@ contract DeployTSA is Utils { ) ); - PrincipalProtectedTSA(address(proxy)).setTSAParams( - BaseTSA.TSAParams({ - depositCap: 100000000e18, - minDepositValue: 0, - depositScale: 1e18, - withdrawScale: 1e18, - managementFee: 0, - feeRecipient: address(0) - }) - ); PrincipalProtectedTSA pptsa = PrincipalProtectedTSA(address(proxy)); pptsa.setPPTSAParams(defaultLrtppTSAParams); pptsa.setCollateralManagementParams(defaultCollateralManagementParams); diff --git a/src/AtomicSigningExecutor.sol b/src/AtomicSigningExecutor.sol index b1d3df8..2abe126 100644 --- a/src/AtomicSigningExecutor.sol +++ b/src/AtomicSigningExecutor.sol @@ -35,6 +35,16 @@ contract AtomicSigningExecutor { } } matching.verifyAndMatch(actions, signatures, actionData); + + // Post-trade hook. Allows signer to update state internally and revert if they don't like the trade + for (uint i = 0; i < actions.length; i++) { + AtomicAction memory atomicAction = atomicActionData[i]; + if (atomicAction.isAtomic) { + // Call the signer of the action + IAtomicSigner signer = IAtomicSigner(actions[i].signer); + signer.postTradeHook(actions[i], atomicAction.extraData); + } + } } modifier onlyTradeExecutor() { diff --git a/src/interfaces/IAtomicSigner.sol b/src/interfaces/IAtomicSigner.sol index 1e2690b..cb340cf 100644 --- a/src/interfaces/IAtomicSigner.sol +++ b/src/interfaces/IAtomicSigner.sol @@ -5,4 +5,5 @@ import "./IMatching.sol"; interface IAtomicSigner { function signActionViaPermit(IMatching.Action memory action, bytes memory extraData, bytes memory signerSig) external; + function postTradeHook(IMatching.Action memory action, bytes memory extraData) external; } diff --git a/src/tokenizedSubaccounts/CCTSA.sol b/src/tokenizedSubaccounts/CCTSA.sol index dacd7fe..eba5f82 100644 --- a/src/tokenizedSubaccounts/CCTSA.sol +++ b/src/tokenizedSubaccounts/CCTSA.sol @@ -9,7 +9,7 @@ import {DecimalMath} from "lyra-utils/decimals/DecimalMath.sol"; import {SignedDecimalMath} from "lyra-utils/decimals/SignedDecimalMath.sol"; import {ConvertDecimals} from "lyra-utils/decimals/ConvertDecimals.sol"; -import {BaseOnChainSigningTSA, BaseTSA} from "./BaseOnChainSigningTSA.sol"; +import {BaseOnChainSigningTSA, BaseTSA} from "./shared/BaseOnChainSigningTSA.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {IOptionAsset} from "v2-core/src/interfaces/IOptionAsset.sol"; import {ISpotFeed} from "v2-core/src/interfaces/ISpotFeed.sol"; @@ -21,7 +21,7 @@ import {IMatching} from "../interfaces/IMatching.sol"; import { StandardManager, IStandardManager, IVolFeed, IForwardFeed } from "v2-core/src/risk-managers/StandardManager.sol"; -import "./CollateralManagementTSA.sol"; +import "./shared/CollateralManagementTSA.sol"; /// @title CoveredCallTSA /// @notice TSA that accepts any deposited collateral, and sells covered calls on it. Assumes options sold are @@ -91,8 +91,6 @@ contract CoveredCallTSA is CollateralManagementTSA { BaseTSA.BaseTSAInitParams memory initParams, CCTSAInitParams memory ccInitParams ) external reinitializer(5) { - __BaseTSA_init(initialOwner, initParams); - CCTSAStorage storage $ = _getCCTSAStorage(); $.depositModule = ccInitParams.depositModule; @@ -101,6 +99,9 @@ contract CoveredCallTSA is CollateralManagementTSA { $.optionAsset = ccInitParams.optionAsset; $.baseFeed = ccInitParams.baseFeed; + // Must set baseFeed before initialising BaseTSA + __BaseTSA_init(initialOwner, initParams); + BaseTSAAddresses memory tsaAddresses = getBaseTSAAddresses(); tsaAddresses.depositAsset.approve(address($.depositModule), type(uint).max); diff --git a/src/tokenizedSubaccounts/EMAGeneralisedTSA.sol b/src/tokenizedSubaccounts/EMAGeneralisedTSA.sol new file mode 100644 index 0000000..c63d164 --- /dev/null +++ b/src/tokenizedSubaccounts/EMAGeneralisedTSA.sol @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.20; + +import "forge-std/console.sol"; +import { + StandardManager, IStandardManager, IVolFeed, IForwardFeed +} from "v2-core/src/risk-managers/StandardManager.sol"; +import {BaseTSA} from "./shared/BaseOnChainSigningTSA.sol"; +import {Black76} from "lyra-utils/math/Black76.sol"; +import {ConvertDecimals} from "lyra-utils/decimals/ConvertDecimals.sol"; +import {DecimalMath} from "lyra-utils/decimals/DecimalMath.sol"; +import {EmptyTSA} from "./shared/EmptyTSA.sol"; +import {FixedPointMathLib} from "lyra-utils/math/FixedPointMathLib.sol"; + +import {IDepositModule} from "../interfaces/IDepositModule.sol"; +import {IMatching} from "../interfaces/IMatching.sol"; +import {IOptionAsset} from "v2-core/src/interfaces/IOptionAsset.sol"; +import {IPerpAsset} from "v2-core/src/interfaces/IPerpAsset.sol"; +import {IRfqModule} from "../interfaces/IRfqModule.sol"; +import {ISpotFeed} from "v2-core/src/interfaces/ISpotFeed.sol"; +import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; +import {ITradeModule} from "../interfaces/ITradeModule.sol"; +import {IWithdrawalModule} from "../interfaces/IWithdrawalModule.sol"; + +import {IWrappedERC20Asset} from "v2-core/src/interfaces/IWrappedERC20Asset.sol"; +import {IntLib} from "lyra-utils/math/IntLib.sol"; +import {OptionEncoding} from "lyra-utils/encoding/OptionEncoding.sol"; +import {SafeCast} from "openzeppelin/utils/math/SafeCast.sol"; +import {SignedDecimalMath} from "lyra-utils/decimals/SignedDecimalMath.sol"; + +/// @title GeneralisedTSA +/// @notice A TSA that allows the owner/signer to trade assets freely, with limited guardrails +/// EMA using share price instead of mark loss, that way its way more general +contract EMAGeneralisedTSA is EmptyTSA { + using IntLib for int; + using SafeCast for int; + using SafeCast for uint; + using DecimalMath for uint; + using SignedDecimalMath for int; + + struct GTSAInitParams { + ISpotFeed baseFeed; + IDepositModule depositModule; + IWithdrawalModule withdrawalModule; + ITradeModule tradeModule; + IRfqModule rfqModule; + } + + /// @custom:storage-location erc7201:lyra.storage.GeneralisedTSA + struct GTSAStorage { + ISpotFeed baseFeed; + IDepositModule depositModule; + IWithdrawalModule withdrawalModule; + ITradeModule tradeModule; + IRfqModule rfqModule; + /// @dev Only one hash is considered valid at a time, and it is revoked when a new one comes in. + /// Note: off-chain multiple "atomic" actions can be considered valid at once. Only once they are partially filled + /// would a new order on-chain invalidate the previous one. + bytes32 lastSeenHash; + // This vault can only actively trade whatever assets are enabled. Cash and deposit asset are always enabled. + mapping(address => bool) enabledAssets; + // EMA + uint lastSeenSharePrice; + uint markLossLastTs; + int markLossEma; + // EMA params + uint markLossEmaTarget; + uint emaDecayFactor; + } + + // keccak256(abi.encode(uint256(keccak256("lyra.storage.GeneralisedTSA")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant GENERALISED_TSA_STORAGE_LOCATION = + 0xca73a9a2e8745f9d6c402f73c120b8ce08191d5aebf9cedbfba8c4e5ed90cb00; + + function _getGTSAStorage() internal pure returns (GTSAStorage storage $) { + assembly { + $.slot := GENERALISED_TSA_STORAGE_LOCATION + } + } + + constructor() { + _disableInitializers(); + } + + function initialize( + address initialOwner, + BaseTSA.BaseTSAInitParams memory initParams, + GTSAInitParams memory lbInitParams + ) external reinitializer(2) { + GTSAStorage storage $ = _getGTSAStorage(); + + $.baseFeed = lbInitParams.baseFeed; + $.depositModule = lbInitParams.depositModule; + $.withdrawalModule = lbInitParams.withdrawalModule; + $.tradeModule = lbInitParams.tradeModule; + $.rfqModule = lbInitParams.rfqModule; + + __BaseTSA_init(initialOwner, initParams); + + BaseTSAAddresses memory tsaAddresses = getBaseTSAAddresses(); + + tsaAddresses.depositAsset.approve(address($.depositModule), type(uint).max); + } + + /////////// + // Admin // + /////////// + function setGTSAParams(uint emaDecayFactor, uint markLossEmaTarget) external onlyOwner { + // Decay factor must be non-zero + require(emaDecayFactor != 0 && markLossEmaTarget < 0.5e18, GT_InvalidParams()); + + GTSAStorage storage $ = _getGTSAStorage(); + + $.emaDecayFactor = emaDecayFactor; + $.markLossEmaTarget = markLossEmaTarget; + + $.lastSeenSharePrice = this.getSharesValue(1e18); + + emit GTSAParamsSet(emaDecayFactor, markLossEmaTarget); + } + + function resetDecay() external onlyOwner { + GTSAStorage storage $ = _getGTSAStorage(); + $.markLossLastTs = block.timestamp; + $.markLossEma = 0; + $.lastSeenSharePrice = this.getSharesValue(1e18); + } + + function enableAsset(address asset) external onlyOwner { + GTSAStorage storage $ = _getGTSAStorage(); + $.enabledAssets[asset] = true; + + emit AssetEnabled(asset); + } + + //////////// + // Keeper // + //////////// + + /// @dev This function is called by the keeper to update the EMA and mark loss values + /// This is required as the EMA is not updated when the actions are reverted - so could get into a state where if the + /// share price drops below a certain point, the EMA would never be able to recover + function updateEMA() external returns (int, int) { + return _updateEma(); + } + + ///////////////////// + // Post-trade hook // + ///////////////////// + + function postTradeHook(IMatching.Action memory /*action*/, bytes memory /*extraData*/) external override onlySubmitters { + _verifyEmaMarkLoss(); + } + + /////////////////////// + // Action Validation // + /////////////////////// + function _verifyAction(IMatching.Action memory action, bytes32 actionHash, bytes memory extraData) + internal + virtual + override + checkBlocked + { + GTSAStorage storage $ = _getGTSAStorage(); + + // if the action hash is the same as the last one, we revoke and then re-enable it afterwards (see _signActionData) + _revokeSignature($.lastSeenHash); + $.lastSeenHash = actionHash; + + BaseTSAAddresses memory tsaAddresses = getBaseTSAAddresses(); + + tsaAddresses.manager.settlePerpsWithIndex(subAccount()); + + if (address(action.module) == address($.depositModule)) { + _verifyDepositAction(action, tsaAddresses); + } else if (address(action.module) == address($.tradeModule)) { + _verifyTradeAction(action, tsaAddresses); + } else if (address(action.module) == address($.rfqModule)) { + _verifyRfqAction(action, extraData, tsaAddresses); + } else if (address(action.module) == address($.withdrawalModule)) { + _verifyWithdrawalAction(action, tsaAddresses); + } else { + revert GT_InvalidModule(); + } + } + + function _verifyTradeAction(IMatching.Action memory action, BaseTSAAddresses memory tsaAddresses) internal { + GTSAStorage storage $ = _getGTSAStorage(); + + ITradeModule.TradeData memory tradeData = abi.decode(action.data, (ITradeModule.TradeData)); + + require( + tradeData.asset == address(tsaAddresses.wrappedDepositAsset) || $.enabledAssets[tradeData.asset], + GT_InvalidTradeAsset() + ); + + _verifyEmaMarkLoss(); + } + + function _verifyRfqAction( + IMatching.Action memory action, + bytes memory extraData, + BaseTSAAddresses memory tsaAddresses + ) internal { + GTSAStorage storage $ = _getGTSAStorage(); + + IRfqModule.TradeData[] memory makerTrades; + if (extraData.length == 0) { + IRfqModule.RfqOrder memory makerOrder = abi.decode(action.data, (IRfqModule.RfqOrder)); + makerTrades = makerOrder.trades; + } else { + IRfqModule.TakerOrder memory takerOrder = abi.decode(action.data, (IRfqModule.TakerOrder)); + if (keccak256(extraData) != takerOrder.orderHash) { + revert GT_TradeDataDoesNotMatchOrderHash(); + } + makerTrades = abi.decode(extraData, (IRfqModule.TradeData[])); + } + + for (uint i = 0; i < makerTrades.length; i++) { + IRfqModule.TradeData memory makerTrade = makerTrades[i]; + require( + makerTrade.asset == address(tsaAddresses.wrappedDepositAsset) || $.enabledAssets[makerTrade.asset], + GT_InvalidTradeAsset() + ); + } + + _verifyEmaMarkLoss(); + } + + function _verifyWithdrawalAction(IMatching.Action memory action, BaseTSAAddresses memory tsaAddresses) internal view { + GTSAStorage storage $ = _getGTSAStorage(); + + IWithdrawalModule.WithdrawalData memory withdrawData = abi.decode(action.data, (IWithdrawalModule.WithdrawalData)); + + require( + withdrawData.asset != address(tsaAddresses.cash) + && (withdrawData.asset == address(tsaAddresses.wrappedDepositAsset) || !$.enabledAssets[withdrawData.asset]), // If the asset is not enabled, we can withdraw it - to remove dust + GT_InvalidWithdrawAsset() + ); + + // Note; no restriction to borrow when withdrawing. So a USDC based vault could swap for ETH and then allow + // withdrawals of USDC (that borrow against the ETH) + } + + function _verifyEmaMarkLoss() internal { + (int preMarkLossEma, int emaLoss) = _updateEma(); + // Note: no "recovery" check. Since that will almost constantly trigger when we only look at share + // price/it is gameable/skippable by donating to the pool after each trade + require(emaLoss <= int(_getGTSAStorage().markLossEmaTarget), GT_MarkLossTooHigh()); + } + + function _updateEma() internal returns (int preMarkLossEma, int emaLoss) { + GTSAStorage storage $ = _getGTSAStorage(); + + uint currentSharePrice = this.getSharesValue(1e18); + uint lastSharePrice = $.lastSeenSharePrice; + + // Positive value == loss + int markLossPercent = (int(lastSharePrice) - int(currentSharePrice)) * 1e18 / int(lastSharePrice); + + int preMarkLossEma = _decayAndFetchEma(); + int emaLoss = preMarkLossEma + markLossPercent; + + $.markLossEma = emaLoss; + $.lastSeenSharePrice = currentSharePrice; + + return (preMarkLossEma, emaLoss); + } + + function _decayAndFetchEma() internal returns (int newEma) { + GTSAStorage storage $ = _getGTSAStorage(); + + uint dt = block.timestamp - $.markLossLastTs; + uint decay = FixedPointMathLib.exp(-int($.emaDecayFactor * dt)); + $.markLossEma = $.markLossEma.multiplyDecimal(int(decay)); + $.markLossLastTs = block.timestamp; + + return $.markLossEma; + } + + /////////////////// + // Account Value // + /////////////////// + + function _getBasePrice() internal view override returns (uint spotPrice) { + (spotPrice,) = _getGTSAStorage().baseFeed.getSpot(); + } + + /////////// + // Views // + /////////// + + function getAccountValue(bool includePending) public view returns (uint) { + return _getAccountValue(includePending); + } + + function getBasePrice() public view returns (uint) { + return _getBasePrice(); + } + + function lastSeenHash() public view returns (bytes32) { + return _getGTSAStorage().lastSeenHash; + } + + function getEmaValues() public view returns (int markLossEma, uint markLossLastTs) { + return (_getGTSAStorage().markLossEma, _getGTSAStorage().markLossLastTs); + } + + function getGTSAAddresses() + public + view + returns (ISpotFeed, IDepositModule, IWithdrawalModule, ITradeModule, IRfqModule) + { + GTSAStorage storage $ = _getGTSAStorage(); + return ($.baseFeed, $.depositModule, $.withdrawalModule, $.tradeModule, $.rfqModule); + } + + /////////////////// + // Events/Errors // + /////////////////// + event GTSAParamsSet(uint emaDecayFactor, uint markLossEmaTarget); + event AssetEnabled(address indexed asset); + + error GT_TradeDataDoesNotMatchOrderHash(); + error GT_InvalidWithdrawAsset(); + error GT_InvalidTradeAsset(); + error GT_InvalidParams(); + error GT_InvalidModule(); + error GT_MarkLossTooHigh(); +} diff --git a/src/tokenizedSubaccounts/GeneralisedTSA.sol b/src/tokenizedSubaccounts/GeneralisedTSA.sol new file mode 100644 index 0000000..1d88d6b --- /dev/null +++ b/src/tokenizedSubaccounts/GeneralisedTSA.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.20; + +import "forge-std/console.sol"; +import { + StandardManager, IStandardManager, IVolFeed, IForwardFeed +} from "v2-core/src/risk-managers/StandardManager.sol"; +import {BaseTSA} from "./shared/BaseOnChainSigningTSA.sol"; +import {Black76} from "lyra-utils/math/Black76.sol"; +import {ConvertDecimals} from "lyra-utils/decimals/ConvertDecimals.sol"; +import {DecimalMath} from "lyra-utils/decimals/DecimalMath.sol"; +import {EmptyTSA} from "./shared/EmptyTSA.sol"; +import {FixedPointMathLib} from "lyra-utils/math/FixedPointMathLib.sol"; + +import {IDepositModule} from "../interfaces/IDepositModule.sol"; +import {IMatching} from "../interfaces/IMatching.sol"; +import {IOptionAsset} from "v2-core/src/interfaces/IOptionAsset.sol"; +import {IPerpAsset} from "v2-core/src/interfaces/IPerpAsset.sol"; +import {IRfqModule} from "../interfaces/IRfqModule.sol"; +import {ISpotFeed} from "v2-core/src/interfaces/ISpotFeed.sol"; +import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; +import {ITradeModule} from "../interfaces/ITradeModule.sol"; +import {IWithdrawalModule} from "../interfaces/IWithdrawalModule.sol"; + +import {IWrappedERC20Asset} from "v2-core/src/interfaces/IWrappedERC20Asset.sol"; +import {IntLib} from "lyra-utils/math/IntLib.sol"; +import {OptionEncoding} from "lyra-utils/encoding/OptionEncoding.sol"; +import {SafeCast} from "openzeppelin/utils/math/SafeCast.sol"; +import {SignedDecimalMath} from "lyra-utils/decimals/SignedDecimalMath.sol"; + +/// @title GeneralisedTSA +/// @notice A TSA that allows the owner/signer to trade assets freely, with limited guardrails +/// TODO: EMA using share price instead of mark loss, that way its way more general +contract GeneralisedTSA is EmptyTSA { + using IntLib for int; + using SafeCast for int; + using SafeCast for uint; + using DecimalMath for uint; + using SignedDecimalMath for int; + + struct GTSAInitParams { + ISpotFeed baseFeed; + IDepositModule depositModule; + IWithdrawalModule withdrawalModule; + ITradeModule tradeModule; + IRfqModule rfqModule; + } + + /// @custom:storage-location erc7201:lyra.storage.GeneralisedTSA + struct GTSAStorage { + ISpotFeed baseFeed; + IDepositModule depositModule; + IWithdrawalModule withdrawalModule; + ITradeModule tradeModule; + IRfqModule rfqModule; + /// @dev Only one hash is considered valid at a time, and it is revoked when a new one comes in. + /// Note: off-chain multiple "atomic" actions can be considered valid at once. Only once they are partially filled + /// would a new order on-chain invalidate the previous one. + bytes32 lastSeenHash; + // This vault can only actively trade whatever assets are enabled. Cash and deposit asset are always enabled. + mapping(address => bool) enabledAssets; + bool rfqEnabled; + } + + // keccak256(abi.encode(uint256(keccak256("lyra.storage.GeneralisedTSA")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant GENERALISED_TSA_STORAGE_LOCATION = + 0xca73a9a2e8745f9d6c402f73c120b8ce08191d5aebf9cedbfba8c4e5ed90cb00; + + function _getGTSAStorage() internal pure returns (GTSAStorage storage $) { + assembly { + $.slot := GENERALISED_TSA_STORAGE_LOCATION + } + } + + constructor() { + _disableInitializers(); + } + + function initialize( + address initialOwner, + BaseTSA.BaseTSAInitParams memory initParams, + GTSAInitParams memory lbInitParams + ) external reinitializer(2) { + GTSAStorage storage $ = _getGTSAStorage(); + + $.baseFeed = lbInitParams.baseFeed; + $.depositModule = lbInitParams.depositModule; + $.withdrawalModule = lbInitParams.withdrawalModule; + $.tradeModule = lbInitParams.tradeModule; + $.rfqModule = lbInitParams.rfqModule; + + __BaseTSA_init(initialOwner, initParams); + + BaseTSAAddresses memory tsaAddresses = getBaseTSAAddresses(); + + tsaAddresses.depositAsset.approve(address($.depositModule), type(uint).max); + } + + /////////// + // Admin // + /////////// + function setGTSAParams(bool rfqEnabled) external onlyOwner { + _getGTSAStorage().rfqEnabled = rfqEnabled; + + emit GTSAParamsSet(rfqEnabled); + } + + function enableAsset(address asset) external onlyOwner { + GTSAStorage storage $ = _getGTSAStorage(); + $.enabledAssets[asset] = true; + + emit AssetEnabled(asset); + } + + /////////////////////// + // Action Validation // + /////////////////////// + function _verifyAction(IMatching.Action memory action, bytes32 actionHash, bytes memory extraData) + internal + virtual + override + checkBlocked + { + GTSAStorage storage $ = _getGTSAStorage(); + + // if the action hash is the same as the last one, we revoke and then re-enable it afterwards (see _signActionData) + _revokeSignature($.lastSeenHash); + $.lastSeenHash = actionHash; + + BaseTSAAddresses memory tsaAddresses = getBaseTSAAddresses(); + + tsaAddresses.manager.settlePerpsWithIndex(subAccount()); + + if (address(action.module) == address($.depositModule)) { + _verifyDepositAction(action, tsaAddresses); + } else if (address(action.module) == address($.tradeModule)) { + _verifyTradeAction(action, tsaAddresses); + } else if (address(action.module) == address($.rfqModule)) { + _verifyRfqAction(action, extraData, tsaAddresses); + } else if (address(action.module) == address($.withdrawalModule)) { + _verifyWithdrawalAction(action, tsaAddresses); + } else { + revert GT_InvalidModule(); + } + } + + function _verifyTradeAction(IMatching.Action memory action, BaseTSAAddresses memory tsaAddresses) internal { + GTSAStorage storage $ = _getGTSAStorage(); + + ITradeModule.TradeData memory tradeData = abi.decode(action.data, (ITradeModule.TradeData)); + + require( + tradeData.asset == address(tsaAddresses.wrappedDepositAsset) || $.enabledAssets[tradeData.asset], + GT_InvalidTradeAsset() + ); + } + + function _verifyRfqAction( + IMatching.Action memory action, + bytes memory extraData, + BaseTSAAddresses memory tsaAddresses + ) internal { + GTSAStorage storage $ = _getGTSAStorage(); + + IRfqModule.TradeData[] memory makerTrades; + if (extraData.length == 0) { + IRfqModule.RfqOrder memory makerOrder = abi.decode(action.data, (IRfqModule.RfqOrder)); + makerTrades = makerOrder.trades; + } else { + IRfqModule.TakerOrder memory takerOrder = abi.decode(action.data, (IRfqModule.TakerOrder)); + if (keccak256(extraData) != takerOrder.orderHash) { + revert GT_TradeDataDoesNotMatchOrderHash(); + } + makerTrades = abi.decode(extraData, (IRfqModule.TradeData[])); + } + + for (uint i = 0; i < makerTrades.length; i++) { + IRfqModule.TradeData memory makerTrade = makerTrades[i]; + require( + makerTrade.asset == address(tsaAddresses.wrappedDepositAsset) || $.enabledAssets[makerTrade.asset], + GT_InvalidTradeAsset() + ); + } + } + + function _verifyWithdrawalAction(IMatching.Action memory action, BaseTSAAddresses memory tsaAddresses) internal view { + GTSAStorage storage $ = _getGTSAStorage(); + + IWithdrawalModule.WithdrawalData memory withdrawData = abi.decode(action.data, (IWithdrawalModule.WithdrawalData)); + + require( + withdrawData.asset != address(tsaAddresses.cash) + && (withdrawData.asset == address(tsaAddresses.wrappedDepositAsset) || !$.enabledAssets[withdrawData.asset]), // If the asset is not enabled, we can withdraw it - to remove dust + GT_InvalidWithdrawAsset() + ); + + // Note; no restriction to borrow when withdrawing. So a USDC based vault could swap for ETH and then allow + // withdrawals of USDC (that borrow against the ETH) + } + + /////////////////// + // Account Value // + /////////////////// + + function _getBasePrice() internal view override returns (uint spotPrice) { + (spotPrice,) = _getGTSAStorage().baseFeed.getSpot(); + } + + /////////// + // Views // + /////////// + + function getAccountValue(bool includePending) public view returns (uint) { + return _getAccountValue(includePending); + } + + function getBasePrice() public view returns (uint) { + return _getBasePrice(); + } + + function lastSeenHash() public view returns (bytes32) { + return _getGTSAStorage().lastSeenHash; + } + + function getGTSAAddresses() + public + view + returns (ISpotFeed, IDepositModule, IWithdrawalModule, ITradeModule, IRfqModule) + { + GTSAStorage storage $ = _getGTSAStorage(); + return ($.baseFeed, $.depositModule, $.withdrawalModule, $.tradeModule, $.rfqModule); + } + + /////////////////// + // Events/Errors // + /////////////////// + event GTSAParamsSet(bool rfqEnabled); + event AssetEnabled(address indexed asset); + + error GT_TradeDataDoesNotMatchOrderHash(); + error GT_InvalidWithdrawAsset(); + error GT_InvalidTradeAsset(); + error GT_InvalidModule(); +} diff --git a/src/tokenizedSubaccounts/LevBasisTSA.sol b/src/tokenizedSubaccounts/LevBasisTSA.sol index 3cc969b..6d60c57 100644 --- a/src/tokenizedSubaccounts/LevBasisTSA.sol +++ b/src/tokenizedSubaccounts/LevBasisTSA.sol @@ -10,7 +10,7 @@ import {DecimalMath} from "lyra-utils/decimals/DecimalMath.sol"; import {SignedDecimalMath} from "lyra-utils/decimals/SignedDecimalMath.sol"; import {ConvertDecimals} from "lyra-utils/decimals/ConvertDecimals.sol"; -import {BaseTSA} from "./BaseOnChainSigningTSA.sol"; +import {BaseTSA} from "./shared/BaseOnChainSigningTSA.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {IOptionAsset} from "v2-core/src/interfaces/IOptionAsset.sol"; import {ISpotFeed} from "v2-core/src/interfaces/ISpotFeed.sol"; @@ -24,7 +24,7 @@ import { StandardManager, IStandardManager, IVolFeed, IForwardFeed } from "v2-core/src/risk-managers/StandardManager.sol"; import {ITradeModule} from "../interfaces/ITradeModule.sol"; -import {CollateralManagementTSA} from "./CollateralManagementTSA.sol"; +import {CollateralManagementTSA} from "./shared/CollateralManagementTSA.sol"; /// @title LeveragedBasisTSA /// @notice A TSA that accepts a base asset, borrows against it to buy more, and then opens short perps to neutralise @@ -123,8 +123,6 @@ contract LeveragedBasisTSA is CollateralManagementTSA { BaseTSA.BaseTSAInitParams memory initParams, LBTSAInitParams memory lbInitParams ) external reinitializer(5) { - __BaseTSA_init(initialOwner, initParams); - LBTSAStorage storage $ = _getLBTSAStorage(); $.depositModule = lbInitParams.depositModule; @@ -133,6 +131,8 @@ contract LeveragedBasisTSA is CollateralManagementTSA { $.baseFeed = lbInitParams.baseFeed; $.perpAsset = lbInitParams.perpAsset; + __BaseTSA_init(initialOwner, initParams); + BaseTSAAddresses memory tsaAddresses = getBaseTSAAddresses(); tsaAddresses.depositAsset.approve(address($.depositModule), type(uint).max); diff --git a/src/tokenizedSubaccounts/PPTSA.sol b/src/tokenizedSubaccounts/PPTSA.sol index 7801516..9317d95 100644 --- a/src/tokenizedSubaccounts/PPTSA.sol +++ b/src/tokenizedSubaccounts/PPTSA.sol @@ -9,7 +9,7 @@ import {DecimalMath} from "lyra-utils/decimals/DecimalMath.sol"; import {SignedDecimalMath} from "lyra-utils/decimals/SignedDecimalMath.sol"; import {ConvertDecimals} from "lyra-utils/decimals/ConvertDecimals.sol"; -import {BaseTSA} from "./BaseOnChainSigningTSA.sol"; +import {BaseTSA} from "./shared/BaseOnChainSigningTSA.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {IOptionAsset} from "v2-core/src/interfaces/IOptionAsset.sol"; import {ISpotFeed} from "v2-core/src/interfaces/ISpotFeed.sol"; @@ -22,7 +22,7 @@ import { StandardManager, IStandardManager, IVolFeed, IForwardFeed } from "v2-core/src/risk-managers/StandardManager.sol"; import {ITradeModule} from "../interfaces/ITradeModule.sol"; -import {CollateralManagementTSA} from "./CollateralManagementTSA.sol"; +import {CollateralManagementTSA} from "./shared/CollateralManagementTSA.sol"; /// @title PrincipalProtectedTSA contract PrincipalProtectedTSA is CollateralManagementTSA { @@ -112,8 +112,6 @@ contract PrincipalProtectedTSA is CollateralManagementTSA { BaseTSA.BaseTSAInitParams memory initParams, PPTSAInitParams memory ppInitParams ) external reinitializer(5) { - __BaseTSA_init(initialOwner, initParams); - PPTSAStorage storage $ = _getPPTSAStorage(); $.depositModule = ppInitParams.depositModule; @@ -124,6 +122,9 @@ contract PrincipalProtectedTSA is CollateralManagementTSA { $.baseFeed = ppInitParams.baseFeed; $.isCallSpread = ppInitParams.isCallSpread; $.isLongSpread = ppInitParams.isLongSpread; + + __BaseTSA_init(initialOwner, initParams); + BaseTSAAddresses memory tsaAddresses = getBaseTSAAddresses(); tsaAddresses.depositAsset.approve(address($.depositModule), type(uint).max); diff --git a/src/tokenizedSubaccounts/BaseOnChainSigningTSA.sol b/src/tokenizedSubaccounts/shared/BaseOnChainSigningTSA.sol similarity index 97% rename from src/tokenizedSubaccounts/BaseOnChainSigningTSA.sol rename to src/tokenizedSubaccounts/shared/BaseOnChainSigningTSA.sol index 787b638..3c2f3d3 100644 --- a/src/tokenizedSubaccounts/BaseOnChainSigningTSA.sol +++ b/src/tokenizedSubaccounts/shared/BaseOnChainSigningTSA.sol @@ -109,6 +109,10 @@ abstract contract BaseOnChainSigningTSA is BaseTSA { ); } + function postTradeHook(IMatching.Action memory /*action*/, bytes memory /*extraData*/) external virtual onlySubmitters { + // No-op + } + //////////////// // Validation // //////////////// diff --git a/src/tokenizedSubaccounts/BaseTSA.sol b/src/tokenizedSubaccounts/shared/BaseTSA.sol similarity index 75% rename from src/tokenizedSubaccounts/BaseTSA.sol rename to src/tokenizedSubaccounts/shared/BaseTSA.sol index 55df7ae..0d6346e 100644 --- a/src/tokenizedSubaccounts/BaseTSA.sol +++ b/src/tokenizedSubaccounts/shared/BaseTSA.sol @@ -7,7 +7,7 @@ import {DutchAuction} from "v2-core/src/liquidation/DutchAuction.sol"; import {ERC20Upgradeable} from "openzeppelin-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol"; import {ILiquidatableManager} from "v2-core/src/interfaces/ILiquidatableManager.sol"; -import {IMatching} from "../interfaces/IMatching.sol"; +import {IMatching} from "../../interfaces/IMatching.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {IWrappedERC20Asset} from "v2-core/src/interfaces/IWrappedERC20Asset.sol"; import {Ownable2StepUpgradeable} from "openzeppelin-upgradeable/access/Ownable2StepUpgradeable.sol"; @@ -28,6 +28,7 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran IMatching matching; string symbol; string name; + TSAParams initialParams; } struct BaseTSAAddresses { @@ -51,6 +52,9 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran uint withdrawScale; uint managementFee; address feeRecipient; + // Performance fee + uint performanceFee; + uint performanceFeeWindow; } /// @dev A withdrawal is considered complete when amountShares is 0. They can be partially completed. @@ -92,6 +96,9 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran uint totalPendingWithdrawals; /// @dev Last time the fee was collected uint lastFeeCollected; + // Performance fee + uint lastPerfSnapshotTime; + uint lastPerfSnapshotValue; } // keccak256(abi.encode(uint256(keccak256("lyra.storage.BaseTSA")) - 1)) & ~bytes32(uint256(0xff)) @@ -123,6 +130,8 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran $.depositAsset = $.wrappedDepositAsset.wrappedAsset(); $.matching = initParams.matching; + _setTSAParams(initParams.initialParams); + if ($.subAccount == 0) { $.subAccount = $.subAccounts.createAccountWithApproval(address(this), address($.matching), $.manager); $.matching.depositSubAccount($.subAccount); @@ -138,13 +147,19 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran /////////// function setTSAParams(TSAParams memory _params) external onlyOwner { - _collectFee(); + _setTSAParams(_params); + } + + function _setTSAParams(TSAParams memory _params) internal { + _collectFees(); uint scaleRatio = _params.depositScale * 1e18 / _params.withdrawScale; - if (_params.managementFee > 0.02e18 || scaleRatio > 1.12e18 || scaleRatio < 0.9e18) { - revert BTSA_InvalidParams(); - } + require( + _params.managementFee <= 0.2e18 && scaleRatio <= 1.12e18 && scaleRatio >= 0.9e18 && _params.performanceFee <= 1e18 + && _params.performanceFeeWindow > 0, + BTSA_InvalidParams() + ); _getBaseTSAStorage().tsaParams = _params; @@ -203,13 +218,13 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran } function processDeposit(uint depositId) external onlyShareKeeper checkBlocked { - _collectFee(); + _collectFees(); _processDeposit(depositId); } /// @notice Process a number of deposit requests. function processDeposits(uint[] memory depositIds) external onlyShareKeeper checkBlocked { - _collectFee(); + _collectFees(); for (uint i = 0; i < depositIds.length; ++i) { _processDeposit(depositIds[i]); } @@ -284,7 +299,8 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran function processWithdrawalRequests(uint limit) external checkBlocked onlyShareKeeper nonReentrant { BaseTSAStorage storage $ = _getBaseTSAStorage(); - _collectFee(); + _collectFees(); + (uint perfFee, uint sharePrice) = _getPerfFee(); for (uint i = 0; i < limit; ++i) { if ($.queuedWithdrawalHead >= $.nextQueuedWithdrawalId) { @@ -293,39 +309,47 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran WithdrawalRequest storage request = $.queuedWithdrawals[$.queuedWithdrawalHead]; - uint totalBalance = $.depositAsset.balanceOf(address(this)) - $.totalPendingDeposits; - + uint maxWithdrawableBalance = $.depositAsset.balanceOf(address(this)) - $.totalPendingDeposits; + if (perfFee > 0) { + maxWithdrawableBalance = maxWithdrawableBalance * 1e18 / (1e18 - perfFee); + } uint requiredAmount = _getSharesToWithdrawAmount(request.amountShares); - if (totalBalance == 0) { + if (maxWithdrawableBalance == 0) { break; } - if (totalBalance < requiredAmount) { + if (maxWithdrawableBalance < requiredAmount) { // withdraw a portion - uint withdrawAmount = totalBalance; + uint withdrawAmount = maxWithdrawableBalance; uint difference = requiredAmount - withdrawAmount; uint finalShareAmount = request.amountShares * difference / requiredAmount; uint sharesRedeemed = request.amountShares - finalShareAmount; + uint finalWithdrawAmount = _collectWithdrawalPerfFee(sharesRedeemed, withdrawAmount, perfFee, sharePrice); + $.totalPendingWithdrawals -= sharesRedeemed; request.amountShares = finalShareAmount; - request.assetsReceived += withdrawAmount; + request.assetsReceived += finalWithdrawAmount; - emit WithdrawalProcessed($.queuedWithdrawalHead, request.beneficiary, false, sharesRedeemed, withdrawAmount); + emit WithdrawalProcessed( + $.queuedWithdrawalHead, request.beneficiary, false, sharesRedeemed, finalWithdrawAmount + ); - $.depositAsset.transfer(request.beneficiary, withdrawAmount); + $.depositAsset.transfer(request.beneficiary, finalWithdrawAmount); break; } else { uint sharesRedeemed = request.amountShares; + uint finalWithdrawAmount = _collectWithdrawalPerfFee(sharesRedeemed, requiredAmount, perfFee, sharePrice); + $.totalPendingWithdrawals -= sharesRedeemed; request.amountShares = 0; - request.assetsReceived += requiredAmount; + request.assetsReceived += finalWithdrawAmount; - emit WithdrawalProcessed($.queuedWithdrawalHead, request.beneficiary, true, sharesRedeemed, requiredAmount); + emit WithdrawalProcessed($.queuedWithdrawalHead, request.beneficiary, true, sharesRedeemed, finalWithdrawAmount); - $.depositAsset.transfer(request.beneficiary, requiredAmount); + $.depositAsset.transfer(request.beneficiary, finalWithdrawAmount); } $.queuedWithdrawalHead++; } @@ -346,11 +370,16 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran /// @notice Public function to trigger fee collection function collectFee() external { - _collectFee(); + _collectFees(); + } + + function _collectFees() internal { + _collectManagementFee(); + _collectPerformanceFee(); } /// @dev Must be called before totalSupply is modified to keep amount charged fair - function _collectFee() internal { + function _collectManagementFee() internal { BaseTSAStorage storage $ = _getBaseTSAStorage(); if ($.lastFeeCollected == block.timestamp) { @@ -376,7 +405,82 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran $.lastFeeCollected = block.timestamp; - emit FeeCollected($.tsaParams.feeRecipient, amountCollected, block.timestamp, totalShares); + emit ManagementFeeCollected($.tsaParams.feeRecipient, amountCollected, totalShares); + } + + function _collectPerformanceFee() internal { + BaseTSAStorage storage $ = _getBaseTSAStorage(); + + uint lastSnapshotTime = $.lastPerfSnapshotTime; + + if ($.tsaParams.performanceFee == 0 || $.tsaParams.feeRecipient == address(0) || lastSnapshotTime == 0) { + $.lastPerfSnapshotTime = block.timestamp; + $.lastPerfSnapshotValue = _getSharePrice(); + return; + } + + if (block.timestamp >= lastSnapshotTime + $.tsaParams.performanceFeeWindow) { + address feeRecipient = $.tsaParams.feeRecipient; + (uint perfFee, uint sharePrice) = _getPerfFee(); + + uint amountCollected = 0; + if (perfFee > 0) { + amountCollected = this.totalSupply() * perfFee / (1e18 - perfFee); + _mint(feeRecipient, amountCollected); + } + + // Get new share price after fee collection + sharePrice = _getSharePrice(); + + emit PerformanceFeeCollected( + feeRecipient, amountCollected, this.totalSupply(), sharePrice, $.lastPerfSnapshotValue, lastSnapshotTime + ); + + $.lastPerfSnapshotTime = block.timestamp; + $.lastPerfSnapshotValue = sharePrice; + } + } + + function _collectWithdrawalPerfFee(uint sharesBurnt, uint withdrawAmount, uint perfFee, uint currentSharePrice) + internal + returns (uint finalWithdrawAmount) + { + BaseTSAStorage storage $ = _getBaseTSAStorage(); + + if (perfFee > 0) { + address feeRecipient = $.tsaParams.feeRecipient; + uint feeSharesMinted = sharesBurnt * perfFee / 1e18; + + // we avoid diluting the pool by minting the same amount of shares as the "extra" amount burnt by the user + // withdrawing/by the amount of shares valued at the withdrawal that is withheld + _mint(feeRecipient, feeSharesMinted); + + emit WithdrawPerformanceFeeCollected( + feeRecipient, sharesBurnt, feeSharesMinted, withdrawAmount, currentSharePrice, $.lastPerfSnapshotValue + ); + + return withdrawAmount * (1e18 - perfFee) / 1e18; + } + return withdrawAmount; + } + + function _getPerfFee() internal view returns (uint perfFee, uint sharePrice) { + BaseTSAStorage storage $ = _getBaseTSAStorage(); + sharePrice = _getSharePrice(); + + if ($.tsaParams.performanceFee == 0 || $.tsaParams.feeRecipient == address(0)) { + return (0, sharePrice); + } + + if ($.lastPerfSnapshotTime == 0) { + return (0, sharePrice); + } + + if (sharePrice > $.lastPerfSnapshotValue) { + perfFee = (sharePrice - $.lastPerfSnapshotValue) * $.tsaParams.performanceFee / sharePrice; + return (perfFee, sharePrice); + } + return (0, sharePrice); } ///////////////////////////// @@ -457,8 +561,13 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran return _isBlocked(); } - function lastFeeCollected() public view returns (uint) { - return _getBaseTSAStorage().lastFeeCollected; + function getFeeValues() + public + view + returns (uint lastManagementFeeTime, uint lastPerfSnapshotTime, uint lastPerfSnapshotValue) + { + BaseTSAStorage storage $ = _getBaseTSAStorage(); + return ($.lastFeeCollected, $.lastPerfSnapshotTime, $.lastPerfSnapshotValue); } /////////////// @@ -504,7 +613,28 @@ abstract contract BaseTSA is ERC20Upgradeable, Ownable2StepUpgradeable, Reentran uint indexed withdrawalId, address indexed beneficiary, bool complete, uint sharesProcessed, uint amountReceived ); - event FeeCollected(address indexed recipient, uint amount, uint timestamp, uint totalSupply); + event ManagementFeeCollected(address indexed recipient, uint amount, uint totalSupply); + event PerformanceFeeCollected( + address indexed recipient, + uint amount, + uint totalSupply, + uint newSnapshotSharePrice, + uint lastPerfSnapshotValue, + uint lastPerfSnapshotTime + ); + + event WithdrawPerformanceFeeCollected( + address indexed feeRecipient, + uint sharesBurnt, + uint feeSharesMinted, + uint withdrawAmount, + uint currentSharePrice, + uint lastPerfSnapshotValue + ); + + //////////// + // Errors // + //////////// error BTSA_InvalidParams(); error BTSA_MustReceiveShares(); diff --git a/src/tokenizedSubaccounts/CollateralManagementTSA.sol b/src/tokenizedSubaccounts/shared/CollateralManagementTSA.sol similarity index 60% rename from src/tokenizedSubaccounts/CollateralManagementTSA.sol rename to src/tokenizedSubaccounts/shared/CollateralManagementTSA.sol index 5785719..7ee91a9 100644 --- a/src/tokenizedSubaccounts/CollateralManagementTSA.sol +++ b/src/tokenizedSubaccounts/shared/CollateralManagementTSA.sol @@ -1,18 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -import {IntLib} from "lyra-utils/math/IntLib.sol"; -import {OptionEncoding} from "lyra-utils/encoding/OptionEncoding.sol"; -import {SafeCast} from "openzeppelin/utils/math/SafeCast.sol"; -import {DecimalMath} from "lyra-utils/decimals/DecimalMath.sol"; -import {SignedDecimalMath} from "lyra-utils/decimals/SignedDecimalMath.sol"; - -import {BaseOnChainSigningTSA} from "./BaseOnChainSigningTSA.sol"; -import {ITradeModule} from "../interfaces/ITradeModule.sol"; -import {IMatching} from "../interfaces/IMatching.sol"; -import "../interfaces/IDepositModule.sol"; - -abstract contract CollateralManagementTSA is BaseOnChainSigningTSA { +import "./EmptyTSA.sol"; + +abstract contract CollateralManagementTSA is EmptyTSA { using IntLib for int; using SafeCast for int; using SafeCast for uint; @@ -112,74 +103,11 @@ abstract contract CollateralManagementTSA is BaseOnChainSigningTSA { } } - ////////////// - // Deposits // - ////////////// - function _verifyDepositAction(IMatching.Action memory action, BaseTSAAddresses memory tsaAddresses) internal view { - IDepositModule.DepositData memory depositData = abi.decode(action.data, (IDepositModule.DepositData)); - - if (depositData.asset != address(tsaAddresses.wrappedDepositAsset)) { - revert CMTSA_InvalidAsset(); - } - - if (depositData.amount > tsaAddresses.depositAsset.balanceOf(address(this)) - totalPendingDeposits()) { - revert CMTSA_DepositingTooMuch(); - } - } - - /////////////////// - // Account Value // - /////////////////// - - function _getAccountValue(bool includePending) internal view override returns (uint) { - BaseTSAAddresses memory tsaAddresses = getBaseTSAAddresses(); - - uint depositAssetBalance = tsaAddresses.depositAsset.balanceOf(address(this)); - if (!includePending) { - depositAssetBalance -= totalPendingDeposits(); - } - - return _getConvertedMtM(true) + depositAssetBalance; - } - - function _getConvertedMtM(bool nativeDecimals) internal view returns (uint) { - BaseTSAAddresses memory tsaAddresses = getBaseTSAAddresses(); - - // Note: scenario 0 wont calculate full margin for PMRM subaccounts - (, int mtm) = tsaAddresses.manager.getMarginAndMarkToMarket(subAccount(), false, 0); - uint spotPrice = _getBasePrice(); - - // convert to depositAsset value but in 18dp - int convertedMtM = mtm.divideDecimal(spotPrice.toInt256()); - - if (nativeDecimals) { - // Now convert to appropriate decimals - uint8 decimals = tsaAddresses.depositAsset.decimals(); - if (decimals > 18) { - convertedMtM = convertedMtM * int(10 ** (decimals - 18)); - } else if (decimals < 18) { - convertedMtM = convertedMtM / int(10 ** (18 - decimals)); - } - } - - // Might not be technically insolvent (could have enough depositAsset to cover the deficit), but we block deposits - if (convertedMtM < 0) { - revert CMTSA_PositionInsolvent(); - } - - return uint(convertedMtM); - } - - function _getBasePrice() internal view virtual returns (uint spotPrice); - /////////////////// // Events/Errors // /////////////////// event CMTSAParamsSet(CollateralManagementParams collateralManagementParams); - error CMTSA_DepositingTooMuch(); - error CMTSA_PositionInsolvent(); - error CMTSA_InvalidAsset(); error CMTSA_MustHavePositiveCash(); error CMTSA_SpotLimitPriceTooHigh(); error CMTSA_BuyingTooMuchCollateral(); diff --git a/src/tokenizedSubaccounts/shared/EmptyTSA.sol b/src/tokenizedSubaccounts/shared/EmptyTSA.sol new file mode 100644 index 0000000..beddc78 --- /dev/null +++ b/src/tokenizedSubaccounts/shared/EmptyTSA.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {IntLib} from "lyra-utils/math/IntLib.sol"; +import {OptionEncoding} from "lyra-utils/encoding/OptionEncoding.sol"; +import {SafeCast} from "openzeppelin/utils/math/SafeCast.sol"; +import {DecimalMath} from "lyra-utils/decimals/DecimalMath.sol"; +import {SignedDecimalMath} from "lyra-utils/decimals/SignedDecimalMath.sol"; + +import {BaseOnChainSigningTSA} from "./BaseOnChainSigningTSA.sol"; +import {ITradeModule} from "../../interfaces/ITradeModule.sol"; +import {IMatching} from "../../interfaces/IMatching.sol"; +import {IDepositModule} from "../../interfaces/IDepositModule.sol"; + +abstract contract EmptyTSA is BaseOnChainSigningTSA { + using IntLib for int; + using SafeCast for int; + using SafeCast for uint; + using DecimalMath for uint; + using SignedDecimalMath for int; + + ////////////// + // Deposits // + ////////////// + function _verifyDepositAction(IMatching.Action memory action, BaseTSAAddresses memory tsaAddresses) internal view { + IDepositModule.DepositData memory depositData = abi.decode(action.data, (IDepositModule.DepositData)); + + if (depositData.asset != address(tsaAddresses.wrappedDepositAsset)) { + revert ETSA_InvalidAsset(); + } + + if (depositData.amount > tsaAddresses.depositAsset.balanceOf(address(this)) - totalPendingDeposits()) { + revert ETSA_DepositingTooMuch(); + } + } + + /////////////////// + // Account Value // + /////////////////// + + function _getAccountValue(bool includePending) internal view override returns (uint) { + BaseTSAAddresses memory tsaAddresses = getBaseTSAAddresses(); + + uint depositAssetBalance = tsaAddresses.depositAsset.balanceOf(address(this)); + if (!includePending) { + depositAssetBalance -= totalPendingDeposits(); + } + + return _getConvertedMtM(true) + depositAssetBalance; + } + + function _getConvertedMtM(bool nativeDecimals) internal view returns (uint) { + BaseTSAAddresses memory tsaAddresses = getBaseTSAAddresses(); + + // Note: scenario 0 wont calculate full margin for PMRM subaccounts + (, int mtm) = tsaAddresses.manager.getMarginAndMarkToMarket(subAccount(), false, 0); + uint spotPrice = _getBasePrice(); + + // convert to depositAsset value but in 18dp + int convertedMtM = mtm.divideDecimal(spotPrice.toInt256()); + + if (nativeDecimals) { + // Now convert to appropriate decimals + uint8 decimals = tsaAddresses.depositAsset.decimals(); + if (decimals > 18) { + convertedMtM = convertedMtM * int(10 ** (decimals - 18)); + } else if (decimals < 18) { + convertedMtM = convertedMtM / int(10 ** (18 - decimals)); + } + } + + // Might not be technically insolvent (could have enough depositAsset to cover the deficit), but we block deposits + if (convertedMtM < 0) { + revert ETSA_PositionInsolvent(); + } + + return uint(convertedMtM); + } + + function _getBasePrice() internal view virtual returns (uint spotPrice); + + error ETSA_InvalidAsset(); + error ETSA_DepositingTooMuch(); + error ETSA_PositionInsolvent(); +} diff --git a/test/LyraForkUpgrade.t.sol b/test/LyraForkUpgrade.t.sol index 388ce4a..2798c91 100644 --- a/test/LyraForkUpgrade.t.sol +++ b/test/LyraForkUpgrade.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "forge-std/console2.sol"; import "../src/periphery/LyraSettlementUtils.sol"; -import {BaseTSA} from "../src/tokenizedSubaccounts/BaseTSA.sol"; +import {BaseTSA} from "../src/tokenizedSubaccounts/shared/BaseTSA.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {CashAsset} from "v2-core/src/assets/CashAsset.sol"; import {DutchAuction} from "v2-core/src/liquidation/DutchAuction.sol"; @@ -78,7 +78,17 @@ contract LyraForkUpgradeTest is ForkBase { manager: ILiquidatableManager(_getV2CoreContract("core", "srm")), matching: IMatching(_getV2CoreContract("matching", "matching")), symbol: tsaName, - name: string.concat("sUSDe ", "Principal Protected Bull Call Spread") + name: string.concat("sUSDe ", "Principal Protected Bull Call Spread"), + initialParams: BaseTSA.TSAParams({ + depositCap: 100000000e18, + minDepositValue: 0.01e18, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0, + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) }), PrincipalProtectedTSA.PPTSAInitParams({ baseFeed: ISpotFeed(_getV2CoreContract("sUSDe", "spotFeed")), @@ -95,16 +105,6 @@ contract LyraForkUpgradeTest is ForkBase { PrincipalProtectedTSA pptsa = PrincipalProtectedTSA(address(_getV2CoreContract(tsaName, "proxy"))); - pptsa.setTSAParams( - BaseTSA.TSAParams({ - depositCap: 100000000e18, - minDepositValue: 0.01e18, - depositScale: 1e18, - withdrawScale: 1e18, - managementFee: 0, - feeRecipient: address(0) - }) - ); pptsa.setPPTSAParams(defaultLrtppTSAParams); pptsa.setCollateralManagementParams(defaultCollateralManagementParams); pptsa.setShareKeeper(deployer, true); diff --git a/test/LyraForkUpgradeLBTC.t.sol b/test/LyraForkUpgradeLBTC.t.sol index 5b5f961..35b8248 100644 --- a/test/LyraForkUpgradeLBTC.t.sol +++ b/test/LyraForkUpgradeLBTC.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "forge-std/console2.sol"; import "../src/periphery/LyraSettlementUtils.sol"; -import {BaseTSA} from "../src/tokenizedSubaccounts/BaseTSA.sol"; +import {BaseTSA} from "../src/tokenizedSubaccounts/shared/BaseTSA.sol"; import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol"; import {CashAsset} from "v2-core/src/assets/CashAsset.sol"; import {DutchAuction} from "v2-core/src/liquidation/DutchAuction.sol"; @@ -31,88 +31,88 @@ import {LeveragedBasisTSA} from "../src/tokenizedSubaccounts/LevBasisTSA.sol"; contract LyraForkUpgradeTestLBTC is ForkBase { function setUp() external {} - function testForkUpgrade() external checkFork { - vm.assertEq(block.chainid, 957); // Owner is only prod - - address owner = 0xB176A44D819372A38cee878fB0603AEd4d26C5a5; - - LeveragedBasisTSA implementation = LeveragedBasisTSA(0x61B7A841965aC574E0f82644aD15327d50E7431C); - - vm.deal(owner, 1 ether); - vm.startPrank(owner); - - { - string memory marketName = "LBTC"; - string memory perpMarketName = "BTC"; - string memory vaultTokenName = string.concat("b", marketName); - string memory vaultFileName = string.concat(marketName, "B"); - - LeveragedBasisTSA tsa = LeveragedBasisTSA(_getMatchingContract(vaultFileName, "token")); - - BaseTSA.BaseTSAInitParams memory baseTsaInitParams = BaseTSA.BaseTSAInitParams({ - subAccounts: ISubAccounts(_getCoreContract("subAccounts")), - auction: DutchAuction(_getCoreContract("auction")), - cash: CashAsset(_getCoreContract("cash")), - wrappedDepositAsset: IWrappedERC20Asset(_getV2CoreContract(marketName, "base")), - manager: ILiquidatableManager(_getCoreContract("srm")), - matching: IMatching(_getMatchingModule("matching")), - symbol: vaultTokenName, - name: string.concat("Basis traded ", marketName) - }); - - LeveragedBasisTSA.LBTSAInitParams memory levBasisInitParams = LeveragedBasisTSA.LBTSAInitParams({ - baseFeed: ISpotFeed(_getV2CoreContract(marketName, "spotFeed")), - depositModule: IDepositModule(_getMatchingModule("deposit")), - withdrawalModule: IWithdrawalModule(_getMatchingModule("withdrawal")), - tradeModule: ITradeModule(_getMatchingModule("trade")), - perpAsset: IPerpAsset(_getV2CoreContract(perpMarketName, "perp")) - }); - - ProxyAdmin(_getMatchingContract(vaultFileName, "proxyAdmin")).upgradeAndCall( - ITransparentUpgradeableProxy(_getMatchingContract(vaultFileName, "token")), - address(implementation), - abi.encodeWithSelector(implementation.initialize.selector, owner, baseTsaInitParams, levBasisInitParams) - ); - - tsa.isSigner(0x76a4A01f5159674e21196E9e68847694F5f2e988); - tsa.setSubmitter(_getMatchingModule("atomicExecutor"), true); - } - - { - string memory marketName = "WEETH"; - string memory perpMarketName = "ETH"; - string memory vaultTokenName = string.concat("b", marketName); - string memory vaultFileName = string.concat(marketName, "B"); - - LeveragedBasisTSA tsa = LeveragedBasisTSA(_getMatchingContract(vaultFileName, "token")); - - BaseTSA.BaseTSAInitParams memory baseTsaInitParams = BaseTSA.BaseTSAInitParams({ - subAccounts: ISubAccounts(_getCoreContract("subAccounts")), - auction: DutchAuction(_getCoreContract("auction")), - cash: CashAsset(_getCoreContract("cash")), - wrappedDepositAsset: IWrappedERC20Asset(_getV2CoreContract(marketName, "base")), - manager: ILiquidatableManager(_getCoreContract("srm")), - matching: IMatching(_getMatchingModule("matching")), - symbol: vaultTokenName, - name: string.concat("Basis traded ", marketName) - }); - - LeveragedBasisTSA.LBTSAInitParams memory levBasisInitParams = LeveragedBasisTSA.LBTSAInitParams({ - baseFeed: ISpotFeed(_getV2CoreContract(marketName, "spotFeed")), - depositModule: IDepositModule(_getMatchingModule("deposit")), - withdrawalModule: IWithdrawalModule(_getMatchingModule("withdrawal")), - tradeModule: ITradeModule(_getMatchingModule("trade")), - perpAsset: IPerpAsset(_getV2CoreContract(perpMarketName, "perp")) - }); - - ProxyAdmin(_getMatchingContract(vaultFileName, "proxyAdmin")).upgradeAndCall( - ITransparentUpgradeableProxy(_getMatchingContract(vaultFileName, "token")), - address(implementation), - abi.encodeWithSelector(implementation.initialize.selector, owner, baseTsaInitParams, levBasisInitParams) - ); - - tsa.isSigner(0x76a4A01f5159674e21196E9e68847694F5f2e988); - tsa.setSubmitter(_getMatchingModule("atomicExecutor"), true); - } - } + // function testForkUpgrade() external checkFork { + // vm.assertEq(block.chainid, 957); // Owner is only prod + // + // address owner = 0xB176A44D819372A38cee878fB0603AEd4d26C5a5; + // + // LeveragedBasisTSA implementation = LeveragedBasisTSA(0x61B7A841965aC574E0f82644aD15327d50E7431C); + // + // vm.deal(owner, 1 ether); + // vm.startPrank(owner); + // + // { + // string memory marketName = "LBTC"; + // string memory perpMarketName = "BTC"; + // string memory vaultTokenName = string.concat("b", marketName); + // string memory vaultFileName = string.concat(marketName, "B"); + // + // LeveragedBasisTSA tsa = LeveragedBasisTSA(_getMatchingContract(vaultFileName, "token")); + // + // BaseTSA.BaseTSAInitParams memory baseTsaInitParams = BaseTSA.BaseTSAInitParams({ + // subAccounts: ISubAccounts(_getCoreContract("subAccounts")), + // auction: DutchAuction(_getCoreContract("auction")), + // cash: CashAsset(_getCoreContract("cash")), + // wrappedDepositAsset: IWrappedERC20Asset(_getV2CoreContract(marketName, "base")), + // manager: ILiquidatableManager(_getCoreContract("srm")), + // matching: IMatching(_getMatchingModule("matching")), + // symbol: vaultTokenName, + // name: string.concat("Basis traded ", marketName) + // }); + // + // LeveragedBasisTSA.LBTSAInitParams memory levBasisInitParams = LeveragedBasisTSA.LBTSAInitParams({ + // baseFeed: ISpotFeed(_getV2CoreContract(marketName, "spotFeed")), + // depositModule: IDepositModule(_getMatchingModule("deposit")), + // withdrawalModule: IWithdrawalModule(_getMatchingModule("withdrawal")), + // tradeModule: ITradeModule(_getMatchingModule("trade")), + // perpAsset: IPerpAsset(_getV2CoreContract(perpMarketName, "perp")) + // }); + // + // ProxyAdmin(_getMatchingContract(vaultFileName, "proxyAdmin")).upgradeAndCall( + // ITransparentUpgradeableProxy(_getMatchingContract(vaultFileName, "token")), + // address(implementation), + // abi.encodeWithSelector(implementation.initialize.selector, owner, baseTsaInitParams, levBasisInitParams) + // ); + // + // tsa.isSigner(0x76a4A01f5159674e21196E9e68847694F5f2e988); + // tsa.setSubmitter(_getMatchingModule("atomicExecutor"), true); + // } + // + // { + // string memory marketName = "WEETH"; + // string memory perpMarketName = "ETH"; + // string memory vaultTokenName = string.concat("b", marketName); + // string memory vaultFileName = string.concat(marketName, "B"); + // + // LeveragedBasisTSA tsa = LeveragedBasisTSA(_getMatchingContract(vaultFileName, "token")); + // + // BaseTSA.BaseTSAInitParams memory baseTsaInitParams = BaseTSA.BaseTSAInitParams({ + // subAccounts: ISubAccounts(_getCoreContract("subAccounts")), + // auction: DutchAuction(_getCoreContract("auction")), + // cash: CashAsset(_getCoreContract("cash")), + // wrappedDepositAsset: IWrappedERC20Asset(_getV2CoreContract(marketName, "base")), + // manager: ILiquidatableManager(_getCoreContract("srm")), + // matching: IMatching(_getMatchingModule("matching")), + // symbol: vaultTokenName, + // name: string.concat("Basis traded ", marketName) + // }); + // + // LeveragedBasisTSA.LBTSAInitParams memory levBasisInitParams = LeveragedBasisTSA.LBTSAInitParams({ + // baseFeed: ISpotFeed(_getV2CoreContract(marketName, "spotFeed")), + // depositModule: IDepositModule(_getMatchingModule("deposit")), + // withdrawalModule: IWithdrawalModule(_getMatchingModule("withdrawal")), + // tradeModule: ITradeModule(_getMatchingModule("trade")), + // perpAsset: IPerpAsset(_getV2CoreContract(perpMarketName, "perp")) + // }); + // + // ProxyAdmin(_getMatchingContract(vaultFileName, "proxyAdmin")).upgradeAndCall( + // ITransparentUpgradeableProxy(_getMatchingContract(vaultFileName, "token")), + // address(implementation), + // abi.encodeWithSelector(implementation.initialize.selector, owner, baseTsaInitParams, levBasisInitParams) + // ); + // + // tsa.isSigner(0x76a4A01f5159674e21196E9e68847694F5f2e988); + // tsa.setSubmitter(_getMatchingModule("atomicExecutor"), true); + // } + // } } diff --git a/test/tokenizedSubaccounts/BaseTSA/BaseTSA_Admin.t.sol b/test/tokenizedSubaccounts/BaseTSA/BaseTSA_Admin.t.sol index 86e71d9..72551d7 100644 --- a/test/tokenizedSubaccounts/BaseTSA/BaseTSA_Admin.t.sol +++ b/test/tokenizedSubaccounts/BaseTSA/BaseTSA_Admin.t.sol @@ -29,7 +29,7 @@ contract CCTSA_BaseTSA_Admin is CCTSATestUtils { BaseTSA.TSAParams memory params = cctsa.getTSAParams(); - params.managementFee = 0.02e18 + 1; + params.managementFee = 0.2e18 + 1; vm.expectRevert(BaseTSA.BTSA_InvalidParams.selector); cctsa.setTSAParams(params); diff --git a/test/tokenizedSubaccounts/BaseTSA/BaseTSA_Fees.t.sol b/test/tokenizedSubaccounts/BaseTSA/BaseTSA_Fees.t.sol index a00517d..8837ede 100644 --- a/test/tokenizedSubaccounts/BaseTSA/BaseTSA_Fees.t.sol +++ b/test/tokenizedSubaccounts/BaseTSA/BaseTSA_Fees.t.sol @@ -19,14 +19,14 @@ contract CCTSA_BaseTSA_FeesTests is CCTSATestUtils { function testFeeCollection() public { // - check params are as expected for a fresh deploy - assertEq(cctsa.lastFeeCollected(), block.timestamp); + assertEq(_getLastFeeCollected(), block.timestamp); // - check params are set correctly when a deposit comes through vm.warp(block.timestamp + 1 hours); - assertEq(cctsa.lastFeeCollected(), block.timestamp - 1 hours); + assertEq(_getLastFeeCollected(), block.timestamp - 1 hours); _depositToTSA(1e18); - assertEq(cctsa.lastFeeCollected(), block.timestamp); + assertEq(_getLastFeeCollected(), block.timestamp); // - check no fee is collected when feeRecipient is the zero address CoveredCallTSA.TSAParams memory params = cctsa.getTSAParams(); @@ -38,7 +38,7 @@ contract CCTSA_BaseTSA_FeesTests is CCTSATestUtils { vm.warp(block.timestamp + 1 hours); cctsa.collectFee(); - assertEq(cctsa.lastFeeCollected(), block.timestamp); + assertEq(_getLastFeeCollected(), block.timestamp); assertEq(cctsa.balanceOf(address(0)), 0); // - check no fee is collected when feeRate is 0 @@ -50,7 +50,7 @@ contract CCTSA_BaseTSA_FeesTests is CCTSATestUtils { vm.warp(block.timestamp + 1 hours); cctsa.collectFee(); - assertEq(cctsa.lastFeeCollected(), block.timestamp); + assertEq(_getLastFeeCollected(), block.timestamp); assertEq(cctsa.balanceOf(address(alice)), 0); // - check fees are collected correctly @@ -63,7 +63,7 @@ contract CCTSA_BaseTSA_FeesTests is CCTSATestUtils { vm.warp(block.timestamp + 365 days); cctsa.collectFee(); - assertEq(cctsa.lastFeeCollected(), block.timestamp); + assertEq(_getLastFeeCollected(), block.timestamp); assertEq(cctsa.balanceOf(address(alice)), 0.01e18); // - check fees are collected correctly with pending withdrawals @@ -74,7 +74,7 @@ contract CCTSA_BaseTSA_FeesTests is CCTSATestUtils { vm.warp(block.timestamp + 365 days); cctsa.collectFee(); - assertEq(cctsa.lastFeeCollected(), block.timestamp); + assertEq(_getLastFeeCollected(), block.timestamp); // alice gets more than 1% as total supply is now 1.01 instead of 1 assertEq(cctsa.balanceOf(address(alice)), 0.0201e18); @@ -101,15 +101,20 @@ contract CCTSA_BaseTSA_FeesTests is CCTSATestUtils { cctsa.setTSAParams(params); - assertEq(cctsa.lastFeeCollected(), block.timestamp); + assertEq(_getLastFeeCollected(), block.timestamp); // Collecing fee with 0 total supply still updates timestamp vm.warp(block.timestamp + 1); cctsa.collectFee(); - assertEq(cctsa.lastFeeCollected(), block.timestamp); + assertEq(_getLastFeeCollected(), block.timestamp); } function testFeeCollectionWithDifferentDecimals() public { // TODO } + + function _getLastFeeCollected() internal view returns (uint) { + (uint lastFeeCollected,,) = cctsa.getFeeValues(); + return lastFeeCollected; + } } diff --git a/test/tokenizedSubaccounts/BaseTSA/BaseTSA_PerformanceFee.sol b/test/tokenizedSubaccounts/BaseTSA/BaseTSA_PerformanceFee.sol new file mode 100644 index 0000000..9fd4edf --- /dev/null +++ b/test/tokenizedSubaccounts/BaseTSA/BaseTSA_PerformanceFee.sol @@ -0,0 +1,324 @@ +import "../../../src/tokenizedSubaccounts/shared/BaseTSA.sol"; +import "../utils/CCTSATestUtils.sol"; + +contract CCTSA_BaseTSA_PerformanceFeesTests is CCTSATestUtils { + address public feeRecipient = address(0xaaafff); + + BaseTSA.TSAParams public defaultPerfTestParams = BaseTSA.TSAParams({ + depositCap: 10000e18, + minDepositValue: 1e18, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0, + feeRecipient: feeRecipient, + performanceFeeWindow: 1 weeks, + performanceFee: 0.2e18 + }); + + function setUp() public override { + vm.warp(block.timestamp + 1); + super.setUp(); + deployPredeposit(address(0)); + upgradeToCCTSA(MARKET); + setupCCTSA(); + + tsa.setTSAParams(defaultPerfTestParams); + } + + function testAdminCannotSetInvalidParams() public { + BaseTSA.TSAParams memory params = defaultPerfTestParams; + params.performanceFeeWindow = 0; + vm.expectRevert(BaseTSA.BTSA_InvalidParams.selector); + tsa.setTSAParams(params); + + params.performanceFeeWindow = defaultPerfTestParams.performanceFeeWindow; + params.performanceFee = 1e18 + 1; + + vm.expectRevert(BaseTSA.BTSA_InvalidParams.selector); + tsa.setTSAParams(params); + + params.performanceFee = 1e18; + tsa.setTSAParams(params); + + vm.assertEq(tsa.getTSAParams().performanceFee, 1e18); + } + + //## Basic Performance Fee Collection + //- Test performance fee collection when share price increases within fee window + //- Test no fee collection when share price decreases within fee window + //- Test no fee collection when share price remains unchanged + + function testPerformanceFeeCollection() public { + // Initial deposit to set up the account + _depositToTSA(1000 * MARKET_UNIT); + + (uint lastFeeCollected, uint perfSnapshotTime, uint perfSnapshotValue) = tsa.getFeeValues(); + + vm.assertEq(perfSnapshotTime, block.timestamp); + vm.assertEq(perfSnapshotValue, 1e18); + + // mint tokens to the TSA directly to simulate positive performance + + markets[MARKET].erc20.mint(address(tsa), 1500 * MARKET_UNIT); + + vm.assertEq(tsa.getSharesValue(1e18), 2.5e18); + + // Collect performance fee + vm.warp(block.timestamp + 1 weeks); + tsa.collectFee(); + + // profit of 1500, fee of 0.2 * 1500 = $300 of value + // 300 / 2.5 = 120 shares BUT that doesnt account for dilution + // correct amount would be ~136.36 + + vm.assertApproxEqAbs(tsa.balanceOf(feeRecipient), 136.3636363636e18 * MARKET_UNIT / 1e18, 1e14); + + vm.assertApproxEqAbs(tsa.getSharesValue(tsa.balanceOf(feeRecipient)), 300e18, 1e14); + } + + function testNoFeeCollectionWhenSharePriceDecreases() public { + // Initial deposit to set up the account + _depositToTSA(1000 * MARKET_UNIT); + + (uint lastFeeCollected, uint perfSnapshotTime, uint perfSnapshotValue) = tsa.getFeeValues(); + + vm.assertEq(perfSnapshotTime, block.timestamp); + vm.assertEq(perfSnapshotValue, 1e18); + + // burn tokens from the TSA directly to simulate negative performance + markets[MARKET].erc20.burn(address(tsa), 500 * MARKET_UNIT); + + vm.assertEq(tsa.getSharesValue(1e18), 0.5e18); + + // Collect performance fee + vm.warp(block.timestamp + 1 weeks); + tsa.collectFee(); + + // No fee should be collected since share price decreased + vm.assertApproxEqAbs(tsa.balanceOf(feeRecipient), 0, 1e14); + } + + function testNoFeeCollectionWhenSharePriceUnchanged() public { + // Initial deposit to set up the account + _depositToTSA(1000 * MARKET_UNIT); + + (uint lastFeeCollected, uint perfSnapshotTime, uint perfSnapshotValue) = tsa.getFeeValues(); + + vm.assertEq(perfSnapshotTime, block.timestamp); + vm.assertEq(perfSnapshotValue, 1e18); + + // No change in share price + vm.assertEq(tsa.getSharesValue(1e18), 1e18); + + // Collect performance fee + vm.warp(block.timestamp + 1 weeks); + tsa.collectFee(); + + // No fee should be collected since share price remained unchanged + vm.assertApproxEqAbs(tsa.balanceOf(feeRecipient), 0, 1e14); + } + + //## Fee Window Behavior + //- Test performance snapshot reset after fee window elapses + //- Test multiple fee collection attempts within same window don't collect additional fees + //- Test snapshot value updates correctly when window passes + + function testFeeWindowModification() public { + // Initial deposit to set up the account + _depositToTSA(1000 * MARKET_UNIT); + + (uint lastFeeCollected, uint perfSnapshotTime, uint perfSnapshotValue) = tsa.getFeeValues(); + + vm.assertEq(perfSnapshotTime, block.timestamp); + vm.assertEq(perfSnapshotValue, 1e18); + + vm.warp(block.timestamp + 3 days); + + tsa.collectFee(); + vm.assertEq(tsa.getSharesValue(tsa.balanceOf(feeRecipient)), 0); + + (lastFeeCollected, perfSnapshotTime, perfSnapshotValue) = tsa.getFeeValues(); + + vm.assertEq(perfSnapshotTime, block.timestamp - 3 days); + vm.assertEq(perfSnapshotValue, 1e18); + + // If you skip past the window (+4 days) it will still record current block.timestamp + vm.warp(block.timestamp + 5 weeks); + tsa.collectFee(); + vm.assertEq(tsa.getSharesValue(tsa.balanceOf(feeRecipient)), 0); + + (lastFeeCollected, perfSnapshotTime, perfSnapshotValue) = tsa.getFeeValues(); + vm.assertEq(perfSnapshotTime, block.timestamp); + + vm.warp(block.timestamp + 2 days); + + BaseTSA.TSAParams memory params = defaultPerfTestParams; + params.performanceFeeWindow = 1 days; + tsa.setTSAParams(params); + + // The perfSnapshotTime doesnt update when params are changed, as they get snapshot BEFORE the change is applied + (lastFeeCollected, perfSnapshotTime, perfSnapshotValue) = tsa.getFeeValues(); + vm.assertEq(lastFeeCollected, block.timestamp); + vm.assertEq(perfSnapshotTime, block.timestamp - 2 days); + + // but even 1 second later, you can collect the fee + vm.warp(block.timestamp + 1); + tsa.collectFee(); + (lastFeeCollected, perfSnapshotTime, perfSnapshotValue) = tsa.getFeeValues(); + vm.assertEq(lastFeeCollected, block.timestamp); + vm.assertEq(perfSnapshotTime, block.timestamp); + vm.assertApproxEqAbs(tsa.balanceOf(feeRecipient), 0, 1e14); + + // The opposite is also true - increasing the window AFTER the old window has passed, will still collect the fee + vm.warp(block.timestamp + 2 days); + params.performanceFeeWindow = 1 weeks; + tsa.setTSAParams(params); + + (lastFeeCollected, perfSnapshotTime, perfSnapshotValue) = tsa.getFeeValues(); + vm.assertEq(lastFeeCollected, block.timestamp); + vm.assertEq(perfSnapshotTime, block.timestamp); + vm.assertApproxEqAbs(tsa.balanceOf(feeRecipient), 0, 1e14); + } + + //## Withdrawal-Specific Performance Fees + //- Test withdrawal performance fee calculation accuracy + //- Test partial withdrawal fee calculation + //- Test withdrawal fees with rising/falling share prices + + function testWithdrawalPerformanceFee() public { + _depositToTSA(1000 * MARKET_UNIT); + // +100% pnl + markets[MARKET].erc20.mint(address(tsa), 1000 * MARKET_UNIT); + + _executeDeposit(2000 * MARKET_UNIT); + + tsa.requestWithdrawal(1000 * MARKET_UNIT); + + // no funds available, so no fee + tsa.processWithdrawalRequests(1); + vm.assertApproxEqAbs(tsa.balanceOf(feeRecipient), 0, 1e14); + + ///////// + + _executeWithdrawal(100 * MARKET_UNIT); + + // $100 now available for withdraw + tsa.processWithdrawalRequests(1); + + // 50 shares processed as only $100 available + // since the fee is $10 on "100", that is 10% + // 100 / (1 - 0.1) = 111.11 "available" + // we process $111.11 worth of shares, the value of $100 is paid out to the user, $11.11 to the feeRecipient as + // shares that are burnt from the user first + + // 55.5555 removed, then 5.5555 added + vm.assertApproxEqAbs(tsa.getSharesValue(1e18), 2e18, 1e14, "share price still 2"); + vm.assertApproxEqAbs(tsa.totalSupply(), 950e18, 1e14, "feeRecipient shares value"); + vm.assertApproxEqAbs(markets[MARKET].erc20.balanceOf(address(tsa)), 0, 1e14); + vm.assertApproxEqAbs(tsa.balanceOf(feeRecipient), 5.55555555e18, 1e14, "feeRecipient shares balance"); + vm.assertApproxEqAbs( + tsa.getSharesValue(tsa.balanceOf(feeRecipient)), 11.11111111e18, 1e14, "feeRecipient shares value" + ); + vm.assertApproxEqAbs( + markets[MARKET].erc20.balanceOf(address(this)), 100 * MARKET_UNIT, 1e14, "feeRecipient shares value" + ); + + ///////// + + // snapshot the state, we will try to complete this withdrawal a few different ways + uint snapshot = vm.snapshot(); + + // Case 1 complete withdrawal in full + { + // now we withdraw the rest + _executeWithdrawal(1900 * MARKET_UNIT); + // we process the rest of the shares + tsa.processWithdrawalRequests(1); + + // User has 950 - 5.555555 = 944.4444 shares + // 944.4444 * 2 = 1888.8888 + // performance fee is still 10% of the value + // so we expect 1888.888 * 0.1 = 188.8888 value to be sent to the feeRecipient + // 188.8888 / 2 = 94.4444 shares + // and the user to receive 1888.888 - 188.8888 = 1700.0000 + // So the fee recipient is left with 94.4444 + 5.5555 = 100 shares, worth 200, which is 10% of initial amount + + vm.assertApproxEqAbs(tsa.getSharesValue(1e18), 2e18, 1e14, "share price still 2"); + vm.assertApproxEqAbs(tsa.totalSupply(), 100 * MARKET_UNIT, 1e14, "feeRecipient shares value"); + vm.assertApproxEqAbs(tsa.balanceOf(feeRecipient), 100 * MARKET_UNIT, 1e14, "feeRecipient shares balance"); + vm.assertApproxEqAbs( + tsa.getSharesValue(tsa.balanceOf(feeRecipient)), 200 * MARKET_UNIT, 1e14, "feeRecipient shares value" + ); + vm.assertApproxEqAbs(markets[MARKET].erc20.balanceOf(address(tsa)), 200 * MARKET_UNIT, 1e14); + } + + vm.revertTo(snapshot); + snapshot = vm.snapshot(); + // Case 2, gather performance fee normally first, then withdraw remainder with no fee + { + vm.warp(block.timestamp + 1 weeks); + tsa.collectFee(); + + // total supply is increased + // shares are diluted + // but the fee is still worth 10% of the value, and the user still has the same amount of share value + + vm.assertApproxEqAbs(tsa.getSharesValue(1e18), 1.8e18, 1e14, "2a: share value"); + vm.assertApproxEqAbs(tsa.totalSupply(), 950e18 + 105.55555555e18, 1e14, "2a: totalSupply"); + // fee recipient still only has $200 worth of shares, even though shares are diluted (they have more total shares!) + vm.assertApproxEqAbs( + tsa.getSharesValue(tsa.balanceOf(feeRecipient)), 200e18, 1e14, "2a: fee collected share value" + ); + + // now we withdraw the rest + _executeWithdrawal(1900 * MARKET_UNIT); + tsa.processWithdrawalRequests(1); + + vm.assertApproxEqAbs(tsa.getSharesValue(1e18), 1.8e18, 1e14, "2b: share price"); + vm.assertApproxEqAbs(tsa.totalSupply(), 111.111111e18, 1e14, "2b: total supply"); + vm.assertApproxEqAbs(tsa.balanceOf(feeRecipient), 111.111111e18, 1e14, "2b: feeRecipient shares balance"); + vm.assertApproxEqAbs( + tsa.getSharesValue(tsa.balanceOf(feeRecipient)), 200e18, 1e14, "2b: feeRecipient shares value" + ); + } + + vm.revertTo(snapshot); + // Case 3, gather performance fee normally first, increase Share value, and withdraw partially with fee + { + vm.warp(block.timestamp + 1 weeks); + tsa.collectFee(); + + // total supply is increased + // shares are diluted + // but the fee is still worth 10% of the value, and the user still has the same amount of share value + + // 1900 in total pool value (including shares minted to fee recipient), so we add 950 to add 50% profit + markets[MARKET].erc20.mint(address(tsa), 950 * MARKET_UNIT); + + // This increases the value of fees collected already from 200 to 300 (+50%) + // User is left with 950 shares, worth 1900 + + vm.assertApproxEqAbs(tsa.getSharesValue(1e18), 2.7e18, 1e14, "3a: share price"); + vm.assertApproxEqAbs(tsa.totalSupply(), 950e18 + 105.55555555e18, 1e14, "3a: totalSupply"); + // fee recipient still only has $200 worth of shares, even though shares are diluted (they have more total shares!) + vm.assertApproxEqAbs( + tsa.getSharesValue(tsa.balanceOf(feeRecipient)), 300e18, 1e14, "3a: fee collected share value" + ); + vm.assertApproxEqAbs(tsa.queuedWithdrawal(0).amountShares, 944.44444e18, 1e14, "3a: user share amount"); + vm.assertApproxEqAbs( + tsa.getSharesValue(tsa.queuedWithdrawal(0).amountShares), 2550e18, 1e14, "3a: user share value" + ); + + // now we withdraw the rest (the 950 was not deposited to subaccount) + _executeWithdrawal(1900 * MARKET_UNIT); + tsa.processWithdrawalRequests(1); + + vm.assertApproxEqAbs(tsa.getSharesValue(1e18), 2.7e18, 1e14, "3b: share price"); + vm.assertApproxEqAbs(tsa.totalSupply(), 174.074074e18, 1e14, "3b: total supply"); + vm.assertApproxEqAbs(tsa.balanceOf(feeRecipient), 174.074074e18, 1e14, "3b: feeRecipient shares balance"); + vm.assertApproxEqAbs( + tsa.getSharesValue(tsa.balanceOf(feeRecipient)), 470e18, 1e14, "3b: feeRecipient shares value" + ); + } + } +} diff --git a/test/tokenizedSubaccounts/CCTSA/CCTSA_Validation.t.sol b/test/tokenizedSubaccounts/CCTSA/CCTSA_Validation.t.sol index 165769a..f13089b 100644 --- a/test/tokenizedSubaccounts/CCTSA/CCTSA_Validation.t.sol +++ b/test/tokenizedSubaccounts/CCTSA/CCTSA_Validation.t.sol @@ -1,6 +1,7 @@ pragma solidity ^0.8.18; import "../utils/CCTSATestUtils.sol"; +import {EmptyTSA} from "../../../src/tokenizedSubaccounts/shared/EmptyTSA.sol"; /* TODO: liquidation of subaccount @@ -214,12 +215,12 @@ contract CCTSA_ValidationTests is CCTSATestUtils { // cannot deposit more than is available IActionVerifier.Action memory action = _createDepositAction(2e18); - vm.expectRevert(CollateralManagementTSA.CMTSA_DepositingTooMuch.selector); + vm.expectRevert(EmptyTSA.ETSA_DepositingTooMuch.selector); cctsa.signActionData(action, ""); // reverts for invalid assets. action.data = _encodeDepositData(1e18, address(11111), address(0)); - vm.expectRevert(CollateralManagementTSA.CMTSA_InvalidAsset.selector); + vm.expectRevert(EmptyTSA.ETSA_InvalidAsset.selector); cctsa.signActionData(action, ""); action.expiry = block.timestamp + defaultCCTSAParams.minSignatureExpiry - 1; diff --git a/test/tokenizedSubaccounts/CCTSA/CCTSA_Views.t.sol b/test/tokenizedSubaccounts/CCTSA/CCTSA_Views.t.sol index 1205b7d..dd6d046 100644 --- a/test/tokenizedSubaccounts/CCTSA/CCTSA_Views.t.sol +++ b/test/tokenizedSubaccounts/CCTSA/CCTSA_Views.t.sol @@ -1,6 +1,7 @@ pragma solidity ^0.8.18; import "../utils/CCTSATestUtils.sol"; +import {EmptyTSA} from "../../../src/tokenizedSubaccounts/shared/EmptyTSA.sol"; /* Account Value - correctly calculates the account value when there is no ongoing liquidation. @@ -111,7 +112,7 @@ contract CCTSA_ViewsTests is CCTSATestUtils { assertLt(margin, 0, "mm: mm & mtm < 0"); assertLt(mtm, 0, "mtm: mm & mtm < 0"); - vm.expectRevert(CollateralManagementTSA.CMTSA_PositionInsolvent.selector); + vm.expectRevert(EmptyTSA.ETSA_PositionInsolvent.selector); cctsa.getAccountValue(false); } diff --git a/test/tokenizedSubaccounts/GeneralisedTSA/GeneralisedTSA.t.sol b/test/tokenizedSubaccounts/GeneralisedTSA/GeneralisedTSA.t.sol new file mode 100644 index 0000000..c9c84dd --- /dev/null +++ b/test/tokenizedSubaccounts/GeneralisedTSA/GeneralisedTSA.t.sol @@ -0,0 +1,333 @@ +import {Initializable} from "openzeppelin-upgradeable/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "openzeppelin-upgradeable/access/OwnableUpgradeable.sol"; +import "../utils/EGTSATestUtils.sol"; + +//# Test Cases for EMAGeneralisedTSA + +contract EMAGeneralisedTSA_Tests is EGTSATestUtils { + using SignedMath for int; + + function setUp() public override { + super.setUp(); + deployPredeposit(address(0)); + upgradeToGTSA(); + setupGTSA(); + } + + function test_init() external { + //## Initialization Tests + //- Initialize with valid parameters and verify all modules are set correctly + //- Attempt re-initialization (should fail with reinitializer modifier) + //- Verify deposit asset approval to deposit module after initialization + + (ISpotFeed sf, IDepositModule dm, IWithdrawalModule wm, ITradeModule tm, IRfqModule rm) = gtsa.getGTSAAddresses(); + + vm.assertEq(address(sf), address(markets[MARKET].spotFeed)); + vm.assertEq(address(dm), address(depositModule)); + vm.assertEq(address(wm), address(withdrawalModule)); + vm.assertEq(address(tm), address(tradeModule)); + vm.assertEq(address(rm), address(rfqModule)); + + vm.assertEq(gtsa.getBasePrice(), MARKET_REF_SPOT); + + (int markLossEma, uint markLossLastTs) = gtsa.getEmaValues(); + + vm.assertEq(markLossEma, 0); + vm.assertEq(markLossLastTs, 0); + + vm.expectRevert(Initializable.InvalidInitialization.selector); + proxyAdmin.upgradeAndCall( + ITransparentUpgradeableProxy(address(proxy)), + address(tsaImplementation), + abi.encodeWithSelector( + tsaImplementation.initialize.selector, + address(this), + BaseTSA.BaseTSAInitParams({ + subAccounts: subAccounts, + auction: auction, + cash: cash, + wrappedDepositAsset: markets[MARKET].base, + manager: srm, + matching: matching, + symbol: "GTSA", + name: "GTSA", + initialParams: BaseTSA.TSAParams({ + depositCap: 10000e18, + minDepositValue: 1e18, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0, + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) + }), + EMAGeneralisedTSA.GTSAInitParams({ + baseFeed: markets[MARKET].spotFeed, + depositModule: depositModule, + withdrawalModule: withdrawalModule, + tradeModule: tradeModule, + rfqModule: rfqModule + }) + ) + ); + + // Check deposit asset approval to the deposit module + vm.assertEq(markets[MARKET].erc20.allowance(address(gtsa), address(depositModule)), type(uint).max); + + gtsa.setGTSAParams(0.002e18, 0.05e18); + } + + //## Admin Function Tests + //- Set valid EMA parameters (emaDecayFactor > 0, markLossEmaTarget < 0.5e18) + //- Set invalid EMA parameters and verify revert (emaDecayFactor = 0) + //- Set invalid EMA parameters and verify revert (markLossEmaTarget >= 0.5e18) + //- Reset decay parameters and verify markLossLastTs updated to current timestamp + //- Enable various assets and verify they're properly recorded + //- Verify only owner can call admin functions + function test_adminFunctions() external { + // Set valid EMA parameters + gtsa.setGTSAParams(0.001e18, 0.05e18); + (int markLossEma, uint markLossLastTs) = gtsa.getEmaValues(); + vm.assertEq(markLossEma, 0); + vm.assertEq(markLossLastTs, 0); + + // Set invalid EMA parameters and verify revert + vm.expectRevert(EMAGeneralisedTSA.GT_InvalidParams.selector); + gtsa.setGTSAParams(0, 0.02e18); + + vm.expectRevert(EMAGeneralisedTSA.GT_InvalidParams.selector); + gtsa.setGTSAParams(0.0002e18, 0.6e18); + + // Reset decay parameters + gtsa.resetDecay(); + (markLossEma, markLossLastTs) = gtsa.getEmaValues(); + vm.assertEq(markLossEma, 0); + // resetting will update the last timestamp + vm.assertEq(markLossLastTs, block.timestamp); + + // Enable various assets + gtsa.enableAsset(address(markets[MARKET].erc20)); + gtsa.enableAsset(address(markets[MARKET].base)); + + // Verify only owner can call admin functions + vm.startPrank(address(1)); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, address(1))); + gtsa.setGTSAParams(0.0002e18, 0.02e18); + } + + //### Trade Action Tests + //- Verify successful trade with wrapped deposit asset + //- Verify successful trade with enabled asset + //- Verify trade with non-enabled asset fails + //- Verify trade when EMA mark loss exceeds threshold fails + // + //## EMA Logic Tests + //- Test mark loss calculation when share price increases + //- Test mark loss calculation when share price decreases + //- Verify action succeeds when EMA loss is below target + //- Verify action succeeds when current EMA loss <= previous EMA loss (allows recovery) + //- Verify action fails when EMA loss exceeds target and is increasing + function test_GTSA_tradeActions() external { + _depositToTSA(100 * MARKET_UNIT); + _executeDeposit(50 * MARKET_UNIT); + // Verify successful trade with wrapped deposit asset + + _tradeSpot(-0.2e18, MARKET_REF_SPOT); + ISubAccounts.AssetBalance[] memory assetBalances = subAccounts.getAccountBalances(tsa.subAccount()); + + vm.assertEq(assetBalances.length, 2); + + // cannot trade perp until the asset is enabled + (IActionVerifier.Action[] memory actions, bytes[] memory signatures, bytes memory actionData) = + _getPerpTradeData(-0.2e18, MARKET_REF_SPOT); + + vm.prank(signer); + vm.expectRevert(EMAGeneralisedTSA.GT_InvalidTradeAsset.selector); + tsa.signActionData(actions[0], ""); + + // After enabling the asset, can sign the action/execute the trade + gtsa.enableAsset(address(markets[MARKET].perp)); + + vm.prank(signer); + tsa.signActionData(actions[0], ""); + + _verifyAndMatch(actions, signatures, actionData); + + assetBalances = subAccounts.getAccountBalances(tsa.subAccount()); + vm.assertEq(assetBalances.length, 3); + + // Trade will fail if EMA mark loss exceeds threshold + + // Lose about 10% of the vaults value by burning + markets[MARKET].erc20.burn(address(tsa), 10 * MARKET_UNIT); + + // can just sign the previous action to verify it fails + vm.prank(signer); + vm.expectRevert(EMAGeneralisedTSA.GT_MarkLossTooHigh.selector); + tsa.signActionData(actions[0], ""); + + // Check the mark loss + // doesnt update because the above reverted + (int markLossEma, uint markLossLastTs) = gtsa.getEmaValues(); + vm.assertEq(markLossEma, 0); + + // but can be prodded to be updated + gtsa.updateEMA(); + (markLossEma, markLossLastTs) = gtsa.getEmaValues(); + vm.assertEq(markLossEma, 0.1e18); + + // now we can wait to decay this, 0.002 would decay approx 50% every 1hr + + vm.warp(block.timestamp + 1 hours); + gtsa.updateEMA(); + (markLossEma, markLossLastTs) = gtsa.getEmaValues(); + vm.assertApproxEqRel(markLossEma, 0.05e18, 0.1e18); // within 10% + + // We don't accept "recovery" like we did with mark loss in the LevBasisTSA, since this would be too easily + // manipulable (updateEMA, donate a tiny amount, trade, repeat...) + vm.expectRevert(EMAGeneralisedTSA.GT_MarkLossTooHigh.selector); + vm.prank(signer); + tsa.signActionData(actions[0], ""); + + // since the threshold is 2%, we can trade after 2 more hours! (signature passes) + vm.warp(block.timestamp + 2 hours); + vm.prank(signer); + tsa.signActionData(actions[0], ""); + } + + //### RFQ Action Tests + //- Verify valid RFQ action with extraData = 0 + //- Verify valid RFQ action with matching orderHash + //- Verify RFQ action with mismatched orderHash fails + //- Verify RFQ action with non-enabled assets fails + + function test_GTSA_rfqMaker() external { + _depositToTSA(100 * MARKET_UNIT); + _executeDeposit(50 * MARKET_UNIT); + + (IRfqModule.RfqOrder memory makerOrder, IRfqModule.TakerOrder memory takerOrder) = _setupRfq( + 10e18, + MARKET_REF_SPOT / 10, + block.timestamp + 1 weeks, + MARKET_REF_SPOT, + MARKET_REF_SPOT / 20, + MARKET_REF_SPOT * 12 / 10, + true + ); + + (IActionVerifier.Action[] memory actions, bytes[] memory signatures) = + _getRfqAsMakerSignaturesAndActions(makerOrder, takerOrder); + + vm.prank(signer); + vm.expectRevert(EMAGeneralisedTSA.GT_InvalidTradeAsset.selector); + tsa.signActionData(actions[0], ""); + + // Enable the asset + gtsa.enableAsset(address(markets[MARKET].option)); + + vm.prank(signer); + tsa.signActionData(actions[0], ""); + + // now verify we can execute the trade + + IRfqModule.FillData memory fill = IRfqModule.FillData({ + makerAccount: tsaSubacc, + takerAccount: nonVaultSubacc, + makerFee: 0, + takerFee: 0, + managerData: bytes("") + }); + + _verifyAndMatch(actions, signatures, abi.encode(fill)); + } + + function test_GTSA_rfqTaker() external { + _depositToTSA(100 * MARKET_UNIT); + _executeDeposit(50 * MARKET_UNIT); + + (IRfqModule.RfqOrder memory makerOrder, IRfqModule.TakerOrder memory takerOrder) = _setupRfq( + 10e18, + MARKET_REF_SPOT / 10, + block.timestamp + 1 weeks, + MARKET_REF_SPOT, + MARKET_REF_SPOT / 20, + MARKET_REF_SPOT * 12 / 10, + true + ); + + IActionVerifier.Action memory takerAction = _createRfqAction(takerOrder); + + vm.prank(signer); + vm.expectRevert(EMAGeneralisedTSA.GT_InvalidTradeAsset.selector); + tsa.signActionData(takerAction, abi.encode(makerOrder.trades)); + + bytes32 orderHash = takerOrder.orderHash; + takerOrder.orderHash = bytes32(0); + takerAction.data = abi.encode(takerOrder); + + vm.prank(signer); + vm.expectRevert(EMAGeneralisedTSA.GT_TradeDataDoesNotMatchOrderHash.selector); + tsa.signActionData(takerAction, abi.encode(makerOrder.trades)); + + takerOrder.orderHash = orderHash; + takerAction.data = abi.encode(takerOrder); + + // Enable the asset + gtsa.enableAsset(address(markets[MARKET].option)); + + vm.prank(signer); + tsa.signActionData(takerAction, abi.encode(makerOrder.trades)); + } + + // + //### Withdrawal Action Tests + //- Verify withdrawal of wrapped deposit asset + //- Verify withdrawal of non-wrapped deposit asset fails + //- Verify withdrawal of non-enabled asset (dust removal scenario) + + function test_GTSA_withdrawal() public { + _depositToTSA(100 * MARKET_UNIT); + _executeDeposit(50 * MARKET_UNIT); + + // Verify withdrawal of wrapped deposit asset + _executeWithdrawal(50 * MARKET_UNIT); + + // donate to subaccount + markets[NOT_MARKET].erc20.mint(address(this), 1e18); + markets[NOT_MARKET].erc20.approve(address(markets[NOT_MARKET].base), type(uint).max); + markets[NOT_MARKET].base.deposit(tsa.subAccount(), 1e18); + + IActionVerifier.Action memory action = _createWithdrawalAction(0.5e18, address(markets[NOT_MARKET].base)); + + vm.prank(signer); + tsa.signActionData(action, ""); + + _submitToMatching(action); + + gtsa.enableAsset(address(markets[NOT_MARKET].base)); + + // Verify withdrawal of enabled asset fails + vm.expectRevert(EMAGeneralisedTSA.GT_InvalidWithdrawAsset.selector); + vm.prank(signer); + tsa.signActionData(action, ""); + } + //## View Function Tests + //- Verify getAccountValue returns correct values with includePending=true/false + //- Verify getBasePrice returns correct price from feed + //- Verify lastSeenHash returns latest action hash + //- Verify getLBTSAEmaValues returns current EMA state + //- Verify getLBTSAAddresses returns correct module addresses + + function _createRfqAction(IRfqModule.TakerOrder memory takerOrder) internal returns (IActionVerifier.Action memory) { + return IActionVerifier.Action({ + subaccountId: tsaSubacc, + nonce: ++tsaNonce, + module: rfqModule, + data: abi.encode(takerOrder), + expiry: block.timestamp + 8 minutes, + owner: address(tsa), + signer: address(tsa) + }); + } +} diff --git a/test/tokenizedSubaccounts/utils/CCTSATestUtils.sol b/test/tokenizedSubaccounts/utils/CCTSATestUtils.sol index d7e27c6..c9b2d99 100644 --- a/test/tokenizedSubaccounts/utils/CCTSATestUtils.sol +++ b/test/tokenizedSubaccounts/utils/CCTSATestUtils.sol @@ -46,6 +46,8 @@ contract CCTSATestUtils is TSATestUtils { tsaImplementation = new CoveredCallTSA(); + console.log("baseFeed: ", address(baseFeed)); + proxyAdmin.upgradeAndCall( ITransparentUpgradeableProxy(address(proxy)), address(tsaImplementation), @@ -60,7 +62,17 @@ contract CCTSATestUtils is TSATestUtils { manager: srm, matching: matching, symbol: "Tokenised SubAccount", - name: "TSA" + name: "TSA", + initialParams: BaseTSA.TSAParams({ + depositCap: 10000e18, + minDepositValue: 1e18, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0, + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) }), CoveredCallTSA.CCTSAInitParams({ baseFeed: baseFeed, @@ -78,17 +90,6 @@ contract CCTSATestUtils is TSATestUtils { } function setupCCTSA() internal { - tsa.setTSAParams( - BaseTSA.TSAParams({ - depositCap: 10000e18, - minDepositValue: 1e18, - depositScale: 1e18, - withdrawScale: 1e18, - managementFee: 0, - feeRecipient: address(0) - }) - ); - CoveredCallTSA(address(tsa)).setCCTSAParams(defaultCCTSAParams); CoveredCallTSA(address(tsa)).setCollateralManagementParams(defaultCollateralManagementParams); diff --git a/test/tokenizedSubaccounts/utils/EGTSATestUtils.sol b/test/tokenizedSubaccounts/utils/EGTSATestUtils.sol new file mode 100644 index 0000000..8bbe0d2 --- /dev/null +++ b/test/tokenizedSubaccounts/utils/EGTSATestUtils.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.18; + +import {PublicLBTSA} from "./PublicLBTSA.sol"; +import "./TSATestUtils.sol"; +import {EMAGeneralisedTSA} from "../../../src/tokenizedSubaccounts/EMAGeneralisedTSA.sol"; + +contract EGTSATestUtils is TSATestUtils { + using SignedMath for int; + + EMAGeneralisedTSA public tsaImplementation; + + EMAGeneralisedTSA internal gtsa; + + function upgradeToGTSA() internal { + tsaImplementation = new EMAGeneralisedTSA(); + + BaseTSA.BaseTSAInitParams memory initParams = BaseTSA.BaseTSAInitParams({ + subAccounts: subAccounts, + auction: auction, + cash: cash, + wrappedDepositAsset: markets[MARKET].base, + manager: srm, + matching: matching, + symbol: "GTSA", + name: "GTSA", + initialParams: BaseTSA.TSAParams({ + depositCap: 10000e18, + minDepositValue: 1e18, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0, + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) + }); + + EMAGeneralisedTSA.GTSAInitParams memory gInitParams = EMAGeneralisedTSA.GTSAInitParams({ + baseFeed: markets[MARKET].spotFeed, + depositModule: depositModule, + withdrawalModule: withdrawalModule, + tradeModule: tradeModule, + rfqModule: rfqModule + }); + + proxyAdmin.upgradeAndCall( + ITransparentUpgradeableProxy(address(proxy)), + address(tsaImplementation), + abi.encodeWithSelector(tsaImplementation.initialize.selector, address(this), initParams, gInitParams) + ); + + gtsa = EMAGeneralisedTSA(address(proxy)); + tsa = BaseOnChainSigningTSA(address(proxy)); + tsaSubacc = gtsa.subAccount(); + } + + function setupGTSA() internal { + EMAGeneralisedTSA(address(tsa)).setGTSAParams(0.0002e18, 0.02e18); + + tsa.setShareKeeper(address(this), true); + + signerPk = 0xBEEF; + signer = vm.addr(signerPk); + + tsa.setSigner(signer, true); + } +} diff --git a/test/tokenizedSubaccounts/utils/LBTSATestUtils.sol b/test/tokenizedSubaccounts/utils/LBTSATestUtils.sol index 90e20f0..2d1d6a7 100644 --- a/test/tokenizedSubaccounts/utils/LBTSATestUtils.sol +++ b/test/tokenizedSubaccounts/utils/LBTSATestUtils.sol @@ -45,7 +45,17 @@ contract LBTSATestUtils is TSATestUtils { manager: srm, matching: matching, symbol: "LBTSA", - name: "Leveraged Basis TSA" + name: "Leveraged Basis TSA", + initialParams: BaseTSA.TSAParams({ + depositCap: 10000e18, + minDepositValue: 1e18, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0, + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) }); LeveragedBasisTSA.LBTSAInitParams memory lbInitParams = LeveragedBasisTSA.LBTSAInitParams({ @@ -68,17 +78,6 @@ contract LBTSATestUtils is TSATestUtils { } function setupLBTSA() internal { - tsa.setTSAParams( - BaseTSA.TSAParams({ - depositCap: 10000e18, - minDepositValue: 1e18, - depositScale: 1e18, - withdrawScale: 1e18, - managementFee: 0, - feeRecipient: address(0) - }) - ); - LeveragedBasisTSA(address(tsa)).setLBTSAParams(defaultLBTSAParams); LeveragedBasisTSA(address(tsa)).setCollateralManagementParams(defaultCollateralManagementParams); diff --git a/test/tokenizedSubaccounts/utils/MockTSA.sol b/test/tokenizedSubaccounts/utils/MockTSA.sol index c7ac531..53a6906 100644 --- a/test/tokenizedSubaccounts/utils/MockTSA.sol +++ b/test/tokenizedSubaccounts/utils/MockTSA.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.18; -import "../../../src/tokenizedSubaccounts/BaseOnChainSigningTSA.sol"; +import "../../../src/tokenizedSubaccounts/shared/BaseOnChainSigningTSA.sol"; /// @title MockTSA contract MockTSA is BaseOnChainSigningTSA { diff --git a/test/tokenizedSubaccounts/utils/PPTSATestUtils.sol b/test/tokenizedSubaccounts/utils/PPTSATestUtils.sol index 1bd9045..3625c22 100644 --- a/test/tokenizedSubaccounts/utils/PPTSATestUtils.sol +++ b/test/tokenizedSubaccounts/utils/PPTSATestUtils.sol @@ -65,7 +65,17 @@ contract PPTSATestUtils is TSATestUtils { manager: srm, matching: matching, symbol: "Tokenised SubAccount", - name: "TSA" + name: "TSA", + initialParams: BaseTSA.TSAParams({ + depositCap: 10000e18, + minDepositValue: 1e18, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0, + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) }), PrincipalProtectedTSA.PPTSAInitParams({ baseFeed: baseFeed, @@ -79,25 +89,13 @@ contract PPTSATestUtils is TSATestUtils { }) ) ); - tsa = BaseOnChainSigningTSA(address(proxy)); pptsa = PrincipalProtectedTSA(address(tsa)); + pptsa.setPPTSAParams(defaultPPTSAParams); tsaSubacc = pptsa.subAccount(); } function setupPPTSA() internal { - tsa.setTSAParams( - BaseTSA.TSAParams({ - depositCap: 10000e18, - minDepositValue: 1e18, - depositScale: 1e18, - withdrawScale: 1e18, - managementFee: 0, - feeRecipient: address(0) - }) - ); - - PrincipalProtectedTSA(address(tsa)).setPPTSAParams(defaultPPTSAParams); PrincipalProtectedTSA(address(tsa)).setCollateralManagementParams(defaultCollateralManagementParams); tsa.setShareKeeper(address(this), true); diff --git a/test/tokenizedSubaccounts/utils/TSATestUtils.sol b/test/tokenizedSubaccounts/utils/TSATestUtils.sol index 8f0b33e..cc0ff3b 100644 --- a/test/tokenizedSubaccounts/utils/TSATestUtils.sol +++ b/test/tokenizedSubaccounts/utils/TSATestUtils.sol @@ -30,6 +30,8 @@ contract TSATestUtils is IntegrationTestBase, MatchingHelpers { using SignedMath for int; string public MARKET = "wbtc"; + string public NOT_MARKET = "weth"; + uint public MARKET_UNIT; uint public MARKET_REF_SPOT; @@ -50,6 +52,12 @@ contract TSATestUtils is IntegrationTestBase, MatchingHelpers { BaseOnChainSigningTSA public tsa; uint public tsaSubacc; + constructor() { + require( + keccak256(abi.encode(MARKET)) != keccak256(abi.encode(NOT_MARKET)), "MARKET and NOT_MARKET must be different" + ); + } + function setUp() public virtual { _setupIntegrationTestComplete(); _setupMatching(); @@ -149,24 +157,23 @@ contract TSATestUtils is IntegrationTestBase, MatchingHelpers { manager: srm, matching: matching, symbol: "Tokenised SubAccount", - name: "TSA" + name: "TSA", + initialParams: BaseTSA.TSAParams({ + depositCap: type(uint).max, + minDepositValue: 0, + depositScale: 1e18, + withdrawScale: 1e18, + managementFee: 0, + feeRecipient: address(0), + performanceFeeWindow: 1 weeks, + performanceFee: 0 + }) }) ) ); mockTsa = MockTSA(address(proxy)); - mockTsa.setTSAParams( - BaseTSA.TSAParams({ - depositCap: type(uint).max, - minDepositValue: 0, - depositScale: 1e18, - withdrawScale: 1e18, - managementFee: 0, - feeRecipient: address(0) - }) - ); - mockTsa.setShareKeeper(address(this), true); signerPk = 0xBEEF; @@ -399,7 +406,10 @@ contract TSATestUtils is IntegrationTestBase, MatchingHelpers { return action; } - function _tradePerp(int amount, uint price) internal { + function _getPerpTradeData(int amount, uint price) + internal + returns (IActionVerifier.Action[] memory, bytes[] memory, bytes memory) + { bytes memory tradeMaker = abi.encode( ITradeModule.TradeData({ asset: address(markets[MARKET].perp), @@ -446,10 +456,17 @@ contract TSATestUtils is IntegrationTestBase, MatchingHelpers { nonVaultPk ); + return (actions, signatures, _createMatchedTrade(tsaSubacc, nonVaultSubacc, amount.abs(), int(price), 0, 0)); + } + + function _tradePerp(int amount, uint price) internal { + (IActionVerifier.Action[] memory actions, bytes[] memory signatures, bytes memory actionData) = + _getPerpTradeData(amount, price); + vm.prank(signer); tsa.signActionData(actions[0], ""); - _verifyAndMatch(actions, signatures, _createMatchedTrade(tsaSubacc, nonVaultSubacc, amount.abs(), int(price), 0, 0)); + _verifyAndMatch(actions, signatures, actionData); } function _createDepositAction(uint amount) internal returns (IActionVerifier.Action memory) { @@ -477,7 +494,11 @@ contract TSATestUtils is IntegrationTestBase, MatchingHelpers { } function _createWithdrawalAction(uint amount) internal returns (IActionVerifier.Action memory) { - bytes memory withdrawalData = _encodeWithdrawData(amount, address(markets[MARKET].base)); + return _createWithdrawalAction(amount, address(markets[MARKET].base)); + } + + function _createWithdrawalAction(uint amount, address asset) internal returns (IActionVerifier.Action memory) { + bytes memory withdrawalData = _encodeWithdrawData(amount, asset); IActionVerifier.Action memory action = IActionVerifier.Action({ subaccountId: tsaSubacc, @@ -587,14 +608,36 @@ contract TSATestUtils is IntegrationTestBase, MatchingHelpers { ) internal { (IRfqModule.RfqOrder memory order, IRfqModule.TakerOrder memory takerOrder) = _setupRfq(amount, price, expiry, strike, price2, strike2, isCallSpread); + (IActionVerifier.Action[] memory actions, bytes[] memory signatures) = + _getRfqAsTakerSignaturesAndActions(order, takerOrder); + + vm.prank(signer); + tsa.signActionData(actions[1], abi.encode(order.trades)); + + IRfqModule.FillData memory fill = IRfqModule.FillData({ + makerAccount: nonVaultSubacc, + takerAccount: tsaSubacc, + makerFee: 0, + takerFee: 0, + managerData: bytes("") + }); + + _verifyAndMatch(actions, signatures, abi.encode(fill)); + } + + function _getRfqAsTakerSignaturesAndActions( + IRfqModule.RfqOrder memory makerOrder, + IRfqModule.TakerOrder memory takerOrder + ) internal returns (IActionVerifier.Action[] memory, bytes[] memory) { IActionVerifier.Action[] memory actions = new IActionVerifier.Action[](2); bytes[] memory signatures = new bytes[](2); + // maker order (actions[0], signatures[0]) = _createActionAndSign( nonVaultSubacc, ++nonVaultNonce, address(rfqModule), - abi.encode(order), + abi.encode(makerOrder), block.timestamp + 1 days, nonVaultAddr, nonVaultAddr, @@ -611,44 +654,29 @@ contract TSATestUtils is IntegrationTestBase, MatchingHelpers { owner: address(tsa), signer: address(tsa) }); - vm.prank(signer); - tsa.signActionData(actions[1], abi.encode(order.trades)); - - IRfqModule.FillData memory fill = IRfqModule.FillData({ - makerAccount: nonVaultSubacc, - takerAccount: tsaSubacc, - makerFee: 0, - takerFee: 0, - managerData: bytes("") - }); - _verifyAndMatch(actions, signatures, abi.encode(fill)); + return (actions, signatures); } - function _tradeRfqAsMaker( - int amount, - uint price, - uint expiry, - uint strike, - uint price2, - uint strike2, - bool isCallSpread - ) internal { - (IRfqModule.RfqOrder memory order, IRfqModule.TakerOrder memory takerOrder) = - _setupRfq(amount, price, expiry, strike, price2, strike2, isCallSpread); + function _getRfqAsMakerSignaturesAndActions( + IRfqModule.RfqOrder memory makerOrder, + IRfqModule.TakerOrder memory takerOrder + ) internal returns (IActionVerifier.Action[] memory, bytes[] memory) { IActionVerifier.Action[] memory actions = new IActionVerifier.Action[](2); bytes[] memory signatures = new bytes[](2); + // maker order actions[0] = IActionVerifier.Action({ subaccountId: tsaSubacc, nonce: ++tsaNonce, module: rfqModule, - data: abi.encode(order), + data: abi.encode(makerOrder), expiry: block.timestamp + 8 minutes, owner: address(tsa), signer: address(tsa) }); + // taker order (actions[1], signatures[1]) = _createActionAndSign( nonVaultSubacc, ++nonVaultNonce, @@ -659,6 +687,25 @@ contract TSATestUtils is IntegrationTestBase, MatchingHelpers { nonVaultAddr, nonVaultPk ); + + return (actions, signatures); + } + + function _tradeRfqAsMaker( + int amount, + uint price, + uint expiry, + uint strike, + uint price2, + uint strike2, + bool isCallSpread + ) internal { + (IRfqModule.RfqOrder memory order, IRfqModule.TakerOrder memory takerOrder) = + _setupRfq(amount, price, expiry, strike, price2, strike2, isCallSpread); + + (IActionVerifier.Action[] memory actions, bytes[] memory signatures) = + _getRfqAsMakerSignaturesAndActions(order, takerOrder); + vm.prank(signer); tsa.signActionData(actions[0], "");