diff --git a/.gitignore b/.gitignore index c2658d7d1..2752eb92e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules/ +.DS_Store diff --git a/.gitmodules b/.gitmodules index 5ad0fdc56..4ace94b1d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "contracts/lib/V2-gov"] path = contracts/lib/V2-gov url = https://github.com/liquity/V2-gov +[submodule "contracts/lib/superfluid-protocol-monorepo"] + path = contracts/lib/superfluid-protocol-monorepo + url = https://github.com/superfluid-finance/protocol-monorepo diff --git a/contracts/.gitignore b/contracts/.gitignore index 5efec8a76..ba22ab59a 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -45,3 +45,8 @@ testMatrix.json # E2E broadcast logs /broadcast-e2e + +# Recon +medusa +echidna +crytic-export \ No newline at end of file diff --git a/contracts/echidna.yaml b/contracts/echidna.yaml new file mode 100644 index 000000000..9a35f2ab1 --- /dev/null +++ b/contracts/echidna.yaml @@ -0,0 +1,39 @@ + +testMode: "assertion" +prefix: "optimize_" +coverage: true +corpusDir: "echidna" +balanceAddr: 0x1043561a8829300000 +balanceContract: 0x1043561a8829300000 +filterFunctions: [] +cryticArgs: ["--foundry-compile-all","--compile-libraries=(CallUtils,0xf01),(CallbackUtils,0xf02),(BaseRelayRecipient,0xf03),(SuperfluidPoolDeployerLibrary,0xf04),(SolvencyHelperLibrary,0xf06),(SlotsBitmapLibrary,0xf08),(SuperfluidGovDeployerLibrary,0xf09),(SuperfluidHostDeployerLibrary,0xf0a),(SuperfluidCFAv1DeployerLibrary,0xf0b),(SuperfluidIDAv1DeployerLibrary,0xf0c),(SuperfluidPoolLogicDeployerLibrary,0xf0d),(SuperfluidGDAv1DeployerLibrary,0xf0e),(CFAv1ForwarderDeployerLibrary,0xf0f),(GDAv1ForwarderDeployerLibrary,0xf10),(SuperTokenDeployerLibrary,0xf11),(SuperfluidPoolNFTLogicDeployerLibrary,0xf12), (ProxyDeployerLibrary,0xf13),(TokenDeployerLibrary,0xf14),(SuperTokenFactoryDeployerLibrary,0xf15),(SuperfluidPeripheryDeployerLibrary,0xf16)"] +deployContracts: [ + ["0xf01", "CallUtils"], + ["0xf02", "CallbackUtils"], + ["0xf03", "BaseRelayRecipient"], + ["0xf04", "SuperfluidPoolDeployerLibrary"], + ["0xf06", "SolvencyHelperLibrary"], + ["0xf08", "SlotsBitmapLibrary"], + + ["0xf09", "SuperfluidGovDeployerLibrary"], + ["0xf0a", "SuperfluidHostDeployerLibrary"], + ["0xf0b", "SuperfluidCFAv1DeployerLibrary"], + ["0xf0c", "SuperfluidIDAv1DeployerLibrary"], + ["0xf0d", "SuperfluidPoolLogicDeployerLibrary"], + ["0xf0e", "SuperfluidGDAv1DeployerLibrary"], + ["0xf0f", "CFAv1ForwarderDeployerLibrary"], + ["0xf10", "GDAv1ForwarderDeployerLibrary"], + ["0xf11", "SuperTokenDeployerLibrary"], + ["0xf12", "SuperfluidPoolNFTLogicDeployerLibrary"], + ["0xf13", "ProxyDeployerLibrary"], + ["0xf14", "TokenDeployerLibrary"], + ["0xf15", "SuperTokenFactoryDeployerLibrary"], + ["0xf16", "SuperfluidPeripheryDeployerLibrary"] + +] +deployer: "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496" +contractAddr: "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496" +shrinkLimit: 100000 + +## Deploy ERC1820RegistryCompiled as we don't have access to vm.etch +deployBytecodes: [["0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24", "608060405234801561001057600080fd5b506109c5806100206000396000f3fe608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c0029"]] \ No newline at end of file diff --git a/contracts/lib/superfluid-protocol-monorepo b/contracts/lib/superfluid-protocol-monorepo new file mode 160000 index 000000000..1edc4ed3d --- /dev/null +++ b/contracts/lib/superfluid-protocol-monorepo @@ -0,0 +1 @@ +Subproject commit 1edc4ed3ddafa87d42c1155e870532e2a5a80470 diff --git a/contracts/medusa.json b/contracts/medusa.json new file mode 100644 index 000000000..2973e5add --- /dev/null +++ b/contracts/medusa.json @@ -0,0 +1,86 @@ +{ + "fuzzing": { + "workers": 16, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 0, + "callSequenceLength": 100, + "corpusDirectory": "medusa", + "coverageEnabled": true, + "deploymentOrder": [ + "CryticTester" + ], + "targetContracts": [ + "CryticTester" + ], + "targetContractsBalances": [ + "0x27b46536c66c8e3000000" + ], + "constructorArgs": {}, + "deployerAddress": "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496", + "senderAddresses": [ + "0x10000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 125000000, + "transactionGasLimit": 12500000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": true, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": [ + "invariant_" + ] + }, + "optimizationTesting": { + "enabled": true, + "testPrefixes": [ + "optimize_" + ] + } + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": [ + "--foundry-compile-all" + ] + } + }, + "logging": { + "level": "info", + "logDirectory": "" + } +} diff --git a/contracts/remappings.txt b/contracts/remappings.txt index c17beb9f8..2da5bf9ee 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1 +1,4 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ openzeppelin/=lib/V2-gov/lib/openzeppelin-contracts/ +@superfluid-finance/=lib/superfluid-protocol-monorepo/packages/ + diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 99bfff55a..afd419ed4 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -92,6 +92,8 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, address internal stakingV1; address internal lusd; + uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + // Curve ICurveStableswapNGFactory curveStableswapFactory; // https://docs.curve.fi/deployments/amm/#stableswap-ng @@ -190,6 +192,7 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, uint256 SCR; uint256 LIQUIDATION_PENALTY_SP; uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; + uint256 debtLimit; } struct DeploymentVars { @@ -227,6 +230,9 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, string memory saltStr = vm.envOr("SALT", block.timestamp.toString()); SALT = keccak256(bytes(saltStr)); + //setup SF factories + ISuperTokenFactory superTokenFactory = ISuperTokenFactory(0x0000000000000000000000000000000000000000); + if (vm.envBytes("DEPLOYER").length == 20) { // address deployer = vm.envAddress("DEPLOYER"); @@ -262,18 +268,18 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, // Deploy Bold or pick up existing deployment bytes memory boldBytecode = bytes.concat(type(BoldToken).creationCode, abi.encode(deployer)); address boldAddress = vm.computeCreate2Address(SALT, keccak256(boldBytecode)); - BoldToken boldToken; + IBoldToken boldToken; if (deploymentMode.eq(DEPLOYMENT_MODE_USE_EXISTING_BOLD)) { require(boldAddress.code.length > 0, string.concat("BOLD not found at ", boldAddress.toHexString())); - boldToken = BoldToken(boldAddress); + boldToken = IBoldToken(payable(boldAddress)); // Check BOLD is untouched require(boldToken.totalSupply() == 0, "Some BOLD has been minted!"); require(boldToken.collateralRegistryAddress() == address(0), "Collateral registry already set"); - require(boldToken.owner() == deployer, "Not BOLD owner"); + require(BoldToken(payable(address(boldToken))).owner() == deployer, "Not BOLD owner"); } else { - boldToken = new BoldToken{salt: SALT}(deployer); + boldToken = IBoldToken(address(new BoldToken{salt: SALT}(deployer, superTokenFactory))); assert(address(boldToken) == boldAddress); } @@ -322,9 +328,9 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, TroveManagerParams[] memory troveManagerParamsArray = new TroveManagerParams[](3); // TODO: move params out of here - troveManagerParamsArray[0] = TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16); // WETH - troveManagerParamsArray[1] = TroveManagerParams(150e16, 120e16, 110e16, 5e16, 10e16); // wstETH - troveManagerParamsArray[2] = TroveManagerParams(150e16, 120e16, 110e16, 5e16, 10e16); // rETH + troveManagerParamsArray[0] = TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2); // WETH + troveManagerParamsArray[1] = TroveManagerParams(150e16, 120e16, 110e16, 5e16, 10e16, MAX_INT/2); // wstETH + troveManagerParamsArray[2] = TroveManagerParams(150e16, 120e16, 110e16, 5e16, 10e16, MAX_INT/2); // rETH string[] memory collNames = new string[](2); string[] memory collSymbols = new string[](2); @@ -526,7 +532,7 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, DeploymentVars memory vars; vars.numCollaterals = troveManagerParamsArray.length; - r.boldToken = BoldToken(_deployGovernanceParams.bold); + r.boldToken = IBoldToken(payable(_deployGovernanceParams.bold)); // USDC and USDC-BOLD pool r.usdcCurvePool = _deployCurvePool(r.boldToken, USDC); @@ -588,9 +594,11 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, _deployAddressesRegistry(troveManagerParamsArray[vars.i]); vars.addressesRegistries[vars.i] = addressesRegistry; vars.troveManagers[vars.i] = ITroveManager(troveManagerAddress); + //updateDebtLimit + r.collateralRegistry.updateDebtLimit(vars.i, troveManagerParamsArray[vars.i].debtLimit); } - r.collateralRegistry = new CollateralRegistry(r.boldToken, vars.collaterals, vars.troveManagers); + r.collateralRegistry = new CollateralRegistry(r.boldToken, vars.collaterals, vars.troveManagers, msg.sender); r.hintHelpers = new HintHelpers(r.collateralRegistry); r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); @@ -635,6 +643,7 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, _troveManagerParams.CCR, _troveManagerParams.MCR, _troveManagerParams.SCR, + _troveManagerParams.debtLimit, _troveManagerParams.LIQUIDATION_PENALTY_SP, _troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION ); diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index 2289ffa0c..4a49e3f56 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.24; import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Votes.sol"; import "openzeppelin-contracts/contracts/utils/math/Math.sol"; import "./Dependencies/Constants.sol"; @@ -61,6 +62,9 @@ contract ActivePool is IActivePool { // Last time at which the aggregate batch fees and weighted sum were updated uint256 public lastAggBatchManagementFeesUpdateTime; + address public governor; + address public delegateRepresentative; + // --- Events --- event CollTokenAddressChanged(address _newCollTokenAddress); @@ -88,6 +92,9 @@ contract ActivePool is IActivePool { // Allow funds movements between Liquity contracts collToken.approve(defaultPoolAddress, type(uint256).max); + + governor = 0x108f48E558078C8eF2eb428E0774d7eCd01F6B1d; + delegateRepresentative = 0x108f48E558078C8eF2eb428E0774d7eCd01F6B1d; } // --- Getters for public variables. Required by IPool interface --- @@ -342,4 +349,24 @@ contract ActivePool is IActivePool { function _requireCallerIsTroveManager() internal view { require(msg.sender == troveManagerAddress, "ActivePool: Caller is not TroveManager"); } + + modifier onlyGovernor() { + require(msg.sender == governor, "ActivePool: Caller is not Governor"); + _; + } + + function setGovernor(address _governor) external onlyGovernor { + governor = _governor; + } + + function setDelegateRepresentative(address _delegateRepresentative) external onlyGovernor { + delegateRepresentative = _delegateRepresentative; + } + + //Delegate collateral tokens to delegateRepresentativecollToken + //Anyone can call this safely + function delegateTokens() external { + ERC20Votes(address(collToken)).delegate(delegateRepresentative); + } + } diff --git a/contracts/src/AddressesRegistry.sol b/contracts/src/AddressesRegistry.sol index 606c88337..f332439f1 100644 --- a/contracts/src/AddressesRegistry.sol +++ b/contracts/src/AddressesRegistry.sol @@ -35,6 +35,9 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { // Minimum collateral ratio for individual troves uint256 public immutable MCR; + // Debt limit for the system + uint256 public debtLimit; + // Liquidation penalty for troves offset to the SP uint256 public immutable LIQUIDATION_PENALTY_SP; // Liquidation penalty for troves redistributed @@ -43,6 +46,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { error InvalidCCR(); error InvalidMCR(); error InvalidSCR(); + error InvalidDebtLimit(); error SPPenaltyTooLow(); error SPPenaltyGtRedist(); error RedistPenaltyTooHigh(); @@ -71,12 +75,14 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { uint256 _ccr, uint256 _mcr, uint256 _scr, + uint256 _debtLimit, uint256 _liquidationPenaltySP, uint256 _liquidationPenaltyRedistribution ) Ownable(_owner) { if (_ccr <= 1e18 || _ccr >= 2e18) revert InvalidCCR(); if (_mcr <= 1e18 || _mcr >= 2e18) revert InvalidMCR(); if (_scr <= 1e18 || _scr >= 2e18) revert InvalidSCR(); + if (_debtLimit <= 0) revert InvalidDebtLimit(); if (_liquidationPenaltySP < MIN_LIQUIDATION_PENALTY_SP) revert SPPenaltyTooLow(); if (_liquidationPenaltySP > _liquidationPenaltyRedistribution) revert SPPenaltyGtRedist(); if (_liquidationPenaltyRedistribution > MAX_LIQUIDATION_PENALTY_REDISTRIBUTION) revert RedistPenaltyTooHigh(); @@ -84,6 +90,7 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { CCR = _ccr; SCR = _scr; MCR = _mcr; + debtLimit = _debtLimit; LIQUIDATION_PENALTY_SP = _liquidationPenaltySP; LIQUIDATION_PENALTY_REDISTRIBUTION = _liquidationPenaltyRedistribution; } diff --git a/contracts/src/BoldToken.sol b/contracts/src/BoldToken.sol index a709c746b..3028a1779 100644 --- a/contracts/src/BoldToken.sol +++ b/contracts/src/BoldToken.sol @@ -2,6 +2,14 @@ pragma solidity 0.8.24; +// This abstract contract provides storage padding for the proxy +import { CustomSuperTokenBase } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/CustomSuperTokenBase.sol"; +// Implementation of UUPSProxy (see https://eips.ethereum.org/EIPS/eip-1822) +import { UUPSProxy } from "@superfluid-finance/ethereum-contracts/contracts/upgradability/UUPSProxy.sol"; +// Superfluid framework interfaces we need +import { ISuperToken, ISuperTokenFactory, IERC20 } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; + + import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "./Dependencies/Ownable.sol"; import "./Interfaces/IBoldToken.sol"; @@ -16,9 +24,15 @@ import "./Interfaces/IBoldToken.sol"; * 2) sendToPool() and returnFromPool(): functions callable only Liquity core contracts, which move Bold tokens between Liquity <-> user. */ -contract BoldToken is Ownable, IBoldToken, ERC20Permit { - string internal constant _NAME = "Bold Stablecoin"; - string internal constant _SYMBOL = "Bold"; + //TODO double check the erc20 the proxy is using implements permit. Add just the permit function without the rest of erc20. + //TODO: remove erc20 from constructor. Ask bold if removing permit breaks anything else. + //INFO: permit involves approve, so invoke safeApproveFor in supertoken + +contract BoldToken is CustomSuperTokenBase, Ownable, IBoldTokenCustom, UUPSProxy { + + string internal constant _NAME = "Nerite Stablecoin"; + string internal constant _SYMBOL = "USDN"; + // --- Addresses --- @@ -35,7 +49,27 @@ contract BoldToken is Ownable, IBoldToken, ERC20Permit { event BorrowerOperationsAddressAdded(address _newBorrowerOperationsAddress); event ActivePoolAddressAdded(address _newActivePoolAddress); - constructor(address _owner) Ownable(_owner) ERC20(_NAME, _SYMBOL) ERC20Permit(_NAME) {} + //TODO update deployment script for new constructor params. + //TODO move supertoken init to another function possibley. + //TODO lookup address of factory deployment and include that in the deployment scripts to int the factory. + //TODO BOLD token now has a payable fallback function. verify this is not a problem. + //TODO clean up construtor and update tests that use the constructor to not have the factory param. + constructor(address _owner, ISuperTokenFactory factory) Ownable(_owner) {} + + function initialize(ISuperTokenFactory factory) external { + // This call to the factory invokes `UUPSProxy.initialize`, which connects the proxy to the canonical SuperToken implementation. + // It also emits an event which facilitates discovery of this token. + ISuperTokenFactory(factory).initializeCustomSuperToken(address(this)); + + // This initializes the token storage and sets the `initialized` flag of OpenZeppelin Initializable. + // This makes sure that it will revert if invoked more than once. + ISuperToken(address(this)).initialize( + IERC20(address(0)), + 18, + _NAME, + _SYMBOL + ); + } function setBranchAddresses( address _troveManagerAddress, @@ -67,39 +101,26 @@ contract BoldToken is Ownable, IBoldToken, ERC20Permit { function mint(address _account, uint256 _amount) external override { _requireCallerIsBOorAP(); - _mint(_account, _amount); + ISuperToken(address(this)).selfMint(_account, _amount, ""); } function burn(address _account, uint256 _amount) external override { _requireCallerIsCRorBOorTMorSP(); - _burn(_account, _amount); + ISuperToken(address(this)).selfBurn(_account, _amount, ""); } + //TODO verify spender is correct when making pool calls. function sendToPool(address _sender, address _poolAddress, uint256 _amount) external override { _requireCallerIsStabilityPool(); - _transfer(_sender, _poolAddress, _amount); + ISuperToken(address(this)).selfTransferFrom(_sender, _sender, _poolAddress, _amount); } function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external override { _requireCallerIsStabilityPool(); - _transfer(_poolAddress, _receiver, _amount); + ISuperToken(address(this)).selfTransferFrom(_poolAddress, _poolAddress, _receiver, _amount); } - // --- External functions --- - - function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) { - _requireValidRecipient(recipient); - return super.transfer(recipient, amount); - } - - function transferFrom(address sender, address recipient, uint256 amount) - public - override(ERC20, IERC20) - returns (bool) - { - _requireValidRecipient(recipient); - return super.transferFrom(sender, recipient, amount); - } + // TODO: check that SF already checks for no sending to 0 or this contract. // --- 'require' functions --- @@ -128,4 +149,89 @@ contract BoldToken is Ownable, IBoldToken, ERC20Permit { function _requireCallerIsStabilityPool() internal view { require(stabilityPoolAddresses[msg.sender], "Bold: Caller is not the StabilityPool"); } + + //[================================] + //Permit functions + //[================================] + + /// @dev Returns the EIP-712 domain separator for the EIP-2612 permit. + // Required to implement ERC20Permit. + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(ISuperToken(address(this)).name())), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } + + // Storage slot for nonces (from ERC20Permit) + mapping(address => uint256) private _nonces; + + function eip712Domain() external view override returns ( + bytes1 fields, + string memory name712, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) { + return ( + hex"0f", // 01111 (all fields except salt and extensions) + ISuperToken(address(this)).name(), + "1", // version + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } + + function nonces(address owner) external view override returns (uint256) { + return _nonces[owner]; + } + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external override { + if (deadline < block.timestamp) revert("PERMIT_DEADLINE_EXPIRED"); + + // Compute the digest + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + spender, + value, + _nonces[owner]++, + deadline + ) + ); + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + structHash + ) + ); + + address signer = ecrecover(digest, v, r, s); + if (signer == address(0)) revert("INVALID_SIGNATURE"); + if (signer != owner) revert("INVALID_SIGNER"); + + // Finally, approve the spender + ISuperToken(address(this)).selfApproveFor(owner, spender, value); + } + } diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index c8bc441d9..c2381cf1c 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -311,6 +311,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio vars.activePool = activePool; vars.boldToken = boldToken; + require(troveManager.getDebtLimit() >= troveManager.getEntireSystemDebt() + _boldAmount, "BorrowerOperations: Debt limit exceeded."); + vars.price = _requireOraclesLive(); // --- Checks --- @@ -403,6 +405,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio ITroveManager troveManagerCached = troveManager; _requireTroveIsActive(troveManagerCached, _troveId); + require(troveManagerCached.getDebtLimit() >= troveManagerCached.getEntireSystemDebt() + _boldAmount, "BorrowerOperations: Debt limit exceeded."); + TroveChange memory troveChange; troveChange.debtIncrease = _boldAmount; _adjustTrove(troveManagerCached, _troveId, troveChange, _maxUpfrontFee); @@ -1220,6 +1224,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio IBoldToken _boldToken, IActivePool _activePool ) internal { + require(troveManager.getDebtLimit() >= troveManager.getEntireSystemDebt() + _troveChange.debtIncrease, "BorrowerOperations: Debt limit exceeded."); + if (_troveChange.debtIncrease > 0) { _boldToken.mint(withdrawalReceiver, _troveChange.debtIncrease); } else if (_troveChange.debtDecrease > 0) { diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index 85fb1e995..28c542d35 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -39,6 +39,8 @@ contract CollateralRegistry is ICollateralRegistry { IBoldToken public immutable boldToken; + address public governor; + uint256 public baseRate; // The timestamp of the latest fee operation (redemption or new Bold issuance) @@ -47,7 +49,7 @@ contract CollateralRegistry is ICollateralRegistry { event BaseRateUpdated(uint256 _baseRate); event LastFeeOpTimeUpdated(uint256 _lastFeeOpTime); - constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers) { + constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers, address _governor) { uint256 numTokens = _tokens.length; require(numTokens > 0, "Collateral list cannot be empty"); require(numTokens <= 10, "Collateral list too long"); @@ -80,6 +82,8 @@ contract CollateralRegistry is ICollateralRegistry { // Initialize the baseRate state variable baseRate = INITIAL_BASE_RATE; emit BaseRateUpdated(INITIAL_BASE_RATE); + + governor = _governor; } struct RedemptionTotals { @@ -302,4 +306,27 @@ contract CollateralRegistry is ICollateralRegistry { function _requireAmountGreaterThanZero(uint256 _amount) internal pure { require(_amount > 0, "CollateralRegistry: Amount must be greater than zero"); } + + // Update the debt limit for a specific TroveManager + function updateDebtLimit(uint256 _indexTroveManager, uint256 _newDebtLimit) external onlyGovernor { + //limited to increasing by 2x at a time, maximum. Decrease by any amount. + uint256 currentDebtLimit = getTroveManager(_indexTroveManager).getDebtLimit(); + if (_newDebtLimit > currentDebtLimit) { + require(_newDebtLimit <= currentDebtLimit * 2, "CollateralRegistry: Debt limit increase by more than 2x is not allowed"); + } + getTroveManager(_indexTroveManager).setDebtLimit(_newDebtLimit); + } + + function getDebtLimit(uint256 _indexTroveManager) external view returns (uint256) { + return getTroveManager(_indexTroveManager).getDebtLimit(); + } + + function updateGovernor(address _newGovernor) external onlyGovernor { + governor = _newGovernor; + } + + modifier onlyGovernor() { + require(msg.sender == governor, "CollateralRegistry: Only governor can call this function"); + _; + } } diff --git a/contracts/src/Interfaces/IAddressesRegistry.sol b/contracts/src/Interfaces/IAddressesRegistry.sol index e85fb5bcc..c9b5a5de3 100644 --- a/contracts/src/Interfaces/IAddressesRegistry.sol +++ b/contracts/src/Interfaces/IAddressesRegistry.sol @@ -43,6 +43,7 @@ interface IAddressesRegistry { function CCR() external returns (uint256); function SCR() external returns (uint256); function MCR() external returns (uint256); + function debtLimit() external returns (uint256); function LIQUIDATION_PENALTY_SP() external returns (uint256); function LIQUIDATION_PENALTY_REDISTRIBUTION() external returns (uint256); diff --git a/contracts/src/Interfaces/IBoldToken.sol b/contracts/src/Interfaces/IBoldToken.sol index 0d58c3999..1af9213c1 100644 --- a/contracts/src/Interfaces/IBoldToken.sol +++ b/contracts/src/Interfaces/IBoldToken.sol @@ -6,7 +6,13 @@ import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.s import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; import "openzeppelin-contracts/contracts/interfaces/IERC5267.sol"; -interface IBoldToken is IERC20Metadata, IERC20Permit, IERC5267 { +import { ISuperToken } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; + + +interface IBoldTokenCustom is IERC20Permit, IERC5267 { + function collateralRegistryAddress() external view returns (address); + + function setBranchAddresses( address _troveManagerAddress, address _stabilityPoolAddress, @@ -24,3 +30,5 @@ interface IBoldToken is IERC20Metadata, IERC20Permit, IERC5267 { function returnFromPool(address poolAddress, address user, uint256 _amount) external; } + +interface IBoldToken is IBoldTokenCustom, ISuperToken {} diff --git a/contracts/src/Interfaces/ICollateralRegistry.sol b/contracts/src/Interfaces/ICollateralRegistry.sol index 418db3f78..f007d9783 100644 --- a/contracts/src/Interfaces/ICollateralRegistry.sol +++ b/contracts/src/Interfaces/ICollateralRegistry.sol @@ -23,4 +23,9 @@ interface ICollateralRegistry { function getRedemptionFeeWithDecay(uint256 _ETHDrawn) external view returns (uint256); function getEffectiveRedemptionFeeInBold(uint256 _redeemAmount) external view returns (uint256); + + function updateDebtLimit(uint256 _indexTroveManager, uint256 _newDebtLimit) external; + function updateGovernor(address _newGovernor) external; + function getDebtLimit(uint256 _indexTroveManager) external view returns (uint256); + } diff --git a/contracts/src/Interfaces/ITreeETHPriceFeed.sol b/contracts/src/Interfaces/ITreeETHPriceFeed.sol new file mode 100644 index 000000000..00cdffca3 --- /dev/null +++ b/contracts/src/Interfaces/ITreeETHPriceFeed.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +import "./IMainnetPriceFeed.sol"; +import "../Dependencies/AggregatorV3Interface.sol"; + +pragma solidity ^0.8.0; + +interface ITreeETHPriceFeed is IMainnetPriceFeed { + function treeEthEthOracle() external view returns (AggregatorV3Interface, uint256, uint8); +} diff --git a/contracts/src/Interfaces/ITreeETHToken.sol b/contracts/src/Interfaces/ITreeETHToken.sol new file mode 100644 index 000000000..7161521ec --- /dev/null +++ b/contracts/src/Interfaces/ITreeETHToken.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +interface ITreeETHToken { + function getExchangeRate() external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 6af670f28..ab5ff57fc 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -13,6 +13,8 @@ import "../Types/LatestBatchData.sol"; // Common interface for the Trove Manager. interface ITroveManager is ILiquityBase { + + enum Status { nonExistent, active, @@ -170,5 +172,8 @@ interface ITroveManager is ILiquityBase { uint256 _newAnnualInterestRate ) external; + function setDebtLimit(uint256 _newDebtLimit) external; + function getDebtLimit() external view returns (uint256); + // -- end of permissioned functions -- } diff --git a/contracts/src/Interfaces/IWeETHPriceFeed.sol b/contracts/src/Interfaces/IWeETHPriceFeed.sol new file mode 100644 index 000000000..e8c93023c --- /dev/null +++ b/contracts/src/Interfaces/IWeETHPriceFeed.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +import "./IMainnetPriceFeed.sol"; +import "../Dependencies/AggregatorV3Interface.sol"; + +pragma solidity ^0.8.0; + +interface IWeETHPriceFeed is IMainnetPriceFeed { + function weEthEthOracle() external view returns (AggregatorV3Interface, uint256, uint8); +} diff --git a/contracts/src/Interfaces/IWeETHToken.sol b/contracts/src/Interfaces/IWeETHToken.sol new file mode 100644 index 000000000..c01a7d9c4 --- /dev/null +++ b/contracts/src/Interfaces/IWeETHToken.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +interface IWeETHToken { + function getExchangeRate() external view returns (uint256); +} diff --git a/contracts/src/PriceFeeds/ARBPriceFeed.sol b/contracts/src/PriceFeeds/ARBPriceFeed.sol new file mode 100644 index 000000000..063232f64 --- /dev/null +++ b/contracts/src/PriceFeeds/ARBPriceFeed.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + + +import "./TokenPriceFeedBase.sol"; + +contract ARBPriceFeed is TokenPriceFeedBase { + constructor(address _owner, address _arbUsdOracleAddress, uint256 _arbUsdStalenessThreshold) + TokenPriceFeedBase(_owner, _arbUsdOracleAddress, _arbUsdStalenessThreshold) + { + _fetchPricePrimary(); + + // Check the oracle didn't already fail + assert(priceSource == PriceSource.primary); + } + + function fetchPrice() public returns (uint256, bool) { + // If branch is live and the primary oracle setup has been working, try to use it + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + // Use same price for redemption as all other ops in ARB branch + return fetchPrice(); + } + + // _fetchPricePrimary returns: + // - The price + // - A bool indicating whether a new oracle failure was detected in the call + function _fetchPricePrimary(bool /* _isRedemption */ ) internal virtual returns (uint256, bool) { + return _fetchPricePrimary(); + } + + function _fetchPricePrimary() internal returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 tokenUsdPrice, bool tokenUsdOracleDown) = _getOracleAnswer(tokenUsdOracle); + + // If the ARB-USD Chainlink response was invalid in this transaction, return the last good ARB-USD price calculated + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle.aggregator)), true); + + lastGoodPrice = tokenUsdPrice; + return (tokenUsdPrice, false); + } +} + + diff --git a/contracts/src/PriceFeeds/COMPPriceFeed.sol b/contracts/src/PriceFeeds/COMPPriceFeed.sol new file mode 100644 index 000000000..a3b87cdf9 --- /dev/null +++ b/contracts/src/PriceFeeds/COMPPriceFeed.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + + +import "./TokenPriceFeedBase.sol"; + +contract COMPPriceFeed is TokenPriceFeedBase { + constructor(address _owner, address _compUsdOracleAddress, uint256 _compUsdStalenessThreshold) + TokenPriceFeedBase(_owner, _compUsdOracleAddress, _compUsdStalenessThreshold) + { + _fetchPricePrimary(); + + // Check the oracle didn't already fail + assert(priceSource == PriceSource.primary); + } + + function fetchPrice() public returns (uint256, bool) { + // If branch is live and the primary oracle setup has been working, try to use it + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + // Use same price for redemption as all other ops in COMP branch + return fetchPrice(); + } + + // _fetchPricePrimary returns: + // - The price + // - A bool indicating whether a new oracle failure was detected in the call + function _fetchPricePrimary(bool /* _isRedemption */ ) internal virtual returns (uint256, bool) { + return _fetchPricePrimary(); + } + + function _fetchPricePrimary() internal returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 tokenUsdPrice, bool tokenUsdOracleDown) = _getOracleAnswer(tokenUsdOracle); + + // If the COMP-USD Chainlink response was invalid in this transaction, return the last good COMP-USD price calculated + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle.aggregator)), true); + + lastGoodPrice = tokenUsdPrice; + return (tokenUsdPrice, false); + } + +} + + diff --git a/contracts/src/PriceFeeds/TokenPriceFeedBase.sol b/contracts/src/PriceFeeds/TokenPriceFeedBase.sol new file mode 100644 index 000000000..f662830b9 --- /dev/null +++ b/contracts/src/PriceFeeds/TokenPriceFeedBase.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "../Dependencies/Ownable.sol"; +import "../Dependencies/AggregatorV3Interface.sol"; +import "../BorrowerOperations.sol"; + +// import "forge-std/console2.sol"; + +abstract contract TokenPriceFeedBase is Ownable { + // Determines where the PriceFeed sources data from. Possible states: + // - primary: Uses the primary price calcuation, which depends on the specific feed + // - lastGoodPrice: the last good price recorded by this PriceFeed. + + enum PriceSource { + primary, + TokenUSDxCanonical, + lastGoodPrice + } + + PriceSource public priceSource; + + // Last good price tracker for the derived USD price + uint256 public lastGoodPrice; + + struct Oracle { + AggregatorV3Interface aggregator; + uint256 stalenessThreshold; + uint8 decimals; + } + + struct ChainlinkResponse { + uint80 roundId; + int256 answer; + uint256 timestamp; + bool success; + } + + error InsufficientGasForExternalCall(); + + event ShutDownFromOracleFailure(address _failedOracleAddr); + + Oracle public tokenUsdOracle; + + IBorrowerOperations borrowerOperations; + + constructor(address _owner, address _tokenUsdOracleAddress, uint256 _tokenUsdStalenessThreshold) Ownable(_owner) { + // Store token-USD oracle + tokenUsdOracle.aggregator = AggregatorV3Interface(_tokenUsdOracleAddress); + tokenUsdOracle.stalenessThreshold = _tokenUsdStalenessThreshold; + tokenUsdOracle.decimals = tokenUsdOracle.aggregator.decimals(); + + assert(tokenUsdOracle.decimals == 8); + } + + // TODO: remove this and set address in constructor, since we'll use CREATE2 + function setAddresses(address _borrowOperationsAddress) external onlyOwner { + borrowerOperations = IBorrowerOperations(_borrowOperationsAddress); + + _renounceOwnership(); + } + + function _getOracleAnswer(Oracle memory _oracle) internal view returns (uint256, bool) { + ChainlinkResponse memory chainlinkResponse = _getCurrentChainlinkResponse(_oracle.aggregator); + + uint256 scaledPrice; + bool oracleIsDown; + // Check oracle is serving an up-to-date and sensible price. If not, shut down this collateral branch. + if (!_isValidChainlinkPrice(chainlinkResponse, _oracle.stalenessThreshold)) { + oracleIsDown = true; + } else { + scaledPrice = _scaleChainlinkPriceTo18decimals(chainlinkResponse.answer, _oracle.decimals); + } + + return (scaledPrice, oracleIsDown); + } + + function _shutDownAndSwitchToLastGoodPrice(address _failedOracleAddr) internal returns (uint256) { + // Shut down the branch + borrowerOperations.shutdownFromOracleFailure(); + + priceSource = PriceSource.lastGoodPrice; + + emit ShutDownFromOracleFailure(_failedOracleAddr); + return lastGoodPrice; + } + + function _getCurrentChainlinkResponse(AggregatorV3Interface _aggregator) + internal + view + returns (ChainlinkResponse memory chainlinkResponse) + { + uint256 gasBefore = gasleft(); + + // Try to get latest price data: + try _aggregator.latestRoundData() returns ( + uint80 roundId, int256 answer, uint256, /* startedAt */ uint256 updatedAt, uint80 /* answeredInRound */ + ) { + // If call to Chainlink succeeds, return the response and success = true + chainlinkResponse.roundId = roundId; + chainlinkResponse.answer = answer; + chainlinkResponse.timestamp = updatedAt; + chainlinkResponse.success = true; + + return chainlinkResponse; + } catch { + // Require that enough gas was provided to prevent an OOG revert in the call to Chainlink + // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used + // in the check itself. + if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); + + // If call to Chainlink aggregator reverts, return a zero response with success = false + return chainlinkResponse; + } + } + + // False if: + // - Call to Chainlink aggregator reverts + // - price is too stale, i.e. older than the oracle's staleness threshold + // - Price answer is 0 or negative + function _isValidChainlinkPrice(ChainlinkResponse memory chainlinkResponse, uint256 _stalenessThreshold) + internal + view + returns (bool) + { + return chainlinkResponse.success && block.timestamp - chainlinkResponse.timestamp < _stalenessThreshold + && chainlinkResponse.answer > 0; + } + + // Trust assumption: Chainlink won't change the decimal precision on any feed used in v2 after deployment + function _scaleChainlinkPriceTo18decimals(int256 _price, uint256 _decimals) internal pure returns (uint256) { + // Scale an int price to a uint with 18 decimals + return uint256(_price) * 10 ** (18 - _decimals); + } +} diff --git a/contracts/src/PriceFeeds/UNIPriceFeed.sol b/contracts/src/PriceFeeds/UNIPriceFeed.sol new file mode 100644 index 000000000..befb807c6 --- /dev/null +++ b/contracts/src/PriceFeeds/UNIPriceFeed.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./TokenPriceFeedBase.sol"; + +contract UNIPriceFeed is TokenPriceFeedBase { + constructor(address _owner, address _uniUsdOracleAddress, uint256 _uniUsdStalenessThreshold) + TokenPriceFeedBase(_owner, _uniUsdOracleAddress, _uniUsdStalenessThreshold) + { + _fetchPricePrimary(); + + // Check the oracle didn't already fail + assert(priceSource == PriceSource.primary); + } + + function fetchPrice() public returns (uint256, bool) { + // If branch is live and the primary oracle setup has been working, try to use it + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + // Use same price for redemption as all other ops in UNI branch + return fetchPrice(); + } + + // _fetchPricePrimary returns: + // - The price + // - A bool indicating whether a new oracle failure was detected in the call + function _fetchPricePrimary(bool /* _isRedemption */ ) internal virtual returns (uint256, bool) { + return _fetchPricePrimary(); + } + + function _fetchPricePrimary() internal returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 tokenUsdPrice, bool tokenUsdOracleDown) = _getOracleAnswer(tokenUsdOracle); + + // If the UNI-USD Chainlink response was invalid in this transaction, return the last good UNI-USD price calculated + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle.aggregator)), true); + + lastGoodPrice = tokenUsdPrice; + return (tokenUsdPrice, false); + } +} + + diff --git a/contracts/src/PriceFeeds/WeETHPriceFeed.sol b/contracts/src/PriceFeeds/WeETHPriceFeed.sol new file mode 100644 index 000000000..662ca974c --- /dev/null +++ b/contracts/src/PriceFeeds/WeETHPriceFeed.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./CompositePriceFeed.sol"; +import "../Interfaces/IWeETHToken.sol"; +import "../Interfaces/IWeETHPriceFeed.sol"; + + +contract WeETHPriceFeed is CompositePriceFeed, IWeETHPriceFeed { + constructor( + address _owner, + address _ethUsdOracleAddress, + address _weEthEthOracleAddress, + address _weEthTokenAddress, + uint256 _ethUsdStalenessThreshold, + uint256 _weEthEthStalenessThreshold + ) CompositePriceFeed(_owner, _ethUsdOracleAddress, _weEthTokenAddress, _ethUsdStalenessThreshold) { + // Store WeETH-ETH oracle + weEthEthOracle.aggregator = AggregatorV3Interface(_weEthEthOracleAddress); + weEthEthOracle.stalenessThreshold = _weEthEthStalenessThreshold; + weEthEthOracle.decimals = weEthEthOracle.aggregator.decimals(); + + _fetchPricePrimary(false); + + // Check the oracle didn't already fail + assert(priceSource == PriceSource.primary); + } + + Oracle public weEthEthOracle; + + uint256 public constant WEETH_ETH_DEVIATION_THRESHOLD = 2e16; // 2% + + function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); + (uint256 weEthEthPrice, bool weEthEthOracleDown) = _getOracleAnswer(weEthEthOracle); + (uint256 weEthPerEth, bool exchangeRateIsDown) = _getCanonicalRate(); + + // If either the ETH-USD feed or exchange rate is down, shut down and switch to the last good price + // seen by the system since we need both for primary and fallback price calcs + if (ethUsdOracleDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true); + } + if (exchangeRateIsDown) { + return (_shutDownAndSwitchToLastGoodPrice(rateProviderAddress), true); + } + // If the ETH-USD feed is live but the WeETH-ETH oracle is down, shutdown and substitute WeETH-ETH with the canonical rate + if (weEthEthOracleDown) { + return (_shutDownAndSwitchToETHUSDxCanonical(address(weEthEthOracle.aggregator), ethUsdPrice), true); + } + + // Otherwise, use the primary price calculation: + + // Calculate the market WeETH-USD price: USD_per_WeETH = USD_per_ETH * ETH_per_WeETH + uint256 weEthUsdMarketPrice = ethUsdPrice * weEthEthPrice / 1e18; + + // Calculate the canonical LST-USD price: USD_per_WeETH = USD_per_ETH * ETH_per_WeETH + uint256 weEthUsdCanonicalPrice = ethUsdPrice * weEthPerEth / 1e18; + + uint256 weEthUsdPrice; + + // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb + if ( + _isRedemption + && _withinDeviationThreshold(weEthUsdMarketPrice, weEthUsdCanonicalPrice, WEETH_ETH_DEVIATION_THRESHOLD) + ) { + weEthUsdPrice = LiquityMath._max(weEthUsdMarketPrice, weEthUsdCanonicalPrice); + } else { + // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. + // Assumes a deviation between market <> canonical of >2% represents a legitimate market price difference. + weEthUsdPrice = LiquityMath._min(weEthUsdMarketPrice, weEthUsdCanonicalPrice); + } + + lastGoodPrice = weEthUsdPrice; + + return (weEthUsdPrice, false); + } + + function _getCanonicalRate() internal view override returns (uint256, bool) { + uint256 gasBefore = gasleft(); + + try IWeETHToken(rateProviderAddress).getExchangeRate() returns (uint256 ethPerWeEth) { + // If rate is 0, return true + if (ethPerWeEth == 0) return (0, true); + + return (ethPerWeEth, false); + } catch { + // Require that enough gas was provided to prevent an OOG revert in the external call + // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used + // in the check itself. + if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); + + // If call to exchange rate reverts, return true + return (0, true); + } + } +} + + diff --git a/contracts/src/PriceFeeds/XVSPriceFeed.sol b/contracts/src/PriceFeeds/XVSPriceFeed.sol new file mode 100644 index 000000000..8769973f3 --- /dev/null +++ b/contracts/src/PriceFeeds/XVSPriceFeed.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./TokenPriceFeedBase.sol"; + +contract XVSPriceFeed is TokenPriceFeedBase { + constructor(address _owner, address _xvsUsdOracleAddress, uint256 _xvsUsdStalenessThreshold) + TokenPriceFeedBase(_owner, _xvsUsdOracleAddress, _xvsUsdStalenessThreshold) + { + _fetchPricePrimary(); + + // Check the oracle didn't already fail + assert(priceSource == PriceSource.primary); + } + + function fetchPrice() public returns (uint256, bool) { + // If branch is live and the primary oracle setup has been working, try to use it + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + // Use same price for redemption as all other ops in XVS branch + return fetchPrice(); + } + + // _fetchPricePrimary returns: + // - The price + // - A bool indicating whether a new oracle failure was detected in the call + function _fetchPricePrimary(bool /* _isRedemption */ ) internal virtual returns (uint256, bool) { + return _fetchPricePrimary(); + } + + function _fetchPricePrimary() internal returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 tokenUsdPrice, bool tokenUsdOracleDown) = _getOracleAnswer(tokenUsdOracle); + + // If the XVS-USD Chainlink response was invalid in this transaction, return the last good XVS-USD price calculated + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle.aggregator)), true); + + lastGoodPrice = tokenUsdPrice; + return (tokenUsdPrice, false); + } +} + + diff --git a/contracts/src/PriceFeeds/tBTCPriceFeed.sol b/contracts/src/PriceFeeds/tBTCPriceFeed.sol new file mode 100644 index 000000000..85ef274be --- /dev/null +++ b/contracts/src/PriceFeeds/tBTCPriceFeed.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./TokenPriceFeedBase.sol"; + +contract tBTCPriceFeed is TokenPriceFeedBase { + constructor(address _owner, address _tBTCUsdOracleAddress, uint256 _tBTCUsdStalenessThreshold) + TokenPriceFeedBase(_owner, _tBTCUsdOracleAddress, _tBTCUsdStalenessThreshold) + { + _fetchPricePrimary(); + + // Check the oracle didn't already fail + assert(priceSource == PriceSource.primary); + } + + function fetchPrice() public returns (uint256, bool) { + // If branch is live and the primary oracle setup has been working, try to use it + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + // Use same price for redemption as all other ops in tBTC branch + return fetchPrice(); + } + + // _fetchPricePrimary returns: + // - The price + // - A bool indicating whether a new oracle failure was detected in the call + function _fetchPricePrimary(bool /* _isRedemption */ ) internal virtual returns (uint256, bool) { + return _fetchPricePrimary(); + } + + function _fetchPricePrimary() internal returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 tokenUsdPrice, bool tokenUsdOracleDown) = _getOracleAnswer(tokenUsdOracle); + + // If the tBTC-USD Chainlink response was invalid in this transaction, return the last good tBTC-USD price calculated + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle.aggregator)), true); + + lastGoodPrice = tokenUsdPrice; + return (tokenUsdPrice, false); + } +} + + diff --git a/contracts/src/PriceFeeds/treeETHPriceFeed.sol b/contracts/src/PriceFeeds/treeETHPriceFeed.sol new file mode 100644 index 000000000..5d53dd569 --- /dev/null +++ b/contracts/src/PriceFeeds/treeETHPriceFeed.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + + +import "./CompositePriceFeed.sol"; +import "../Interfaces/ITreeETHToken.sol"; +import "../Interfaces/ITreeETHPriceFeed.sol"; + + +contract TreeETHPriceFeed is CompositePriceFeed, ITreeETHPriceFeed { + constructor( + address _owner, + address _ethUsdOracleAddress, + address _treeEthEthOracleAddress, + address _treeEthTokenAddress, + uint256 _ethUsdStalenessThreshold, + uint256 _treeEthEthStalenessThreshold + ) CompositePriceFeed(_owner, _ethUsdOracleAddress, _treeEthTokenAddress, _ethUsdStalenessThreshold) { + // Store TreeETH-ETH oracle + treeEthEthOracle.aggregator = AggregatorV3Interface(_treeEthEthOracleAddress); + treeEthEthOracle.stalenessThreshold = _treeEthEthStalenessThreshold; + treeEthEthOracle.decimals = treeEthEthOracle.aggregator.decimals(); + + _fetchPricePrimary(false); + + // Check the oracle didn't already fail + assert(priceSource == PriceSource.primary); + } + + Oracle public treeEthEthOracle; + + uint256 public constant TREEETH_ETH_DEVIATION_THRESHOLD = 2e16; // 2% + + function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); + (uint256 treeEthEthPrice, bool treeEthEthOracleDown) = _getOracleAnswer(treeEthEthOracle); + (uint256 treeEthPerEth, bool exchangeRateIsDown) = _getCanonicalRate(); + + // If either the ETH-USD feed or exchange rate is down, shut down and switch to the last good price + // seen by the system since we need both for primary and fallback price calcs + if (ethUsdOracleDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true); + } + if (exchangeRateIsDown) { + return (_shutDownAndSwitchToLastGoodPrice(rateProviderAddress), true); + } + // If the ETH-USD feed is live but the TreeETH-ETH oracle is down, shutdown and substitute TreeETH-ETH with the canonical rate + if (treeEthEthOracleDown) { + return (_shutDownAndSwitchToETHUSDxCanonical(address(treeEthEthOracle.aggregator), ethUsdPrice), true); + } + + // Otherwise, use the primary price calculation: + + // Calculate the market TreeETH-USD price: USD_per_TreeETH = USD_per_ETH * ETH_per_TreeETH + uint256 treeEthUsdMarketPrice = ethUsdPrice * treeEthEthPrice / 1e18; + + // Calculate the canonical LST-USD price: USD_per_TreeETH = USD_per_ETH * ETH_per_TreeETH + uint256 treeEthUsdCanonicalPrice = ethUsdPrice * treeEthPerEth / 1e18; + + uint256 treeEthUsdPrice; + + // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb + if ( + _isRedemption + && _withinDeviationThreshold(treeEthUsdMarketPrice, treeEthUsdCanonicalPrice, TREEETH_ETH_DEVIATION_THRESHOLD) + ) { + treeEthUsdPrice = LiquityMath._max(treeEthUsdMarketPrice, treeEthUsdCanonicalPrice); + } else { + // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. + // Assumes a deviation between market <> canonical of >2% represents a legitimate market price difference. + treeEthUsdPrice = LiquityMath._min(treeEthUsdMarketPrice, treeEthUsdCanonicalPrice); + } + + lastGoodPrice = treeEthUsdPrice; + + return (treeEthUsdPrice, false); + } + + function _getCanonicalRate() internal view override returns (uint256, bool) { + uint256 gasBefore = gasleft(); + + try ITreeETHToken(rateProviderAddress).getExchangeRate() returns (uint256 ethPerTreeEth) { + // If rate is 0, return true + if (ethPerTreeEth == 0) return (0, true); + + return (ethPerTreeEth, false); + } catch { + // Require that enough gas was provided to prevent an OOG revert in the external call + // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used + // in the check itself. + if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); + + // If call to exchange rate reverts, return true + return (0, true); + } + } +} + + diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index eb7efc5aa..98a381038 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -43,6 +43,10 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // Liquidation penalty for troves redistributed uint256 internal immutable LIQUIDATION_PENALTY_REDISTRIBUTION; + // Maximum debt allowed on this branch + //Current debt on this branch is tracked via getEntireSystemDebt() in LiquityBase.sol + uint256 public debtLimit; + // --- Data structures --- // Store the necessary data for a trove @@ -183,6 +187,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { CCR = _addressesRegistry.CCR(); MCR = _addressesRegistry.MCR(); SCR = _addressesRegistry.SCR(); + debtLimit = _addressesRegistry.debtLimit(); LIQUIDATION_PENALTY_SP = _addressesRegistry.LIQUIDATION_PENALTY_SP(); LIQUIDATION_PENALTY_REDISTRIBUTION = _addressesRegistry.LIQUIDATION_PENALTY_REDISTRIBUTION(); @@ -1967,4 +1972,13 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { Troves[_troveId].interestBatchManager = address(0); Troves[_troveId].batchDebtShares = 0; } + + function getDebtLimit() external view returns (uint256) { + return debtLimit; + } + + function setDebtLimit(uint256 _newDebtLimit) external { + _requireCallerIsCollateralRegistry(); + debtLimit = _newDebtLimit; + } } diff --git a/contracts/test/AnchoredInvariantsTest.t.sol b/contracts/test/AnchoredInvariantsTest.t.sol index f8c6b3fb7..0c05c1a85 100644 --- a/contracts/test/AnchoredInvariantsTest.t.sol +++ b/contracts/test/AnchoredInvariantsTest.t.sol @@ -14,16 +14,18 @@ contract AnchoredInvariantsTest is Logging, BaseInvariantTest, BaseMultiCollater using Strings for uint256; using StringFormatting for uint256; + uint256 public MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + InvariantsTestHandler handler; function setUp() public override { super.setUp(); TestDeployer.TroveManagerParams[] memory p = new TestDeployer.TroveManagerParams[](4); - p[0] = TestDeployer.TroveManagerParams(1.5 ether, 1.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); - p[1] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.01 ether, 0.05 ether, 0.1 ether); - p[2] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.01 ether, 0.05 ether, 0.1 ether); - p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 1.01 ether, 0.05 ether, 0.1 ether); + p[0] = TestDeployer.TroveManagerParams(1.5 ether, 1.1 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT/2); + p[1] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT/2); + p[2] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT/2); + p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT/2); TestDeployer deployer = new TestDeployer(); Contracts memory contracts; (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth,) diff --git a/contracts/test/Invariants.t.sol b/contracts/test/Invariants.t.sol index 09da32b07..01d085615 100644 --- a/contracts/test/Invariants.t.sol +++ b/contracts/test/Invariants.t.sol @@ -69,12 +69,14 @@ contract InvariantsTest is Assertions, Logging, BaseInvariantTest, BaseMultiColl n = 4; } + uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + // TODO: randomize params? How to do it with Foundry invariant testing? TestDeployer.TroveManagerParams[] memory p = new TestDeployer.TroveManagerParams[](n); - if (n > 0) p[0] = TestDeployer.TroveManagerParams(1.5 ether, 1.1 ether, 1.1 ether, 0.05 ether, 0.1 ether); - if (n > 1) p[1] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.2 ether, 0.05 ether, 0.2 ether); - if (n > 2) p[2] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.2 ether, 0.05 ether, 0.2 ether); - if (n > 3) p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 1.01 ether, 0.05 ether, 0.1 ether); + if (n > 0) p[0] = TestDeployer.TroveManagerParams(1.5 ether, 1.1 ether, 1.1 ether, 0.05 ether, 0.1 ether, MAX_INT/2); + if (n > 1) p[1] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.2 ether, 0.05 ether, 0.2 ether, MAX_INT/2); + if (n > 2) p[2] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.2 ether, 0.05 ether, 0.2 ether, MAX_INT/2); + if (n > 3) p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT/2); TestDeployer deployer = new TestDeployer(); Contracts memory contracts; diff --git a/contracts/test/OracleMainnet.t.sol b/contracts/test/OracleMainnet.t.sol index 6a8a36a66..e21060446 100644 --- a/contracts/test/OracleMainnet.t.sol +++ b/contracts/test/OracleMainnet.t.sol @@ -71,6 +71,8 @@ contract OraclesMainnet is TestAccounts { Vars memory vars; + uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + accounts = new Accounts(); createAccounts(); @@ -79,7 +81,7 @@ contract OraclesMainnet is TestAccounts { vars.numCollaterals = 3; TestDeployer.TroveManagerParams memory tmParams = - TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16); + TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2); TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = new TestDeployer.TroveManagerParams[](vars.numCollaterals); for (uint256 i = 0; i < troveManagerParamsArray.length; i++) { diff --git a/contracts/test/SFBold.t.sol b/contracts/test/SFBold.t.sol new file mode 100644 index 000000000..17eea1b74 --- /dev/null +++ b/contracts/test/SFBold.t.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; +import { BoldToken, IBoldToken } from "../src/BoldToken.sol"; + +import {Test} from "forge-std/Test.sol"; +import {ISuperTokenFactory} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; +import {SuperfluidFrameworkDeployer} from + "@superfluid-finance/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.t.sol"; +import {ERC1820RegistryCompiled} from + "@superfluid-finance/ethereum-contracts/contracts/libs/ERC1820RegistryCompiled.sol"; +import { SuperTokenV1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; + +using SuperTokenV1Library for IBoldToken; + +contract SFBold is Test { + string internal constant _NAME = "TestToken"; + address internal constant _OWNER = address(0x1); + uint256 internal constant _PERMIT_SIGNER_PK = 0xA11CE; + address internal constant _ALICE = address(0x4242); + address internal constant _BOB = address(0x4243); + address internal _permitSigner; + IBoldToken internal _boldToken; + SuperfluidFrameworkDeployer.Framework internal _sf; + + function setUp() public { + vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin); + SuperfluidFrameworkDeployer sfDeployer = new SuperfluidFrameworkDeployer(); + sfDeployer.deployTestFramework(); + _sf = sfDeployer.getFramework(); + + BoldToken superTokenPermitProxy = new BoldToken(_OWNER, _sf.superTokenFactory); + superTokenPermitProxy.initialize(_sf.superTokenFactory); + _boldToken = IBoldToken(address(superTokenPermitProxy)); + + // Generate signer address from private key + _permitSigner = vm.addr(_PERMIT_SIGNER_PK); + + // Fund the signer with some tokens + vm.startPrank(_OWNER); + _boldToken.setBranchAddresses(_OWNER, _OWNER, _OWNER, _OWNER); + _boldToken.mint(_permitSigner, 500 ether); + _boldToken.mint(_ALICE, 500 ether); + vm.stopPrank(); + } + + function testPermit() public { + // Test parameters + address spender = address(0x2); + uint256 value = 100; + uint256 deadline = block.timestamp + 1 hours; + + // Get the current nonce for signer + uint256 nonce = _boldToken.nonces(_permitSigner); + + assertEq(_boldToken.allowance(_permitSigner, spender), 0, "Allowance should be 0"); + + // Create permit digest + bytes32 digest = _createPermitDigest(_permitSigner, spender, value, nonce, deadline); + + // Create signature + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_PERMIT_SIGNER_PK, digest); + + // Execute permit as a different address + vm.startPrank(address(0x3)); + + // expect revert if spender doesn't match + vm.expectRevert(); + _boldToken.permit(_permitSigner, address(0xfefe), value, deadline, v, r, s); + + // expect revert if value doesn't match + vm.expectRevert(); + _boldToken.permit(_permitSigner, spender, value + 1, deadline, v, r, s); + + // expect revert if signature is invalid + vm.expectRevert(); + _boldToken.permit(_permitSigner, spender, value, deadline, v + 1, r, s); + + uint256 prevBlockTS = block.timestamp; + vm.warp(block.timestamp + deadline + 1); + // expect revert if deadline is in the past + vm.expectRevert(); + _boldToken.permit(_permitSigner, spender, value, deadline, v, r, s); + + vm.warp(prevBlockTS); + + // Now test with correct parameters - should succeed + _boldToken.permit(_permitSigner, spender, value, deadline, v, r, s); + + vm.stopPrank(); + + // Verify results + assertEq(_boldToken.nonces(_permitSigner), 1, "Nonce should be incremented"); + assertEq(_boldToken.allowance(_permitSigner, spender), value, "Allowance should be set"); + } + + function testFlow() public { + int96 flowRate = 1e12; + uint256 duration = 3600; + + uint256 aliceInitialBalance = _boldToken.balanceOf(_ALICE); + assertEq(_boldToken.balanceOf(_BOB), 0, "Bob should start with balance 0"); + + vm.startPrank(_ALICE); + _boldToken.createFlow(_BOB, flowRate); + vm.stopPrank(); + + vm.warp(block.timestamp + duration); + + uint256 flowAmount = uint96(flowRate) * duration; + assertEq(_boldToken.balanceOf(_BOB), flowAmount, "Bob unexpected balance"); + + vm.startPrank(_ALICE); + _boldToken.deleteFlow(_ALICE, _BOB); + vm.stopPrank(); + + assertEq(_boldToken.balanceOf(_BOB), flowAmount, "Bob unexpected balance"); + assertEq(_boldToken.balanceOf(_ALICE), aliceInitialBalance - flowAmount, "Alice unexpected balance"); + } + + function testStorageLayout() public { + SFBoldStorageLayoutTest testContract = new SFBoldStorageLayoutTest(_OWNER, _sf.superTokenFactory); + testContract.validateStorageLayout(); + } + + // ============================ Internal Functions ============================ + + function _createPermitDigest(address owner, address spender, uint256 value, uint256 nonce, uint256 deadline) + internal + view + returns (bytes32) + { + bytes32 PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline)); + + bytes32 DOMAIN_SEPARATOR = _boldToken.DOMAIN_SEPARATOR(); + + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); + } +} + +/// Validation of the storage layout +contract SFBoldStorageLayoutTest is BoldToken { + + constructor(address _owner, ISuperTokenFactory factory) BoldToken(_owner, factory) {} + error STORAGE_LOCATION_CHANGED(string _name); + + function validateStorageLayout() public pure { + uint256 slot; + uint256 offset; + + // storage slots 0-31 are reserved for SuperToken logic via CustomSuperTokenBase + // storage slot 32: Ownable | address _owner + + assembly { slot := collateralRegistryAddress.slot offset := collateralRegistryAddress.offset } + if (slot != 33 || offset != 0) revert STORAGE_LOCATION_CHANGED("collateralRegistryAddress"); + + assembly { slot := troveManagerAddresses.slot offset := troveManagerAddresses.offset } + if (slot != 34 || offset != 0) revert STORAGE_LOCATION_CHANGED("troveManagerAddresses"); + + assembly { slot := stabilityPoolAddresses.slot offset := stabilityPoolAddresses.offset } + if (slot != 35 || offset != 0) revert STORAGE_LOCATION_CHANGED("stabilityPoolAddresses"); + + assembly { slot := borrowerOperationsAddresses.slot offset := borrowerOperationsAddresses.offset } + if (slot != 36 || offset != 0) revert STORAGE_LOCATION_CHANGED("borrowerOperationsAddresses"); + + assembly { slot := activePoolAddresses.slot offset := activePoolAddresses.offset } + if (slot != 37 || offset != 0) revert STORAGE_LOCATION_CHANGED("activePoolAddresses"); + } +} \ No newline at end of file diff --git a/contracts/test/SortedTroves.t.sol b/contracts/test/SortedTroves.t.sol index eb9898f94..7338ebaeb 100644 --- a/contracts/test/SortedTroves.t.sol +++ b/contracts/test/SortedTroves.t.sol @@ -469,7 +469,8 @@ contract SortedTrovesTest is Test { function setUp() public { bytes32 SALT = keccak256("LiquityV2"); - AddressesRegistry addressesRegistry = new AddressesRegistry(address(this), 150e16, 110e16, 110e16, 5e16, 10e16); + uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + AddressesRegistry addressesRegistry = new AddressesRegistry(address(this), 150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2); bytes32 hash = keccak256( abi.encodePacked( bytes1(0xff), diff --git a/contracts/test/TestContracts/BoldTokenTester.sol b/contracts/test/TestContracts/BoldTokenTester.sol index eaa912470..a77ae6227 100644 --- a/contracts/test/TestContracts/BoldTokenTester.sol +++ b/contracts/test/TestContracts/BoldTokenTester.sol @@ -5,9 +5,9 @@ pragma solidity 0.8.24; import "src/BoldToken.sol"; contract BoldTokenTester is BoldToken { - constructor(address _owner) BoldToken(_owner) {} + constructor(address _owner, ISuperTokenFactory _superTokenFactory) BoldToken(_owner, _superTokenFactory) {} function unprotectedMint(address _account, uint256 _amount) external { - _mint(_account, _amount); + ISuperToken(address(this)).selfMint(_account, _amount, "");(_account, _amount); } } diff --git a/contracts/test/TestContracts/CollateralRegistryTester.sol b/contracts/test/TestContracts/CollateralRegistryTester.sol index e6254944b..5af2dc54a 100644 --- a/contracts/test/TestContracts/CollateralRegistryTester.sol +++ b/contracts/test/TestContracts/CollateralRegistryTester.sol @@ -9,7 +9,7 @@ for testing the parent's internal functions. */ contract CollateralRegistryTester is CollateralRegistry { constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers) - CollateralRegistry(_boldToken, _tokens, _troveManagers) + CollateralRegistry(_boldToken, _tokens, _troveManagers, msg.sender) {} function unprotectedDecayBaseRateFromBorrowing() external returns (uint256) { diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index aa04e76af..879aca88e 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -45,6 +45,14 @@ import "src/PriceFeeds/WETHPriceFeed.sol"; import "src/PriceFeeds/WSTETHPriceFeed.sol"; import "src/PriceFeeds/RETHPriceFeed.sol"; +import {ISuperToken} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; +import {SuperfluidFrameworkDeployer} from + "@superfluid-finance/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.t.sol"; +import {ERC1820RegistryCompiled} from + "@superfluid-finance/ethereum-contracts/contracts/libs/ERC1820RegistryCompiled.sol"; +import { SuperTokenV1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; + + import "forge-std/console2.sol"; uint256 constant _24_HOURS = 86400; @@ -63,6 +71,9 @@ contract TestDeployer is MetadataDeployment { uint256 constant COLL_TOKEN_INDEX = 1; uint128 constant USDC_INDEX = 1; + // Superfluid + SuperfluidFrameworkDeployer.Framework _sf; + // UniV3 ISwapRouter constant uniV3Router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); INonfungiblePositionManager constant uniV3PositionManager = @@ -138,6 +149,7 @@ contract TestDeployer is MetadataDeployment { uint256 SCR; uint256 LIQUIDATION_PENALTY_SP; uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; + uint256 debtLimit; } struct DeploymentVarsDev { @@ -211,6 +223,8 @@ contract TestDeployer is MetadataDeployment { return address(uint160(uint256(hash))); } + uint256 public MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + function deployAndConnectContracts() external returns ( @@ -223,7 +237,7 @@ contract TestDeployer is MetadataDeployment { Zappers memory zappers ) { - return deployAndConnectContracts(TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16)); + return deployAndConnectContracts(TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2)); } function deployAndConnectContracts(TroveManagerParams memory troveManagerParams) @@ -294,14 +308,23 @@ contract TestDeployer is MetadataDeployment { Zappers[] memory zappersArray ) { + //setup SF factories. + vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin); + SuperfluidFrameworkDeployer sfDeployer = new SuperfluidFrameworkDeployer(); + sfDeployer.deployTestFramework(); + _sf = sfDeployer.getFramework(); + DeploymentVarsDev memory vars; vars.numCollaterals = troveManagerParamsArray.length; // Deploy Bold - vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this))); + vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this), _sf.superTokenFactory)); vars.boldTokenAddress = getAddress(address(this), vars.bytecode, SALT); - boldToken = new BoldToken{salt: SALT}(address(this)); + boldToken = IBoldToken(address(new BoldToken{salt: SALT}(address(this), _sf.superTokenFactory))); assert(address(boldToken) == vars.boldTokenAddress); + // Initialize the BoldToken + BoldToken(payable(address(boldToken))).initialize(_sf.superTokenFactory); + contractsArray = new LiquityContractsDev[](vars.numCollaterals); zappersArray = new Zappers[](vars.numCollaterals); vars.collaterals = new IERC20Metadata[](vars.numCollaterals); @@ -328,7 +351,7 @@ contract TestDeployer is MetadataDeployment { vars.troveManagers[vars.i] = ITroveManager(troveManagerAddress); } - collateralRegistry = new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers); + collateralRegistry = new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers, msg.sender); hintHelpers = new HintHelpers(collateralRegistry); multiTroveGetter = new MultiTroveGetter(collateralRegistry); @@ -369,6 +392,7 @@ contract TestDeployer is MetadataDeployment { _troveManagerParams.CCR, _troveManagerParams.MCR, _troveManagerParams.SCR, + _troveManagerParams.debtLimit, _troveManagerParams.LIQUIDATION_PENALTY_SP, _troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION ); @@ -504,6 +528,10 @@ contract TestDeployer is MetadataDeployment { public returns (DeploymentResultMainnet memory result) { + + //setup SF factories. + ISuperTokenFactory superTokenFactory = ISuperTokenFactory(0x0000000000000000000000000000000000000000); + DeploymentVarsMainnet memory vars; result.externalAddresses.ETHOracle = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; @@ -554,9 +582,9 @@ contract TestDeployer is MetadataDeployment { ); // Deploy Bold - vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this))); + vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this), superTokenFactory)); vars.boldTokenAddress = getAddress(address(this), vars.bytecode, SALT); - result.boldToken = new BoldToken{salt: SALT}(address(this)); + result.boldToken = IBoldToken(address(new BoldToken{salt: SALT}(address(this), superTokenFactory))); assert(address(result.boldToken) == vars.boldTokenAddress); // WETH @@ -615,6 +643,7 @@ contract TestDeployer is MetadataDeployment { _troveManagerParams.CCR, _troveManagerParams.MCR, _troveManagerParams.SCR, + _troveManagerParams.debtLimit, _troveManagerParams.LIQUIDATION_PENALTY_SP, _troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION ); diff --git a/contracts/test/TestContracts/DevTestSetup.sol b/contracts/test/TestContracts/DevTestSetup.sol index 221a0c39e..11e78bcd9 100644 --- a/contracts/test/TestContracts/DevTestSetup.sol +++ b/contracts/test/TestContracts/DevTestSetup.sol @@ -4,6 +4,10 @@ pragma solidity 0.8.24; import "./BaseTest.sol"; import {TestDeployer} from "./Deployment.t.sol"; +import { ISuperToken } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; +import { SuperfluidFrameworkDeployer } from "@superfluid-finance/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.t.sol"; +import { ERC1820RegistryCompiled } from "@superfluid-finance/ethereum-contracts/contracts/libs/ERC1820RegistryCompiled.sol"; + contract DevTestSetup is BaseTest { function giveAndApproveColl(address _account, uint256 _amount) public { return giveAndApproveCollateral(collToken, _account, _amount, address(borrowerOperations)); @@ -30,6 +34,19 @@ contract DevTestSetup is BaseTest { assertEq(_token.allowance(_account, _borrowerOperationsAddress), _amount); } + + address constant internal _OWNER = address(0x1); + //BoldToken internal _superBoldToken; + SuperfluidFrameworkDeployer.Framework internal _sf; + + + function setupSuperToken() public { + vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin); + SuperfluidFrameworkDeployer sfDeployer = new SuperfluidFrameworkDeployer(); + sfDeployer.deployTestFramework(); + _sf = sfDeployer.getFramework(); + } + function setUp() public virtual { // Start tests at a non-zero timestamp vm.warp(block.timestamp + 600); @@ -47,6 +64,8 @@ contract DevTestSetup is BaseTest { accountsList[6] ); + setupSuperToken(); + TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev memory contracts; TestDeployer.Zappers memory zappers; diff --git a/contracts/test/TestContracts/TroveManagerTester.t.sol b/contracts/test/TestContracts/TroveManagerTester.t.sol index 3e82745f9..9318f23e5 100644 --- a/contracts/test/TestContracts/TroveManagerTester.t.sol +++ b/contracts/test/TestContracts/TroveManagerTester.t.sol @@ -236,6 +236,11 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { } */ + function getTroveBatchDebtShares(uint256 _troveId) external view returns (uint256) { + Trove memory trove = Troves[_troveId]; + return trove.batchDebtShares; + } + function getTroveStake(uint256 _troveId) external view override returns (uint256) { return Troves[_troveId].stake; } @@ -251,6 +256,17 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { return trove.debt; } + function getbatchDebtAndShares(address batchAddress) external view returns (uint256, uint256) { + LatestBatchData memory batch; + + _getLatestBatchData(batchAddress, batch); + + // Shares are in this mapping + uint256 totalDebtShares = batches[batchAddress].totalDebtShares; + + return (batch.entireDebtWithoutRedistribution, totalDebtShares); + } + function getTroveWeightedRecordedDebt(uint256 _troveId) external view returns (uint256) { Trove memory trove = Troves[_troveId]; address batchAddress = _getBatchManager(trove); @@ -337,6 +353,16 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { return _calcInterest(batch.debt * batch.annualManagementFee, block.timestamp - batch.lastDebtUpdateTime); } + + function getTroveInterestRate(uint256 troveId) external view returns (uint256) { + address batchAddress = _getBatchManager(troveId); + if (batchAddress != address(0)) { + return batches[batchAddress].annualInterestRate; + } + + return Troves[troveId].annualInterestRate; + } + function getBatchAnnualInterestRate(address _batchAddress) external view returns (uint256) { return batches[_batchAddress].annualInterestRate; } @@ -372,4 +398,4 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { batch.totalDebtShares ); } -} +} \ No newline at end of file diff --git a/contracts/test/borrowerOperationsOnBehalfTroveManagament.t.sol b/contracts/test/borrowerOperationsOnBehalfTroveManagament.t.sol index 5f35990a5..29a5e3cd2 100644 --- a/contracts/test/borrowerOperationsOnBehalfTroveManagament.t.sol +++ b/contracts/test/borrowerOperationsOnBehalfTroveManagament.t.sol @@ -109,7 +109,18 @@ contract BorrowerOperationsOnBehalfTroveManagamentTest is DevTestSetup { // Close trove (B opens first, so it’s not the last one) openTroveNoHints100pct(B, 100 ether, 10000e18, 1e17); - deal(address(boldToken), A, troveManager.getTroveEntireDebt(ATroveId)); + + //dev note: this does the same as using deal, + //but because of SF memory usage we cant use deal() with BoldToken. + //so pretend to be the BO, and mint new tokens directly. + //deal(address(boldToken), A, troveManager.getTroveEntireDebt(ATroveId)); + //docs: function deal(address token, address to, uint256 amount); + // Fund the signer with some tokens + vm.startPrank(address(borrowerOperations)); + boldToken.mint(A, troveManager.getTroveEntireDebt(ATroveId)); + vm.stopPrank(); + + closeTrove(A, ATroveId); // Try to reopen trove @@ -489,7 +500,10 @@ contract BorrowerOperationsOnBehalfTroveManagamentTest is DevTestSetup { assertEq(boldToken.balanceOf(A), AInitialBoldBalance - 10e18, "Wrong owner balance 1"); // Manager can repay bold - deal(address(boldToken), B, 100e18); + vm.startPrank(address(borrowerOperations)); + boldToken.mint(B, 100e18); + vm.stopPrank(); + vm.startPrank(B); uint256 BInitialBoldBalance = boldToken.balanceOf(B); @@ -500,8 +514,11 @@ contract BorrowerOperationsOnBehalfTroveManagamentTest is DevTestSetup { assertEq(boldToken.balanceOf(B), BInitialBoldBalance - 10e18, "Wrong manager balance 2"); // Others can’t repay bold - deal(address(boldToken), C, 100e18); - vm.startPrank(C); + vm.startPrank(address(borrowerOperations)); + boldToken.mint(C, 100e18); + vm.stopPrank(); + + vm.startPrank(C); uint256 CInitialBoldBalance = boldToken.balanceOf(C); vm.expectRevert(AddRemoveManagers.NotOwnerNorAddManager.selector); @@ -539,7 +556,9 @@ contract BorrowerOperationsOnBehalfTroveManagamentTest is DevTestSetup { assertEq(borrowerOperations.addManagerOf(ATroveId), address(0)); // Others can repay bold - deal(address(boldToken), B, 100e18); + vm.startPrank(address(borrowerOperations)); + boldToken.mint(B, 100e18); + vm.stopPrank(); uint256 BInitialBoldBalance = boldToken.balanceOf(B); vm.startPrank(B); @@ -617,7 +636,10 @@ contract BorrowerOperationsOnBehalfTroveManagamentTest is DevTestSetup { uint256 AInitialCollBalance = collToken.balanceOf(A); // Owner can close trove - deal(address(boldToken), A, troveManager.getTroveEntireDebt(ATroveId)); + vm.startPrank(address(borrowerOperations)); + boldToken.mint(A, troveManager.getTroveEntireDebt(ATroveId)); + vm.stopPrank(); + vm.startPrank(A); borrowerOperations.closeTrove(ATroveId); vm.stopPrank(); @@ -640,7 +662,10 @@ contract BorrowerOperationsOnBehalfTroveManagamentTest is DevTestSetup { uint256 BInitialCollBalance = collToken.balanceOf(B); // Manager can close trove - deal(address(boldToken), B, troveManager.getTroveEntireDebt(ATroveId)); + vm.startPrank(address(borrowerOperations)); + boldToken.mint(B, troveManager.getTroveEntireDebt(ATroveId)); + vm.stopPrank(); + vm.startPrank(B); borrowerOperations.closeTrove(ATroveId); vm.stopPrank(); @@ -661,7 +686,10 @@ contract BorrowerOperationsOnBehalfTroveManagamentTest is DevTestSetup { vm.stopPrank(); // Other cannot close trove - deal(address(boldToken), C, troveManager.getTroveEntireDebt(ATroveId)); + vm.startPrank(address(borrowerOperations)); + boldToken.mint(C, troveManager.getTroveEntireDebt(ATroveId)); + vm.stopPrank(); + vm.startPrank(C); vm.expectRevert(AddRemoveManagers.NotOwnerNorRemoveManager.selector); borrowerOperations.closeTrove(ATroveId); @@ -676,7 +704,10 @@ contract BorrowerOperationsOnBehalfTroveManagamentTest is DevTestSetup { uint256 BInitialCollBalance = collToken.balanceOf(B); // Other can’t close trove - deal(address(boldToken), B, troveManager.getTroveEntireDebt(ATroveId)); + vm.startPrank(address(borrowerOperations)); + boldToken.mint(B, troveManager.getTroveEntireDebt(ATroveId)); + vm.stopPrank(); + vm.startPrank(B); vm.expectRevert(AddRemoveManagers.NotOwnerNorRemoveManager.selector); borrowerOperations.closeTrove(ATroveId); @@ -687,7 +718,10 @@ contract BorrowerOperationsOnBehalfTroveManagamentTest is DevTestSetup { borrowerOperations.setAddManager(ATroveId, B); vm.stopPrank(); - deal(address(boldToken), B, troveManager.getTroveEntireDebt(ATroveId)); + vm.startPrank(address(borrowerOperations)); + boldToken.mint(B, troveManager.getTroveEntireDebt(ATroveId)); + vm.stopPrank(); + vm.startPrank(B); vm.expectRevert(AddRemoveManagers.NotOwnerNorRemoveManager.selector); borrowerOperations.closeTrove(ATroveId); @@ -696,7 +730,10 @@ contract BorrowerOperationsOnBehalfTroveManagamentTest is DevTestSetup { // Owner can close trove uint256 AInitialCollBalance = collToken.balanceOf(A); - deal(address(boldToken), A, troveManager.getTroveEntireDebt(ATroveId)); + vm.startPrank(address(borrowerOperations)); + boldToken.mint(A, troveManager.getTroveEntireDebt(ATroveId)); + vm.stopPrank(); + vm.startPrank(A); borrowerOperations.closeTrove(ATroveId); vm.stopPrank(); diff --git a/contracts/test/liquidationsLST.t.sol b/contracts/test/liquidationsLST.t.sol index 1b07c9ee6..bed655884 100644 --- a/contracts/test/liquidationsLST.t.sol +++ b/contracts/test/liquidationsLST.t.sol @@ -5,6 +5,8 @@ pragma solidity ^0.8.18; import "./TestContracts/DevTestSetup.sol"; contract LiquidationsLSTTest is DevTestSetup { + uint256 public MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + function setUp() public override { // Start tests at a non-zero timestamp vm.warp(block.timestamp + 600); @@ -25,7 +27,7 @@ contract LiquidationsLSTTest is DevTestSetup { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev memory contracts; (contracts, collateralRegistry, boldToken,,,,) = - deployer.deployAndConnectContracts(TestDeployer.TroveManagerParams(160e16, 120e16, 1.2 ether, 5e16, 10e16)); + deployer.deployAndConnectContracts(TestDeployer.TroveManagerParams(160e16, 120e16, 1.2 ether, 5e16, 10e16, MAX_INT/2)); collToken = contracts.collToken; activePool = contracts.activePool; borrowerOperations = contracts.borrowerOperations; diff --git a/contracts/test/multicollateral.t.sol b/contracts/test/multicollateral.t.sol index 13ab72c63..33e81d177 100644 --- a/contracts/test/multicollateral.t.sol +++ b/contracts/test/multicollateral.t.sol @@ -65,12 +65,14 @@ contract MulticollateralTest is DevTestSetup { accountsList[6] ); + uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = new TestDeployer.TroveManagerParams[](NUM_COLLATERALS); - troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16); - troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16); - troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16); - troveManagerParamsArray[3] = TestDeployer.TroveManagerParams(160e16, 125e16, 125e16, 5e16, 10e16); + troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2); + troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT/2); + troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT/2); + troveManagerParamsArray[3] = TestDeployer.TroveManagerParams(160e16, 125e16, 125e16, 5e16, 10e16, MAX_INT/2); TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; diff --git a/contracts/test/recon/BeforeAfter.sol b/contracts/test/recon/BeforeAfter.sol new file mode 100644 index 000000000..99b48609e --- /dev/null +++ b/contracts/test/recon/BeforeAfter.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {Setup} from "./Setup.sol"; + +import {LatestTroveData} from "../../src/Types/LatestTroveData.sol"; +import {LiquityMath} from "../../src/Dependencies/LiquityMath.sol"; +import {TroveManager} from "../../src/TroveManager.sol"; +import {MIN_DEBT} from "../../src/Dependencies/Constants.sol"; + +// ghost variables for tracking state variable values before and after function calls +abstract contract BeforeAfter is Setup { + struct Vars { + mapping(uint256 => LatestTroveData troveData) dataForTroves; //maps troveId to its given data + mapping(address => TroveManager.Batch) batches; + uint256 collSurplusBalance; + uint256 ghostDebtAccumulator; + uint256 entireSystemDebt; + uint256 ghostWeightedRecordedDebtAccumulator; + uint256 weightedRecordedDebtAccumulator; + uint256 price; + } + + Vars internal _before; + Vars internal _after; + + modifier updateGhosts { + __before(); + _; + __after(); + } + + function __before() internal { + _before.collSurplusBalance = collSurplusPool.getCollateral(_getActor()); + // always zero accumulators at start for clean summation + _before.ghostDebtAccumulator = 0; + _before.ghostWeightedRecordedDebtAccumulator = 0; + _before.weightedRecordedDebtAccumulator = 0; + _before.price = priceFeed.getPrice(); + + uint256 troveArrayLength = troveManager.getTroveIdsCount(); + for(uint256 i; i < troveArrayLength; i++) { + uint256 troveId = troveManager.getTroveFromTroveIdsArray(i); + borrowerOperations.applyPendingDebt(troveId, 0, 1); // NOTE: passing in static hints for simplicity because shouldn't have to worry about gas + + (uint256 debt, uint256 coll, uint64 arrayIndex, uint64 lastDebtUpdateTime, uint64 lastInterestRateAdjTime, uint256 annualInterestRate, uint256 annualManagementFee, uint256 totalDebtShares) = troveManager.getBatch(_getActor()); + + _before.batches[_getActor()] = TroveManager.Batch( + debt, + coll, + arrayIndex, + lastDebtUpdateTime, + lastInterestRateAdjTime, + annualInterestRate, + annualManagementFee, + totalDebtShares + ); + _before.dataForTroves[troveId] = troveManager.getLatestTroveData(troveId); + _before.ghostDebtAccumulator += _before.dataForTroves[troveId].entireDebt; + _before.entireSystemDebt = borrowerOperations.getEntireSystemDebt(); + _before.ghostWeightedRecordedDebtAccumulator += (_before.dataForTroves[troveId].entireDebt * troveManager.getTroveInterestRate(troveId)); + _before.weightedRecordedDebtAccumulator += _before.dataForTroves[troveId].weightedRecordedDebt; + } + } + + function __after() internal { + _after.collSurplusBalance = collSurplusPool.getCollateral(_getActor()); + // always zero accumulators at start for clean summation + _after.ghostDebtAccumulator = 0; + _after.ghostWeightedRecordedDebtAccumulator = 0; + _after.weightedRecordedDebtAccumulator = 0; + _after.price = priceFeed.getPrice(); + + uint256 troveArrayLength = troveManager.getTroveIdsCount(); + for(uint256 i; i < troveArrayLength; i++) { + uint256 troveId = troveManager.getTroveFromTroveIdsArray(i); + borrowerOperations.applyPendingDebt(troveId, 0, 1); // NOTE: passing in static hints for simplicity because shouldn't have to worry about gas + + + (uint256 debt, uint256 coll, uint64 arrayIndex, uint64 lastDebtUpdateTime, uint64 lastInterestRateAdjTime, uint256 annualInterestRate, uint256 annualManagementFee, uint256 totalDebtShares) = troveManager.getBatch(_getActor()); + + _after.batches[_getActor()] = TroveManager.Batch( + debt, + coll, + arrayIndex, + lastDebtUpdateTime, + lastInterestRateAdjTime, + annualInterestRate, + annualManagementFee, + totalDebtShares + ); + _after.dataForTroves[troveId] = troveManager.getLatestTroveData(troveId); + _after.ghostDebtAccumulator += _after.dataForTroves[troveId].entireDebt; + _after.entireSystemDebt = borrowerOperations.getEntireSystemDebt(); + _after.ghostWeightedRecordedDebtAccumulator += (_after.dataForTroves[troveId].entireDebt * troveManager.getTroveInterestRate(troveId)); + _after.weightedRecordedDebtAccumulator += _after.dataForTroves[troveId].weightedRecordedDebt; + + // TODO: Missing Zombie Trove | lastZombieTrove (NOTE: Technically a suite long enough will have more than one) + } + } +} diff --git a/contracts/test/recon/CryticTester.sol b/contracts/test/recon/CryticTester.sol new file mode 100644 index 000000000..5041b93ae --- /dev/null +++ b/contracts/test/recon/CryticTester.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {TargetFunctions} from "./TargetFunctions.sol"; +import {CryticAsserts} from "@chimera/CryticAsserts.sol"; + +// echidna . --contract CryticTester --config echidna.yaml --format text --workers 16 --test-limit 10000000 --test-mode exploration +// medusa fuzz +contract CryticTester is TargetFunctions, CryticAsserts { + constructor() payable { + setup(); + } +} diff --git a/contracts/test/recon/CryticToFoundry.sol b/contracts/test/recon/CryticToFoundry.sol new file mode 100644 index 000000000..bf5c88670 --- /dev/null +++ b/contracts/test/recon/CryticToFoundry.sol @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {TargetFunctions} from "./TargetFunctions.sol"; +import {FoundryAsserts} from "@chimera/FoundryAsserts.sol"; +import "forge-std/console2.sol"; + +import {ERC1820RegistryCompiled} from + "@superfluid-finance/ethereum-contracts/contracts/libs/ERC1820RegistryCompiled.sol"; + +// forge test --match-contract CryticToFoundry -vv +contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { + function setUp() public { + vm.etch(ERC1820RegistryCompiled.at, ERC1820RegistryCompiled.bin); // TODO: Deploy at a new address + + setup(); + } + + // forge test --match-test test_crytic -vvv + function test_crytic() public { + // TODO: add failing property tests here for debugging + borrowerOperations_openTrove(address(this), 123, 100e18, 2000e18, 0, 0, 1e18, 100e18, address(this), address(this), address(this)); + borrowerOperations_openTrove(address(this), 13232, 100e18, 2000e18, 0, 0, 1e18, 100e18, address(this), address(this), address(this)); + borrowerOperations_adjustTrove_clamped(123, true, 0, true, 0); + priceFeed_setPrice(1); + troveManager_liquidate_clamped(); + } + + // forge test --match-test test_property_active_troves_are_above_MIN_DEBT_1 -vvv +function test_property_active_troves_are_above_MIN_DEBT_1() public { + + borrowerOperations_openTrove_clamped(0x0000000000000000000000000000000000000000,0,13339564538247939756636,1996998175339557135783,0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000000); + + priceFeed_setPrice(63326149451276); + + borrowerOperations_shutdown(); + + // Doesn't matter once you're shutdown + troveManager_urgentRedemption_clamped(828122044839760619); + property_active_troves_are_above_MIN_DEBT(); + + } + + // forge test --match-test test_property_AP01_2 -vvv +function test_property_AP01_2() public { + + borrowerOperations_openTrove_clamped(0x0000000000000000000000000000000000000000,0,1506153691784959742,1996342313314219885149,0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000000); + + property_AP01(); + + } + + // forge test --match-test test_optimize_ap01_under_0 -vvv +function test_optimize_ap01_under_0() public { + + // Max value: 2005160758086615185564410686266145742105615563; + + vm.roll(block.number + 5054); + vm.warp(block.timestamp + 81172); + console2.log("before activePool.aggWeightedDebtSum", activePool.aggWeightedDebtSum()); + borrowerOperations_openTrove_clamped(0x00000000000000000000000000000002fFffFffD,93701483398682665984728072936648256433854685342449288884605117331643986361215,79409241918338619357573071,260636221954315249937846958,0xc98D9175A32ca68C4B83dB84B4707AF82ae37cC4,0x0000000000000000000000000000000000000F08,0x00000000000000000000000000000002fFffFffD); + console2.log("optimize_ap01_under 0", optimize_ap01_under()); + console2.log("activePool.aggWeightedDebtSum", activePool.aggWeightedDebtSum()); + console2.log("_before.ghostWeightedRecordedDebtAccumulator", _before.ghostWeightedRecordedDebtAccumulator); + console2.log("_after.ghostWeightedRecordedDebtAccumulator", _after.ghostWeightedRecordedDebtAccumulator); + vm.roll(block.number + 20910); + vm.warp(block.timestamp + 322374); + stabilityPool_provideToSP_clamped(27424305496545741552056768792830585584317965363475532520705411256164270180376,false); + + console2.log("optimize_ap01_under", optimize_ap01_under()); + vm.warp(block.timestamp + 695463); + + vm.roll(block.number + 102155); + + vm.roll(block.number + 40897); + vm.warp(block.timestamp + 856); + borrowerOperations_withdrawBold_clamped(148127151065994630252606205); + + vm.warp(block.timestamp + 717585); + + vm.roll(block.number + 98985); + + vm.roll(block.number + 2526); + vm.warp(block.timestamp + 338920); + canary_liquidation(); + + vm.warp(block.timestamp + 1350578); + + vm.roll(block.number + 88170); + + vm.roll(block.number + 60054); + vm.warp(block.timestamp + 24867); + asset_approve(0x00000000000000000000000000000001fffffffE,254075269958341709303778481485360912762); + console2.log("optimize_ap01_under", optimize_ap01_under()); + + vm.roll(block.number + 24311); + vm.warp(block.timestamp + 277232); + property_CS04(); + + vm.warp(block.timestamp + 1448730); + + vm.roll(block.number + 132063); + + vm.roll(block.number + 22909); + vm.warp(block.timestamp + 322374); + borrowerOperations_addColl_clamped(32590110299340558042976583); + + vm.warp(block.timestamp + 511822); + + vm.roll(block.number + 11905); + + vm.roll(block.number + 53166); + vm.warp(block.timestamp + 212460); + priceFeed_triggerShutdown(); + console2.log("optimize_ap01_under", optimize_ap01_under()); + + vm.warp(block.timestamp + 135921); + + vm.roll(block.number + 30042); + + vm.roll(block.number + 6234); + vm.warp(block.timestamp + 50417); + property_BT01(); + + vm.roll(block.number + 22699); + vm.warp(block.timestamp + 4177); + property_BT02(); + + vm.roll(block.number + 53451); + vm.warp(block.timestamp + 156190); + property_BT05(); + + vm.warp(block.timestamp + 436727); + + vm.roll(block.number + 59552); + + vm.roll(block.number + 55052); + vm.warp(block.timestamp + 275394); + property_sum_of_batches_debt_and_shares(); + console2.log("optimize_ap01_under", optimize_ap01_under()); + + vm.warp(block.timestamp + 446947); + + vm.roll(block.number + 61528); + + vm.roll(block.number + 4462); + vm.warp(block.timestamp + 254414); + priceFeed_setPrice(184319410175182376593245936); + + vm.warp(block.timestamp + 1234164); + + vm.roll(block.number + 187559); + + vm.roll(block.number + 32435); + vm.warp(block.timestamp + 135921); + stabilityPool_withdrawFromSP_clamped(94239526814803268770816854694085645879100106975629632379034,true); + + vm.roll(block.number + 60054); + vm.warp(block.timestamp + 419861); + asset_mint(0x00000000000000000000000000000001fffffffE,726); + + vm.warp(block.timestamp + 400981); + + vm.roll(block.number + 15367); + + vm.roll(block.number + 1088); + vm.warp(block.timestamp + 73040); + property_weighted_sum(); + console2.log("optimize_ap01_under", optimize_ap01_under()); + + vm.warp(block.timestamp + 522178); + + vm.roll(block.number + 59983); + + vm.roll(block.number + 23978); + vm.warp(block.timestamp + 172101); + collToken_approve(0x00000000000000000000000000000000DeaDBeef,101382401482932389799902259568732152409); + + vm.warp(block.timestamp + 527243); + + vm.roll(block.number + 54311); + + vm.roll(block.number + 57086); + vm.warp(block.timestamp + 100835); + property_BA01(); + + vm.roll(block.number + 7712); + vm.warp(block.timestamp + 45142); + asset_approve(0x00000000000000000000000000000000DeaDBeef,43234010939624185218020403123525591057); + console2.log("optimize_ap01_under", optimize_ap01_under()); + + vm.warp(block.timestamp + 913317); + + vm.roll(block.number + 34710); + + vm.roll(block.number + 20243); + vm.warp(block.timestamp + 31593); + borrowerOperations_adjustTroveInterestRate_clamped(64479505916471466477331997908391885217657807194625147020426390684972435426202,15398573290560247665990881735019176722007117505318547132077308618354371879859,2962310210599114012974028230312168185270655657574208074510281456716751689783); + + vm.warp(block.timestamp + 33271); + + vm.roll(block.number + 59983); + + vm.roll(block.number + 30011); + vm.warp(block.timestamp + 478623); + borrowerOperations_applyPendingDebt_clamped(); + + vm.warp(block.timestamp + 322374); + + vm.roll(block.number + 20152); + + vm.roll(block.number + 59981); + vm.warp(block.timestamp + 358061); + property_CS04(); + console2.log("optimize_ap01_under", optimize_ap01_under()); + + vm.roll(block.number + 46212); + vm.warp(block.timestamp + 405856); + asset_approve(0x0000000000000000000000000000000000000F0A,195760263594747118956609330224137207565); + + vm.roll(block.number + 2526); + vm.warp(block.timestamp + 82671); + canary_liquidation(); + + vm.warp(block.timestamp + 422565); + + vm.roll(block.number + 77402); + + vm.roll(block.number + 8447); + vm.warp(block.timestamp + 48884); + property_BT01(); + + vm.warp(block.timestamp + 526194); + + vm.roll(block.number + 23885); + + vm.roll(block.number + 561); + vm.warp(block.timestamp + 166184); + property_CS04(); + console2.log("optimize_ap01_under", optimize_ap01_under()); + + vm.warp(block.timestamp + 729777); + + vm.roll(block.number + 114136); + + vm.roll(block.number + 5140); + vm.warp(block.timestamp + 13573); + borrowerOperations_addColl_clamped(205933596267234158443431627); + + vm.warp(block.timestamp + 219064); + + vm.roll(block.number + 21894); + + vm.roll(block.number + 49415); + vm.warp(block.timestamp + 115085); + priceFeed_fetchRedemptionPrice(); + + vm.roll(block.number + 5023); + vm.warp(block.timestamp + 490446); + property_BT01(); + + vm.warp(block.timestamp + 402051); + + vm.roll(block.number + 76286); + + vm.roll(block.number + 23978); + vm.warp(block.timestamp + 446755); + collToken_mint(0x00000000000000000000000000000000FFFFfFFF,340282366920938463463374607431768211454); + console2.log("optimize_ap01_under", optimize_ap01_under()); + + vm.warp(block.timestamp + 1035661); + + vm.roll(block.number + 50455); + + vm.roll(block.number + 6729); + vm.warp(block.timestamp + 150273); + priceFeed_setPrice(81060101198367503560470122); + + vm.roll(block.number + 22909); + vm.warp(block.timestamp + 24867); + property_BA01(); + + vm.roll(block.number + 45852); + vm.warp(block.timestamp + 244814); + property_sum_of_batches_debt_and_shares(); + + vm.roll(block.number + 33357); + vm.warp(block.timestamp + 463587); + property_CS05(); + + vm.roll(block.number + 60267); + vm.warp(block.timestamp + 209115); + borrowerOperations_withdrawColl_clamped(1524785991); + console2.log("optimize_ap01_under", optimize_ap01_under()); + + vm.warp(block.timestamp + 259947); + + vm.roll(block.number + 38548); + + vm.roll(block.number + 6721); + vm.warp(block.timestamp + 463587); + priceFeed_setPrice(81898490519679642781584296); + + vm.warp(block.timestamp + 392860); + + vm.roll(block.number + 19692); + + vm.roll(block.number + 27404); + vm.warp(block.timestamp + 437838); + borrowerOperations_repayBold_clamped(507656663); + + console2.log("optimize_ap01_under", optimize_ap01_under()); + + } +} diff --git a/contracts/test/recon/Properties.sol b/contracts/test/recon/Properties.sol new file mode 100644 index 000000000..a82cb6e50 --- /dev/null +++ b/contracts/test/recon/Properties.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {Asserts} from "@chimera/Asserts.sol"; +import {BeforeAfter} from "./BeforeAfter.sol"; + +import {MIN_DEBT} from "../../src/Dependencies/Constants.sol"; +import {LatestBatchData} from "../../src/Types/LatestBatchData.sol"; +import {BatchId} from "../../src/Types/BatchId.sol"; +import {SortedTroves} from "../../src/SortedTroves.sol"; +import {LatestTroveData} from "../../src/Types/LatestTroveData.sol"; + + +abstract contract Properties is BeforeAfter, Asserts { + + /// === NOT IMPLEMENTED === /// + + function property_SR01(uint256 troveId) internal { + (, uint256 tail) = sortedTroves.batches(BatchId.wrap(_getActor())); + eq(tail, troveId, "SR-01: Troves should always be added to the end of a batch in SortedTroves"); + } + + + function property_TR04(uint256 expectedDelta, uint256 debtAfter, uint256 debtBefore, uint256 fee) internal { + // needs to take input value of the expected debt change + eq(expectedDelta, (debtAfter - debtBefore) - fee, "TR-04: excluding update/open fee, trove debt delta is the debtIncrease - debtDecrease"); + } + + // NOTE: You should implement this + function property_TR06(uint256 troveDebtBeforeLiquidation, uint256 beforeGhostDebtAccumulator, uint256 afterGhostDebtAccumulator) internal { + // checking debt redistribution + // current setup only uses one _getActor() so can just sum over all troves in system for the _getActor()'s debt + uint256 debtPercentageOfTotal = (beforeGhostDebtAccumulator / borrowerOperations.getEntireSystemDebt()) * 10_000; + + // check that increase in debt was proportional to their percentage of total + // total system debt should increase by debtPercentageOfTotal * liqAmount + uint256 debtDelta = afterGhostDebtAccumulator - beforeGhostDebtAccumulator; + eq(debtPercentageOfTotal * debtDelta, debtPercentageOfTotal * troveDebtBeforeLiquidation, "TR-06: Contribution of user collateral should be equal to the percent offered in a liquidation"); + } + + + // TODO: More basic liquity properties + + // User should never be able to self liquidate via one action + + // SP & Batch Manager stuff I'm not super sure of + + /// === Liqutiy Basic Properties === /// + // All troves that are active must have debt above the MIN_DEBT -> TODO GOOD PROPERTY + // GetDebt > MIN_DETB + function property_active_troves_are_above_MIN_DEBT() public { + // Skip if shutdown + if(borrowerOperations.hasBeenShutDown()) { + return; + } + + uint256 trove = sortedTroves.getFirst(); /// NOTE: Troves in ST are active + while(trove != 0) { + uint256 debt = troveManager.getTroveEntireDebt(trove); + gte(debt, MIN_DEBT, "Must have min debt"); + + trove = sortedTroves.getNext(trove); + } + } + + function property_CS04() public { + // check collateral balance dependent on the collateral of the activeBranch + uint256 collSurplusPoolBalanceWeth = collToken.balanceOf(address(collSurplusPool)); + gte(collSurplusPoolBalanceWeth, collSurplusPool.getCollBalance(), "CS-04: collSurplusPool balance > getCollBalance"); + } + + function property_CS05() public { + // NOTE: for multi-_getActor() setup this would need to sum over all actors + uint256 accountBalances = collSurplusPool.getCollateral(_getActor()); + uint256 poolBalance = collSurplusPool.getCollBalance(); + eq(accountBalances, poolBalance, "CS-05: sum of _getActor() collaterals should equal pool collateral balance"); + } + + function property_TR03() public { + // loop through all troves + uint256 troveArrayLength = troveManager.getTroveIdsCount(); + for(uint256 i; i < troveArrayLength; i++) { + uint256 troveId = troveManager.getTroveFromTroveIdsArray(i); + // check if trove has pending debt/coll redistribution + (uint256 collateralForDistribution, uint256 boldDebtForDistribution) = troveManager.rewardSnapshots(troveId); + if(boldDebtForDistribution > 0 || collateralForDistribution > 0) { + // store current values for trove debt/coll + (uint256 debt, uint256 coll,,,,,,,,) = troveManager.Troves(troveId); + // call getLatestTroveData to update + LatestTroveData memory troveData = troveManager.getLatestTroveData(troveId); + + // check if debt/coll increased + if(boldDebtForDistribution > 0) { + gte(troveData.entireDebt, debt, "TR-03: getLatestTroveData always returns up-to-date, post-accrual debt value"); + } else if(collateralForDistribution > 0) { + gte(troveData.entireColl, coll, "TR-03: getLatestTroveData always returns up-to-date, post-accrual collateral value"); + } else { + gte(troveData.entireDebt, debt, "TR-03: getLatestTroveData always returns up-to-date, post-accrual debt value"); + gte(troveData.entireColl, coll, "TR-03: getLatestTroveData always returns up-to-date, post-accrual collateral value"); + } + } + } + } + + + // If there's a batch maanger + // And they have 0 shares, then they have 0 debt + // if they have 0 debt then they have 0 shares + // NOTE: Can be massively expanded + function property_BT01() public { + // NOTE: Could be extended to check on unclamped + (uint256 debt, uint256 shares) = troveManager.getbatchDebtAndShares(clampedBatchManager); + + if(debt == 0) { + eq(shares, 0, "Must have 0 shares on 0 debt"); + } + if(shares == 0) { + eq(debt, 0, "Must have 0 debt on 0 shares"); + } + } + + function property_SR02() public { + // get first node of the linked-list + uint256 firstNode = sortedTroves.getFirst(); + uint256 listSize = sortedTroves.getSize(); + + if(listSize < 2) { + return; // It will loop around and mess things up + } + + uint256 currentNodeId = firstNode; // start search from the first node + + uint256 previousNodeId; + for(uint256 i; i < listSize + 1; i++) { + (uint256 nextId, uint256 prevId,,) = sortedTroves.nodes(currentNodeId); + + if(nextId == firstNode) { + // if the nextId is same as the firstNode, the list has looped around + break; + } + + // descend down the list and verify that the debt of each subsequent node is smaller than the previous + if(previousNodeId != 0) { + uint256 currentAnnualInterestRate = troveManager.getTroveInterestRate(currentNodeId); + uint256 nextAnnualInterestRate = troveManager.getTroveInterestRate(nextId); + gte(currentAnnualInterestRate, nextAnnualInterestRate, "SR-02: Troves are sorted by interest rate in descending order"); + } + + // update current/previous node + previousNodeId = currentNodeId; + currentNodeId = nextId; + } + } + + /// === Ghost variables === /// + function property_BA01() public { + eq(_after.ghostWeightedRecordedDebtAccumulator, _after.weightedRecordedDebtAccumulator, "BA-01: For all operations, weightedRecordedDebt is equal to the sum of trove debt * rate"); + } + function property_weighted_sum() public { + + } + + function property_CP01() public { + if(_after.entireSystemDebt > _after.ghostDebtAccumulator + 1e18) { + t(false, "CP-01: Total debt == SUM(userDebt) - With precision"); + } + + if(_after.entireSystemDebt < _after.ghostDebtAccumulator - 1e18) { + t(false, "CP-01: Total debt == SUM(userDebt) - With precision"); + } + // NOTE: Changed from exact to have bounds + // eq(_after.entireSystemDebt, _after.ghostDebtAccumulator, "CP-01: Total debt == SUM(userDebt)"); + } + + + function property_CS01() public { + t(_before.collSurplusBalance >= _after.collSurplusBalance, "CS-01: Collateral surplus balances can increase only after a liquidation"); + } + + function property_BT02() public { + + if(_before.batches[_getActor()].totalDebtShares == 0 && _after.batches[_getActor()].totalDebtShares == 0) return; + + uint256 ppfsBefore; + uint256 ppfsAfter; + + if(_before.batches[_getActor()].totalDebtShares != 0) ppfsBefore = _before.batches[_getActor()].debt / _before.batches[_getActor()].totalDebtShares; + else ppfsBefore = 0; + + if(_after.batches[_getActor()].totalDebtShares != 0) ppfsAfter = _after.batches[_getActor()].debt / _after.batches[_getActor()].totalDebtShares; + else ppfsAfter = 0; + + if(_after.batches[_getActor()].debt != 0) { + gte(ppfsAfter, ppfsBefore, "BT-02: Batch share PPFS always increases, unless the total debt in the batch is reset to 0"); + } + } + + function property_BT05() public { + if(_before.batches[_getActor()].totalDebtShares == 0 && _after.batches[_getActor()].totalDebtShares == 0) return; + + // PPFS = Debt / Shares for a batch + // current setup permits only one batch because have one _getActor(), so can query it from TM directly + uint256 ppfsBefore; + uint256 ppfsAfter; + + if(_before.batches[_getActor()].totalDebtShares != 0) ppfsBefore = _before.batches[_getActor()].debt / _before.batches[_getActor()].totalDebtShares; + else ppfsBefore = 0; + + if(_after.batches[_getActor()].totalDebtShares != 0) ppfsAfter = _after.batches[_getActor()].debt / _after.batches[_getActor()].totalDebtShares; + else ppfsAfter = 0; + + // Assert that the Debt / Shares should never double (should never go to 2 times Debt / Shares) + t(ppfsAfter != ppfsBefore * 2, "BT-05: PPFS (debt /shares) should never double"); + } + + function property_AP01() public { + eq(_after.ghostWeightedRecordedDebtAccumulator, activePool.aggWeightedDebtSum(), "AP-01: newAggWeightedDebtSum == aggRecordedDebt if all troves have been synced in this block"); + } + + function property_sum_of_batches_debt_and_shares() public { + if(clampedBatchManager == address(0)) { + return; + } + // total debt + (uint256 batchDebt, uint256 batchShares) = troveManager.getbatchDebtAndShares(clampedBatchManager); + + (uint256 sumBatchDebt, uint256 sumbBatchShares) = _sumBatchSharesAndDebt(clampedBatchManager); + + eq(batchShares, sumbBatchShares, "Sum of batch shares matches"); + eq(batchDebt, sumBatchDebt, "Sum of batch debt matches"); + } + + function _sumBatchSharesAndDebt(address batchManager) internal returns (uint256 sumBatchDebt, uint256 sumbBatchShares) { + + uint256 trove = sortedTroves.getFirst(); /// NOTE: Troves in ST are active + while(trove != 0) { + + if(borrowerOperations.interestBatchManagerOf(trove) == clampedBatchManager) { + sumBatchDebt += troveManager.getTroveEntireDebt(trove); + sumbBatchShares += troveManager.getTroveBatchDebtShares(trove); + } + + trove = sortedTroves.getNext(trove); + } + + // Add lastZombieTroveId if necessary + uint256 lastZombieTroveId = troveManager.lastZombieTroveId(); + if(borrowerOperations.interestBatchManagerOf(lastZombieTroveId) == clampedBatchManager) { + sumBatchDebt += troveManager.getTroveEntireDebt(lastZombieTroveId); + sumbBatchShares += troveManager.getTroveBatchDebtShares(lastZombieTroveId); + } + } + + + // Optimization of loss on debt and inaccuracies | AP_01 + + // Optimization on SP math for both Debt and Coll (See if we can repro stuff) + + // TODO: Missing Debt Caps Properties + // Missing Griefability of Debt Caps and Governance + // TODO: Missing handlers for Superfluid Token +} diff --git a/contracts/test/recon/Setup.sol b/contracts/test/recon/Setup.sol new file mode 100644 index 000000000..67068a45f --- /dev/null +++ b/contracts/test/recon/Setup.sol @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseSetup} from "@chimera/BaseSetup.sol"; +import {vm} from "chimera/Hevm.sol"; +import "forge-std/console2.sol"; + +import {MockERC20} from "./mocks/MockERC20.sol"; + +import {ActorManager} from "./managers/ActorManager.sol"; +import {AssetManager} from "./managers/AssetManager.sol"; + +import {AddressesRegistry} from "../../src/AddressesRegistry.sol"; +import {ActivePool} from "../../src/ActivePool.sol"; +import {BoldToken} from "../../src/BoldToken.sol"; +import {BorrowerOperationsTester} from "../TestContracts/BorrowerOperationsTester.t.sol"; +import {CollateralRegistry} from "../../src/CollateralRegistry.sol"; +import {CollSurplusPool} from "../../src/CollSurplusPool.sol"; +import {DefaultPool} from "../../src/DefaultPool.sol"; +import {SortedTroves} from "../../src/SortedTroves.sol"; +import {StabilityPool} from "../../src/StabilityPool.sol"; +import {TroveManagerTester} from "../TestContracts/TroveManagerTester.t.sol"; +import {TroveNFT} from "../../src/TroveNFT.sol"; +import {PriceFeedTestnet} from "../TestContracts/PriceFeedTestnet.sol"; +import {GasPool} from "../../src/GasPool.sol"; +import {HintHelpers} from "../../src/HintHelpers.sol"; +import {MultiTroveGetter} from "../../src/MultiTroveGetter.sol"; + +import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IBoldToken} from "../../src/Interfaces/IBoldToken.sol"; +import { ISuperToken, ISuperTokenFactory, IERC20 } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; +import {IWETH} from "../../src/Interfaces/IWETH.sol"; +import {IAddressesRegistry} from "../../src/Interfaces/IAddressesRegistry.sol"; +import {ICollateralRegistry} from "../../src/Interfaces/ICollateralRegistry.sol"; +import {IHintHelpers} from "../../src/Interfaces/IHintHelpers.sol"; +import {IMultiTroveGetter} from "../../src/Interfaces/IMultiTroveGetter.sol"; +import {IInterestRouter} from "../../src/Interfaces/IInterestRouter.sol"; +import {IPriceFeed} from "../../src/Interfaces/IPriceFeed.sol"; +import {MetadataNFT, IMetadataNFT} from "../../src/NFTMetadata/MetadataNFT.sol"; + +// Add these interface imports +import {IBorrowerOperations} from "../../src/Interfaces/IBorrowerOperations.sol"; +import {ITroveManager} from "../../src/Interfaces/ITroveManager.sol"; +import {ITroveNFT} from "../../src/Interfaces/ITroveNFT.sol"; +import {IStabilityPool} from "../../src/Interfaces/IStabilityPool.sol"; +import {IActivePool} from "../../src/Interfaces/IActivePool.sol"; +import {IDefaultPool} from "../../src/Interfaces/IDefaultPool.sol"; +import {ICollSurplusPool} from "../../src/Interfaces/ICollSurplusPool.sol"; +import {ISortedTroves} from "../../src/Interfaces/ISortedTroves.sol"; + +// Superfluid +import {SuperfluidFrameworkDeployer} from + "@superfluid-finance/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeployer.t.sol"; +import { SuperTokenV1Library } from "@superfluid-finance/ethereum-contracts/contracts/apps/SuperTokenV1Library.sol"; + + +interface IInitializableBold { + function initialize(ISuperTokenFactory factory) external; +} + +contract InterestRouter { + +} + +// TODO: Figure out ways to shutdown +contract MultiTokenPriceFeedTestnet { + event LastGoodPriceUpdated(uint256 _lastGoodPrice); + + uint256 private _price = 2000 * 1e18; + + // --- Functions --- + + // View price getter for simplicity in tests + function getPrice() external view returns (uint256) { + return _price; + } + + function lastGoodPrice() external view returns (uint256) { + return _price; + } + + // TODO: Redemptions and non redemptions + function fetchPrice() external returns (uint256, bool) { + // Fire an event just like the mainnet version would. + // This lets the subgraph rely on events to get the latest price even when developing locally. + emit LastGoodPriceUpdated(_price); + return (_price, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + // Fire an event just like the mainnet version would. + // This lets the subgraph rely on events to get the latest price even when developing locally. + emit LastGoodPriceUpdated(_price); + return (_price, false); + } + + // Manual external price setter. + function setPrice(uint256 price) external returns (bool) { + _price = price; + return true; + } + + // TODO: Shutdowns + function triggerShutdown() external { + // TODO: pass BO + // borrowerOperations.shutdownFromOracleFailure(); + } +} + +struct LiquityContractsDev { + AddressesRegistry addressesRegistry; + ActivePool activePool; + BorrowerOperationsTester borrowerOperations; // Tester + CollSurplusPool collSurplusPool; + DefaultPool defaultPool; + SortedTroves sortedTroves; + StabilityPool stabilityPool; + TroveManagerTester troveManager; // Tester + TroveNFT troveNFT; + MultiTokenPriceFeedTestnet priceFeed; // Tester + GasPool gasPool; + InterestRouter interestRouter; + MockERC20 collToken; +} + +// Collateral Registry + +abstract contract Setup is BaseSetup, ActorManager, AssetManager { + LiquityContractsDev[] branches; + + LiquityContractsDev activeBranch; + + // Global contracts + IBoldToken boldToken; + InterestRouter interestRouter; + CollateralRegistry collateralRegistry; + HintHelpers hintHelpers; + MultiTroveGetter multiTroveGetter; + + // Branch Contracts + AddressesRegistry addressesRegistry; + ActivePool activePool; + BorrowerOperationsTester borrowerOperations; // Tester + CollSurplusPool collSurplusPool; + DefaultPool defaultPool; + SortedTroves sortedTroves; + StabilityPool stabilityPool; + TroveManagerTester troveManager; // Tester + TroveNFT troveNFT; + MultiTokenPriceFeedTestnet priceFeed; // Tester + GasPool gasPool; + MockERC20 collToken; + + // List of Managers + // TODO: Consider adding + + // List of TroveIds + uint256[] troveIds; + uint256 clampedTroveId; + address clampedBatchManager; + + // Canaries + bool hasDoneLiquidation; + bool hasDoneRedemption; + + // Fake addresses + address factory = address(this); + address governor = address(this); + + function setNewClampedTroveId(uint256 entropy) public returns (uint256) { + clampedTroveId = troveIds[entropy % troveIds.length]; + + return clampedTroveId; // So it gets added to the dictionary + } + + + + // bold token + + // TODO: Chunk these out, no point in not + + bytes32 SALT = bytes32(uint256(0x123123)); + + + // Assets + MockERC20 weth; + MockERC20 stETH; + MockERC20 reETH; + + uint256 currentBranch; + + // Superfluid + SuperfluidFrameworkDeployer.Framework _sf; + + function _setupAddressRegistryAndTroveManager( + address coll, + TroveManagerParams memory params + ) internal returns (address, address) { + IAddressesRegistry addressesRegistry = new AddressesRegistry( + address(this), + params.CCR, + params.MCR, + params.SCR, + type(uint256).max, // TODO: DEBT LIMIT? + // TODO: Debt Limit + params.LIQUIDATION_PENALTY_SP, + params.LIQUIDATION_PENALTY_REDISTRIBUTION + ); + address troveManagerAddress = + getAddress(address(this), getBytecode(type(TroveManagerTester).creationCode, address(addressesRegistry)), SALT); + + return (address(addressesRegistry), troveManagerAddress); + } + + function setup() internal virtual override { + _addActor(address(0x7333333337)); + _addActor(address(0xb4d455555)); + + /// === NERITE / Superfluid Custom === /// + // Using `deployBytecode` + SuperfluidFrameworkDeployer sfDeployer = new SuperfluidFrameworkDeployer(); + sfDeployer.deployTestFramework(); + _sf = sfDeployer.getFramework(); + factory = address(_sf.superTokenFactory); + + // === Before === /// + // Bold and interst router + interestRouter = new InterestRouter(); + boldToken = boldToken = IBoldToken(address(new BoldToken{salt: SALT}(address(this), ISuperTokenFactory(factory)))); + // NOTE: Unclear interface? + IInitializableBold(address(boldToken)).initialize(ISuperTokenFactory(factory)); + + weth = MockERC20(_newAsset(18)); + + TroveManagerParams memory _troveManagerParams = TroveManagerParams({ + CCR: 150e16, // 150% + MCR: 110e16, // 110% + SCR: 130e16, // 130% + LIQUIDATION_PENALTY_SP: 1e17, // 10% + LIQUIDATION_PENALTY_REDISTRIBUTION: 1e17 // 10% + }); + + (address addressesRegistry, address troveManagerAddress) = _setupAddressRegistryAndTroveManager( + address(weth), + _troveManagerParams + ); + + // Initialize arrays for collateral registry + IERC20Metadata[] memory collaterals = new IERC20Metadata[](1); + collaterals[0] = IERC20Metadata(address(weth)); + + ITroveManager[] memory troveManagers = new ITroveManager[](1); + troveManagers[0] = ITroveManager(troveManagerAddress); + + /// MID + // Deploy registry and register the TMs + collateralRegistry = new CollateralRegistry(IBoldToken(address(boldToken)), collaterals, troveManagers, governor); + hintHelpers = new HintHelpers(collateralRegistry); + multiTroveGetter = new MultiTroveGetter(collateralRegistry); + + // Deploy here + // Receive the data format we need + // Push to the list + + // Then ad-hoc branch setup + // Ad hoc branch setup + // Deploy and add branch! + branches.push(_copiedDeploy( + IERC20Metadata(address(weth)), + IBoldToken(address(boldToken)), + IWETH(address(weth)), + troveManagerAddress, + IAddressesRegistry(addressesRegistry), + ICollateralRegistry(address(collateralRegistry)), + IHintHelpers(address(hintHelpers)), + IMultiTroveGetter(address(multiTroveGetter)) + )); + + _switchToActiveBranch(0); + + + _onActorEnabled(address(this)); + _onActorEnabled(address(0x7333333337)); + _onActorEnabled(address(0xb4d455555)); + } + + function _switchToActiveBranch(uint256 index) internal { + activeBranch = branches[index]; + + addressesRegistry = activeBranch.addressesRegistry; + activePool = activeBranch.activePool; + borrowerOperations = activeBranch.borrowerOperations; + collSurplusPool = activeBranch.collSurplusPool; + defaultPool = activeBranch.defaultPool; + sortedTroves = activeBranch.sortedTroves; + stabilityPool = activeBranch.stabilityPool; + troveManager = activeBranch.troveManager; + troveNFT = activeBranch.troveNFT; + priceFeed = activeBranch.priceFeed; + gasPool = activeBranch.gasPool; + collToken = activeBranch.collToken; + + + } + + + // STRUCTS + struct LiquityContractAddresses { + address activePool; + address borrowerOperations; + address collSurplusPool; + address defaultPool; + address sortedTroves; + address stabilityPool; + address troveManager; + address troveNFT; + address metadataNFT; + address priceFeed; + address gasPool; + address interestRouter; + } + + struct TroveManagerParams { + uint256 CCR; + uint256 MCR; + uint256 SCR; + uint256 LIQUIDATION_PENALTY_SP; + uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; + } + + function _copiedDeploy( + IERC20Metadata _collToken, + IBoldToken _boldToken, + IWETH _weth, + address _troveManagerAddress, + IAddressesRegistry _addressesRegistry, + ICollateralRegistry _collateralRegistry, + IHintHelpers _hintHelpers, + IMultiTroveGetter _multiTroveGetter + ) internal returns (LiquityContractsDev memory contracts) { + LiquityContractAddresses memory addresses; + + // Deploy all contracts, using testers for TM and PriceFeed + contracts.addressesRegistry = AddressesRegistry(address(_addressesRegistry)); + contracts.priceFeed = new MultiTokenPriceFeedTestnet(); + contracts.interestRouter = new InterestRouter(); + contracts.collToken = MockERC20(address(_collToken)); + + // Deploy Metadata + addresses.metadataNFT = deployMetadata(SALT); + // assert(address(metadataNFT) == addresses.metadataNFT); // NOTE: Skip + + // Pre-calc addresses + addresses.borrowerOperations = getAddress( + address(this), + getBytecode(type(BorrowerOperationsTester).creationCode, address(contracts.addressesRegistry)), + SALT + ); + addresses.troveManager = _troveManagerAddress; + addresses.troveNFT = getAddress( + address(this), getBytecode(type(TroveNFT).creationCode, address(contracts.addressesRegistry)), SALT + ); + addresses.stabilityPool = getAddress( + address(this), getBytecode(type(StabilityPool).creationCode, address(contracts.addressesRegistry)), SALT + ); + addresses.activePool = getAddress( + address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry)), SALT + ); + addresses.defaultPool = getAddress( + address(this), getBytecode(type(DefaultPool).creationCode, address(contracts.addressesRegistry)), SALT + ); + addresses.gasPool = getAddress( + address(this), getBytecode(type(GasPool).creationCode, address(contracts.addressesRegistry)), SALT + ); + addresses.collSurplusPool = getAddress( + address(this), getBytecode(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry)), SALT + ); + addresses.sortedTroves = getAddress( + address(this), getBytecode(type(SortedTroves).creationCode, address(contracts.addressesRegistry)), SALT + ); + + // Deploy contracts + IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ + collToken: _collToken, + borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), + troveManager: ITroveManager(addresses.troveManager), + troveNFT: ITroveNFT(addresses.troveNFT), + metadataNFT: IMetadataNFT(addresses.metadataNFT), + stabilityPool: IStabilityPool(addresses.stabilityPool), + priceFeed: IPriceFeed(address(contracts.priceFeed)), + activePool: IActivePool(addresses.activePool), + defaultPool: IDefaultPool(addresses.defaultPool), + gasPoolAddress: addresses.gasPool, + collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), + sortedTroves: ISortedTroves(addresses.sortedTroves), + interestRouter: IInterestRouter(address(contracts.interestRouter)), + hintHelpers: _hintHelpers, + multiTroveGetter: _multiTroveGetter, + collateralRegistry: _collateralRegistry, + boldToken: _boldToken, + WETH: _weth + }); + contracts.addressesRegistry.setAddresses(addressVars); + + contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry); + contracts.troveManager = new TroveManagerTester{salt: SALT}(contracts.addressesRegistry); + contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); + contracts.stabilityPool = new StabilityPool{salt: SALT}(contracts.addressesRegistry); + contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); + contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); + contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); + contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); + contracts.sortedTroves = new SortedTroves{salt: SALT}(contracts.addressesRegistry); + + assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); + assert(address(contracts.troveManager) == addresses.troveManager); + assert(address(contracts.troveNFT) == addresses.troveNFT); + assert(address(contracts.stabilityPool) == addresses.stabilityPool); + assert(address(contracts.activePool) == addresses.activePool); + assert(address(contracts.defaultPool) == addresses.defaultPool); + assert(address(contracts.gasPool) == addresses.gasPool); + assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); + assert(address(contracts.sortedTroves) == addresses.sortedTroves); + + // Connect contracts + _boldToken.setBranchAddresses( + address(contracts.troveManager), + address(contracts.stabilityPool), + address(contracts.borrowerOperations), + address(contracts.activePool) + ); + } + + + + // TODO: NEED + function switchBranch(uint256 index) public { + // Switch to active branch + activeBranch = branches[index]; + } + + + // TODO: Programmatic Deployment of a Branch + // Way to add more? + + // TODO: Replace with TroveManager2 + + + /// === ACTOR MANAGER HOOKS === /// + // Given each actor a swap function that is called on swap + // This hook ensures each actor is setup correctly + function _onActorEnabled(address actor) internal { + // TODO: Mint tokens and add stuff? + vm.prank(actor); + collToken.approve(address(borrowerOperations), type(uint256).max); + + vm.prank(actor); + boldToken.approve(address(stabilityPool), type(uint256).max); + + collToken.mint(actor, type(uint88).max); + } + + + /// === Actor Modifiers === /// + + // NOTE: LIMITATION You can use these modifier only for one call, so use them for BASIC TARGETS + modifier asAdmin { + vm.prank(address(this)); + _; + } + + modifier asActor { + vm.prank(_getActor()); + _; + } + + + + + /// === Deplyoment crap === /// + function getBytecode(bytes memory _creationCode, address _addressesRegistry) internal pure returns (bytes memory) { + return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); + } + + function getAddress(address _deployer, bytes memory _bytecode, bytes32 _salt) internal pure returns (address) { + bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), _deployer, _salt, keccak256(_bytecode))); + + // NOTE: cast last 20 bytes of hash to address + return address(uint160(uint256(hash))); + } + + function deployMetadata(bytes32 salt) internal returns (address) { + return (address(0x123123)); + } + +} \ No newline at end of file diff --git a/contracts/test/recon/TargetFunctions.sol b/contracts/test/recon/TargetFunctions.sol new file mode 100644 index 000000000..ee21b9fc4 --- /dev/null +++ b/contracts/test/recon/TargetFunctions.sol @@ -0,0 +1,39 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import "forge-std/console2.sol"; + +import {ActivePoolTargets} from "./targets/ActivePoolTargets.sol"; +import {BorrowerOperationsTargets} from "./targets/BorrowerOperationsTargets.sol"; +import {CollateralRegistryTargets} from "./targets/CollateralRegistryTargets.sol"; +import {CollTokenTargets} from "./targets/CollTokenTargets.sol"; +import {ManagersTargets} from "./targets/ManagersTargets.sol"; +import {OptimizationTargets} from "./targets/OptimizationTargets.sol"; +import {PriceFeedTargets} from "./targets/PriceFeedTargets.sol"; +import {StabilityPoolTargets} from "./targets/StabilityPoolTargets.sol"; +import {TroveManagerTargets} from "./targets/TroveManagerTargets.sol"; + +abstract contract TargetFunctions is + ActivePoolTargets, + BorrowerOperationsTargets, + CollateralRegistryTargets, + CollTokenTargets, + ManagersTargets, + OptimizationTargets, + PriceFeedTargets, + StabilityPoolTargets, + TroveManagerTargets + + { + + function canary_liquidation() public { + t(!hasDoneLiquidation, "canary_liquidation"); + } + function canary_redemption() public { + t(!hasDoneRedemption, "canary_redemption"); + } + +} \ No newline at end of file diff --git a/contracts/test/recon/helpers/RevertHelper.sol b/contracts/test/recon/helpers/RevertHelper.sol new file mode 100644 index 000000000..69477f093 --- /dev/null +++ b/contracts/test/recon/helpers/RevertHelper.sol @@ -0,0 +1,71 @@ + + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {Asserts} from "@chimera/Asserts.sol"; +import {vm} from "@chimera/Hevm.sol"; + +abstract contract RevertHelper is Asserts { + function assertRevertReasonNotEqual(bytes memory returnData, string memory reason) internal { + bool isEqual = _isRevertReasonEqual(returnData, reason); + t(!isEqual, reason); + } + + function assertRevertReasonEqual(bytes memory returnData, string memory reason) internal { + bool isEqual = _isRevertReasonEqual(returnData, reason); + t(isEqual, reason); + } + + function _getRevertMsg(bytes memory returnData) internal pure returns (string memory) { + // Check that the data has the right size: 4 bytes for signature + 32 bytes for panic code + if (returnData.length == 4 + 32) { + // Check that the data starts with the Panic signature + bytes4 panicSignature = bytes4(keccak256(bytes("Panic(uint256)"))); + for (uint i = 0; i < 4; i++) { + if (returnData[i] != panicSignature[i]) return "Undefined signature"; + } + + uint256 panicCode; + for (uint i = 4; i < 36; i++) { + panicCode = panicCode << 8; + panicCode |= uint8(returnData[i]); + } + + // Now convert the panic code into its string representation + if (panicCode == 17) { + return "Panic(17)"; + } + if (panicCode == 18) { + return "Panic(18)"; + } + + // Add other panic codes as needed or return a generic "Unknown panic" + return "Undefined panic code"; + } + + // If the returnData length is less than 68, then the transaction failed silently (without a revert message) + if (returnData.length < 68) return "Transaction reverted silently"; + + assembly { + // Slice the sighash. + returnData := add(returnData, 0x04) + } + return abi.decode(returnData, (string)); // All that remains is the revert string + } + + function _getStringAsSig(string memory reason) internal pure returns (bytes4 expectedSig) { + // return the bytes4 representation of the string + expectedSig = bytes4(keccak256(bytes(reason))); + } + + function _isRevertReasonEqual( + bytes memory returnData, + string memory reason + ) internal pure returns (bool) { + bytes4 sig = bytes4(returnData); + bytes4 expectedSig = _getStringAsSig(reason); + return (sig == expectedSig); + } +} \ No newline at end of file diff --git a/contracts/test/recon/managers/ActorManager.sol b/contracts/test/recon/managers/ActorManager.sol new file mode 100644 index 000000000..d0adcd52a --- /dev/null +++ b/contracts/test/recon/managers/ActorManager.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseSetup} from "@chimera/BaseSetup.sol"; +import {vm} from "@chimera/Hevm.sol"; +import {vm} from "@chimera/Hevm.sol"; +import {EnumerableSet} from "openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IERC20} from "openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// TODO: Should we separate actor from lender from borrower from admin? +// I feel that's the cleanest way +abstract contract ActorManager { + using EnumerableSet for EnumerableSet.AddressSet; + + address private _actor; + + EnumerableSet.AddressSet private _actors; + + // If the current target is address(0) then it has not been setup yet and should revert + error ActorNotSetup(); + // Do not allow duplicates + error ActorExists(); + // If the actor does not exist + error ActorNotAdded(); + // Do not allow the default actor + error DefaultActor(); + + // TODO: We have defined the library + // But we need to make this more explicit + // So it's very clean in the story what's going on + + constructor() { + // address(this) is the default actor + _actors.add(address(this)); + _actor = address(this); + } + + modifier useActor() { + vm.prank(_getActor()); + _; + } + + // use this function to get the current active actor + function _getActor() internal view returns (address) { + return _actor; + } + + // returns an actor different from the currently set one + function _getDifferentActor() internal view returns (address differentActor) { + address[] memory actors_ = _getActors(); + for(uint256 i; i < actors_.length; i++) { + if(actors_[i] != _actor) { + differentActor = actors_[i]; + } + } + } + + function _getRandomActor(uint256 entropy) internal view returns (address randomActor) { + address[] memory actorsArray = _getActors(); + randomActor = actorsArray[entropy % actorsArray.length]; + } + + // Get regular users + function _getActors() internal view returns (address[] memory) { + return _actors.values(); + } + + function _enableActor(address target) internal { + _actor = target; + } + + // NOTE: disabling an actor set the default actor (address(this)) as the current actor + function _disableActor() internal { + _actor = address(this); + } + + function _addActor(address target) internal { + if (_actors.contains(target)) { + revert ActorExists(); + } + + if (target == address(this)) { + revert DefaultActor(); + } + + _actors.add(target); + } + + function _removeActor(address target) internal { + if (!_actors.contains(target)) { + revert ActorNotAdded(); + } + + if (target == address(this)) { + revert DefaultActor(); + } + + _actors.remove(target); + } + + // Note: expose this function _in `TargetFunctions` for actor switching + function _switchActor(uint256 entropy) internal { + _disableActor(); + + address target = _actors.at(entropy % _actors.length()); + _enableActor(target); + } +} \ No newline at end of file diff --git a/contracts/test/recon/managers/AssetManager.sol b/contracts/test/recon/managers/AssetManager.sol new file mode 100644 index 000000000..851b3e92b --- /dev/null +++ b/contracts/test/recon/managers/AssetManager.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseSetup} from "@chimera/BaseSetup.sol"; +import {vm} from "@chimera/Hevm.sol"; +import {vm} from "@chimera/Hevm.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {MockERC20} from "../mocks/MockERC20.sol"; +import {console} from "forge-std/console.sol"; + +abstract contract AssetManager { + using EnumerableSet for EnumerableSet.AddressSet; + + // The current target for this set of variables + address private __asset; + + EnumerableSet.AddressSet private _assets; + + // If the current target is address(0) then it has not been setup yet and should revert + error NotSetup(); + // Do not allow duplicates + error Exists(); + // Enable only added assets + error NotAdded(); + + // Note: use this function _to get the current active asset + function _getAsset() internal view returns (address) { + if (__asset == address(0)) { + revert NotSetup(); + } + + return __asset; + } + + // Note: returns an asset different from the currently set one + function _getDifferentAsset() internal view returns (address differentAsset) { + address[] memory assets_ = _getAssets(); + for(uint256 i; i < assets_.length; i++) { + if(assets_[i] != __asset) { + differentAsset = assets_[i]; + } + } + } + + function _getAssets() internal view returns (address[] memory) { + return _assets.values(); + } + + function _newAsset(uint8 decimals) internal returns (address) { + address asset_ = address(new MockERC20("Test Token", "TST", decimals)); // If names get confusing, concatenate the decimals to the name + _addAsset(asset_); + _enableAsset(asset_); + return asset_; + } + + function _enableAsset(address target) internal { + if (!_assets.contains(target)) { + revert NotAdded(); + } + __asset = target; + } + + function _disableAsset(address asset_) internal { + // Here there are actions that would be needed when removing a asset. + } + + function _addAsset(address target) internal { + if (_assets.contains(target)) { + revert Exists(); + } + + _assets.add(target); + } + + function _removeAsset(address target) internal { + _assets.remove(target); + } + + // Note: expose this function _in `TargetFunctions` for asset switching + function _switchAsset(uint256 entropy) internal { + _disableAsset(__asset); // NOTE: May not be necessary + + address target = _assets.at(entropy % _assets.length()); + _enableAsset(target); + } + + // mint initial balance and approve allowances for the active asset + function _finalizeAssetDeployment(address[] memory actorsArray, address[] memory approvalArray, uint256 amount) internal { + _mintAssetToAllActors(actorsArray, amount); + for(uint256 i; i < approvalArray.length; i++) { + _approveAssetToAddressForAllActors(actorsArray, approvalArray[i]); + } + } + + function _mintAssetToAllActors(address[] memory actorsArray, uint256 amount) internal { + // mint all actors + address asset = _getAsset(); + for (uint256 i; i < actorsArray.length; i++) { + vm.prank(actorsArray[i]); + MockERC20(asset).mint(actorsArray[i], amount); + } + } + + function _approveAssetToAddressForAllActors(address[] memory actorsArray, address addressToApprove) internal { + // approve to all actors + address asset = _getAsset(); + for (uint256 i; i < actorsArray.length; i++) { + vm.prank(actorsArray[i]); + MockERC20(asset).approve(addressToApprove, type(uint256).max); + } + } +} diff --git a/contracts/test/recon/mocks/MockERC20.sol b/contracts/test/recon/mocks/MockERC20.sol new file mode 100644 index 000000000..221c35642 --- /dev/null +++ b/contracts/test/recon/mocks/MockERC20.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.18; + +import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; + +contract MockERC20 is ERC20 { + + uint8 _decimals = 18; + + constructor(string memory _name, string memory _symbol, uint8 decimals_) ERC20(_name, _symbol) { + _decimals = decimals_; + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + function mint(address to, uint256 amt) external { + _mint(to, amt); + } +} \ No newline at end of file diff --git a/contracts/test/recon/targets/ActivePoolTargets.sol b/contracts/test/recon/targets/ActivePoolTargets.sol new file mode 100644 index 000000000..7d9f3f8f0 --- /dev/null +++ b/contracts/test/recon/targets/ActivePoolTargets.sol @@ -0,0 +1,45 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import "forge-std/console2.sol"; + +import {Properties} from "../Properties.sol"; + +abstract contract ActivePoolTargets is BaseTargetFunctions, Properties { + // AP should prob never be called directly + + // function activePool_accountForReceivedColl(uint256 _amount) public { + // activePool.accountForReceivedColl(_amount); + // } + + // function activePool_mintAggInterest() public { + // activePool.mintAggInterest(); + // } + + // function activePool_mintAggInterestAndAccountForTroveChange(TroveChange memory _troveChange, address _batchAddress) public { + // activePool.mintAggInterestAndAccountForTroveChange(_troveChange, _batchAddress); + // } + + // function activePool_mintBatchManagementFeeAndAccountForChange(TroveChange memory _troveChange, address _batchAddress) public { + // activePool.mintBatchManagementFeeAndAccountForChange(_troveChange, _batchAddress); + // } + + // function activePool_receiveColl(uint256 _amount) public { + // activePool.receiveColl(_amount); + // } + + // function activePool_sendColl(address _account, uint256 _amount) public { + // activePool.sendColl(_account, _amount); + // } + + // function activePool_sendCollToDefaultPool(uint256 _amount) public { + // activePool.sendCollToDefaultPool(_amount); + // } + + // function activePool_setShutdownFlag() public { + // activePool.setShutdownFlag(); + // } +} \ No newline at end of file diff --git a/contracts/test/recon/targets/BorrowerOperationsTargets.sol b/contracts/test/recon/targets/BorrowerOperationsTargets.sol new file mode 100644 index 000000000..1b4fd9cd0 --- /dev/null +++ b/contracts/test/recon/targets/BorrowerOperationsTargets.sol @@ -0,0 +1,301 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import "forge-std/console2.sol"; + +import {Properties} from "../Properties.sol"; + +import {IBorrowerOperations} from "../../../src/Interfaces/IBorrowerOperations.sol"; + +import {LiquityMath} from "../../../src/Dependencies/LiquityMath.sol"; + +abstract contract BorrowerOperationsTargets is BaseTargetFunctions, Properties { + + /// === Inlined Test === /// + function borrowerOperations_claimCollateral() public updateGhosts { + uint256 currentSurplus = collSurplusPool.getCollateral(_getActor()); + + uint256 balB4 = collToken.balanceOf(_getActor()); + + vm.prank(_getActor()); + try borrowerOperations.claimCollateral() { + + } catch { + eq(currentSurplus, 0, "An owner with collateral surplus can always claim"); + } + + uint256 balAfter = collToken.balanceOf(_getActor()); + + eq(balAfter - balB4, currentSurplus, "An owner that claims collateral surplus always receives the exact amount they are owed"); + } + + function inlined_test_adding_to_a_batch() public { + uint256 debtB4 = troveManager.getTroveEntireDebt(clampedTroveId); + borrowerOperations_setInterestBatchManager(clampedTroveId, clampedBatchManager, 0, 0, type(uint256).max); + + uint256 debtAfter = troveManager.getTroveEntireDebt(clampedTroveId); + + gte(debtAfter, debtB4, "BT-03: Adding a trove to a Batch should never decrease the Trove debt"); + + revert("Stateless"); // Reverting here means the function has no impact on ghost variables + } + + function inlined_test_removing_from_a_batch() public { + uint256 debtB4 = troveManager.getTroveEntireDebt(clampedTroveId); + // Get current borrow rate so we don't trigger adjustment + // Get current batch rate + uint256 annualRate = troveManager.getTroveAnnualInterestRate(clampedTroveId); + borrowerOperations_removeFromBatch(clampedTroveId, annualRate, 0, 0, type(uint256).max); + + uint256 debtAfter = troveManager.getTroveEntireDebt(clampedTroveId); + + gte(debtAfter, debtB4, "BT-04: Removing a trove from a Batch should never decrease the Trove debt"); + + revert("Stateless"); // Reverting here means the function has no impact on ghost variables + } + + /// NOTE: Inlined test to check that the trove can never set itself to insolvent + function inlined_property_check_not_insolvent(uint256 troveId) internal { + uint256 price = priceFeed.getPrice(); + uint256 mcr = borrowerOperations.MCR(); + + // Get Current Coll + uint256 currentColl = troveManager.getTroveEntireColl(troveId); + // Get current debt + uint256 currentDebt = troveManager.getTroveEntireDebt(troveId); + + uint256 cr = LiquityMath._computeCR(currentColl, currentDebt, price); + gte(cr, mcr, "Can never self liquidate"); + } + + + + /// === Handlers === /// + + + function borrowerOperations_addColl(uint256 _troveId, uint256 _collAmount) public updateGhosts asActor { + borrowerOperations.addColl(_troveId, _collAmount); + inlined_property_check_not_insolvent(_troveId); + } + + function borrowerOperations_addColl_clamped(uint88 _collAmount) public { + uint256 collChange = _collAmount % (collToken.balanceOf(_getActor()) + 1); + + borrowerOperations_addColl(clampedTroveId, collChange); + } + + + function borrowerOperations_adjustTrove(uint256 _troveId, uint256 _collChange, bool _isCollIncrease, uint256 _boldChange, bool _isDebtIncrease, uint256 _maxUpfrontFee) public updateGhosts asActor { + borrowerOperations.adjustTrove(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _maxUpfrontFee); + inlined_property_check_not_insolvent(_troveId); + } + + function borrowerOperations_adjustTrove_clamped(uint88 _collChange, bool _isCollIncrease, uint88 _boldChange, bool _isDebtIncrease, uint256 _maxUpfrontFee) public { + uint256 collChange; + uint256 boldChange; + if(_isCollIncrease) { + collChange = _collChange % (collToken.balanceOf(_getActor()) + 1); + } else { + collChange = _collChange % (troveManager.getTroveColl(clampedTroveId) + 1); + } + + if(!_isDebtIncrease) { + boldChange = _boldChange % (troveManager.getTroveEntireDebt(clampedTroveId) + 1); + } + borrowerOperations_adjustTrove(clampedTroveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, type(uint256).max); + } + + + function borrowerOperations_adjustTroveInterestRate(uint256 _troveId, uint256 _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public updateGhosts asActor { + borrowerOperations.adjustTroveInterestRate(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee); + } + + function borrowerOperations_adjustTroveInterestRate_clamped(uint256 _troveId, uint256 _newAnnualInterestRate, uint256 _maxUpfrontFee) public { + _newAnnualInterestRate = _newAnnualInterestRate % (2.5e18 + 1); // NOTE: TODO: Change based on codebase + borrowerOperations_adjustTroveInterestRate(clampedTroveId, _newAnnualInterestRate, 0, 0, type(uint256).max); + } + + + function borrowerOperations_adjustZombieTrove(uint256 _troveId, uint256 _collChange, bool _isCollIncrease, uint256 _boldChange, bool _isDebtIncrease, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public updateGhosts asActor { + borrowerOperations.adjustZombieTrove(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _upperHint, _lowerHint, _maxUpfrontFee); + } + + function borrowerOperations_adjustZombieTrove_clamped(uint88 _collChange, bool _isCollIncrease, uint88 _boldChange, bool _isDebtIncrease, uint256 _upperHint, uint256 _lowerHint) public { + uint256 collChange; + uint256 boldChange; + if(_isCollIncrease) { + collChange = _collChange % (collToken.balanceOf(_getActor()) + 1); + } else { + collChange = _collChange % (troveManager.getTroveColl(clampedTroveId) + 1); + } + + if(!_isDebtIncrease) { + boldChange = _boldChange % (troveManager.getTroveEntireDebt(clampedTroveId) + 1); + } + borrowerOperations_adjustZombieTrove(clampedTroveId, collChange, _isCollIncrease, boldChange, _isDebtIncrease, _upperHint, _lowerHint, type(uint256).max); + } + + + function borrowerOperations_applyPendingDebt(uint256 _troveId, uint256 _lowerHint, uint256 _upperHint) public updateGhosts asActor { + borrowerOperations.applyPendingDebt(_troveId, _lowerHint, _upperHint); + } + + + function borrowerOperations_applyPendingDebt(uint256 _troveId) public updateGhosts asActor { + borrowerOperations.applyPendingDebt(_troveId); + } + + function borrowerOperations_applyPendingDebt_clamped() public { + borrowerOperations_applyPendingDebt(clampedTroveId); + } + + + function borrowerOperations_closeTrove(uint256 _troveId) public updateGhosts asActor { + borrowerOperations.closeTrove(_troveId); + } + + function borrowerOperations_closeTrove_clamped() public { + borrowerOperations_closeTrove(clampedTroveId); + } + + + function borrowerOperations_lowerBatchManagementFee(uint256 _newAnnualManagementFee) public updateGhosts asActor { + borrowerOperations.lowerBatchManagementFee(_newAnnualManagementFee); + } + + function borrowerOperations_onLiquidateTrove(uint256 _troveId) public updateGhosts asActor { + borrowerOperations.onLiquidateTrove(_troveId); + } + + function borrowerOperations_openTrove(address _owner, uint256 _ownerIndex, uint256 _collAmount, uint256 _boldAmount, uint256 _upperHint, uint256 _lowerHint, uint256 _annualInterestRate, uint256 _maxUpfrontFee, address _addManager, address _removeManager, address _receiver) public updateGhosts asActor returns (uint256) { + uint256 troveId = borrowerOperations.openTrove(_owner, _ownerIndex, _collAmount, _boldAmount, _upperHint, _lowerHint, _annualInterestRate, _maxUpfrontFee, _addManager, _removeManager, _receiver); + clampedTroveId = troveId; + inlined_property_check_not_insolvent(troveId); + return troveId; + } + + function borrowerOperations_openTrove_clamped(address _owner, uint256 _ownerIndex, uint88 _collAmount, uint88 _boldAmount, address _addManager, address _removeManager, address _receiver) public returns (uint256) { + return borrowerOperations_openTrove(_getActor(), _ownerIndex, _collAmount, _boldAmount, 0, 0, 1e17, type(uint256).max, _getActor(), _getActor(), _getActor()); + } + + function borrowerOperations_openTroveAndJoinInterestBatchManager(IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory _params) public updateGhosts asActor { + borrowerOperations.openTroveAndJoinInterestBatchManager(_params); + } + + function borrowerOperations_registerBatchManager(uint128 _minInterestRate, uint128 _maxInterestRate, uint128 _currentInterestRate, uint128 _annualManagementFee, uint128 _minInterestRateChangePeriod) public updateGhosts asActor { + borrowerOperations.registerBatchManager(_minInterestRate, _maxInterestRate, _currentInterestRate, _annualManagementFee, _minInterestRateChangePeriod); + } + + function borrowerOperations_registerBatchManager_clamped() public returns (address) { + borrowerOperations_registerBatchManager(1e18 / 100, 1e18 - 100, 1e17, 1e17, 1 hours); + clampedBatchManager = _getActor(); + return clampedBatchManager; // Add to dictionary + } + + + function borrowerOperations_removeFromBatch(uint256 _troveId, uint256 _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public updateGhosts asActor { + borrowerOperations.removeFromBatch(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee); + } + + function borrowerOperations_removeFromBatch_clamped(uint256 _troveId, uint256 _newAnnualInterestRate) public { + borrowerOperations_removeFromBatch(clampedTroveId, _newAnnualInterestRate, 0, 0, type(uint256).max); + } + + + + function borrowerOperations_removeInterestIndividualDelegate(uint256 _troveId) public updateGhosts asActor { + borrowerOperations.removeInterestIndividualDelegate(_troveId); + } + + + function borrowerOperations_repayBold(uint256 _troveId, uint256 _boldAmount) public updateGhosts asActor { + borrowerOperations.repayBold(_troveId, _boldAmount); + inlined_property_check_not_insolvent(_troveId); + } + + function borrowerOperations_repayBold_clamped(uint88 _boldAmount) public updateGhosts asActor { + uint256 amt = _boldAmount % (troveManager.getTroveEntireDebt(clampedTroveId) + 1); + // TODO: Should use max as the max debt + borrowerOperations_repayBold(clampedTroveId, amt); + } + + + function borrowerOperations_setAddManager(uint256 _troveId, address _manager) public updateGhosts asActor { + borrowerOperations.setAddManager(_troveId, _manager); + } + + function borrowerOperations_setBatchManagerAnnualInterestRate(uint128 _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public updateGhosts asActor { + borrowerOperations.setBatchManagerAnnualInterestRate(_newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee); + } + + // NOTE: Non standard handler! We need to prank the maanger to get this to work + function borrowerOperations_setBatchManagerAnnualInterestRate_clamped(uint128 _newAnnualInterestRate) public updateGhosts { + vm.prank(clampedBatchManager); + borrowerOperations.setBatchManagerAnnualInterestRate(_newAnnualInterestRate, 0, 0, type(uint256).max); + } + + function borrowerOperations_setInterestBatchManager(uint256 _troveId, address _newBatchManager, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public updateGhosts asActor { + borrowerOperations.setInterestBatchManager(_troveId, _newBatchManager, _upperHint, _lowerHint, _maxUpfrontFee); + } + + function borrowerOperations_setInterestBatchManager_clamped() public { + borrowerOperations_setInterestBatchManager(clampedTroveId, clampedBatchManager, 0, 0, type(uint256).max); + } + + function borrowerOperations_setInterestIndividualDelegate(uint256 _troveId, address _delegate, uint128 _minInterestRate, uint128 _maxInterestRate, uint256 _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee, uint256 _minInterestRateChangePeriod) public updateGhosts asActor { + borrowerOperations.setInterestIndividualDelegate(_troveId, _delegate, _minInterestRate, _maxInterestRate, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee, _minInterestRateChangePeriod); + } + + function borrowerOperations_setRemoveManager(uint256 _troveId, address _manager) public updateGhosts asActor { + borrowerOperations.setRemoveManager(_troveId, _manager); + } + + function borrowerOperations_setRemoveManagerWithReceiver(uint256 _troveId, address _manager, address _receiver) public updateGhosts asActor { + borrowerOperations.setRemoveManagerWithReceiver(_troveId, _manager, _receiver); + } + + function borrowerOperations_shutdown() public updateGhosts asActor { + borrowerOperations.shutdown(); + } + + function borrowerOperations_shutdownFromOracleFailure() public updateGhosts asActor { + borrowerOperations.shutdownFromOracleFailure(); + } + + // === Switch Batch Manager === // + + function borrowerOperations_switchBatchManager(uint256 _troveId, uint256 _removeUpperHint, uint256 _removeLowerHint, address _newBatchManager, uint256 _addUpperHint, uint256 _addLowerHint, uint256 _maxUpfrontFee) public updateGhosts asActor { + borrowerOperations.switchBatchManager(_troveId, _removeUpperHint, _removeLowerHint, _newBatchManager, _addUpperHint, _addLowerHint, _maxUpfrontFee); + } + function borrowerOperations_switchBatchManager_clamped() public { + borrowerOperations_switchBatchManager(clampedTroveId, 0, 0, clampedBatchManager, 0, 0, type(uint256).max); + } + + + // === Withdraw Bold === // + function borrowerOperations_withdrawBold(uint256 _troveId, uint256 _boldAmount, uint256 _maxUpfrontFee) public updateGhosts asActor { + borrowerOperations.withdrawBold(_troveId, _boldAmount, _maxUpfrontFee); + inlined_property_check_not_insolvent(_troveId); + } + + function borrowerOperations_withdrawBold_clamped(uint88 _boldAmount) public { + borrowerOperations_withdrawBold(clampedTroveId, _boldAmount, type(uint256).max); + } + + + // === Withdraw Coll === // + function borrowerOperations_withdrawColl(uint256 _troveId, uint256 _collWithdrawal) public updateGhosts asActor { + borrowerOperations.withdrawColl(_troveId, _collWithdrawal); + inlined_property_check_not_insolvent(_troveId); + } + + function borrowerOperations_withdrawColl_clamped(uint256 _collWithdrawal) public { + uint256 amt = _collWithdrawal% (troveManager.getTroveColl(clampedTroveId) + 1); + borrowerOperations_withdrawColl(clampedTroveId, _collWithdrawal); + } + + +} \ No newline at end of file diff --git a/contracts/test/recon/targets/CollTokenTargets.sol b/contracts/test/recon/targets/CollTokenTargets.sol new file mode 100644 index 000000000..5332e0cea --- /dev/null +++ b/contracts/test/recon/targets/CollTokenTargets.sol @@ -0,0 +1,36 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import "forge-std/console2.sol"; + +import {Properties} from "../Properties.sol"; + +abstract contract CollTokenTargets is BaseTargetFunctions, Properties { + + function collToken_approve(address spender, uint256 amount) public updateGhosts asActor { + collToken.approve(spender, amount); + } + + // function collToken_decreaseAllowance(address spender, uint256 subtractedValue) public updateGhosts asActor { + // collToken.decreaseAllowance(spender, subtractedValue); + // } + + // function collToken_increaseAllowance(address spender, uint256 addedValue) public updateGhosts asActor { + // collToken.increaseAllowance(spender, addedValue); + // } + + function collToken_mint(address to, uint256 amt) public updateGhosts asActor { + collToken.mint(to, amt); + } + + function collToken_transfer(address to, uint256 amount) public updateGhosts asActor { + collToken.transfer(to, amount); + } + + function collToken_transferFrom(address from, address to, uint256 amount) public updateGhosts asActor { + collToken.transferFrom(from, to, amount); + } +} \ No newline at end of file diff --git a/contracts/test/recon/targets/CollateralRegistryTargets.sol b/contracts/test/recon/targets/CollateralRegistryTargets.sol new file mode 100644 index 000000000..407b310fb --- /dev/null +++ b/contracts/test/recon/targets/CollateralRegistryTargets.sol @@ -0,0 +1,21 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import "forge-std/console2.sol"; + +import {Properties} from "../Properties.sol"; + +abstract contract CollateralRegistryTargets is BaseTargetFunctions, Properties { + function collateralRegistry_redeemCollateral(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _maxFeePercentage) public updateGhosts asActor { + collateralRegistry.redeemCollateral(_boldAmount, _maxIterationsPerCollateral, _maxFeePercentage); + hasDoneRedemption = true; + } + + function collateralRegistry_redeemCollateral_clamped(uint256 _boldAmount) public { + _boldAmount = _boldAmount % (boldToken.balanceOf(_getActor()) + 1); + collateralRegistry_redeemCollateral(_boldAmount, 100, 1e18); // DECIMAL_PRECISION = 1e18 + } +} \ No newline at end of file diff --git a/contracts/test/recon/targets/ManagersTargets.sol b/contracts/test/recon/targets/ManagersTargets.sol new file mode 100644 index 000000000..4dec4bf2e --- /dev/null +++ b/contracts/test/recon/targets/ManagersTargets.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {BeforeAfter} from "../BeforeAfter.sol"; +import {Properties} from "../Properties.sol"; +import {vm} from "@chimera/Hevm.sol"; + +import {MockERC20} from "../mocks/MockERC20.sol"; + + +// Target functions that are effectively inherited from the Actor and AssetManagers +// Once properly standardized, managers will expose these by default +// Keeping them out makes your project more custom +abstract contract ManagersTargets is + BaseTargetFunctions, + Properties +{ + // == ACTOR HANDLERS == // + + /// @dev Start acting as another actor + function switchActor(uint256 entropy) public returns (address) { + _switchActor(entropy); + + return _getActor(); + } + + + // /// @dev Starts using a new asset // NOTE: Unused for now + function switch_asset(uint256 entropy) public returns (address) { + _switchAsset(entropy); + + return _getAsset(); + } + + + /// === GHOST UPDATING HANDLERS ===/// + /// We `updateGhosts` cause you never know (e.g. donations) + /// If you don't want to track donations, remove the `updateGhosts` + + /// @dev Approve to arbitrary address, uses Actor by default + /// NOTE: You're almost always better off setting approvals in `Setup` + function asset_approve(address to, uint128 amt) public asActor returns (uint256) { + MockERC20(_getAsset()).approve(to, amt); + + return amt; + } + + /// @dev Mint to arbitrary address, uses owner by default, even though MockERC20 doesn't check + function asset_mint(address to, uint128 amt) public asAdmin returns (uint256) { + MockERC20(_getAsset()).mint(to, amt); + + return amt; + } +} \ No newline at end of file diff --git a/contracts/test/recon/targets/OptimizationTargets.sol b/contracts/test/recon/targets/OptimizationTargets.sol new file mode 100644 index 000000000..194c46b7c --- /dev/null +++ b/contracts/test/recon/targets/OptimizationTargets.sol @@ -0,0 +1,135 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import "forge-std/console2.sol"; + +import {Properties} from "../Properties.sol"; + +abstract contract OptimizationTargets is BaseTargetFunctions, Properties { + + // == property_CP01 Optimization == // + function optimize_max_delta_debt_overstated() public returns (int256) { + if(_after.entireSystemDebt > _after.ghostDebtAccumulator) { + uint256 delta = _after.entireSystemDebt - _after.ghostDebtAccumulator; + if(delta > uint256(type(int256).max)) { + return type(int256).max; + } + + return int256(delta); + } + } + + function optimize_max_delta_debt_understated() public returns (int256) { + if(_after.ghostDebtAccumulator > _after.entireSystemDebt) { + uint256 delta = _after.ghostDebtAccumulator - _after.entireSystemDebt; + if(delta > uint256(type(int256).max)) { + return type(int256).max; + } + + return int256(delta); + } + } + + // == property_AP01 Optimization == // + function optimize_ap01_over() public returns (int256) { + if(_after.ghostWeightedRecordedDebtAccumulator > activePool.aggWeightedDebtSum()) { + uint256 delta = _after.ghostWeightedRecordedDebtAccumulator - activePool.aggWeightedDebtSum(); + if(delta > uint256(type(int256).max)) { + return type(int256).max; + } + + return int256(delta); + } + } + + function optimize_ap01_under() public returns (int256) { + if(activePool.aggWeightedDebtSum() > _after.ghostWeightedRecordedDebtAccumulator) { + uint256 delta = activePool.aggWeightedDebtSum() - _after.ghostWeightedRecordedDebtAccumulator; + if(delta > uint256(type(int256).max)) { + return type(int256).max; + } + + return int256(delta); + } + } + + + // == property_sum_of_batches_debt == // + function optimize_property_sum_of_batches_debt_over() public returns (int256) { + if(clampedBatchManager == address(0)) { + return 0; + } + + (uint256 batchDebt, ) = troveManager.getbatchDebtAndShares(clampedBatchManager); + (uint256 sumBatchDebt, ) = _sumBatchSharesAndDebt(clampedBatchManager); + + if(sumBatchDebt > batchDebt) { + uint256 delta = sumBatchDebt - batchDebt; + if(delta > uint256(type(int256).max)) { + return type(int256).max; + } + + return int256(delta); + } + } + + function optimize_property_sum_of_batches_debt_under() public returns (int256) { + (uint256 batchDebt, ) = troveManager.getbatchDebtAndShares(clampedBatchManager); + (uint256 sumBatchDebt, ) = _sumBatchSharesAndDebt(clampedBatchManager); + + if(batchDebt > sumBatchDebt) { + uint256 delta = batchDebt - sumBatchDebt; + if(delta > uint256(type(int256).max)) { + return type(int256).max; + } + + return int256(delta); + } + } + + // == property_sum_of_batches_shares == // + function optimize_property_sum_of_batches_shares_over() public returns (int256) { + (, uint256 batchShares) = troveManager.getbatchDebtAndShares(clampedBatchManager); + (, uint256 sumbBatchShares) = _sumBatchSharesAndDebt(clampedBatchManager); + + if(sumbBatchShares > batchShares) { + uint256 delta = sumbBatchShares - batchShares; + if(delta > uint256(type(int256).max)) { + return type(int256).max; + } + + return int256(delta); + } + } + + // == property_sum_of_batches_shares == // + function optimize_property_sum_of_batches_shares_under() public returns (int256) { + (, uint256 batchShares) = troveManager.getbatchDebtAndShares(clampedBatchManager); + (, uint256 sumbBatchShares) = _sumBatchSharesAndDebt(clampedBatchManager); + + if(batchShares > sumbBatchShares) { + uint256 delta = batchShares - sumbBatchShares; + if(delta > uint256(type(int256).max)) { + return type(int256).max; + } + + return int256(delta); + } + } + + + /// @dev Helper function to standardize return values for optimization tests + function _optimizationHelper(uint256 left, uint256 right, bool swap) internal returns (int256) { + if(left > right) { + uint256 delta = left - right; + if(delta > uint256(type(int256).max)) { + return type(int256).max; + } + + return int256(delta); + } + } +} \ No newline at end of file diff --git a/contracts/test/recon/targets/PriceFeedTargets.sol b/contracts/test/recon/targets/PriceFeedTargets.sol new file mode 100644 index 000000000..70471c50f --- /dev/null +++ b/contracts/test/recon/targets/PriceFeedTargets.sol @@ -0,0 +1,27 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import "forge-std/console2.sol"; + +import {Properties} from "../Properties.sol"; + +abstract contract PriceFeedTargets is BaseTargetFunctions, Properties { + function priceFeed_fetchPrice() public { + priceFeed.fetchPrice(); + } + + function priceFeed_fetchRedemptionPrice() public { + priceFeed.fetchRedemptionPrice(); + } + + function priceFeed_setPrice(uint88 price) public { + priceFeed.setPrice(price); + } + + function priceFeed_triggerShutdown() public { + priceFeed.triggerShutdown(); + } +} \ No newline at end of file diff --git a/contracts/test/recon/targets/SampleTargets.sol b/contracts/test/recon/targets/SampleTargets.sol new file mode 100644 index 000000000..d1be2b3c3 --- /dev/null +++ b/contracts/test/recon/targets/SampleTargets.sol @@ -0,0 +1,13 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import "forge-std/console2.sol"; + +import {Properties} from "../Properties.sol"; + +abstract contract SampleTargets is BaseTargetFunctions, Properties { + +} \ No newline at end of file diff --git a/contracts/test/recon/targets/StabilityPoolTargets.sol b/contracts/test/recon/targets/StabilityPoolTargets.sol new file mode 100644 index 000000000..15e4eabac --- /dev/null +++ b/contracts/test/recon/targets/StabilityPoolTargets.sol @@ -0,0 +1,46 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import "forge-std/console2.sol"; + +import {Properties} from "../Properties.sol"; + +abstract contract StabilityPoolTargets is BaseTargetFunctions, Properties { + + function stabilityPool_claimAllCollGains() public updateGhosts asActor { + stabilityPool.claimAllCollGains(); + } + + + function stabilityPool_provideToSP(uint256 _topUp, bool _doClaim) public updateGhosts asActor { + stabilityPool.provideToSP(_topUp, _doClaim); + } + + function stabilityPool_provideToSP_clamped(uint256 _topUp, bool _doClaim) public { + _topUp = _topUp % (boldToken.balanceOf(_getActor()) + 1); + stabilityPool_provideToSP(_topUp, _doClaim); + } + + function stabilityPool_withdrawFromSP(uint256 _amount, bool _doClaim) public updateGhosts asActor { + stabilityPool.withdrawFromSP(_amount, _doClaim); + } + + function stabilityPool_withdrawFromSP_clamped(uint256 _amount, bool _doClaim) public { + _amount = _amount % (stabilityPool.getCompoundedBoldDeposit(_getActor()) + 1); + stabilityPool_withdrawFromSP(_amount, _doClaim); + } + + + + function stabilityPool_offset(uint256 _debtToOffset, uint256 _collToAdd) public updateGhosts asActor { + stabilityPool.offset(_debtToOffset, _collToAdd); + } + + function stabilityPool_triggerBoldRewards(uint256 _boldYield) public updateGhosts asActor { + stabilityPool.triggerBoldRewards(_boldYield); + } + +} \ No newline at end of file diff --git a/contracts/test/recon/targets/TroveManagerTargets.sol b/contracts/test/recon/targets/TroveManagerTargets.sol new file mode 100644 index 000000000..9097a6867 --- /dev/null +++ b/contracts/test/recon/targets/TroveManagerTargets.sol @@ -0,0 +1,121 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import "forge-std/console2.sol"; + +import {Properties} from "../Properties.sol"; + +abstract contract TroveManagerTargets is BaseTargetFunctions, Properties { + + function troveManager_batchLiquidateTroves(uint256[] memory _troveArray) public updateGhosts asActor { + troveManager.batchLiquidateTroves(_troveArray); + } + + + function troveManager_liquidate(uint256 _troveId) public updateGhosts asActor { + troveManager.liquidate(_troveId); + hasDoneLiquidation = true; + } + + function troveManager_liquidate_clamped() public { + troveManager_liquidate(clampedTroveId); + } + + function troveManager_liquidate_with_oracle_clamped() public { + uint256 prevPrice = priceFeed.getPrice(); + priceFeed.setPrice(1); // Set to insanely low price + + troveManager_liquidate(clampedTroveId); // Liquidate + + priceFeed.setPrice(prevPrice); //Bring back prev price + } + + + function troveManager_urgentRedemption(uint256 _boldAmount, uint256[] memory _troveIds, uint256 _minCollateral) public updateGhosts asActor { + troveManager.urgentRedemption(_boldAmount, _troveIds, _minCollateral); + } + + + function troveManager_urgentRedemption_clamped(uint256 _boldAmount) public { + uint256[] memory ids = new uint256[](1); + ids[0] = clampedTroveId; + + _boldAmount = _boldAmount % troveManager.getTroveDebt(clampedTroveId) + 1; + troveManager_urgentRedemption(_boldAmount, ids, 0); + } + + + // function troveManager_callInternalRemoveTroveId(uint256 _troveId) public updateGhosts asActor { + // troveManager.callInternalRemoveTroveId(_troveId); + // } + + // function troveManager_getUnbackedPortionPriceAndRedeemability() public updateGhosts asActor { + // troveManager.getUnbackedPortionPriceAndRedeemability(); + // } + + + // function troveManager_onAdjustTrove(uint256 _troveId, uint256 _newColl, uint256 _newDebt, TroveChange memory _troveChange) public updateGhosts asActor { + // troveManager.onAdjustTrove(_troveId, _newColl, _newDebt, _troveChange); + // } + + // function troveManager_onAdjustTroveInsideBatch(uint256 _troveId, uint256 _newTroveColl, uint256 _newTroveDebt, TroveChange memory _troveChange, address _batchAddress, uint256 _newBatchColl, uint256 _newBatchDebt) public updateGhosts asActor { + // troveManager.onAdjustTroveInsideBatch(_troveId, _newTroveColl, _newTroveDebt, _troveChange, _batchAddress, _newBatchColl, _newBatchDebt); + // } + + // function troveManager_onAdjustTroveInterestRate(uint256 _troveId, uint256 _newColl, uint256 _newDebt, uint256 _newAnnualInterestRate, TroveChange memory _troveChange) public updateGhosts asActor { + // troveManager.onAdjustTroveInterestRate(_troveId, _newColl, _newDebt, _newAnnualInterestRate, _troveChange); + // } + + // function troveManager_onApplyTroveInterest(uint256 _troveId, uint256 _newTroveColl, uint256 _newTroveDebt, address _batchAddress, uint256 _newBatchColl, uint256 _newBatchDebt, TroveChange memory _troveChange) public updateGhosts asActor { + // troveManager.onApplyTroveInterest(_troveId, _newTroveColl, _newTroveDebt, _batchAddress, _newBatchColl, _newBatchDebt, _troveChange); + // } + + // function troveManager_onCloseTrove(uint256 _troveId, TroveChange memory _troveChange, address _batchAddress, uint256 _newBatchColl, uint256 _newBatchDebt) public updateGhosts asActor { + // troveManager.onCloseTrove(_troveId, _troveChange, _batchAddress, _newBatchColl, _newBatchDebt); + // } + + // function troveManager_onLowerBatchManagerAnnualFee(address _batchAddress, uint256 _newColl, uint256 _newDebt, uint256 _newAnnualManagementFee) public updateGhosts asActor { + // troveManager.onLowerBatchManagerAnnualFee(_batchAddress, _newColl, _newDebt, _newAnnualManagementFee); + // } + + // function troveManager_onOpenTrove(address _owner, uint256 _troveId, TroveChange memory _troveChange, uint256 _annualInterestRate) public updateGhosts asActor { + // troveManager.onOpenTrove(_owner, _troveId, _troveChange, _annualInterestRate); + // } + + // function troveManager_onOpenTroveAndJoinBatch(address _owner, uint256 _troveId, TroveChange memory _troveChange, address _batchAddress, uint256 _batchColl, uint256 _batchDebt) public updateGhosts asActor { + // troveManager.onOpenTroveAndJoinBatch(_owner, _troveId, _troveChange, _batchAddress, _batchColl, _batchDebt); + // } + + // function troveManager_onRegisterBatchManager(address _account, uint256 _annualInterestRate, uint256 _annualManagementFee) public updateGhosts asActor { + // troveManager.onRegisterBatchManager(_account, _annualInterestRate, _annualManagementFee); + // } + + // function troveManager_onRemoveFromBatch(uint256 _troveId, uint256 _newTroveColl, uint256 _newTroveDebt, TroveChange memory _troveChange, address _batchAddress, uint256 _newBatchColl, uint256 _newBatchDebt, uint256 _newAnnualInterestRate) public updateGhosts asActor { + // troveManager.onRemoveFromBatch(_troveId, _newTroveColl, _newTroveDebt, _troveChange, _batchAddress, _newBatchColl, _newBatchDebt, _newAnnualInterestRate); + // } + + // function troveManager_onSetBatchManagerAnnualInterestRate(address _batchAddress, uint256 _newColl, uint256 _newDebt, uint256 _newAnnualInterestRate, uint256 _upfrontFee) public updateGhosts asActor { + // troveManager.onSetBatchManagerAnnualInterestRate(_batchAddress, _newColl, _newDebt, _newAnnualInterestRate, _upfrontFee); + // } + + // function troveManager_onSetInterestBatchManager(ITroveManager.OnSetInterestBatchManagerParams memory _params) public updateGhosts asActor { + // troveManager.onSetInterestBatchManager(_params); + // } + + // function troveManager_redeemCollateral(address _redeemer, uint256 _boldamount, uint256 _price, uint256 _redemptionRate, uint256 _maxIterations) public updateGhosts asActor { + // troveManager.redeemCollateral(_redeemer, _boldamount, _price, _redemptionRate, _maxIterations); + // } + + // function troveManager_setTroveStatusToActive(uint256 _troveId) public updateGhosts asActor { + // troveManager.setTroveStatusToActive(_troveId); + // } + + // function troveManager_shutdown() public updateGhosts asActor { + // troveManager.shutdown(); + // } + + +} \ No newline at end of file diff --git a/contracts/test/redemptions.t.sol b/contracts/test/redemptions.t.sol index a4a029a4a..2b358d3eb 100644 --- a/contracts/test/redemptions.t.sol +++ b/contracts/test/redemptions.t.sol @@ -453,7 +453,9 @@ contract Redemptions is DevTestSetup { uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); assertGt(debt_B, 0, "B debt should be non zero"); - deal(address(boldToken), B, debt_B); + vm.startPrank(address(borrowerOperations)); + boldToken.mint(B, debt_B); + vm.stopPrank(); closeTrove(B, troveIDs.B); // Check B is closed @@ -479,7 +481,9 @@ contract Redemptions is DevTestSetup { uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); assertGt(debt_B, 0, "B debt should be non zero"); - deal(address(boldToken), B, debt_B); + vm.startPrank(address(borrowerOperations)); + boldToken.mint(B, debt_B); + vm.stopPrank(); closeTrove(B, troveIDs.B); // Check B is closed diff --git a/contracts/test/shutdown.t.sol b/contracts/test/shutdown.t.sol index e7103a2fc..11ea77a4f 100644 --- a/contracts/test/shutdown.t.sol +++ b/contracts/test/shutdown.t.sol @@ -8,6 +8,8 @@ contract ShutdownTest is DevTestSetup { uint256 NUM_COLLATERALS = 4; TestDeployer.LiquityContractsDev[] public contractsArray; + uint256 public MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + function setUp() public override { // Start tests at a non-zero timestamp vm.warp(block.timestamp + 600); @@ -27,10 +29,10 @@ contract ShutdownTest is DevTestSetup { TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = new TestDeployer.TroveManagerParams[](NUM_COLLATERALS); - troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16); - troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16); - troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16); - troveManagerParamsArray[3] = TestDeployer.TroveManagerParams(160e16, 125e16, 125e16, 5e16, 10e16); + troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2); + troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT/2); + troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT/2); + troveManagerParamsArray[3] = TestDeployer.TroveManagerParams(160e16, 125e16, 125e16, 5e16, 10e16, MAX_INT/2); TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; diff --git a/contracts/test/troveNFT.t.sol b/contracts/test/troveNFT.t.sol index 08608594d..c7a420e91 100644 --- a/contracts/test/troveNFT.t.sol +++ b/contracts/test/troveNFT.t.sol @@ -50,6 +50,8 @@ contract troveNFTTest is DevTestSetup { vm.stopPrank(); } + uint256 public MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + function setUp() public override { // Start tests at a non-zero timestamp vm.warp(block.timestamp + 600); @@ -69,9 +71,9 @@ contract troveNFTTest is DevTestSetup { TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = new TestDeployer.TroveManagerParams[](NUM_COLLATERALS); - troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16); - troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16); - troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16); + troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2); + troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT/2); + troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT/2); TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; diff --git a/contracts/test/zapperGasComp.t.sol b/contracts/test/zapperGasComp.t.sol index 0d7d2d2e1..48c66d532 100644 --- a/contracts/test/zapperGasComp.t.sol +++ b/contracts/test/zapperGasComp.t.sol @@ -27,8 +27,9 @@ contract ZapperGasCompTest is DevTestSetup { WETH = new WETH9(); TestDeployer.TroveManagerParams[] memory troveManagerParams = new TestDeployer.TroveManagerParams[](2); - troveManagerParams[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16); - troveManagerParams[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16); + uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + troveManagerParams[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2); + troveManagerParams[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT/2); TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory contractsArray; diff --git a/contracts/test/zapperLeverage.t.sol b/contracts/test/zapperLeverage.t.sol index e15ed8dbb..b9d86f88c 100644 --- a/contracts/test/zapperLeverage.t.sol +++ b/contracts/test/zapperLeverage.t.sol @@ -124,11 +124,13 @@ contract ZapperLeverageMainnet is DevTestSetup { WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = new TestDeployer.TroveManagerParams[](NUM_COLLATERALS); - troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16); + troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2); for (uint256 c = 0; c < NUM_COLLATERALS; c++) { - troveManagerParamsArray[c] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16); + troveManagerParamsArray[c] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT/2); } TestDeployer deployer = new TestDeployer(); diff --git a/contracts/test/zapperWETH.t.sol b/contracts/test/zapperWETH.t.sol index baf3ca223..8c8e8e55f 100644 --- a/contracts/test/zapperWETH.t.sol +++ b/contracts/test/zapperWETH.t.sol @@ -26,8 +26,10 @@ contract ZapperWETHTest is DevTestSetup { WETH = new WETH9(); + uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + TestDeployer.TroveManagerParams[] memory troveManagerParams = new TestDeployer.TroveManagerParams[](1); - troveManagerParams[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16); + troveManagerParams[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2); TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory contractsArray; diff --git a/script/DeployLiquity2.s.sol b/script/DeployLiquity2.s.sol new file mode 100644 index 000000000..3e5d5ac3a --- /dev/null +++ b/script/DeployLiquity2.s.sol @@ -0,0 +1 @@ +boldToken = BoldToken(payable(boldAddress)); \ No newline at end of file