From a4fb2f3366a9932aeb8025e2f5c6fc9e8c85a928 Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Tue, 21 Jan 2025 00:30:41 -0500 Subject: [PATCH 01/26] SF library import and remappings --- .gitmodules | 3 + contracts/lib/superfluid-protocol-monorepo | 1 + contracts/remappings.txt | 2 + contracts/src/BoldToken.sol | 58 ++++++++++++------- .../test/TestContracts/BoldTokenTester.sol | 2 +- 5 files changed, 44 insertions(+), 22 deletions(-) create mode 160000 contracts/lib/superfluid-protocol-monorepo 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/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/remappings.txt b/contracts/remappings.txt index c17beb9f8..04cd3cfee 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1 +1,3 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ openzeppelin/=lib/V2-gov/lib/openzeppelin-contracts/ +@superfluid-finance/ethereum-contracts/=lib/superfluid-protocol-monorepo/packages/ethereum-contracts/ diff --git a/contracts/src/BoldToken.sol b/contracts/src/BoldToken.sol index a709c746b..b6bc81097 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,7 +24,12 @@ 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 { + //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 Ownable, IBoldToken, CustomSuperTokenBase, UUPSProxy { + string internal constant _NAME = "Bold Stablecoin"; string internal constant _SYMBOL = "Bold"; @@ -35,7 +48,23 @@ 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. + constructor(address _owner, ISuperTokenFactory factory) Ownable(_owner) { + // 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, + "USD Nerite", + "USDN" + ); + } function setBranchAddresses( address _troveManagerAddress, @@ -67,39 +96,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 --- diff --git a/contracts/test/TestContracts/BoldTokenTester.sol b/contracts/test/TestContracts/BoldTokenTester.sol index eaa912470..80579d531 100644 --- a/contracts/test/TestContracts/BoldTokenTester.sol +++ b/contracts/test/TestContracts/BoldTokenTester.sol @@ -8,6 +8,6 @@ contract BoldTokenTester is BoldToken { constructor(address _owner) BoldToken(_owner) {} function unprotectedMint(address _account, uint256 _amount) external { - _mint(_account, _amount); + ISuperToken(address(this)).selfMint(_account, _amount, "");(_account, _amount); } } From 7d21df85bb20583e898e80b03d355ef599bae829 Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Wed, 22 Jan 2025 01:13:17 -0500 Subject: [PATCH 02/26] add implementation to bold token and fix tests --- contracts/script/DeployLiquity2.s.sol | 9 +- contracts/src/BoldToken.sol | 139 +++++++++++++++++- .../test/TestContracts/BoldTokenTester.sol | 2 +- contracts/test/TestContracts/Deployment.t.sol | 15 +- script/DeployLiquity2.s.sol | 1 + 5 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 script/DeployLiquity2.s.sol diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 99bfff55a..1ee56e845 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -227,6 +227,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"); @@ -266,14 +269,14 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, 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 = BoldToken(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"); } else { - boldToken = new BoldToken{salt: SALT}(deployer); + boldToken = new BoldToken{salt: SALT}(deployer, superTokenFactory); assert(address(boldToken) == boldAddress); } @@ -526,7 +529,7 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, DeploymentVars memory vars; vars.numCollaterals = troveManagerParamsArray.length; - r.boldToken = BoldToken(_deployGovernanceParams.bold); + r.boldToken = BoldToken(payable(_deployGovernanceParams.bold)); // USDC and USDC-BOLD pool r.usdcCurvePool = _deployCurvePool(r.boldToken, USDC); diff --git a/contracts/src/BoldToken.sol b/contracts/src/BoldToken.sol index b6bc81097..e53d689d1 100644 --- a/contracts/src/BoldToken.sol +++ b/contracts/src/BoldToken.sol @@ -13,6 +13,7 @@ import { ISuperToken, ISuperTokenFactory, IERC20 } from "@superfluid-finance/eth import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "./Dependencies/Ownable.sol"; import "./Interfaces/IBoldToken.sol"; +import "@superfluid-finance/ethereum-contracts/contracts/upgradability/UUPSProxiable.sol"; /* * --- Functionality added specific to the BoldToken --- @@ -28,10 +29,11 @@ import "./Interfaces/IBoldToken.sol"; //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 Ownable, IBoldToken, CustomSuperTokenBase, UUPSProxy { +contract BoldToken is Ownable, IBoldToken, CustomSuperTokenBase, UUPSProxy, UUPSProxiable { + + string internal constant _NAME = "Nerite Stablecoin"; + string internal constant _SYMBOL = "USDN"; - string internal constant _NAME = "Bold Stablecoin"; - string internal constant _SYMBOL = "Bold"; // --- Addresses --- @@ -51,6 +53,7 @@ contract BoldToken is Ownable, IBoldToken, CustomSuperTokenBase, UUPSProxy { //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. constructor(address _owner, ISuperTokenFactory factory) Ownable(_owner) { // 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. @@ -144,4 +147,134 @@ contract BoldToken is Ownable, IBoldToken, CustomSuperTokenBase, UUPSProxy { function _requireCallerIsStabilityPool() internal view { require(stabilityPoolAddresses[msg.sender], "Bold: Caller is not the StabilityPool"); } + + //[================================] + //interface implementation functions + //[================================] + + function name() external view override returns (string memory) { + return _NAME; + } + + /// @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) + ) + ); + } + + function balanceOf(address account) external view returns (uint256){ + return ISuperToken(address(this)).balanceOf(account); + } + + function allowance(address owner, address spender) external view override returns (uint256) { + return ISuperToken(address(this)).allowance(owner, spender); + } + + function approve(address spender, uint256 amount) external override returns (bool) { + return ISuperToken(address(this)).approve(spender, amount); + } + + function decimals() external pure override returns (uint8) { + return 18; // SuperTokens always use 18 decimals + } + + // Storage slot for nonces (from ERC20Permit) + mapping(address => uint256) private _nonces; + + function eip712Domain() external view override returns ( + bytes1 fields, + string memory name, + 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)).approve(spender, value); + } + + function proxiableUUID() public view override returns (bytes32) { + return keccak256("org.superfluid-finance.contracts.SuperToken.implementation"); + } + + function symbol() external view override returns (string memory) { + return ISuperToken(address(this)).symbol(); + } + + function totalSupply() external view override returns (uint256) { + return ISuperToken(address(this)).totalSupply(); + } + + function transfer(address to, uint256 amount) external override returns (bool) { + ISuperToken(address(this)).transfer(to, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) external override returns (bool) { + ISuperToken(address(this)).transferFrom(from, to, amount); + return true; + } + + function updateCode(address newAddress) external override { + return; + } } diff --git a/contracts/test/TestContracts/BoldTokenTester.sol b/contracts/test/TestContracts/BoldTokenTester.sol index 80579d531..a77ae6227 100644 --- a/contracts/test/TestContracts/BoldTokenTester.sol +++ b/contracts/test/TestContracts/BoldTokenTester.sol @@ -5,7 +5,7 @@ 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 { ISuperToken(address(this)).selfMint(_account, _amount, "");(_account, _amount); diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index aa04e76af..cf11499e6 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -294,12 +294,15 @@ contract TestDeployer is MetadataDeployment { Zappers[] memory zappersArray ) { + //setup SF factories. + ISuperTokenFactory superTokenFactory = ISuperTokenFactory(0x0000000000000000000000000000000000000000); + 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), superTokenFactory)); vars.boldTokenAddress = getAddress(address(this), vars.bytecode, SALT); - boldToken = new BoldToken{salt: SALT}(address(this)); + boldToken = new BoldToken{salt: SALT}(address(this), superTokenFactory); assert(address(boldToken) == vars.boldTokenAddress); contractsArray = new LiquityContractsDev[](vars.numCollaterals); @@ -504,6 +507,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 +561,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 = new BoldToken{salt: SALT}(address(this), superTokenFactory); assert(address(result.boldToken) == vars.boldTokenAddress); // WETH 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 From 815c134d431c3b2390d31d43232c801a3b1fa322 Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:40:55 -0500 Subject: [PATCH 03/26] sf permit test complete --- .gitignore | 1 + contracts/remappings.txt | 3 +- contracts/script/DeployLiquity2.s.sol | 10 +- contracts/src/BoldToken.sol | 63 ++--------- contracts/src/Interfaces/IBoldToken.sol | 10 +- contracts/test/SFBold.t.sol | 106 ++++++++++++++++++ contracts/test/TestContracts/Deployment.t.sol | 4 +- contracts/test/TestContracts/DevTestSetup.sol | 19 ++++ 8 files changed, 154 insertions(+), 62 deletions(-) create mode 100644 contracts/test/SFBold.t.sol diff --git a/.gitignore b/.gitignore index c2658d7d1..2752eb92e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules/ +.DS_Store diff --git a/contracts/remappings.txt b/contracts/remappings.txt index 04cd3cfee..2da5bf9ee 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1,3 +1,4 @@ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ openzeppelin/=lib/V2-gov/lib/openzeppelin-contracts/ -@superfluid-finance/ethereum-contracts/=lib/superfluid-protocol-monorepo/packages/ethereum-contracts/ +@superfluid-finance/=lib/superfluid-protocol-monorepo/packages/ + diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 1ee56e845..f877db26d 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -265,18 +265,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(payable(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, superTokenFactory); + boldToken = IBoldToken(address(new BoldToken{salt: SALT}(deployer, superTokenFactory))); assert(address(boldToken) == boldAddress); } @@ -529,7 +529,7 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, DeploymentVars memory vars; vars.numCollaterals = troveManagerParamsArray.length; - r.boldToken = BoldToken(payable(_deployGovernanceParams.bold)); + r.boldToken = IBoldToken(payable(_deployGovernanceParams.bold)); // USDC and USDC-BOLD pool r.usdcCurvePool = _deployCurvePool(r.boldToken, USDC); diff --git a/contracts/src/BoldToken.sol b/contracts/src/BoldToken.sol index e53d689d1..3028a1779 100644 --- a/contracts/src/BoldToken.sol +++ b/contracts/src/BoldToken.sol @@ -13,7 +13,6 @@ import { ISuperToken, ISuperTokenFactory, IERC20 } from "@superfluid-finance/eth import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "./Dependencies/Ownable.sol"; import "./Interfaces/IBoldToken.sol"; -import "@superfluid-finance/ethereum-contracts/contracts/upgradability/UUPSProxiable.sol"; /* * --- Functionality added specific to the BoldToken --- @@ -29,7 +28,7 @@ import "@superfluid-finance/ethereum-contracts/contracts/upgradability/UUPSProxi //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 Ownable, IBoldToken, CustomSuperTokenBase, UUPSProxy, UUPSProxiable { +contract BoldToken is CustomSuperTokenBase, Ownable, IBoldTokenCustom, UUPSProxy { string internal constant _NAME = "Nerite Stablecoin"; string internal constant _SYMBOL = "USDN"; @@ -54,7 +53,10 @@ contract BoldToken is Ownable, IBoldToken, CustomSuperTokenBase, UUPSProxy, UUPS //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. - constructor(address _owner, ISuperTokenFactory factory) Ownable(_owner) { + //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)); @@ -64,8 +66,8 @@ contract BoldToken is Ownable, IBoldToken, CustomSuperTokenBase, UUPSProxy, UUPS ISuperToken(address(this)).initialize( IERC20(address(0)), 18, - "USD Nerite", - "USDN" + _NAME, + _SYMBOL ); } @@ -149,13 +151,9 @@ contract BoldToken is Ownable, IBoldToken, CustomSuperTokenBase, UUPSProxy, UUPS } //[================================] - //interface implementation functions + //Permit functions //[================================] - function name() external view override returns (string memory) { - return _NAME; - } - /// @dev Returns the EIP-712 domain separator for the EIP-2612 permit. // Required to implement ERC20Permit. // solhint-disable-next-line func-name-mixedcase @@ -171,28 +169,12 @@ contract BoldToken is Ownable, IBoldToken, CustomSuperTokenBase, UUPSProxy, UUPS ); } - function balanceOf(address account) external view returns (uint256){ - return ISuperToken(address(this)).balanceOf(account); - } - - function allowance(address owner, address spender) external view override returns (uint256) { - return ISuperToken(address(this)).allowance(owner, spender); - } - - function approve(address spender, uint256 amount) external override returns (bool) { - return ISuperToken(address(this)).approve(spender, amount); - } - - function decimals() external pure override returns (uint8) { - return 18; // SuperTokens always use 18 decimals - } - // Storage slot for nonces (from ERC20Permit) mapping(address => uint256) private _nonces; function eip712Domain() external view override returns ( bytes1 fields, - string memory name, + string memory name712, string memory version, uint256 chainId, address verifyingContract, @@ -249,32 +231,7 @@ contract BoldToken is Ownable, IBoldToken, CustomSuperTokenBase, UUPSProxy, UUPS if (signer != owner) revert("INVALID_SIGNER"); // Finally, approve the spender - ISuperToken(address(this)).approve(spender, value); + ISuperToken(address(this)).selfApproveFor(owner, spender, value); } - function proxiableUUID() public view override returns (bytes32) { - return keccak256("org.superfluid-finance.contracts.SuperToken.implementation"); - } - - function symbol() external view override returns (string memory) { - return ISuperToken(address(this)).symbol(); - } - - function totalSupply() external view override returns (uint256) { - return ISuperToken(address(this)).totalSupply(); - } - - function transfer(address to, uint256 amount) external override returns (bool) { - ISuperToken(address(this)).transfer(to, amount); - return true; - } - - function transferFrom(address from, address to, uint256 amount) external override returns (bool) { - ISuperToken(address(this)).transferFrom(from, to, amount); - return true; - } - - function updateCode(address newAddress) external override { - return; - } } 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/test/SFBold.t.sol b/contracts/test/SFBold.t.sol new file mode 100644 index 000000000..9d23b6603 --- /dev/null +++ b/contracts/test/SFBold.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; +import { BoldToken, IBoldToken } from "../src/BoldToken.sol"; + +import {Test} from "forge-std/Test.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 SFBold is Test { + string internal constant _NAME = "TestToken"; + address internal constant _OWNER = address(0x1); + uint256 internal constant _PERMIT_SIGNER_PK = 0xA11CE; + 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); + 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"); + } + + // ============================ 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)); + } +} \ No newline at end of file diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index cf11499e6..e47c1edb1 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -302,7 +302,7 @@ contract TestDeployer is MetadataDeployment { // Deploy Bold vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this), superTokenFactory)); vars.boldTokenAddress = getAddress(address(this), vars.bytecode, SALT); - boldToken = new BoldToken{salt: SALT}(address(this), superTokenFactory); + boldToken = IBoldToken(address(new BoldToken{salt: SALT}(address(this), superTokenFactory))); assert(address(boldToken) == vars.boldTokenAddress); contractsArray = new LiquityContractsDev[](vars.numCollaterals); @@ -563,7 +563,7 @@ contract TestDeployer is MetadataDeployment { // Deploy Bold 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), superTokenFactory); + result.boldToken = IBoldToken(address(new BoldToken{salt: SALT}(address(this), superTokenFactory))); assert(address(result.boldToken) == vars.boldTokenAddress); // WETH 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; From 403f21fca2b1184eb745ac1798b25c0e68a3fdd6 Mon Sep 17 00:00:00 2001 From: didi Date: Mon, 27 Jan 2025 22:25:10 +0100 Subject: [PATCH 04/26] added flow test --- contracts/test/SFBold.t.sol | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/contracts/test/SFBold.t.sol b/contracts/test/SFBold.t.sol index 9d23b6603..30fdbfa74 100644 --- a/contracts/test/SFBold.t.sol +++ b/contracts/test/SFBold.t.sol @@ -8,11 +8,16 @@ 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; @@ -33,7 +38,8 @@ contract SFBold is Test { // Fund the signer with some tokens vm.startPrank(_OWNER); _boldToken.setBranchAddresses(_OWNER, _OWNER, _OWNER, _OWNER); - _boldToken.mint(_permitSigner, 500); + _boldToken.mint(_permitSigner, 500 ether); + _boldToken.mint(_ALICE, 500 ether); vm.stopPrank(); } @@ -87,6 +93,30 @@ contract SFBold is Test { 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"); + } + // ============================ Internal Functions ============================ function _createPermitDigest(address owner, address spender, uint256 value, uint256 nonce, uint256 deadline) From 870a08264960cf901f52f937149dfd5b2031cbf7 Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:18:21 -0500 Subject: [PATCH 05/26] fix inits for bold token --- contracts/test/TestContracts/Deployment.t.sol | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index e47c1edb1..f93c66b6f 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 = @@ -295,16 +306,22 @@ contract TestDeployer is MetadataDeployment { ) { //setup SF factories. - ISuperTokenFactory superTokenFactory = ISuperTokenFactory(0x0000000000000000000000000000000000000000); + 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), superTokenFactory)); + vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this), _sf.superTokenFactory)); vars.boldTokenAddress = getAddress(address(this), vars.bytecode, SALT); - boldToken = IBoldToken(address(new BoldToken{salt: SALT}(address(this), superTokenFactory))); + 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); From 07fa5e9720ad3d67c7a98e3a02a28353b82d1282 Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:30:20 -0500 Subject: [PATCH 06/26] add new feeds --- contracts/src/PriceFeeds/ARBPriceFeed.sol | 24 ++++++++++++ contracts/src/PriceFeeds/COMPPriceFeed.sol | 24 ++++++++++++ contracts/src/PriceFeeds/EZETHPriceFeed.sol | 43 +++++++++++++++++++++ contracts/src/PriceFeeds/GMXPriceFeed.sol | 24 ++++++++++++ contracts/src/PriceFeeds/UNIPriceFeed.sol | 24 ++++++++++++ contracts/src/PriceFeeds/WEETHPriceFeed.sol | 43 +++++++++++++++++++++ contracts/src/PriceFeeds/XVSPriceFeed.sol | 24 ++++++++++++ 7 files changed, 206 insertions(+) create mode 100644 contracts/src/PriceFeeds/ARBPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/COMPPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/EZETHPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/GMXPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/UNIPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/WEETHPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/XVSPriceFeed.sol diff --git a/contracts/src/PriceFeeds/ARBPriceFeed.sol b/contracts/src/PriceFeeds/ARBPriceFeed.sol new file mode 100644 index 000000000..ea637b8ec --- /dev/null +++ b/contracts/src/PriceFeeds/ARBPriceFeed.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./MainnetPriceFeedBase.sol"; + +contract ARBPriceFeed is MainnetPriceFeedBase { + constructor(address _owner, address _arbUsdOracleAddress, uint256 _arbUsdStalenessThreshold) + MainnetPriceFeedBase(_owner, _arbUsdOracleAddress, _arbUsdStalenessThreshold) + { + _fetchPricePrimary(); + assert(priceSource == PriceSource.primary); + } + + function fetchPrice() public returns (uint256, bool) { + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + return fetchPrice(); + } +} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/COMPPriceFeed.sol b/contracts/src/PriceFeeds/COMPPriceFeed.sol new file mode 100644 index 000000000..c203575a7 --- /dev/null +++ b/contracts/src/PriceFeeds/COMPPriceFeed.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./MainnetPriceFeedBase.sol"; + +contract COMPPriceFeed is MainnetPriceFeedBase { + constructor(address _owner, address _compUsdOracleAddress, uint256 _compUsdStalenessThreshold) + MainnetPriceFeedBase(_owner, _compUsdOracleAddress, _compUsdStalenessThreshold) + { + _fetchPricePrimary(); + assert(priceSource == PriceSource.primary); + } + + function fetchPrice() public returns (uint256, bool) { + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + return fetchPrice(); + } +} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/EZETHPriceFeed.sol b/contracts/src/PriceFeeds/EZETHPriceFeed.sol new file mode 100644 index 000000000..ede9ce60a --- /dev/null +++ b/contracts/src/PriceFeeds/EZETHPriceFeed.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./CompositePriceFeed.sol"; +import "../Interfaces/IEZETH.sol"; +import "../Interfaces/IEZETHPriceFeed.sol"; + +contract EZETHPriceFeed is CompositePriceFeed, IEZETHPriceFeed { + Oracle public ezethEthOracle; + + uint256 public constant EZETH_ETH_DEVIATION_THRESHOLD = 1e16; // 1% + + constructor( + address _owner, + address _ethUsdOracleAddress, + address _ezethEthOracleAddress, + address _ezethTokenAddress, + uint256 _ethUsdStalenessThreshold, + uint256 _ezethEthStalenessThreshold + ) CompositePriceFeed(_owner, _ethUsdOracleAddress, _ezethTokenAddress, _ethUsdStalenessThreshold) { + ezethEthOracle.aggregator = AggregatorV3Interface(_ezethEthOracleAddress); + ezethEthOracle.stalenessThreshold = _ezethEthStalenessThreshold; + ezethEthOracle.decimals = ezethEthOracle.aggregator.decimals(); + + _fetchPricePrimary(false); + + // Check the oracle didn't already fail + assert(priceSource == PriceSource.primary); + } + + function _getCanonicalRate() internal view override returns (uint256, bool) { + uint256 gasBefore = gasleft(); + + try IEZETH(rateProviderAddress).getExchangeRate() returns (uint256 ethPerEzeth) { + if (ethPerEzeth == 0) return (0, true); + return (ethPerEzeth, false); + } catch { + if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); + return (0, true); + } + } +} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/GMXPriceFeed.sol b/contracts/src/PriceFeeds/GMXPriceFeed.sol new file mode 100644 index 000000000..e2d80ef93 --- /dev/null +++ b/contracts/src/PriceFeeds/GMXPriceFeed.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./MainnetPriceFeedBase.sol"; + +contract GMXPriceFeed is MainnetPriceFeedBase { + constructor(address _owner, address _gmxUsdOracleAddress, uint256 _gmxUsdStalenessThreshold) + MainnetPriceFeedBase(_owner, _gmxUsdOracleAddress, _gmxUsdStalenessThreshold) + { + _fetchPricePrimary(); + assert(priceSource == PriceSource.primary); + } + + function fetchPrice() public returns (uint256, bool) { + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + return fetchPrice(); + } +} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/UNIPriceFeed.sol b/contracts/src/PriceFeeds/UNIPriceFeed.sol new file mode 100644 index 000000000..68d004291 --- /dev/null +++ b/contracts/src/PriceFeeds/UNIPriceFeed.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./MainnetPriceFeedBase.sol"; + +contract UNIPriceFeed is MainnetPriceFeedBase { + constructor(address _owner, address _uniUsdOracleAddress, uint256 _uniUsdStalenessThreshold) + MainnetPriceFeedBase(_owner, _uniUsdOracleAddress, _uniUsdStalenessThreshold) + { + _fetchPricePrimary(); + assert(priceSource == PriceSource.primary); + } + + function fetchPrice() public returns (uint256, bool) { + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + return fetchPrice(); + } +} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/WEETHPriceFeed.sol b/contracts/src/PriceFeeds/WEETHPriceFeed.sol new file mode 100644 index 000000000..584a68fda --- /dev/null +++ b/contracts/src/PriceFeeds/WEETHPriceFeed.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./CompositePriceFeed.sol"; +import "../Interfaces/IWEETH.sol"; +import "../Interfaces/IWEETHPriceFeed.sol"; + +contract WEETHPriceFeed is CompositePriceFeed, IWEETHPriceFeed { + Oracle public weethEthOracle; + + uint256 public constant WEETH_ETH_DEVIATION_THRESHOLD = 1e16; // 1% + + constructor( + address _owner, + address _ethUsdOracleAddress, + address _weethEthOracleAddress, + address _weethTokenAddress, + uint256 _ethUsdStalenessThreshold, + uint256 _weethEthStalenessThreshold + ) CompositePriceFeed(_owner, _ethUsdOracleAddress, _weethTokenAddress, _ethUsdStalenessThreshold) { + 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); + } + + function _getCanonicalRate() internal view override returns (uint256, bool) { + uint256 gasBefore = gasleft(); + + try IWEETH(rateProviderAddress).getExchangeRate() returns (uint256 ethPerWeeth) { + if (ethPerWeeth == 0) return (0, true); + return (ethPerWeeth, false); + } catch { + if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); + return (0, true); + } + } +} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/XVSPriceFeed.sol b/contracts/src/PriceFeeds/XVSPriceFeed.sol new file mode 100644 index 000000000..af0a2c7e5 --- /dev/null +++ b/contracts/src/PriceFeeds/XVSPriceFeed.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./MainnetPriceFeedBase.sol"; + +contract XVSPriceFeed is MainnetPriceFeedBase { + constructor(address _owner, address _xvsUsdOracleAddress, uint256 _xvsUsdStalenessThreshold) + MainnetPriceFeedBase(_owner, _xvsUsdOracleAddress, _xvsUsdStalenessThreshold) + { + _fetchPricePrimary(); + assert(priceSource == PriceSource.primary); + } + + function fetchPrice() public returns (uint256, bool) { + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } + + function fetchRedemptionPrice() external returns (uint256, bool) { + return fetchPrice(); + } +} \ No newline at end of file From 6926f4ebb8cf07e069e2c874cb209d32a8bff8ef Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Tue, 28 Jan 2025 21:55:46 -0500 Subject: [PATCH 07/26] replace deal with direct mint --- ...werOperationsOnBehalfTroveManagament.t.sol | 59 +++++++++++++++---- contracts/test/redemptions.t.sol | 8 ++- 2 files changed, 54 insertions(+), 13 deletions(-) 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/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 From ac9d0e93b0e0b63875032ef8eddfec65c2b813ea Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Wed, 29 Jan 2025 21:34:39 -0500 Subject: [PATCH 08/26] Add limits --- contracts/script/DeployLiquity2.s.sol | 13 ++++-- contracts/src/BorrowerOperations.sol | 14 ++++++ contracts/src/CollateralRegistry.sol | 29 ++++++++++++- .../src/Interfaces/ICollateralRegistry.sol | 5 +++ contracts/src/Interfaces/ITroveManager.sol | 5 +++ contracts/src/PriceFeeds/ARBPriceFeed.sol | 24 ----------- contracts/src/PriceFeeds/COMPPriceFeed.sol | 24 ----------- contracts/src/PriceFeeds/EZETHPriceFeed.sol | 43 ------------------- contracts/src/PriceFeeds/GMXPriceFeed.sol | 24 ----------- contracts/src/PriceFeeds/UNIPriceFeed.sol | 24 ----------- contracts/src/PriceFeeds/WEETHPriceFeed.sol | 43 ------------------- contracts/src/PriceFeeds/XVSPriceFeed.sol | 24 ----------- contracts/src/TroveManager.sol | 13 ++++++ contracts/test/AnchoredInvariantsTest.t.sol | 10 +++-- contracts/test/Invariants.t.sol | 10 +++-- contracts/test/OracleMainnet.t.sol | 4 +- .../CollateralRegistryTester.sol | 2 +- contracts/test/TestContracts/Deployment.t.sol | 7 ++- contracts/test/liquidationsLST.t.sol | 4 +- contracts/test/multicollateral.t.sol | 10 +++-- contracts/test/shutdown.t.sol | 10 +++-- contracts/test/troveNFT.t.sol | 8 ++-- contracts/test/zapperGasComp.t.sol | 5 ++- contracts/test/zapperLeverage.t.sol | 6 ++- contracts/test/zapperWETH.t.sol | 4 +- 25 files changed, 125 insertions(+), 240 deletions(-) delete mode 100644 contracts/src/PriceFeeds/ARBPriceFeed.sol delete mode 100644 contracts/src/PriceFeeds/COMPPriceFeed.sol delete mode 100644 contracts/src/PriceFeeds/EZETHPriceFeed.sol delete mode 100644 contracts/src/PriceFeeds/GMXPriceFeed.sol delete mode 100644 contracts/src/PriceFeeds/UNIPriceFeed.sol delete mode 100644 contracts/src/PriceFeeds/WEETHPriceFeed.sol delete mode 100644 contracts/src/PriceFeeds/XVSPriceFeed.sol diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index f877db26d..78dc5adb7 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 { @@ -325,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); // WETH + troveManagerParamsArray[1] = TroveManagerParams(150e16, 120e16, 110e16, 5e16, 10e16, MAX_INT); // wstETH + troveManagerParamsArray[2] = TroveManagerParams(150e16, 120e16, 110e16, 5e16, 10e16, MAX_INT); // rETH string[] memory collNames = new string[](2); string[] memory collSymbols = new string[](2); @@ -591,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); diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index c8bc441d9..ae15b58fa 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.24; +import {console} from "forge-std/console.sol"; + import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Interfaces/IBorrowerOperations.sol"; @@ -311,6 +313,14 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio vars.activePool = activePool; vars.boldToken = boldToken; + console.log("debt limit: ", troveManager.getDebtLimit()); + console.log("entire system debt: ", troveManager.getEntireSystemDebt()); + console.log("bold amount being minted: ", _boldAmount); + console.log("debt limit + bold amount: ", troveManager.getDebtLimit() + _boldAmount); + + require(troveManager.getDebtLimit() >= troveManager.getEntireSystemDebt() + _boldAmount, "BorrowerOperations: Debt limit exceeded."); + + vars.price = _requireOraclesLive(); // --- Checks --- @@ -403,6 +413,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 +1232,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/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/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/PriceFeeds/ARBPriceFeed.sol b/contracts/src/PriceFeeds/ARBPriceFeed.sol deleted file mode 100644 index ea637b8ec..000000000 --- a/contracts/src/PriceFeeds/ARBPriceFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./MainnetPriceFeedBase.sol"; - -contract ARBPriceFeed is MainnetPriceFeedBase { - constructor(address _owner, address _arbUsdOracleAddress, uint256 _arbUsdStalenessThreshold) - MainnetPriceFeedBase(_owner, _arbUsdOracleAddress, _arbUsdStalenessThreshold) - { - _fetchPricePrimary(); - assert(priceSource == PriceSource.primary); - } - - function fetchPrice() public returns (uint256, bool) { - if (priceSource == PriceSource.primary) return _fetchPricePrimary(); - assert(priceSource == PriceSource.lastGoodPrice); - return (lastGoodPrice, false); - } - - function fetchRedemptionPrice() external returns (uint256, bool) { - return fetchPrice(); - } -} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/COMPPriceFeed.sol b/contracts/src/PriceFeeds/COMPPriceFeed.sol deleted file mode 100644 index c203575a7..000000000 --- a/contracts/src/PriceFeeds/COMPPriceFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./MainnetPriceFeedBase.sol"; - -contract COMPPriceFeed is MainnetPriceFeedBase { - constructor(address _owner, address _compUsdOracleAddress, uint256 _compUsdStalenessThreshold) - MainnetPriceFeedBase(_owner, _compUsdOracleAddress, _compUsdStalenessThreshold) - { - _fetchPricePrimary(); - assert(priceSource == PriceSource.primary); - } - - function fetchPrice() public returns (uint256, bool) { - if (priceSource == PriceSource.primary) return _fetchPricePrimary(); - assert(priceSource == PriceSource.lastGoodPrice); - return (lastGoodPrice, false); - } - - function fetchRedemptionPrice() external returns (uint256, bool) { - return fetchPrice(); - } -} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/EZETHPriceFeed.sol b/contracts/src/PriceFeeds/EZETHPriceFeed.sol deleted file mode 100644 index ede9ce60a..000000000 --- a/contracts/src/PriceFeeds/EZETHPriceFeed.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./CompositePriceFeed.sol"; -import "../Interfaces/IEZETH.sol"; -import "../Interfaces/IEZETHPriceFeed.sol"; - -contract EZETHPriceFeed is CompositePriceFeed, IEZETHPriceFeed { - Oracle public ezethEthOracle; - - uint256 public constant EZETH_ETH_DEVIATION_THRESHOLD = 1e16; // 1% - - constructor( - address _owner, - address _ethUsdOracleAddress, - address _ezethEthOracleAddress, - address _ezethTokenAddress, - uint256 _ethUsdStalenessThreshold, - uint256 _ezethEthStalenessThreshold - ) CompositePriceFeed(_owner, _ethUsdOracleAddress, _ezethTokenAddress, _ethUsdStalenessThreshold) { - ezethEthOracle.aggregator = AggregatorV3Interface(_ezethEthOracleAddress); - ezethEthOracle.stalenessThreshold = _ezethEthStalenessThreshold; - ezethEthOracle.decimals = ezethEthOracle.aggregator.decimals(); - - _fetchPricePrimary(false); - - // Check the oracle didn't already fail - assert(priceSource == PriceSource.primary); - } - - function _getCanonicalRate() internal view override returns (uint256, bool) { - uint256 gasBefore = gasleft(); - - try IEZETH(rateProviderAddress).getExchangeRate() returns (uint256 ethPerEzeth) { - if (ethPerEzeth == 0) return (0, true); - return (ethPerEzeth, false); - } catch { - if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); - return (0, true); - } - } -} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/GMXPriceFeed.sol b/contracts/src/PriceFeeds/GMXPriceFeed.sol deleted file mode 100644 index e2d80ef93..000000000 --- a/contracts/src/PriceFeeds/GMXPriceFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./MainnetPriceFeedBase.sol"; - -contract GMXPriceFeed is MainnetPriceFeedBase { - constructor(address _owner, address _gmxUsdOracleAddress, uint256 _gmxUsdStalenessThreshold) - MainnetPriceFeedBase(_owner, _gmxUsdOracleAddress, _gmxUsdStalenessThreshold) - { - _fetchPricePrimary(); - assert(priceSource == PriceSource.primary); - } - - function fetchPrice() public returns (uint256, bool) { - if (priceSource == PriceSource.primary) return _fetchPricePrimary(); - assert(priceSource == PriceSource.lastGoodPrice); - return (lastGoodPrice, false); - } - - function fetchRedemptionPrice() external returns (uint256, bool) { - return fetchPrice(); - } -} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/UNIPriceFeed.sol b/contracts/src/PriceFeeds/UNIPriceFeed.sol deleted file mode 100644 index 68d004291..000000000 --- a/contracts/src/PriceFeeds/UNIPriceFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./MainnetPriceFeedBase.sol"; - -contract UNIPriceFeed is MainnetPriceFeedBase { - constructor(address _owner, address _uniUsdOracleAddress, uint256 _uniUsdStalenessThreshold) - MainnetPriceFeedBase(_owner, _uniUsdOracleAddress, _uniUsdStalenessThreshold) - { - _fetchPricePrimary(); - assert(priceSource == PriceSource.primary); - } - - function fetchPrice() public returns (uint256, bool) { - if (priceSource == PriceSource.primary) return _fetchPricePrimary(); - assert(priceSource == PriceSource.lastGoodPrice); - return (lastGoodPrice, false); - } - - function fetchRedemptionPrice() external returns (uint256, bool) { - return fetchPrice(); - } -} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/WEETHPriceFeed.sol b/contracts/src/PriceFeeds/WEETHPriceFeed.sol deleted file mode 100644 index 584a68fda..000000000 --- a/contracts/src/PriceFeeds/WEETHPriceFeed.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./CompositePriceFeed.sol"; -import "../Interfaces/IWEETH.sol"; -import "../Interfaces/IWEETHPriceFeed.sol"; - -contract WEETHPriceFeed is CompositePriceFeed, IWEETHPriceFeed { - Oracle public weethEthOracle; - - uint256 public constant WEETH_ETH_DEVIATION_THRESHOLD = 1e16; // 1% - - constructor( - address _owner, - address _ethUsdOracleAddress, - address _weethEthOracleAddress, - address _weethTokenAddress, - uint256 _ethUsdStalenessThreshold, - uint256 _weethEthStalenessThreshold - ) CompositePriceFeed(_owner, _ethUsdOracleAddress, _weethTokenAddress, _ethUsdStalenessThreshold) { - 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); - } - - function _getCanonicalRate() internal view override returns (uint256, bool) { - uint256 gasBefore = gasleft(); - - try IWEETH(rateProviderAddress).getExchangeRate() returns (uint256 ethPerWeeth) { - if (ethPerWeeth == 0) return (0, true); - return (ethPerWeeth, false); - } catch { - if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); - return (0, true); - } - } -} \ No newline at end of file diff --git a/contracts/src/PriceFeeds/XVSPriceFeed.sol b/contracts/src/PriceFeeds/XVSPriceFeed.sol deleted file mode 100644 index af0a2c7e5..000000000 --- a/contracts/src/PriceFeeds/XVSPriceFeed.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./MainnetPriceFeedBase.sol"; - -contract XVSPriceFeed is MainnetPriceFeedBase { - constructor(address _owner, address _xvsUsdOracleAddress, uint256 _xvsUsdStalenessThreshold) - MainnetPriceFeedBase(_owner, _xvsUsdOracleAddress, _xvsUsdStalenessThreshold) - { - _fetchPricePrimary(); - assert(priceSource == PriceSource.primary); - } - - function fetchPrice() public returns (uint256, bool) { - if (priceSource == PriceSource.primary) return _fetchPricePrimary(); - assert(priceSource == PriceSource.lastGoodPrice); - return (lastGoodPrice, false); - } - - function fetchRedemptionPrice() external returns (uint256, bool) { - return fetchPrice(); - } -} \ No newline at end of file diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index eb7efc5aa..e12093f07 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 @@ -1967,4 +1971,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..fd5b9bf69 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); + p[1] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT); + p[2] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT); + p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT); 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..97883a4d7 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); + if (n > 1) p[1] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.2 ether, 0.05 ether, 0.2 ether, MAX_INT); + if (n > 2) p[2] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.2 ether, 0.05 ether, 0.2 ether, MAX_INT); + if (n > 3) p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT); TestDeployer deployer = new TestDeployer(); Contracts memory contracts; diff --git a/contracts/test/OracleMainnet.t.sol b/contracts/test/OracleMainnet.t.sol index 6a8a36a66..f1b642c90 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); TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = new TestDeployer.TroveManagerParams[](vars.numCollaterals); for (uint256 i = 0; i < troveManagerParamsArray.length; i++) { 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 e47c1edb1..1f75b318e 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -138,6 +138,7 @@ contract TestDeployer is MetadataDeployment { uint256 SCR; uint256 LIQUIDATION_PENALTY_SP; uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; + uint256 debtLimit; } struct DeploymentVarsDev { @@ -211,6 +212,8 @@ contract TestDeployer is MetadataDeployment { return address(uint160(uint256(hash))); } + uint256 public MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + function deployAndConnectContracts() external returns ( @@ -223,7 +226,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)); } function deployAndConnectContracts(TroveManagerParams memory troveManagerParams) @@ -331,7 +334,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); diff --git a/contracts/test/liquidationsLST.t.sol b/contracts/test/liquidationsLST.t.sol index 1b07c9ee6..8c26164a0 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)); 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..33e550bba 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); + troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); + troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); + troveManagerParamsArray[3] = TestDeployer.TroveManagerParams(160e16, 125e16, 125e16, 5e16, 10e16, MAX_INT); TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; diff --git a/contracts/test/shutdown.t.sol b/contracts/test/shutdown.t.sol index e7103a2fc..409e80f7e 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); + troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); + troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); + troveManagerParamsArray[3] = TestDeployer.TroveManagerParams(160e16, 125e16, 125e16, 5e16, 10e16, MAX_INT); TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; diff --git a/contracts/test/troveNFT.t.sol b/contracts/test/troveNFT.t.sol index 08608594d..1e393dc85 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); + troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); + troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; diff --git a/contracts/test/zapperGasComp.t.sol b/contracts/test/zapperGasComp.t.sol index 0d7d2d2e1..08eca4860 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); + troveManagerParams[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory contractsArray; diff --git a/contracts/test/zapperLeverage.t.sol b/contracts/test/zapperLeverage.t.sol index e15ed8dbb..34e13daaf 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); 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); } TestDeployer deployer = new TestDeployer(); diff --git a/contracts/test/zapperWETH.t.sol b/contracts/test/zapperWETH.t.sol index baf3ca223..0620a79a1 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); TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory contractsArray; From 9657a9875395a27e30aad85b1f42ae68902c6145 Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Wed, 29 Jan 2025 22:05:35 -0500 Subject: [PATCH 09/26] fix overflow --- contracts/script/DeployLiquity2.s.sol | 7 ++++--- contracts/src/AddressesRegistry.sol | 7 +++++++ contracts/src/BorrowerOperations.sol | 8 -------- contracts/src/Interfaces/IAddressesRegistry.sol | 1 + contracts/src/TroveManager.sol | 1 + contracts/test/AnchoredInvariantsTest.t.sol | 8 ++++---- contracts/test/Invariants.t.sol | 8 ++++---- contracts/test/OracleMainnet.t.sol | 2 +- contracts/test/SortedTroves.t.sol | 3 ++- contracts/test/TestContracts/Deployment.t.sol | 4 +++- contracts/test/liquidationsLST.t.sol | 2 +- contracts/test/multicollateral.t.sol | 8 ++++---- contracts/test/shutdown.t.sol | 8 ++++---- contracts/test/troveNFT.t.sol | 6 +++--- contracts/test/zapperGasComp.t.sol | 4 ++-- contracts/test/zapperLeverage.t.sol | 4 ++-- contracts/test/zapperWETH.t.sol | 2 +- 17 files changed, 44 insertions(+), 39 deletions(-) diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index 78dc5adb7..afd419ed4 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -328,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, MAX_INT); // WETH - troveManagerParamsArray[1] = TroveManagerParams(150e16, 120e16, 110e16, 5e16, 10e16, MAX_INT); // wstETH - troveManagerParamsArray[2] = TroveManagerParams(150e16, 120e16, 110e16, 5e16, 10e16, MAX_INT); // 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); @@ -643,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/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/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index ae15b58fa..c2381cf1c 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -2,8 +2,6 @@ pragma solidity 0.8.24; -import {console} from "forge-std/console.sol"; - import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Interfaces/IBorrowerOperations.sol"; @@ -313,13 +311,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio vars.activePool = activePool; vars.boldToken = boldToken; - console.log("debt limit: ", troveManager.getDebtLimit()); - console.log("entire system debt: ", troveManager.getEntireSystemDebt()); - console.log("bold amount being minted: ", _boldAmount); - console.log("debt limit + bold amount: ", troveManager.getDebtLimit() + _boldAmount); - require(troveManager.getDebtLimit() >= troveManager.getEntireSystemDebt() + _boldAmount, "BorrowerOperations: Debt limit exceeded."); - vars.price = _requireOraclesLive(); 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/TroveManager.sol b/contracts/src/TroveManager.sol index e12093f07..98a381038 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -187,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(); diff --git a/contracts/test/AnchoredInvariantsTest.t.sol b/contracts/test/AnchoredInvariantsTest.t.sol index fd5b9bf69..0c05c1a85 100644 --- a/contracts/test/AnchoredInvariantsTest.t.sol +++ b/contracts/test/AnchoredInvariantsTest.t.sol @@ -22,10 +22,10 @@ contract AnchoredInvariantsTest is Logging, BaseInvariantTest, BaseMultiCollater 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, MAX_INT); - p[1] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT); - p[2] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT); - p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT); + 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 97883a4d7..01d085615 100644 --- a/contracts/test/Invariants.t.sol +++ b/contracts/test/Invariants.t.sol @@ -73,10 +73,10 @@ contract InvariantsTest is Assertions, Logging, BaseInvariantTest, BaseMultiColl // 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, MAX_INT); - if (n > 1) p[1] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.2 ether, 0.05 ether, 0.2 ether, MAX_INT); - if (n > 2) p[2] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 1.2 ether, 0.05 ether, 0.2 ether, MAX_INT); - if (n > 3) p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 1.01 ether, 0.05 ether, 0.1 ether, MAX_INT); + 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 f1b642c90..e21060446 100644 --- a/contracts/test/OracleMainnet.t.sol +++ b/contracts/test/OracleMainnet.t.sol @@ -81,7 +81,7 @@ contract OraclesMainnet is TestAccounts { vars.numCollaterals = 3; TestDeployer.TroveManagerParams memory tmParams = - TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT); + 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/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/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 1f75b318e..7a770a6ee 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -226,7 +226,7 @@ contract TestDeployer is MetadataDeployment { Zappers memory zappers ) { - return deployAndConnectContracts(TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT)); + return deployAndConnectContracts(TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2)); } function deployAndConnectContracts(TroveManagerParams memory troveManagerParams) @@ -375,6 +375,7 @@ contract TestDeployer is MetadataDeployment { _troveManagerParams.CCR, _troveManagerParams.MCR, _troveManagerParams.SCR, + _troveManagerParams.debtLimit, _troveManagerParams.LIQUIDATION_PENALTY_SP, _troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION ); @@ -625,6 +626,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/liquidationsLST.t.sol b/contracts/test/liquidationsLST.t.sol index 8c26164a0..bed655884 100644 --- a/contracts/test/liquidationsLST.t.sol +++ b/contracts/test/liquidationsLST.t.sol @@ -27,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, MAX_INT)); + 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 33e550bba..33e81d177 100644 --- a/contracts/test/multicollateral.t.sol +++ b/contracts/test/multicollateral.t.sol @@ -69,10 +69,10 @@ contract MulticollateralTest is DevTestSetup { TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = new TestDeployer.TroveManagerParams[](NUM_COLLATERALS); - troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT); - troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); - troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); - troveManagerParamsArray[3] = TestDeployer.TroveManagerParams(160e16, 125e16, 125e16, 5e16, 10e16, MAX_INT); + 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/shutdown.t.sol b/contracts/test/shutdown.t.sol index 409e80f7e..11ea77a4f 100644 --- a/contracts/test/shutdown.t.sol +++ b/contracts/test/shutdown.t.sol @@ -29,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, MAX_INT); - troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); - troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); - troveManagerParamsArray[3] = TestDeployer.TroveManagerParams(160e16, 125e16, 125e16, 5e16, 10e16, MAX_INT); + 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 1e393dc85..c7a420e91 100644 --- a/contracts/test/troveNFT.t.sol +++ b/contracts/test/troveNFT.t.sol @@ -71,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, MAX_INT); - troveManagerParamsArray[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); - troveManagerParamsArray[2] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); + 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 08eca4860..48c66d532 100644 --- a/contracts/test/zapperGasComp.t.sol +++ b/contracts/test/zapperGasComp.t.sol @@ -28,8 +28,8 @@ contract ZapperGasCompTest is DevTestSetup { TestDeployer.TroveManagerParams[] memory troveManagerParams = new TestDeployer.TroveManagerParams[](2); uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; - troveManagerParams[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT); - troveManagerParams[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 120e16, 5e16, 10e16, MAX_INT); + 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 34e13daaf..b9d86f88c 100644 --- a/contracts/test/zapperLeverage.t.sol +++ b/contracts/test/zapperLeverage.t.sol @@ -128,9 +128,9 @@ contract ZapperLeverageMainnet is DevTestSetup { TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = new TestDeployer.TroveManagerParams[](NUM_COLLATERALS); - troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT); + 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, MAX_INT); + 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 0620a79a1..8c8e8e55f 100644 --- a/contracts/test/zapperWETH.t.sol +++ b/contracts/test/zapperWETH.t.sol @@ -29,7 +29,7 @@ contract ZapperWETHTest is DevTestSetup { uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; TestDeployer.TroveManagerParams[] memory troveManagerParams = new TestDeployer.TroveManagerParams[](1); - troveManagerParams[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT); + troveManagerParams[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16, MAX_INT/2); TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory contractsArray; From 7a020d49762fb6184c4ab12b6ff98f5bfe7cd25d Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Wed, 29 Jan 2025 23:23:00 -0500 Subject: [PATCH 10/26] add delegation of votesTokens --- contracts/src/ActivePool.sol | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) 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); + } + } From ca620774cf1df8b71f74cce8f3ba41b6ca470c94 Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:08:08 -0500 Subject: [PATCH 11/26] corrected price feeds --- contracts/src/PriceFeeds/ARBPriceFeed.sol | 0 contracts/src/PriceFeeds/COMPPriceFeed.sol | 0 contracts/src/PriceFeeds/UNIPriceFeed.sol | 0 contracts/src/PriceFeeds/WeETHPriceFeed.sol | 0 contracts/src/PriceFeeds/XVSPriceFeed.sol | 20 +++++++++++++++++++ contracts/src/PriceFeeds/tBTCPriceFeed.sol | 0 contracts/src/PriceFeeds/treeETHPriceFeed.sol | 0 7 files changed, 20 insertions(+) create mode 100644 contracts/src/PriceFeeds/ARBPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/COMPPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/UNIPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/WeETHPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/XVSPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/tBTCPriceFeed.sol create mode 100644 contracts/src/PriceFeeds/treeETHPriceFeed.sol diff --git a/contracts/src/PriceFeeds/ARBPriceFeed.sol b/contracts/src/PriceFeeds/ARBPriceFeed.sol new file mode 100644 index 000000000..e69de29bb diff --git a/contracts/src/PriceFeeds/COMPPriceFeed.sol b/contracts/src/PriceFeeds/COMPPriceFeed.sol new file mode 100644 index 000000000..e69de29bb diff --git a/contracts/src/PriceFeeds/UNIPriceFeed.sol b/contracts/src/PriceFeeds/UNIPriceFeed.sol new file mode 100644 index 000000000..e69de29bb diff --git a/contracts/src/PriceFeeds/WeETHPriceFeed.sol b/contracts/src/PriceFeeds/WeETHPriceFeed.sol new file mode 100644 index 000000000..e69de29bb diff --git a/contracts/src/PriceFeeds/XVSPriceFeed.sol b/contracts/src/PriceFeeds/XVSPriceFeed.sol new file mode 100644 index 000000000..b600e88c0 --- /dev/null +++ b/contracts/src/PriceFeeds/XVSPriceFeed.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./MainnetPriceFeedBase.sol"; + +contract XVSPriceFeed is MainnetPriceFeedBase { + constructor( + address _owner, + address _ethUsdOracleAddress, + uint256 _ethUsdStalenessThreshold + ) MainnetPriceFeedBase( + _owner, + _ethUsdOracleAddress, + _ethUsdStalenessThreshold) + { + + } +} + diff --git a/contracts/src/PriceFeeds/tBTCPriceFeed.sol b/contracts/src/PriceFeeds/tBTCPriceFeed.sol new file mode 100644 index 000000000..e69de29bb diff --git a/contracts/src/PriceFeeds/treeETHPriceFeed.sol b/contracts/src/PriceFeeds/treeETHPriceFeed.sol new file mode 100644 index 000000000..e69de29bb From 0a085bbfaa1552a377bfc3b31b55e7098233ab3e Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:22:32 -0500 Subject: [PATCH 12/26] update outline of feeds --- contracts/src/PriceFeeds/ARBPriceFeed.sol | 89 +++++++++++++++++++ contracts/src/PriceFeeds/COMPPriceFeed.sol | 89 +++++++++++++++++++ contracts/src/PriceFeeds/UNIPriceFeed.sol | 89 +++++++++++++++++++ contracts/src/PriceFeeds/WeETHPriceFeed.sol | 89 +++++++++++++++++++ contracts/src/PriceFeeds/XVSPriceFeed.sol | 71 ++++++++++++++- contracts/src/PriceFeeds/tBTCPriceFeed.sol | 89 +++++++++++++++++++ contracts/src/PriceFeeds/treeETHPriceFeed.sol | 89 +++++++++++++++++++ 7 files changed, 604 insertions(+), 1 deletion(-) diff --git a/contracts/src/PriceFeeds/ARBPriceFeed.sol b/contracts/src/PriceFeeds/ARBPriceFeed.sol index e69de29bb..bcf514096 100644 --- a/contracts/src/PriceFeeds/ARBPriceFeed.sol +++ b/contracts/src/PriceFeeds/ARBPriceFeed.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +interface IARBOracle { + function getExchangeRate() external view returns (uint256); +} + +import "./MainnetPriceFeedBase.sol"; + +contract ARBPriceFeed is MainnetPriceFeedBase { + constructor( + address _owner, + address _ethUsdOracleAddress, + uint256 _ethUsdStalenessThreshold + ) MainnetPriceFeedBase( + _owner, + _ethUsdOracleAddress, + _ethUsdStalenessThreshold) + { + + } + + Oracle public arbEthOracle; + + uint256 public constant ARB_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 arbEthPrice, bool arbEthOracleDown) = _getOracleAnswer(arbEthOracle); + + //If either the ETH-USD feed or the ARB-ETH oracle 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 (arbEthOracleDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(arbEthOracle.aggregator)), true); + } + + // Otherwise, use the primary price calculation: + + // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 arbUsdMarketPrice = ethUsdPrice * arbEthPrice / 1e18; + + // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 arbUsdCanonicalPrice = ethUsdPrice * arbPerEth / 1e18; + + uint256 arbUsdPrice; + + // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb + if ( + _isRedemption + && _withinDeviationThreshold(xvsUsdMarketPrice, xvsUsdCanonicalPrice, XVS_ETH_DEVIATION_THRESHOLD) + ) { + arbUsdPrice = LiquityMath._max(arbUsdMarketPrice, arbUsdCanonicalPrice); + } 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. + arbUsdPrice = LiquityMath._min(arbUsdMarketPrice, arbUsdCanonicalPrice); + } + + lastGoodPrice = arbUsdPrice; + + return (arbUsdPrice, false); + } + + function _getCanonicalRate() internal view override returns (uint256, bool) { + uint256 gasBefore = gasleft(); + + try IARBOracle(arbEthOracle.aggregator).getExchangeRate() returns (uint256 arbPerEth) { + //If rate is 0, return true + if (arbPerEth == 0) return (0, true); + + return (arbPerEth, 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/COMPPriceFeed.sol b/contracts/src/PriceFeeds/COMPPriceFeed.sol index e69de29bb..ce16aad1c 100644 --- a/contracts/src/PriceFeeds/COMPPriceFeed.sol +++ b/contracts/src/PriceFeeds/COMPPriceFeed.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +interface ICOMPOracle { + function getExchangeRate() external view returns (uint256); +} + +import "./MainnetPriceFeedBase.sol"; + +contract COMPPriceFeed is MainnetPriceFeedBase { + constructor( + address _owner, + address _ethUsdOracleAddress, + uint256 _ethUsdStalenessThreshold + ) MainnetPriceFeedBase( + _owner, + _ethUsdOracleAddress, + _ethUsdStalenessThreshold) + { + + } + + Oracle public compEthOracle; + + uint256 public constant COMP_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 compEthPrice, bool compEthOracleDown) = _getOracleAnswer(compEthOracle); + + //If either the ETH-USD feed or the COMP-ETH oracle 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 (compEthOracleDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(compEthOracle.aggregator)), true); + } + + // Otherwise, use the primary price calculation: + + // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 compUsdMarketPrice = ethUsdPrice * compEthPrice / 1e18; + + // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 compUsdCanonicalPrice = ethUsdPrice * compPerEth / 1e18; + + uint256 compUsdPrice; + + // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb + if ( + _isRedemption + && _withinDeviationThreshold(compUsdMarketPrice, compUsdCanonicalPrice, COMP_ETH_DEVIATION_THRESHOLD) + ) { + compUsdPrice = LiquityMath._max(compUsdMarketPrice, compUsdCanonicalPrice); + } 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. + compUsdPrice = LiquityMath._min(compUsdMarketPrice, compUsdCanonicalPrice); + } + + lastGoodPrice = compUsdPrice; + + return (compUsdPrice, false); + } + + function _getCanonicalRate() internal view override returns (uint256, bool) { + uint256 gasBefore = gasleft(); + + try ICOMPOracle(compEthOracle.aggregator).getExchangeRate() returns (uint256 compPerEth) { + //If rate is 0, return true + if (compPerEth == 0) return (0, true); + + return (compPerEth, 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/UNIPriceFeed.sol b/contracts/src/PriceFeeds/UNIPriceFeed.sol index e69de29bb..849d4a6b1 100644 --- a/contracts/src/PriceFeeds/UNIPriceFeed.sol +++ b/contracts/src/PriceFeeds/UNIPriceFeed.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +interface IUNIOracle { + function getExchangeRate() external view returns (uint256); +} + +import "./MainnetPriceFeedBase.sol"; + +contract UNIPriceFeed is MainnetPriceFeedBase { + constructor( + address _owner, + address _ethUsdOracleAddress, + uint256 _ethUsdStalenessThreshold + ) MainnetPriceFeedBase( + _owner, + _ethUsdOracleAddress, + _ethUsdStalenessThreshold) + { + + } + + Oracle public uniEthOracle; + + uint256 public constant UNI_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 uniEthPrice, bool uniEthOracleDown) = _getOracleAnswer(uniEthOracle); + + //If either the ETH-USD feed or the UNI-ETH oracle 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 (uniEthOracleDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(uniEthOracle.aggregator)), true); + } + + // Otherwise, use the primary price calculation: + + // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 uniUsdMarketPrice = ethUsdPrice * uniEthPrice / 1e18; + + // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 uniUsdCanonicalPrice = ethUsdPrice * uniPerEth / 1e18; + + uint256 uniUsdPrice; + + // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb + if ( + _isRedemption + && _withinDeviationThreshold(uniUsdMarketPrice, uniUsdCanonicalPrice, UNI_ETH_DEVIATION_THRESHOLD) + ) { + uniUsdPrice = LiquityMath._max(uniUsdMarketPrice, uniUsdCanonicalPrice); + } 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. + uniUsdPrice = LiquityMath._min(uniUsdMarketPrice, uniUsdCanonicalPrice); + } + + lastGoodPrice = uniUsdPrice; + + return (uniUsdPrice, false); + } + + function _getCanonicalRate() internal view override returns (uint256, bool) { + uint256 gasBefore = gasleft(); + + try IUNIOracle(uniEthOracle.aggregator).getExchangeRate() returns (uint256 uniPerEth) { + //If rate is 0, return true + if (uniPerEth == 0) return (0, true); + + return (uniPerEth, 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/WeETHPriceFeed.sol b/contracts/src/PriceFeeds/WeETHPriceFeed.sol index e69de29bb..cd11d1b54 100644 --- a/contracts/src/PriceFeeds/WeETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WeETHPriceFeed.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +interface IWeETHOracle { + function getExchangeRate() external view returns (uint256); +} + +import "./MainnetPriceFeedBase.sol"; + +contract WeETHPriceFeed is MainnetPriceFeedBase { + constructor( + address _owner, + address _ethUsdOracleAddress, + uint256 _ethUsdStalenessThreshold + ) MainnetPriceFeedBase( + _owner, + _ethUsdOracleAddress, + _ethUsdStalenessThreshold) + { + + } + + Oracle public weEthOracle; + + 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 weEthPrice, bool weEthOracleDown) = _getOracleAnswer(weEthOracle); + + //If either the ETH-USD feed or the weETH-ETH oracle 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 (weEthOracleDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(weEthOracle.aggregator)), true); + } + + // Otherwise, use the primary price calculation: + + // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 weEthUsdMarketPrice = ethUsdPrice * weEthPrice / 1e18; + + // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + 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(xvsUsdMarketPrice, xvsUsdCanonicalPrice, XVS_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 IWeETHOracle(weEthOracle.aggregator).getExchangeRate() returns (uint256 weEthPerEth) { + //If rate is 0, return true + if (weEthPerEth == 0) return (0, true); + + return (xvsPerEth, 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 index b600e88c0..4a18600ff 100644 --- a/contracts/src/PriceFeeds/XVSPriceFeed.sol +++ b/contracts/src/PriceFeeds/XVSPriceFeed.sol @@ -2,6 +2,10 @@ pragma solidity 0.8.24; +interface IXVSOracle { + function getExchangeRate() external view returns (uint256); +} + import "./MainnetPriceFeedBase.sol"; contract XVSPriceFeed is MainnetPriceFeedBase { @@ -14,7 +18,72 @@ contract XVSPriceFeed is MainnetPriceFeedBase { _ethUsdOracleAddress, _ethUsdStalenessThreshold) { - + + } + + Oracle public xvsEthOracle; + + uint256 public constant XVS_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 xvsEthPrice, bool xvsEthOracleDown) = _getOracleAnswer(xvsEthOracle); + + //If either the ETH-USD feed or the XVS-ETH oracle 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 (xvsEthOracleDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(xvsEthOracle.aggregator)), true); + } + + // Otherwise, use the primary price calculation: + + // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 xvsUsdMarketPrice = ethUsdPrice * xvsEthPrice / 1e18; + + // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 xvsUsdCanonicalPrice = ethUsdPrice * xvsPerEth / 1e18; + + uint256 xvsUsdPrice; + + // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb + if ( + _isRedemption + && _withinDeviationThreshold(xvsUsdMarketPrice, xvsUsdCanonicalPrice, XVS_ETH_DEVIATION_THRESHOLD) + ) { + xvsUsdPrice = LiquityMath._max(xvsUsdMarketPrice, xvsUsdCanonicalPrice); + } 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. + xvsUsdPrice = LiquityMath._min(xvsUsdMarketPrice, xvsUsdCanonicalPrice); + } + + lastGoodPrice = xvsUsdPrice; + + return (xvsUsdPrice, false); + } + + function _getCanonicalRate() internal view override returns (uint256, bool) { + uint256 gasBefore = gasleft(); + + try IXVSOracle(xvsEthOracle.aggregator).getExchangeRate() returns (uint256 xvsPerEth) { + //If rate is 0, return true + if (xvsPerEth == 0) return (0, true); + + return (xvsPerEth, 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/tBTCPriceFeed.sol b/contracts/src/PriceFeeds/tBTCPriceFeed.sol index e69de29bb..7b386609f 100644 --- a/contracts/src/PriceFeeds/tBTCPriceFeed.sol +++ b/contracts/src/PriceFeeds/tBTCPriceFeed.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +interface ItBTCOracle { + function getExchangeRate() external view returns (uint256); +} + +import "./MainnetPriceFeedBase.sol"; + +contract tBTCPriceFeed is MainnetPriceFeedBase { + constructor( + address _owner, + address _ethUsdOracleAddress, + uint256 _ethUsdStalenessThreshold + ) MainnetPriceFeedBase( + _owner, + _ethUsdOracleAddress, + _ethUsdStalenessThreshold) + { + + } + + Oracle public tBTCETHOracle; + + uint256 public constant tBTC_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 tBTCETHPrice, bool tBTCETHOracleDown) = _getOracleAnswer(tBTCETHOracle); + + //If either the ETH-USD feed or the tBTC-ETH oracle 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 (tBTCETHOracleDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(tBTCETHOracle.aggregator)), true); + } + + // Otherwise, use the primary price calculation: + + // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 tBTCUsdMarketPrice = ethUsdPrice * tBTCETHPrice / 1e18; + + // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 tBTCUsdCanonicalPrice = ethUsdPrice * tBTCPerEth / 1e18; + + uint256 tBTCUsdPrice; + + // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb + if ( + _isRedemption + && _withinDeviationThreshold(tBTCUsdMarketPrice, tBTCUsdCanonicalPrice, tBTC_ETH_DEVIATION_THRESHOLD) + ) { + tBTCUsdPrice = LiquityMath._max(tBTCUsdMarketPrice, tBTCUsdCanonicalPrice); + } 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. + tBTCUsdPrice = LiquityMath._min(tBTCUsdMarketPrice, tBTCUsdCanonicalPrice); + } + + lastGoodPrice = tBTCUsdPrice; + + return (tBTCUsdPrice, false); + } + + function _getCanonicalRate() internal view override returns (uint256, bool) { + uint256 gasBefore = gasleft(); + + try ItBTCOracle(tBTCETHOracle.aggregator).getExchangeRate() returns (uint256 tBTCPerEth) { + //If rate is 0, return true + if (tBTCPerEth == 0) return (0, true); + + return (tBTCPerEth, 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/treeETHPriceFeed.sol b/contracts/src/PriceFeeds/treeETHPriceFeed.sol index e69de29bb..bc455746c 100644 --- a/contracts/src/PriceFeeds/treeETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/treeETHPriceFeed.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +interface ITreeETHOracle { + function getExchangeRate() external view returns (uint256); +} + +import "./MainnetPriceFeedBase.sol"; + +contract TreeETHPriceFeed is MainnetPriceFeedBase { + constructor( + address _owner, + address _ethUsdOracleAddress, + uint256 _ethUsdStalenessThreshold + ) MainnetPriceFeedBase( + _owner, + _ethUsdOracleAddress, + _ethUsdStalenessThreshold) + { + + } + + Oracle public treeEthOracle; + + uint256 public constant TREE_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 treeEthPrice, bool treeEthOracleDown) = _getOracleAnswer(treeEthOracle); + + //If either the ETH-USD feed or the treeETH-ETH oracle 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 (treeEthOracleDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(treeEthOracle.aggregator)), true); + } + + // Otherwise, use the primary price calculation: + + // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 treeEthUsdMarketPrice = ethUsdPrice * treeEthPrice / 1e18; + + // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + 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(xvsUsdMarketPrice, xvsUsdCanonicalPrice, XVS_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 ITreeETHOracle(treeEthOracle.aggregator).getExchangeRate() returns (uint256 treeEthPerEth) { + //If rate is 0, return true + if (treeEthPerEth == 0) return (0, true); + + return (treeEthPerEth, 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); + } + } +} + + From d87de3696b4c70d8b89174a0e385bfa758ce5aa2 Mon Sep 17 00:00:00 2001 From: didi Date: Fri, 31 Jan 2025 21:38:52 +0100 Subject: [PATCH 13/26] validate the storage layout of BoldToken --- contracts/test/SFBold.t.sol | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/contracts/test/SFBold.t.sol b/contracts/test/SFBold.t.sol index 30fdbfa74..17eea1b74 100644 --- a/contracts/test/SFBold.t.sol +++ b/contracts/test/SFBold.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; import { BoldToken, IBoldToken } from "../src/BoldToken.sol"; import {Test} from "forge-std/Test.sol"; -import {ISuperToken} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.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 @@ -117,6 +117,11 @@ contract SFBold is Test { 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) @@ -133,4 +138,34 @@ contract SFBold is Test { 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 From a0dd79eb80471e6f7f412265b02edbd5f86a953c Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Sat, 1 Feb 2025 23:59:11 -0500 Subject: [PATCH 14/26] replace new col oracles --- .../src/Interfaces/ITreeETHPriceFeed.sol | 9 ++ contracts/src/Interfaces/ITreeETHToken.sol | 7 + contracts/src/Interfaces/IWeETHPriceFeed.sol | 9 ++ contracts/src/Interfaces/IWeETHToken.sol | 7 + contracts/src/PriceFeeds/ARBPriceFeed.sol | 102 +++++-------- contracts/src/PriceFeeds/COMPPriceFeed.sol | 103 +++++-------- .../src/PriceFeeds/TokenPriceFeedBase.sol | 136 ++++++++++++++++++ contracts/src/PriceFeeds/UNIPriceFeed.sol | 103 +++++-------- contracts/src/PriceFeeds/WeETHPriceFeed.sol | 89 +++++++----- contracts/src/PriceFeeds/XVSPriceFeed.sol | 105 +++++--------- contracts/src/PriceFeeds/tBTCPriceFeed.sol | 105 +++++--------- contracts/src/PriceFeeds/treeETHPriceFeed.sol | 86 ++++++----- 12 files changed, 430 insertions(+), 431 deletions(-) create mode 100644 contracts/src/Interfaces/ITreeETHPriceFeed.sol create mode 100644 contracts/src/Interfaces/ITreeETHToken.sol create mode 100644 contracts/src/Interfaces/IWeETHPriceFeed.sol create mode 100644 contracts/src/Interfaces/IWeETHToken.sol create mode 100644 contracts/src/PriceFeeds/TokenPriceFeedBase.sol 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/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 index bcf514096..1d1edb672 100644 --- a/contracts/src/PriceFeeds/ARBPriceFeed.sol +++ b/contracts/src/PriceFeeds/ARBPriceFeed.sol @@ -2,87 +2,49 @@ pragma solidity 0.8.24; -interface IARBOracle { - function getExchangeRate() external view returns (uint256); -} -import "./MainnetPriceFeedBase.sol"; +import "./TokenPriceFeedBase.sol"; -contract ARBPriceFeed is MainnetPriceFeedBase { - constructor( - address _owner, - address _ethUsdOracleAddress, - uint256 _ethUsdStalenessThreshold - ) MainnetPriceFeedBase( - _owner, - _ethUsdOracleAddress, - _ethUsdStalenessThreshold) - { +contract ARBPriceFeed is TokenPriceFeedBase { + constructor(address _owner, address _arbUsdOracleAddress, uint256 _arbUsdStalenessThreshold) + TokenPriceFeedBase(_owner, _arbUsdOracleAddress, _arbUsdStalenessThreshold) + { + _fetchPricePrimary(); - } - - Oracle public arbEthOracle; - - uint256 public constant ARB_ETH_DEVIATION_THRESHOLD = 2e16; // 2% - - function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { + // Check the oracle didn't already fail assert(priceSource == PriceSource.primary); - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - (uint256 arbEthPrice, bool arbEthOracleDown) = _getOracleAnswer(arbEthOracle); - - //If either the ETH-USD feed or the ARB-ETH oracle 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 (arbEthOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(arbEthOracle.aggregator)), true); - } - - // Otherwise, use the primary price calculation: - - // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 arbUsdMarketPrice = ethUsdPrice * arbEthPrice / 1e18; - - // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 arbUsdCanonicalPrice = ethUsdPrice * arbPerEth / 1e18; - - uint256 arbUsdPrice; + } - // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb - if ( - _isRedemption - && _withinDeviationThreshold(xvsUsdMarketPrice, xvsUsdCanonicalPrice, XVS_ETH_DEVIATION_THRESHOLD) - ) { - arbUsdPrice = LiquityMath._max(arbUsdMarketPrice, arbUsdCanonicalPrice); - } 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. - arbUsdPrice = LiquityMath._min(arbUsdMarketPrice, arbUsdCanonicalPrice); - } + 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(); - lastGoodPrice = arbUsdPrice; + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } - return (arbUsdPrice, false); - } + function fetchRedemptionPrice() external returns (uint256, bool) { + // Use same price for redemption as all other ops in ARB branch + return fetchPrice(); + } - function _getCanonicalRate() internal view override returns (uint256, bool) { - uint256 gasBefore = gasleft(); + // _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(); + } - try IARBOracle(arbEthOracle.aggregator).getExchangeRate() returns (uint256 arbPerEth) { - //If rate is 0, return true - if (arbPerEth == 0) return (0, true); + function _fetchPricePrimary() internal returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 tokenUsdPrice, bool tokenUsdOracleDown) = _getOracleAnswer(tokenUsdOracle); - return (arbPerEth, 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 the ARB-USD Chainlink response was invalid in this transaction, return the last good ARB-USD price calculated + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle)), true); - //If call to exchange rate reverts, return true - return (0, true); - } + lastGoodPrice = tokenUsdPrice; + return (tokenUsdPrice, false); } } diff --git a/contracts/src/PriceFeeds/COMPPriceFeed.sol b/contracts/src/PriceFeeds/COMPPriceFeed.sol index ce16aad1c..6a81070c8 100644 --- a/contracts/src/PriceFeeds/COMPPriceFeed.sol +++ b/contracts/src/PriceFeeds/COMPPriceFeed.sol @@ -2,88 +2,51 @@ pragma solidity 0.8.24; -interface ICOMPOracle { - function getExchangeRate() external view returns (uint256); -} -import "./MainnetPriceFeedBase.sol"; +import "./TokenPriceFeedBase.sol"; -contract COMPPriceFeed is MainnetPriceFeedBase { - constructor( - address _owner, - address _ethUsdOracleAddress, - uint256 _ethUsdStalenessThreshold - ) MainnetPriceFeedBase( - _owner, - _ethUsdOracleAddress, - _ethUsdStalenessThreshold) - { +contract COMPPriceFeed is TokenPriceFeedBase { + constructor(address _owner, address _compUsdOracleAddress, uint256 _compUsdStalenessThreshold) + TokenPriceFeedBase(_owner, _compUsdOracleAddress, _compUsdStalenessThreshold) + { + _fetchPricePrimary(); - } - - Oracle public compEthOracle; - - uint256 public constant COMP_ETH_DEVIATION_THRESHOLD = 2e16; // 2% - - function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { + // Check the oracle didn't already fail assert(priceSource == PriceSource.primary); - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - (uint256 compEthPrice, bool compEthOracleDown) = _getOracleAnswer(compEthOracle); - - //If either the ETH-USD feed or the COMP-ETH oracle 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 (compEthOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(compEthOracle.aggregator)), true); - } - - // Otherwise, use the primary price calculation: - - // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 compUsdMarketPrice = ethUsdPrice * compEthPrice / 1e18; - - // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 compUsdCanonicalPrice = ethUsdPrice * compPerEth / 1e18; + } - uint256 compUsdPrice; + 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(); - // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb - if ( - _isRedemption - && _withinDeviationThreshold(compUsdMarketPrice, compUsdCanonicalPrice, COMP_ETH_DEVIATION_THRESHOLD) - ) { - compUsdPrice = LiquityMath._max(compUsdMarketPrice, compUsdCanonicalPrice); - } 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. - compUsdPrice = LiquityMath._min(compUsdMarketPrice, compUsdCanonicalPrice); - } + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } - lastGoodPrice = compUsdPrice; - - return (compUsdPrice, false); - } + function fetchRedemptionPrice() external returns (uint256, bool) { + // Use same price for redemption as all other ops in COMP branch + return fetchPrice(); + } - function _getCanonicalRate() internal view override returns (uint256, bool) { - uint256 gasBefore = gasleft(); + // _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(); + } - try ICOMPOracle(compEthOracle.aggregator).getExchangeRate() returns (uint256 compPerEth) { - //If rate is 0, return true - if (compPerEth == 0) return (0, true); + function _fetchPricePrimary() internal returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 tokenUsdPrice, bool tokenUsdOracleDown) = _getOracleAnswer(tokenUsdOracle); - return (compPerEth, 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 the COMP-USD Chainlink response was invalid in this transaction, return the last good COMP-USD price calculated + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle)), true); - //If call to exchange rate reverts, return true - return (0, 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 index 849d4a6b1..6ce864331 100644 --- a/contracts/src/PriceFeeds/UNIPriceFeed.sol +++ b/contracts/src/PriceFeeds/UNIPriceFeed.sol @@ -2,87 +2,48 @@ pragma solidity 0.8.24; -interface IUNIOracle { - function getExchangeRate() external view returns (uint256); -} +import "./TokenPriceFeedBase.sol"; -import "./MainnetPriceFeedBase.sol"; +contract UNIPriceFeed is TokenPriceFeedBase { + constructor(address _owner, address _uniUsdOracleAddress, uint256 _uniUsdStalenessThreshold) + TokenPriceFeedBase(_owner, _uniUsdOracleAddress, _uniUsdStalenessThreshold) + { + _fetchPricePrimary(); -contract UNIPriceFeed is MainnetPriceFeedBase { - constructor( - address _owner, - address _ethUsdOracleAddress, - uint256 _ethUsdStalenessThreshold - ) MainnetPriceFeedBase( - _owner, - _ethUsdOracleAddress, - _ethUsdStalenessThreshold) - { - - } - - Oracle public uniEthOracle; - - uint256 public constant UNI_ETH_DEVIATION_THRESHOLD = 2e16; // 2% - - function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { + // Check the oracle didn't already fail assert(priceSource == PriceSource.primary); - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - (uint256 uniEthPrice, bool uniEthOracleDown) = _getOracleAnswer(uniEthOracle); - - //If either the ETH-USD feed or the UNI-ETH oracle 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 (uniEthOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(uniEthOracle.aggregator)), true); - } - - // Otherwise, use the primary price calculation: - - // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 uniUsdMarketPrice = ethUsdPrice * uniEthPrice / 1e18; - - // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 uniUsdCanonicalPrice = ethUsdPrice * uniPerEth / 1e18; - - uint256 uniUsdPrice; + } - // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb - if ( - _isRedemption - && _withinDeviationThreshold(uniUsdMarketPrice, uniUsdCanonicalPrice, UNI_ETH_DEVIATION_THRESHOLD) - ) { - uniUsdPrice = LiquityMath._max(uniUsdMarketPrice, uniUsdCanonicalPrice); - } 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. - uniUsdPrice = LiquityMath._min(uniUsdMarketPrice, uniUsdCanonicalPrice); - } + 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(); - lastGoodPrice = uniUsdPrice; + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } - return (uniUsdPrice, false); - } + function fetchRedemptionPrice() external returns (uint256, bool) { + // Use same price for redemption as all other ops in UNI branch + return fetchPrice(); + } - function _getCanonicalRate() internal view override returns (uint256, bool) { - uint256 gasBefore = gasleft(); + // _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(); + } - try IUNIOracle(uniEthOracle.aggregator).getExchangeRate() returns (uint256 uniPerEth) { - //If rate is 0, return true - if (uniPerEth == 0) return (0, true); + function _fetchPricePrimary() internal returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 tokenUsdPrice, bool tokenUsdOracleDown) = _getOracleAnswer(tokenUsdOracle); - return (uniPerEth, 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 the UNI-USD Chainlink response was invalid in this transaction, return the last good UNI-USD price calculated + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle)), true); - //If call to exchange rate reverts, return true - return (0, true); - } + lastGoodPrice = tokenUsdPrice; + return (tokenUsdPrice, false); } } diff --git a/contracts/src/PriceFeeds/WeETHPriceFeed.sol b/contracts/src/PriceFeeds/WeETHPriceFeed.sol index cd11d1b54..662ca974c 100644 --- a/contracts/src/PriceFeeds/WeETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WeETHPriceFeed.sol @@ -2,49 +2,60 @@ pragma solidity 0.8.24; -interface IWeETHOracle { - function getExchangeRate() external view returns (uint256); -} - -import "./MainnetPriceFeedBase.sol"; - -contract WeETHPriceFeed is MainnetPriceFeedBase { - constructor( - address _owner, - address _ethUsdOracleAddress, - uint256 _ethUsdStalenessThreshold - ) MainnetPriceFeedBase( - _owner, - _ethUsdOracleAddress, - _ethUsdStalenessThreshold) - { +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; - Oracle public weEthOracle; - 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 weEthPrice, bool weEthOracleDown) = _getOracleAnswer(weEthOracle); + (uint256 weEthEthPrice, bool weEthEthOracleDown) = _getOracleAnswer(weEthEthOracle); + (uint256 weEthPerEth, bool exchangeRateIsDown) = _getCanonicalRate(); - //If either the ETH-USD feed or the weETH-ETH oracle 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 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 (weEthOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(weEthOracle.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 RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 weEthUsdMarketPrice = ethUsdPrice * weEthPrice / 1e18; + // 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_RETH = USD_per_ETH * ETH_per_RETH + // Calculate the canonical LST-USD price: USD_per_WeETH = USD_per_ETH * ETH_per_WeETH uint256 weEthUsdCanonicalPrice = ethUsdPrice * weEthPerEth / 1e18; uint256 weEthUsdPrice; @@ -52,9 +63,9 @@ contract WeETHPriceFeed is MainnetPriceFeedBase { // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb if ( _isRedemption - && _withinDeviationThreshold(xvsUsdMarketPrice, xvsUsdCanonicalPrice, XVS_ETH_DEVIATION_THRESHOLD) + && _withinDeviationThreshold(weEthUsdMarketPrice, weEthUsdCanonicalPrice, WEETH_ETH_DEVIATION_THRESHOLD) ) { - weEthUsdPrice = LiquityMath._max(weEthUsdMarketPrice, weEthUsdCanonicalPrice); + 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. @@ -64,23 +75,23 @@ contract WeETHPriceFeed is MainnetPriceFeedBase { lastGoodPrice = weEthUsdPrice; return (weEthUsdPrice, false); - } + } - function _getCanonicalRate() internal view override returns (uint256, bool) { + function _getCanonicalRate() internal view override returns (uint256, bool) { uint256 gasBefore = gasleft(); - try IWeETHOracle(weEthOracle.aggregator).getExchangeRate() returns (uint256 weEthPerEth) { - //If rate is 0, return true - if (weEthPerEth == 0) return (0, true); + try IWeETHToken(rateProviderAddress).getExchangeRate() returns (uint256 ethPerWeEth) { + // If rate is 0, return true + if (ethPerWeEth == 0) return (0, true); - return (xvsPerEth, false); + 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. + // 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 + // 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 index 4a18600ff..ae3f2df14 100644 --- a/contracts/src/PriceFeeds/XVSPriceFeed.sol +++ b/contracts/src/PriceFeeds/XVSPriceFeed.sol @@ -2,88 +2,49 @@ pragma solidity 0.8.24; -interface IXVSOracle { - function getExchangeRate() external view returns (uint256); -} +import "./TokenPriceFeedBase.sol"; -import "./MainnetPriceFeedBase.sol"; +contract XVSPriceFeed is TokenPriceFeedBase { + constructor(address _owner, address _xvsUsdOracleAddress, uint256 _xvsUsdStalenessThreshold) + TokenPriceFeedBase(_owner, _xvsUsdOracleAddress, _xvsUsdStalenessThreshold) + { + _fetchPricePrimary(); -contract XVSPriceFeed is MainnetPriceFeedBase { - constructor( - address _owner, - address _ethUsdOracleAddress, - uint256 _ethUsdStalenessThreshold - ) MainnetPriceFeedBase( - _owner, - _ethUsdOracleAddress, - _ethUsdStalenessThreshold) - { - - } - - Oracle public xvsEthOracle; - - uint256 public constant XVS_ETH_DEVIATION_THRESHOLD = 2e16; // 2% - - function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { + // Check the oracle didn't already fail assert(priceSource == PriceSource.primary); - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - (uint256 xvsEthPrice, bool xvsEthOracleDown) = _getOracleAnswer(xvsEthOracle); - - //If either the ETH-USD feed or the XVS-ETH oracle 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 (xvsEthOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(xvsEthOracle.aggregator)), true); - } - - // Otherwise, use the primary price calculation: - - // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 xvsUsdMarketPrice = ethUsdPrice * xvsEthPrice / 1e18; - - // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 xvsUsdCanonicalPrice = ethUsdPrice * xvsPerEth / 1e18; - - uint256 xvsUsdPrice; + } - // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb - if ( - _isRedemption - && _withinDeviationThreshold(xvsUsdMarketPrice, xvsUsdCanonicalPrice, XVS_ETH_DEVIATION_THRESHOLD) - ) { - xvsUsdPrice = LiquityMath._max(xvsUsdMarketPrice, xvsUsdCanonicalPrice); - } 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. - xvsUsdPrice = LiquityMath._min(xvsUsdMarketPrice, xvsUsdCanonicalPrice); - } + 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(); - lastGoodPrice = xvsUsdPrice; + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } - return (xvsUsdPrice, false); - } + function fetchRedemptionPrice() external returns (uint256, bool) { + // Use same price for redemption as all other ops in XVS branch + return fetchPrice(); + } - function _getCanonicalRate() internal view override returns (uint256, bool) { - uint256 gasBefore = gasleft(); + // _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(); + } - try IXVSOracle(xvsEthOracle.aggregator).getExchangeRate() returns (uint256 xvsPerEth) { - //If rate is 0, return true - if (xvsPerEth == 0) return (0, true); + function _fetchPricePrimary() internal returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 tokenUsdPrice, bool tokenUsdOracleDown) = _getOracleAnswer(tokenUsdOracle); - return (xvsPerEth, 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 the XVS-USD Chainlink response was invalid in this transaction, return the last good XVS-USD price calculated + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle)), true); - //If call to exchange rate reverts, return true - return (0, true); - } - } + lastGoodPrice = tokenUsdPrice; + return (tokenUsdPrice, false); + } } diff --git a/contracts/src/PriceFeeds/tBTCPriceFeed.sol b/contracts/src/PriceFeeds/tBTCPriceFeed.sol index 7b386609f..0a5473250 100644 --- a/contracts/src/PriceFeeds/tBTCPriceFeed.sol +++ b/contracts/src/PriceFeeds/tBTCPriceFeed.sol @@ -1,88 +1,49 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; + +import "./TokenPriceFeedBase.sol"; -interface ItBTCOracle { - function getExchangeRate() external view returns (uint256); -} +contract tBTCPriceFeed is TokenPriceFeedBase { + constructor(address _owner, address _tBTCUsdOracleAddress, uint256 _tBTCUsdStalenessThreshold) + TokenPriceFeedBase(_owner, _tBTCUsdOracleAddress, _tBTCUsdStalenessThreshold) + { + _fetchPricePrimary(); -import "./MainnetPriceFeedBase.sol"; - -contract tBTCPriceFeed is MainnetPriceFeedBase { - constructor( - address _owner, - address _ethUsdOracleAddress, - uint256 _ethUsdStalenessThreshold - ) MainnetPriceFeedBase( - _owner, - _ethUsdOracleAddress, - _ethUsdStalenessThreshold) - { - - } - - Oracle public tBTCETHOracle; - - uint256 public constant tBTC_ETH_DEVIATION_THRESHOLD = 2e16; // 2% - - function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { + // Check the oracle didn't already fail assert(priceSource == PriceSource.primary); - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - (uint256 tBTCETHPrice, bool tBTCETHOracleDown) = _getOracleAnswer(tBTCETHOracle); - - //If either the ETH-USD feed or the tBTC-ETH oracle 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 (tBTCETHOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(tBTCETHOracle.aggregator)), true); - } - - // Otherwise, use the primary price calculation: - - // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 tBTCUsdMarketPrice = ethUsdPrice * tBTCETHPrice / 1e18; - - // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 tBTCUsdCanonicalPrice = ethUsdPrice * tBTCPerEth / 1e18; - - uint256 tBTCUsdPrice; + } - // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb - if ( - _isRedemption - && _withinDeviationThreshold(tBTCUsdMarketPrice, tBTCUsdCanonicalPrice, tBTC_ETH_DEVIATION_THRESHOLD) - ) { - tBTCUsdPrice = LiquityMath._max(tBTCUsdMarketPrice, tBTCUsdCanonicalPrice); - } 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. - tBTCUsdPrice = LiquityMath._min(tBTCUsdMarketPrice, tBTCUsdCanonicalPrice); - } + 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(); - lastGoodPrice = tBTCUsdPrice; + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } - return (tBTCUsdPrice, false); - } + function fetchRedemptionPrice() external returns (uint256, bool) { + // Use same price for redemption as all other ops in tBTC branch + return fetchPrice(); + } - function _getCanonicalRate() internal view override returns (uint256, bool) { - uint256 gasBefore = gasleft(); + // _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(); + } - try ItBTCOracle(tBTCETHOracle.aggregator).getExchangeRate() returns (uint256 tBTCPerEth) { - //If rate is 0, return true - if (tBTCPerEth == 0) return (0, true); + function _fetchPricePrimary() internal returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 tokenUsdPrice, bool tokenUsdOracleDown) = _getOracleAnswer(tokenUsdOracle); - return (tBTCPerEth, 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 the tBTC-USD Chainlink response was invalid in this transaction, return the last good tBTC-USD price calculated + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle)), true); - //If call to exchange rate reverts, return true - return (0, true); - } + lastGoodPrice = tokenUsdPrice; + return (tokenUsdPrice, false); } } diff --git a/contracts/src/PriceFeeds/treeETHPriceFeed.sol b/contracts/src/PriceFeeds/treeETHPriceFeed.sol index bc455746c..5d53dd569 100644 --- a/contracts/src/PriceFeeds/treeETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/treeETHPriceFeed.sol @@ -2,49 +2,61 @@ pragma solidity 0.8.24; -interface ITreeETHOracle { - function getExchangeRate() external view returns (uint256); -} -import "./MainnetPriceFeedBase.sol"; +import "./CompositePriceFeed.sol"; +import "../Interfaces/ITreeETHToken.sol"; +import "../Interfaces/ITreeETHPriceFeed.sol"; -contract TreeETHPriceFeed is MainnetPriceFeedBase { + +contract TreeETHPriceFeed is CompositePriceFeed, ITreeETHPriceFeed { constructor( - address _owner, - address _ethUsdOracleAddress, - uint256 _ethUsdStalenessThreshold - ) MainnetPriceFeedBase( - _owner, - _ethUsdOracleAddress, - _ethUsdStalenessThreshold) - { + 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; - Oracle public treeEthOracle; - - uint256 public constant TREE_ETH_DEVIATION_THRESHOLD = 2e16; // 2% + 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 treeEthPrice, bool treeEthOracleDown) = _getOracleAnswer(treeEthOracle); + (uint256 treeEthEthPrice, bool treeEthEthOracleDown) = _getOracleAnswer(treeEthEthOracle); + (uint256 treeEthPerEth, bool exchangeRateIsDown) = _getCanonicalRate(); - //If either the ETH-USD feed or the treeETH-ETH oracle 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 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 (treeEthOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(treeEthOracle.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 RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 treeEthUsdMarketPrice = ethUsdPrice * treeEthPrice / 1e18; + // 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_RETH = USD_per_ETH * ETH_per_RETH + // Calculate the canonical LST-USD price: USD_per_TreeETH = USD_per_ETH * ETH_per_TreeETH uint256 treeEthUsdCanonicalPrice = ethUsdPrice * treeEthPerEth / 1e18; uint256 treeEthUsdPrice; @@ -52,9 +64,9 @@ contract TreeETHPriceFeed is MainnetPriceFeedBase { // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb if ( _isRedemption - && _withinDeviationThreshold(xvsUsdMarketPrice, xvsUsdCanonicalPrice, XVS_ETH_DEVIATION_THRESHOLD) + && _withinDeviationThreshold(treeEthUsdMarketPrice, treeEthUsdCanonicalPrice, TREEETH_ETH_DEVIATION_THRESHOLD) ) { - treeEthUsdPrice = LiquityMath._max(treeEthUsdMarketPrice, treeEthUsdCanonicalPrice); + 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. @@ -64,23 +76,23 @@ contract TreeETHPriceFeed is MainnetPriceFeedBase { lastGoodPrice = treeEthUsdPrice; return (treeEthUsdPrice, false); - } + } - function _getCanonicalRate() internal view override returns (uint256, bool) { + function _getCanonicalRate() internal view override returns (uint256, bool) { uint256 gasBefore = gasleft(); - try ITreeETHOracle(treeEthOracle.aggregator).getExchangeRate() returns (uint256 treeEthPerEth) { - //If rate is 0, return true - if (treeEthPerEth == 0) return (0, true); + try ITreeETHToken(rateProviderAddress).getExchangeRate() returns (uint256 ethPerTreeEth) { + // If rate is 0, return true + if (ethPerTreeEth == 0) return (0, true); - return (treeEthPerEth, false); + 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. + // 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 + // If call to exchange rate reverts, return true return (0, true); } } From a6b723bb1c0e3c2f4424b01e773a1c683ed76cae Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Sun, 2 Feb 2025 00:15:35 -0500 Subject: [PATCH 15/26] add aggregator --- contracts/src/PriceFeeds/ARBPriceFeed.sol | 2 +- contracts/src/PriceFeeds/COMPPriceFeed.sol | 2 +- contracts/src/PriceFeeds/UNIPriceFeed.sol | 2 +- contracts/src/PriceFeeds/XVSPriceFeed.sol | 2 +- contracts/src/PriceFeeds/tBTCPriceFeed.sol | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/src/PriceFeeds/ARBPriceFeed.sol b/contracts/src/PriceFeeds/ARBPriceFeed.sol index 1d1edb672..063232f64 100644 --- a/contracts/src/PriceFeeds/ARBPriceFeed.sol +++ b/contracts/src/PriceFeeds/ARBPriceFeed.sol @@ -41,7 +41,7 @@ contract ARBPriceFeed is TokenPriceFeedBase { (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)), true); + 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 index 6a81070c8..a3b87cdf9 100644 --- a/contracts/src/PriceFeeds/COMPPriceFeed.sol +++ b/contracts/src/PriceFeeds/COMPPriceFeed.sol @@ -41,7 +41,7 @@ contract COMPPriceFeed is TokenPriceFeedBase { (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)), true); + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle.aggregator)), true); lastGoodPrice = tokenUsdPrice; return (tokenUsdPrice, false); diff --git a/contracts/src/PriceFeeds/UNIPriceFeed.sol b/contracts/src/PriceFeeds/UNIPriceFeed.sol index 6ce864331..befb807c6 100644 --- a/contracts/src/PriceFeeds/UNIPriceFeed.sol +++ b/contracts/src/PriceFeeds/UNIPriceFeed.sol @@ -40,7 +40,7 @@ contract UNIPriceFeed is TokenPriceFeedBase { (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)), true); + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle.aggregator)), true); lastGoodPrice = tokenUsdPrice; return (tokenUsdPrice, false); diff --git a/contracts/src/PriceFeeds/XVSPriceFeed.sol b/contracts/src/PriceFeeds/XVSPriceFeed.sol index ae3f2df14..8769973f3 100644 --- a/contracts/src/PriceFeeds/XVSPriceFeed.sol +++ b/contracts/src/PriceFeeds/XVSPriceFeed.sol @@ -40,7 +40,7 @@ contract XVSPriceFeed is TokenPriceFeedBase { (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)), true); + 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 index 0a5473250..85ef274be 100644 --- a/contracts/src/PriceFeeds/tBTCPriceFeed.sol +++ b/contracts/src/PriceFeeds/tBTCPriceFeed.sol @@ -40,7 +40,7 @@ contract tBTCPriceFeed is TokenPriceFeedBase { (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)), true); + if (tokenUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(tokenUsdOracle.aggregator)), true); lastGoodPrice = tokenUsdPrice; return (tokenUsdPrice, false); From 8a3e4afc1060876a69c3ee8040e533d6a7a17cc3 Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 14 Feb 2025 18:14:25 +0100 Subject: [PATCH 16/26] feat: invariant testing --- contracts/echidna.yaml | 12 + contracts/medusa.json | 86 ++++ .../TestContracts/TroveManagerTester.t.sol | 23 +- contracts/test/recon/BeforeAfter.sol | 99 ++++ contracts/test/recon/CryticTester.sol | 13 + contracts/test/recon/CryticToFoundry.sol | 24 + contracts/test/recon/Properties.sol | 257 ++++++++++ contracts/test/recon/Setup.sol | 479 ++++++++++++++++++ contracts/test/recon/TargetFunctions.sol | 37 ++ contracts/test/recon/helpers/RevertHelper.sol | 71 +++ .../test/recon/managers/ActorManager.sol | 108 ++++ .../test/recon/managers/AssetManager.sol | 113 +++++ contracts/test/recon/mocks/MockERC20.sol | 22 + .../test/recon/targets/ActivePoolTargets.sol | 45 ++ .../targets/BorrowerOperationsTargets.sol | 209 ++++++++ .../test/recon/targets/CollTokenTargets.sol | 36 ++ .../targets/CollateralRegistryTargets.sol | 21 + .../test/recon/targets/ManagersTargets.sol | 55 ++ .../test/recon/targets/PriceFeedTargets.sol | 27 + .../test/recon/targets/SampleTargets.sol | 13 + .../recon/targets/StabilityPoolTargets.sol | 46 ++ .../recon/targets/TroveManagerTargets.sol | 121 +++++ 22 files changed, 1916 insertions(+), 1 deletion(-) create mode 100644 contracts/echidna.yaml create mode 100644 contracts/medusa.json create mode 100644 contracts/test/recon/BeforeAfter.sol create mode 100644 contracts/test/recon/CryticTester.sol create mode 100644 contracts/test/recon/CryticToFoundry.sol create mode 100644 contracts/test/recon/Properties.sol create mode 100644 contracts/test/recon/Setup.sol create mode 100644 contracts/test/recon/TargetFunctions.sol create mode 100644 contracts/test/recon/helpers/RevertHelper.sol create mode 100644 contracts/test/recon/managers/ActorManager.sol create mode 100644 contracts/test/recon/managers/AssetManager.sol create mode 100644 contracts/test/recon/mocks/MockERC20.sol create mode 100644 contracts/test/recon/targets/ActivePoolTargets.sol create mode 100644 contracts/test/recon/targets/BorrowerOperationsTargets.sol create mode 100644 contracts/test/recon/targets/CollTokenTargets.sol create mode 100644 contracts/test/recon/targets/CollateralRegistryTargets.sol create mode 100644 contracts/test/recon/targets/ManagersTargets.sol create mode 100644 contracts/test/recon/targets/PriceFeedTargets.sol create mode 100644 contracts/test/recon/targets/SampleTargets.sol create mode 100644 contracts/test/recon/targets/StabilityPoolTargets.sol create mode 100644 contracts/test/recon/targets/TroveManagerTargets.sol diff --git a/contracts/echidna.yaml b/contracts/echidna.yaml new file mode 100644 index 000000000..448676230 --- /dev/null +++ b/contracts/echidna.yaml @@ -0,0 +1,12 @@ + +testMode: "assertion" +prefix: "optimize_" +coverage: true +corpusDir: "echidna" +balanceAddr: 0x1043561a8829300000 +balanceContract: 0x1043561a8829300000 +filterFunctions: [] +cryticArgs: ["--foundry-compile-all"] +deployer: "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496" +contractAddr: "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496" +shrinkLimit: 100000 \ No newline at end of file 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/test/TestContracts/TroveManagerTester.t.sol b/contracts/test/TestContracts/TroveManagerTester.t.sol index 3e82745f9..444b192e5 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,12 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { return trove.debt; } + function getbatchDebtAndShares(address batchAddress) external view returns (uint256, uint256) { + Batch memory batch = batches[batchAddress]; + + return (batch.debt, batch.totalDebtShares); + } + function getTroveWeightedRecordedDebt(uint256 _troveId) external view returns (uint256) { Trove memory trove = Troves[_troveId]; address batchAddress = _getBatchManager(trove); @@ -337,6 +348,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)) { + batches[batchAddress].annualInterestRate; + } + + return Troves[troveId].annualInterestRate; + } + function getBatchAnnualInterestRate(address _batchAddress) external view returns (uint256) { return batches[_batchAddress].annualInterestRate; } @@ -372,4 +393,4 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { batch.totalDebtShares ); } -} +} \ No newline at end of file diff --git a/contracts/test/recon/BeforeAfter.sol b/contracts/test/recon/BeforeAfter.sol new file mode 100644 index 000000000..669a27aaf --- /dev/null +++ b/contracts/test/recon/BeforeAfter.sol @@ -0,0 +1,99 @@ +// 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; + } + } +} 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..cc04cf20a --- /dev/null +++ b/contracts/test/recon/CryticToFoundry.sol @@ -0,0 +1,24 @@ +// 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"; + +// forge test --match-contract CryticToFoundry -vv +contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { + function setUp() public { + 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(); + } +} diff --git a/contracts/test/recon/Properties.sol b/contracts/test/recon/Properties.sol new file mode 100644 index 000000000..da8e68773 --- /dev/null +++ b/contracts/test/recon/Properties.sol @@ -0,0 +1,257 @@ +// 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 { + 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..5ea9f2e4d --- /dev/null +++ b/contracts/test/recon/Setup.sol @@ -0,0 +1,479 @@ +// 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"; + +// TODO: We need the whole system + + +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; + + 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)); + + // === Before === /// + // Bold and interst router + interestRouter = new InterestRouter(); + boldToken = boldToken = IBoldToken(address(new BoldToken{salt: SALT}(address(this), ISuperTokenFactory(factory)))); + + // Temp branch and then push? + + 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..78463032b --- /dev/null +++ b/contracts/test/recon/TargetFunctions.sol @@ -0,0 +1,37 @@ + +// 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 {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, + 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..ef6dcdcfd --- /dev/null +++ b/contracts/test/recon/targets/BorrowerOperationsTargets.sol @@ -0,0 +1,209 @@ + +// 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"; + +abstract contract BorrowerOperationsTargets is BaseTargetFunctions, Properties { + + function borrowerOperations_addColl(uint256 _troveId, uint256 _collAmount) public asActor { + borrowerOperations.addColl(_troveId, _collAmount); + } + + 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 asActor { + borrowerOperations.adjustTrove(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _maxUpfrontFee); + } + + 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.getTroveDebt(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 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 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.getTroveDebt(clampedTroveId) + 1); + } + borrowerOperations_adjustZombieTrove(clampedTroveId, collChange, _isCollIncrease, boldChange, _isDebtIncrease, _upperHint, _lowerHint, type(uint256).max); + } + + + function borrowerOperations_applyPendingDebt(uint256 _troveId, uint256 _lowerHint, uint256 _upperHint) public asActor { + borrowerOperations.applyPendingDebt(_troveId, _lowerHint, _upperHint); + } + + + function borrowerOperations_applyPendingDebt(uint256 _troveId) public asActor { + borrowerOperations.applyPendingDebt(_troveId); + } + + function borrowerOperations_applyPendingDebt_clamped() public { + borrowerOperations_applyPendingDebt(clampedTroveId); + } + + function borrowerOperations_claimCollateral() public asActor { + borrowerOperations.claimCollateral(); + } + + + function borrowerOperations_closeTrove(uint256 _troveId) public asActor { + borrowerOperations.closeTrove(_troveId); + } + + function borrowerOperations_closeTrove_clamped() public { + borrowerOperations_closeTrove(clampedTroveId); + } + + + function borrowerOperations_lowerBatchManagementFee(uint256 _newAnnualManagementFee) public asActor { + borrowerOperations.lowerBatchManagementFee(_newAnnualManagementFee); + } + + function borrowerOperations_onLiquidateTrove(uint256 _troveId) public 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 returns (uint256) { + uint256 troveId = borrowerOperations.openTrove(_owner, _ownerIndex, _collAmount, _boldAmount, _upperHint, _lowerHint, _annualInterestRate, _maxUpfrontFee, _addManager, _removeManager, _receiver); + clampedTroveId = 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 asActor { + borrowerOperations.openTroveAndJoinInterestBatchManager(_params); + } + + function borrowerOperations_registerBatchManager(uint128 _minInterestRate, uint128 _maxInterestRate, uint128 _currentInterestRate, uint128 _annualManagementFee, uint128 _minInterestRateChangePeriod) public asActor { + borrowerOperations.registerBatchManager(_minInterestRate, _maxInterestRate, _currentInterestRate, _annualManagementFee, _minInterestRateChangePeriod); + } + + function borrowerOperations_removeFromBatch(uint256 _troveId, uint256 _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public asActor { + borrowerOperations.removeFromBatch(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee); + } + + function borrowerOperations_removeInterestIndividualDelegate(uint256 _troveId) public asActor { + borrowerOperations.removeInterestIndividualDelegate(_troveId); + } + + + function borrowerOperations_repayBold(uint256 _troveId, uint256 _boldAmount) public asActor { + borrowerOperations.repayBold(_troveId, _boldAmount); + } + + function borrowerOperations_repayBold_clamped(uint88 _boldAmount) public asActor { + uint256 amt = _boldAmount % (troveManager.getTroveDebt(clampedTroveId) + 1); + // TODO: Should use max as the max debt + borrowerOperations_repayBold(clampedTroveId, amt); + } + + + function borrowerOperations_setAddManager(uint256 _troveId, address _manager) public asActor { + borrowerOperations.setAddManager(_troveId, _manager); + } + + function borrowerOperations_setBatchManagerAnnualInterestRate(uint128 _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public asActor { + borrowerOperations.setBatchManagerAnnualInterestRate(_newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee); + } + + function borrowerOperations_setInterestBatchManager(uint256 _troveId, address _newBatchManager, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public asActor { + borrowerOperations.setInterestBatchManager(_troveId, _newBatchManager, _upperHint, _lowerHint, _maxUpfrontFee); + } + + function borrowerOperations_setInterestIndividualDelegate(uint256 _troveId, address _delegate, uint128 _minInterestRate, uint128 _maxInterestRate, uint256 _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee, uint256 _minInterestRateChangePeriod) public asActor { + borrowerOperations.setInterestIndividualDelegate(_troveId, _delegate, _minInterestRate, _maxInterestRate, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee, _minInterestRateChangePeriod); + } + + function borrowerOperations_setRemoveManager(uint256 _troveId, address _manager) public asActor { + borrowerOperations.setRemoveManager(_troveId, _manager); + } + + function borrowerOperations_setRemoveManagerWithReceiver(uint256 _troveId, address _manager, address _receiver) public asActor { + borrowerOperations.setRemoveManagerWithReceiver(_troveId, _manager, _receiver); + } + + function borrowerOperations_shutdown() public asActor { + borrowerOperations.shutdown(); + } + + function borrowerOperations_shutdownFromOracleFailure() public asActor { + borrowerOperations.shutdownFromOracleFailure(); + } + + // === Switch Batch Manager === // + + function borrowerOperations_switchBatchManager(uint256 _troveId, uint256 _removeUpperHint, uint256 _removeLowerHint, address _newBatchManager, uint256 _addUpperHint, uint256 _addLowerHint, uint256 _maxUpfrontFee) public asActor { + borrowerOperations.switchBatchManager(_troveId, _removeUpperHint, _removeLowerHint, _newBatchManager, _addUpperHint, _addLowerHint, _maxUpfrontFee); + } + + + // === Withdraw Bold === // + function borrowerOperations_withdrawBold(uint256 _troveId, uint256 _boldAmount, uint256 _maxUpfrontFee) public asActor { + borrowerOperations.withdrawBold(_troveId, _boldAmount, _maxUpfrontFee); + } + + function borrowerOperations_withdrawBold_clamped(uint88 _boldAmount) public { + borrowerOperations_withdrawBold(clampedTroveId, _boldAmount, type(uint256).max); + } + + + // === Withdraw Coll === // + function borrowerOperations_withdrawColl(uint256 _troveId, uint256 _collWithdrawal) public asActor { + borrowerOperations.withdrawColl(_troveId, _collWithdrawal); + } + + 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..babd6584d --- /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 asActor { + collToken.approve(spender, amount); + } + + // function collToken_decreaseAllowance(address spender, uint256 subtractedValue) public asActor { + // collToken.decreaseAllowance(spender, subtractedValue); + // } + + // function collToken_increaseAllowance(address spender, uint256 addedValue) public asActor { + // collToken.increaseAllowance(spender, addedValue); + // } + + function collToken_mint(address to, uint256 amt) public asActor { + collToken.mint(to, amt); + } + + function collToken_transfer(address to, uint256 amount) public asActor { + collToken.transfer(to, amount); + } + + function collToken_transferFrom(address from, address to, uint256 amount) public 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..4577f5b7f --- /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 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/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..a25ad660b --- /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 asActor { + stabilityPool.claimAllCollGains(); + } + + + function stabilityPool_provideToSP(uint256 _topUp, bool _doClaim) public 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 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 asActor { + stabilityPool.offset(_debtToOffset, _collToAdd); + } + + function stabilityPool_triggerBoldRewards(uint256 _boldYield) public 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..e1bf7ee0f --- /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 asActor { + troveManager.batchLiquidateTroves(_troveArray); + } + + + function troveManager_liquidate(uint256 _troveId) public 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 asActor { + troveManager.urgentRedemption(_boldAmount, _troveIds, _minCollateral); + } + + + function troveManager_urgentRedemption_clamped(uint256 _boldAmount, uint256[] memory _troveIds, uint256 _minCollateral) 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 asActor { + // troveManager.callInternalRemoveTroveId(_troveId); + // } + + // function troveManager_getUnbackedPortionPriceAndRedeemability() public asActor { + // troveManager.getUnbackedPortionPriceAndRedeemability(); + // } + + + // function troveManager_onAdjustTrove(uint256 _troveId, uint256 _newColl, uint256 _newDebt, TroveChange memory _troveChange) public 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 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 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 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 asActor { + // troveManager.onCloseTrove(_troveId, _troveChange, _batchAddress, _newBatchColl, _newBatchDebt); + // } + + // function troveManager_onLowerBatchManagerAnnualFee(address _batchAddress, uint256 _newColl, uint256 _newDebt, uint256 _newAnnualManagementFee) public asActor { + // troveManager.onLowerBatchManagerAnnualFee(_batchAddress, _newColl, _newDebt, _newAnnualManagementFee); + // } + + // function troveManager_onOpenTrove(address _owner, uint256 _troveId, TroveChange memory _troveChange, uint256 _annualInterestRate) public asActor { + // troveManager.onOpenTrove(_owner, _troveId, _troveChange, _annualInterestRate); + // } + + // function troveManager_onOpenTroveAndJoinBatch(address _owner, uint256 _troveId, TroveChange memory _troveChange, address _batchAddress, uint256 _batchColl, uint256 _batchDebt) public asActor { + // troveManager.onOpenTroveAndJoinBatch(_owner, _troveId, _troveChange, _batchAddress, _batchColl, _batchDebt); + // } + + // function troveManager_onRegisterBatchManager(address _account, uint256 _annualInterestRate, uint256 _annualManagementFee) public 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 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 asActor { + // troveManager.onSetBatchManagerAnnualInterestRate(_batchAddress, _newColl, _newDebt, _newAnnualInterestRate, _upfrontFee); + // } + + // function troveManager_onSetInterestBatchManager(ITroveManager.OnSetInterestBatchManagerParams memory _params) public asActor { + // troveManager.onSetInterestBatchManager(_params); + // } + + // function troveManager_redeemCollateral(address _redeemer, uint256 _boldamount, uint256 _price, uint256 _redemptionRate, uint256 _maxIterations) public asActor { + // troveManager.redeemCollateral(_redeemer, _boldamount, _price, _redemptionRate, _maxIterations); + // } + + // function troveManager_setTroveStatusToActive(uint256 _troveId) public asActor { + // troveManager.setTroveStatusToActive(_troveId); + // } + + // function troveManager_shutdown() public asActor { + // troveManager.shutdown(); + // } + + +} \ No newline at end of file From 6627e0719fcdb86fa5f58c41219acb5c3158b0e3 Mon Sep 17 00:00:00 2001 From: gallo Date: Sat, 15 Feb 2025 12:22:47 +0100 Subject: [PATCH 17/26] fix: echidna linking --- contracts/echidna.yaml | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/contracts/echidna.yaml b/contracts/echidna.yaml index 448676230..7dc74d9c0 100644 --- a/contracts/echidna.yaml +++ b/contracts/echidna.yaml @@ -6,7 +6,40 @@ corpusDir: "echidna" balanceAddr: 0x1043561a8829300000 balanceContract: 0x1043561a8829300000 filterFunctions: [] -cryticArgs: ["--foundry-compile-all"] +cryticArgs: ["--foundry-compile-all","--compile-libraries=(CallUtils,0xf01),(CallbackUtils,0xf02),(BaseRelayRecipient,0fx03),(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: [ + ["0xf00", CryticTester], + ["0xf01", "CallUtils"], + ["0xf02", "CallbackUtils"], + ["0xf03", "BaseRelayRecipient"], + + ## Random libs I found + ["0xf04", "SuperfluidPoolDeployerLibrary"], + ["0xf06", "SolvencyHelperLibrary"], + + ## I did check + ["0xf08", "SlotsBitmapLibrary"], + + ## TODO: Maybe the deployment framework + ["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 \ No newline at end of file +shrinkLimit: 100000 + +## Deploy ERC1820RegistryCompiled as we don't have access to vm.etch +deployBytecodes: [["0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24", "0x608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c0029"]] \ No newline at end of file From b41b1a13eb622f0776d9d46deddb06a020983c19 Mon Sep 17 00:00:00 2001 From: gallo Date: Sat, 15 Feb 2025 12:27:57 +0100 Subject: [PATCH 18/26] chore: move the SF dependency to foundry --- contracts/test/recon/CryticToFoundry.sol | 5 +++++ contracts/test/recon/Setup.sol | 23 ++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/contracts/test/recon/CryticToFoundry.sol b/contracts/test/recon/CryticToFoundry.sol index cc04cf20a..858862571 100644 --- a/contracts/test/recon/CryticToFoundry.sol +++ b/contracts/test/recon/CryticToFoundry.sol @@ -6,9 +6,14 @@ 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(); } diff --git a/contracts/test/recon/Setup.sol b/contracts/test/recon/Setup.sol index 5ea9f2e4d..67068a45f 100644 --- a/contracts/test/recon/Setup.sol +++ b/contracts/test/recon/Setup.sol @@ -48,9 +48,16 @@ import {IDefaultPool} from "../../src/Interfaces/IDefaultPool.sol"; import {ICollSurplusPool} from "../../src/Interfaces/ICollSurplusPool.sol"; import {ISortedTroves} from "../../src/Interfaces/ISortedTroves.sol"; -// TODO: We need the whole system +// 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 { } @@ -182,6 +189,9 @@ abstract contract Setup is BaseSetup, ActorManager, AssetManager { uint256 currentBranch; + // Superfluid + SuperfluidFrameworkDeployer.Framework _sf; + function _setupAddressRegistryAndTroveManager( address coll, TroveManagerParams memory params @@ -206,12 +216,19 @@ abstract contract Setup is BaseSetup, ActorManager, AssetManager { _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)))); - - // Temp branch and then push? + // NOTE: Unclear interface? + IInitializableBold(address(boldToken)).initialize(ISuperTokenFactory(factory)); weth = MockERC20(_newAsset(18)); From 3f8891a45f43159af7c33b4b896d1c8cf9af1718 Mon Sep 17 00:00:00 2001 From: gallo Date: Sat, 15 Feb 2025 12:52:31 +0100 Subject: [PATCH 19/26] feat: finally works --- contracts/echidna.yaml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/contracts/echidna.yaml b/contracts/echidna.yaml index 7dc74d9c0..9a35f2ab1 100644 --- a/contracts/echidna.yaml +++ b/contracts/echidna.yaml @@ -6,21 +6,15 @@ corpusDir: "echidna" balanceAddr: 0x1043561a8829300000 balanceContract: 0x1043561a8829300000 filterFunctions: [] -cryticArgs: ["--foundry-compile-all","--compile-libraries=(CallUtils,0xf01),(CallbackUtils,0xf02),(BaseRelayRecipient,0fx03),(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)"] +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: [ - ["0xf00", CryticTester], ["0xf01", "CallUtils"], ["0xf02", "CallbackUtils"], ["0xf03", "BaseRelayRecipient"], - - ## Random libs I found ["0xf04", "SuperfluidPoolDeployerLibrary"], ["0xf06", "SolvencyHelperLibrary"], - - ## I did check ["0xf08", "SlotsBitmapLibrary"], - - ## TODO: Maybe the deployment framework + ["0xf09", "SuperfluidGovDeployerLibrary"], ["0xf0a", "SuperfluidHostDeployerLibrary"], ["0xf0b", "SuperfluidCFAv1DeployerLibrary"], @@ -42,4 +36,4 @@ contractAddr: "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496" shrinkLimit: 100000 ## Deploy ERC1820RegistryCompiled as we don't have access to vm.etch -deployBytecodes: [["0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24", "0x608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c0029"]] \ No newline at end of file +deployBytecodes: [["0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24", "608060405234801561001057600080fd5b506109c5806100206000396000f3fe608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c0029"]] \ No newline at end of file From d6b97c4b75b8f3d5f6911fd138e1d2f8a4758eda Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 17 Feb 2025 09:57:39 +0100 Subject: [PATCH 20/26] fix: troveManagerTester --- contracts/test/TestContracts/TroveManagerTester.t.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/test/TestContracts/TroveManagerTester.t.sol b/contracts/test/TestContracts/TroveManagerTester.t.sol index 444b192e5..9318f23e5 100644 --- a/contracts/test/TestContracts/TroveManagerTester.t.sol +++ b/contracts/test/TestContracts/TroveManagerTester.t.sol @@ -257,9 +257,14 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { } function getbatchDebtAndShares(address batchAddress) external view returns (uint256, uint256) { - Batch memory batch = batches[batchAddress]; + LatestBatchData memory batch; + + _getLatestBatchData(batchAddress, batch); + + // Shares are in this mapping + uint256 totalDebtShares = batches[batchAddress].totalDebtShares; - return (batch.debt, batch.totalDebtShares); + return (batch.entireDebtWithoutRedistribution, totalDebtShares); } function getTroveWeightedRecordedDebt(uint256 _troveId) external view returns (uint256) { @@ -352,7 +357,7 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { function getTroveInterestRate(uint256 troveId) external view returns (uint256) { address batchAddress = _getBatchManager(troveId); if (batchAddress != address(0)) { - batches[batchAddress].annualInterestRate; + return batches[batchAddress].annualInterestRate; } return Troves[troveId].annualInterestRate; From cec1c6a8a0bf80fd16b0e872687df9fb7fad23c3 Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 17 Feb 2025 09:57:55 +0100 Subject: [PATCH 21/26] chore: note if some properties break --- contracts/test/recon/BeforeAfter.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/test/recon/BeforeAfter.sol b/contracts/test/recon/BeforeAfter.sol index 669a27aaf..99b48609e 100644 --- a/contracts/test/recon/BeforeAfter.sol +++ b/contracts/test/recon/BeforeAfter.sol @@ -94,6 +94,8 @@ abstract contract BeforeAfter is Setup { _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) } } } From 9422df5a1bbfab0a99819ffbb810dbb4a00d35e3 Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 17 Feb 2025 09:58:05 +0100 Subject: [PATCH 22/26] fix: cleanup urgent redemptions --- contracts/test/recon/targets/TroveManagerTargets.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/recon/targets/TroveManagerTargets.sol b/contracts/test/recon/targets/TroveManagerTargets.sol index e1bf7ee0f..d98ff7485 100644 --- a/contracts/test/recon/targets/TroveManagerTargets.sol +++ b/contracts/test/recon/targets/TroveManagerTargets.sol @@ -39,7 +39,7 @@ abstract contract TroveManagerTargets is BaseTargetFunctions, Properties { } - function troveManager_urgentRedemption_clamped(uint256 _boldAmount, uint256[] memory _troveIds, uint256 _minCollateral) public { + function troveManager_urgentRedemption_clamped(uint256 _boldAmount) public { uint256[] memory ids = new uint256[](1); ids[0] = clampedTroveId; From 4e30a7f80e28184ab697da787fe29dad16741d1f Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 17 Feb 2025 09:58:14 +0100 Subject: [PATCH 23/26] fix: check MIN_DEBT if not shutdown --- contracts/test/recon/Properties.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/test/recon/Properties.sol b/contracts/test/recon/Properties.sol index da8e68773..a82cb6e50 100644 --- a/contracts/test/recon/Properties.sol +++ b/contracts/test/recon/Properties.sol @@ -49,6 +49,11 @@ abstract contract Properties is BeforeAfter, Asserts { // 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); From 75481eb0c4979ff436b96eac2e91ab6490315bea Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 17 Feb 2025 11:34:48 +0100 Subject: [PATCH 24/26] fix: ghost varaibles in all handlers --- contracts/test/recon/CryticToFoundry.sol | 287 ++++++++++++++++++ contracts/test/recon/TargetFunctions.sol | 2 + .../targets/BorrowerOperationsTargets.sol | 160 +++++++--- .../test/recon/targets/CollTokenTargets.sol | 12 +- .../targets/CollateralRegistryTargets.sol | 2 +- .../recon/targets/OptimizationTargets.sol | 135 ++++++++ .../recon/targets/StabilityPoolTargets.sol | 10 +- .../recon/targets/TroveManagerTargets.sol | 40 +-- 8 files changed, 582 insertions(+), 66 deletions(-) create mode 100644 contracts/test/recon/targets/OptimizationTargets.sol diff --git a/contracts/test/recon/CryticToFoundry.sol b/contracts/test/recon/CryticToFoundry.sol index 858862571..bf5c88670 100644 --- a/contracts/test/recon/CryticToFoundry.sol +++ b/contracts/test/recon/CryticToFoundry.sol @@ -26,4 +26,291 @@ contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { 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/TargetFunctions.sol b/contracts/test/recon/TargetFunctions.sol index 78463032b..ee21b9fc4 100644 --- a/contracts/test/recon/TargetFunctions.sol +++ b/contracts/test/recon/TargetFunctions.sol @@ -11,6 +11,7 @@ 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"; @@ -21,6 +22,7 @@ abstract contract TargetFunctions is CollateralRegistryTargets, CollTokenTargets, ManagersTargets, + OptimizationTargets, PriceFeedTargets, StabilityPoolTargets, TroveManagerTargets diff --git a/contracts/test/recon/targets/BorrowerOperationsTargets.sol b/contracts/test/recon/targets/BorrowerOperationsTargets.sol index ef6dcdcfd..1b4fd9cd0 100644 --- a/contracts/test/recon/targets/BorrowerOperationsTargets.sol +++ b/contracts/test/recon/targets/BorrowerOperationsTargets.sol @@ -10,10 +10,75 @@ 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 { - function borrowerOperations_addColl(uint256 _troveId, uint256 _collAmount) public asActor { + /// === 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 { @@ -23,8 +88,9 @@ abstract contract BorrowerOperationsTargets is BaseTargetFunctions, Properties } - function borrowerOperations_adjustTrove(uint256 _troveId, uint256 _collChange, bool _isCollIncrease, uint256 _boldChange, bool _isDebtIncrease, uint256 _maxUpfrontFee) public asActor { + 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 { @@ -37,13 +103,13 @@ abstract contract BorrowerOperationsTargets is BaseTargetFunctions, Properties } if(!_isDebtIncrease) { - boldChange = _boldChange % (troveManager.getTroveDebt(clampedTroveId) + 1); + 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 asActor { + function borrowerOperations_adjustTroveInterestRate(uint256 _troveId, uint256 _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public updateGhosts asActor { borrowerOperations.adjustTroveInterestRate(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee); } @@ -53,7 +119,7 @@ abstract contract BorrowerOperationsTargets is BaseTargetFunctions, Properties } - function borrowerOperations_adjustZombieTrove(uint256 _troveId, uint256 _collChange, bool _isCollIncrease, uint256 _boldChange, bool _isDebtIncrease, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public asActor { + 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); } @@ -67,18 +133,18 @@ abstract contract BorrowerOperationsTargets is BaseTargetFunctions, Properties } if(!_isDebtIncrease) { - boldChange = _boldChange % (troveManager.getTroveDebt(clampedTroveId) + 1); + 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 asActor { + function borrowerOperations_applyPendingDebt(uint256 _troveId, uint256 _lowerHint, uint256 _upperHint) public updateGhosts asActor { borrowerOperations.applyPendingDebt(_troveId, _lowerHint, _upperHint); } - function borrowerOperations_applyPendingDebt(uint256 _troveId) public asActor { + function borrowerOperations_applyPendingDebt(uint256 _troveId) public updateGhosts asActor { borrowerOperations.applyPendingDebt(_troveId); } @@ -86,12 +152,8 @@ abstract contract BorrowerOperationsTargets is BaseTargetFunctions, Properties borrowerOperations_applyPendingDebt(clampedTroveId); } - function borrowerOperations_claimCollateral() public asActor { - borrowerOperations.claimCollateral(); - } - - function borrowerOperations_closeTrove(uint256 _troveId) public asActor { + function borrowerOperations_closeTrove(uint256 _troveId) public updateGhosts asActor { borrowerOperations.closeTrove(_troveId); } @@ -100,17 +162,18 @@ abstract contract BorrowerOperationsTargets is BaseTargetFunctions, Properties } - function borrowerOperations_lowerBatchManagementFee(uint256 _newAnnualManagementFee) public asActor { + function borrowerOperations_lowerBatchManagementFee(uint256 _newAnnualManagementFee) public updateGhosts asActor { borrowerOperations.lowerBatchManagementFee(_newAnnualManagementFee); } - function borrowerOperations_onLiquidateTrove(uint256 _troveId) public asActor { + 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 returns (uint256) { + 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; } @@ -118,76 +181,104 @@ abstract contract BorrowerOperationsTargets is BaseTargetFunctions, Properties return borrowerOperations_openTrove(_getActor(), _ownerIndex, _collAmount, _boldAmount, 0, 0, 1e17, type(uint256).max, _getActor(), _getActor(), _getActor()); } - function borrowerOperations_openTroveAndJoinInterestBatchManager(IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory _params) public asActor { + 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 asActor { + 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_removeFromBatch(uint256 _troveId, uint256 _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public asActor { + 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_removeInterestIndividualDelegate(uint256 _troveId) public asActor { + 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 asActor { + 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 asActor { - uint256 amt = _boldAmount % (troveManager.getTroveDebt(clampedTroveId) + 1); + 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 asActor { + 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 asActor { + function borrowerOperations_setBatchManagerAnnualInterestRate(uint128 _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public updateGhosts asActor { borrowerOperations.setBatchManagerAnnualInterestRate(_newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee); } - function borrowerOperations_setInterestBatchManager(uint256 _troveId, address _newBatchManager, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee) public asActor { + // 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_setInterestIndividualDelegate(uint256 _troveId, address _delegate, uint128 _minInterestRate, uint128 _maxInterestRate, uint256 _newAnnualInterestRate, uint256 _upperHint, uint256 _lowerHint, uint256 _maxUpfrontFee, uint256 _minInterestRateChangePeriod) public asActor { + 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 asActor { + function borrowerOperations_setRemoveManager(uint256 _troveId, address _manager) public updateGhosts asActor { borrowerOperations.setRemoveManager(_troveId, _manager); } - function borrowerOperations_setRemoveManagerWithReceiver(uint256 _troveId, address _manager, address _receiver) public asActor { + function borrowerOperations_setRemoveManagerWithReceiver(uint256 _troveId, address _manager, address _receiver) public updateGhosts asActor { borrowerOperations.setRemoveManagerWithReceiver(_troveId, _manager, _receiver); } - function borrowerOperations_shutdown() public asActor { + function borrowerOperations_shutdown() public updateGhosts asActor { borrowerOperations.shutdown(); } - function borrowerOperations_shutdownFromOracleFailure() public asActor { + 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 asActor { + 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 asActor { + 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 { @@ -196,8 +287,9 @@ abstract contract BorrowerOperationsTargets is BaseTargetFunctions, Properties // === Withdraw Coll === // - function borrowerOperations_withdrawColl(uint256 _troveId, uint256 _collWithdrawal) public asActor { + 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 { diff --git a/contracts/test/recon/targets/CollTokenTargets.sol b/contracts/test/recon/targets/CollTokenTargets.sol index babd6584d..5332e0cea 100644 --- a/contracts/test/recon/targets/CollTokenTargets.sol +++ b/contracts/test/recon/targets/CollTokenTargets.sol @@ -10,27 +10,27 @@ import {Properties} from "../Properties.sol"; abstract contract CollTokenTargets is BaseTargetFunctions, Properties { - function collToken_approve(address spender, uint256 amount) public asActor { + function collToken_approve(address spender, uint256 amount) public updateGhosts asActor { collToken.approve(spender, amount); } - // function collToken_decreaseAllowance(address spender, uint256 subtractedValue) public asActor { + // function collToken_decreaseAllowance(address spender, uint256 subtractedValue) public updateGhosts asActor { // collToken.decreaseAllowance(spender, subtractedValue); // } - // function collToken_increaseAllowance(address spender, uint256 addedValue) public asActor { + // function collToken_increaseAllowance(address spender, uint256 addedValue) public updateGhosts asActor { // collToken.increaseAllowance(spender, addedValue); // } - function collToken_mint(address to, uint256 amt) public asActor { + function collToken_mint(address to, uint256 amt) public updateGhosts asActor { collToken.mint(to, amt); } - function collToken_transfer(address to, uint256 amount) public asActor { + function collToken_transfer(address to, uint256 amount) public updateGhosts asActor { collToken.transfer(to, amount); } - function collToken_transferFrom(address from, address to, uint256 amount) public asActor { + 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 index 4577f5b7f..407b310fb 100644 --- a/contracts/test/recon/targets/CollateralRegistryTargets.sol +++ b/contracts/test/recon/targets/CollateralRegistryTargets.sol @@ -9,7 +9,7 @@ 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 asActor { + function collateralRegistry_redeemCollateral(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _maxFeePercentage) public updateGhosts asActor { collateralRegistry.redeemCollateral(_boldAmount, _maxIterationsPerCollateral, _maxFeePercentage); hasDoneRedemption = true; } diff --git a/contracts/test/recon/targets/OptimizationTargets.sol b/contracts/test/recon/targets/OptimizationTargets.sol new file mode 100644 index 000000000..146e1f765 --- /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/StabilityPoolTargets.sol b/contracts/test/recon/targets/StabilityPoolTargets.sol index a25ad660b..15e4eabac 100644 --- a/contracts/test/recon/targets/StabilityPoolTargets.sol +++ b/contracts/test/recon/targets/StabilityPoolTargets.sol @@ -10,12 +10,12 @@ import {Properties} from "../Properties.sol"; abstract contract StabilityPoolTargets is BaseTargetFunctions, Properties { - function stabilityPool_claimAllCollGains() public asActor { + function stabilityPool_claimAllCollGains() public updateGhosts asActor { stabilityPool.claimAllCollGains(); } - function stabilityPool_provideToSP(uint256 _topUp, bool _doClaim) public asActor { + function stabilityPool_provideToSP(uint256 _topUp, bool _doClaim) public updateGhosts asActor { stabilityPool.provideToSP(_topUp, _doClaim); } @@ -24,7 +24,7 @@ abstract contract StabilityPoolTargets is BaseTargetFunctions, Properties { stabilityPool_provideToSP(_topUp, _doClaim); } - function stabilityPool_withdrawFromSP(uint256 _amount, bool _doClaim) public asActor { + function stabilityPool_withdrawFromSP(uint256 _amount, bool _doClaim) public updateGhosts asActor { stabilityPool.withdrawFromSP(_amount, _doClaim); } @@ -35,11 +35,11 @@ abstract contract StabilityPoolTargets is BaseTargetFunctions, Properties { - function stabilityPool_offset(uint256 _debtToOffset, uint256 _collToAdd) public asActor { + function stabilityPool_offset(uint256 _debtToOffset, uint256 _collToAdd) public updateGhosts asActor { stabilityPool.offset(_debtToOffset, _collToAdd); } - function stabilityPool_triggerBoldRewards(uint256 _boldYield) public asActor { + function stabilityPool_triggerBoldRewards(uint256 _boldYield) public updateGhosts asActor { stabilityPool.triggerBoldRewards(_boldYield); } diff --git a/contracts/test/recon/targets/TroveManagerTargets.sol b/contracts/test/recon/targets/TroveManagerTargets.sol index d98ff7485..9097a6867 100644 --- a/contracts/test/recon/targets/TroveManagerTargets.sol +++ b/contracts/test/recon/targets/TroveManagerTargets.sol @@ -10,12 +10,12 @@ import {Properties} from "../Properties.sol"; abstract contract TroveManagerTargets is BaseTargetFunctions, Properties { - function troveManager_batchLiquidateTroves(uint256[] memory _troveArray) public asActor { + function troveManager_batchLiquidateTroves(uint256[] memory _troveArray) public updateGhosts asActor { troveManager.batchLiquidateTroves(_troveArray); } - function troveManager_liquidate(uint256 _troveId) public asActor { + function troveManager_liquidate(uint256 _troveId) public updateGhosts asActor { troveManager.liquidate(_troveId); hasDoneLiquidation = true; } @@ -34,7 +34,7 @@ abstract contract TroveManagerTargets is BaseTargetFunctions, Properties { } - function troveManager_urgentRedemption(uint256 _boldAmount, uint256[] memory _troveIds, uint256 _minCollateral) public asActor { + function troveManager_urgentRedemption(uint256 _boldAmount, uint256[] memory _troveIds, uint256 _minCollateral) public updateGhosts asActor { troveManager.urgentRedemption(_boldAmount, _troveIds, _minCollateral); } @@ -48,72 +48,72 @@ abstract contract TroveManagerTargets is BaseTargetFunctions, Properties { } - // function troveManager_callInternalRemoveTroveId(uint256 _troveId) public asActor { + // function troveManager_callInternalRemoveTroveId(uint256 _troveId) public updateGhosts asActor { // troveManager.callInternalRemoveTroveId(_troveId); // } - // function troveManager_getUnbackedPortionPriceAndRedeemability() public asActor { + // function troveManager_getUnbackedPortionPriceAndRedeemability() public updateGhosts asActor { // troveManager.getUnbackedPortionPriceAndRedeemability(); // } - // function troveManager_onAdjustTrove(uint256 _troveId, uint256 _newColl, uint256 _newDebt, TroveChange memory _troveChange) public asActor { + // 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 asActor { + // 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 asActor { + // 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 asActor { + // 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 asActor { + // 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 asActor { + // 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 asActor { + // 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 asActor { + // 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 asActor { + // 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 asActor { + // 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 asActor { + // 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 asActor { + // 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 asActor { + // 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 asActor { + // function troveManager_setTroveStatusToActive(uint256 _troveId) public updateGhosts asActor { // troveManager.setTroveStatusToActive(_troveId); // } - // function troveManager_shutdown() public asActor { + // function troveManager_shutdown() public updateGhosts asActor { // troveManager.shutdown(); // } From b10b007376567dfa9438c7b4c165451d1159a2bb Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 17 Feb 2025 11:43:07 +0100 Subject: [PATCH 25/26] feat: all targets --- .../recon/targets/OptimizationTargets.sol | 186 +++++++++--------- 1 file changed, 93 insertions(+), 93 deletions(-) diff --git a/contracts/test/recon/targets/OptimizationTargets.sol b/contracts/test/recon/targets/OptimizationTargets.sol index 146e1f765..194c46b7c 100644 --- a/contracts/test/recon/targets/OptimizationTargets.sol +++ b/contracts/test/recon/targets/OptimizationTargets.sol @@ -10,40 +10,40 @@ 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); - // } - // } + // == 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_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) { @@ -57,68 +57,68 @@ abstract contract OptimizationTargets is BaseTargetFunctions, Properties { } - // // == property_sum_of_batches_debt == // - // function optimize_property_sum_of_batches_debt_over() public returns (int256) { - // if(clampedBatchManager == address(0)) { - // return 0; - // } + // == 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); - // } - // } + (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); - // } - // } + // == 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 From e41aa00b71ccba30f474cf3ffeabc720f8490921 Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 17 Feb 2025 11:43:15 +0100 Subject: [PATCH 26/26] chore: gitignore --- contracts/.gitignore | 5 +++++ 1 file changed, 5 insertions(+) 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