diff --git a/contracts/foundry.lock b/contracts/foundry.lock index d8e60026f..bd6608267 100644 --- a/contracts/foundry.lock +++ b/contracts/foundry.lock @@ -11,6 +11,9 @@ "lib/openzeppelin-contracts": { "rev": "bd325d56b4c62c9c5c1aff048c37c6bb18ac0290" }, + "lib/openzeppelin-contracts-upgradeable": { + "rev": "a40cb0bda838c2ef3dfc252c179f5c37c32e80c4" + }, "lib\\openzeppelin-contracts-upgradeable": { "tag": { "name": "v4.9.5", diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index 68657ca17..209db4a32 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -64,6 +64,9 @@ contract ActivePool is Initializable, IActivePool { // Last time at which the aggregate batch fees and weighted sum were updated uint256 public lastAggBatchManagementFeesUpdateTime; + // vault for collateral funds + address public vault; + // --- Events --- event CollTokenAddressChanged(address _newCollTokenAddress); @@ -73,6 +76,7 @@ contract ActivePool is Initializable, IActivePool { event StabilityPoolAddressChanged(address _newStabilityPoolAddress); event ActivePoolBoldDebtUpdated(uint256 _recordedDebtSum); event ActivePoolCollBalanceUpdated(uint256 _collBalance); + event CollateralVaultChanged(address _newCollateralVault); function initialize(IAddressesRegistry _addressesRegistry) external initializer { collToken = _addressesRegistry.collToken(); @@ -83,12 +87,14 @@ contract ActivePool is Initializable, IActivePool { interestRouter = _addressesRegistry.interestRouter(); boldToken = _addressesRegistry.boldToken(); parameters = _addressesRegistry.parameters(); + vault = address(_addressesRegistry.collateralVault()); emit CollTokenAddressChanged(address(collToken)); emit BorrowerOperationsAddressChanged(borrowerOperationsAddress); emit TroveManagerAddressChanged(troveManagerAddress); emit StabilityPoolAddressChanged(address(stabilityPool)); emit DefaultPoolAddressChanged(defaultPoolAddress); + emit CollateralVaultChanged(address(vault)); // Allow funds movements between Liquity contracts collToken.approve(defaultPoolAddress, type(uint256).max); @@ -168,7 +174,9 @@ contract ActivePool is Initializable, IActivePool { _accountForSendColl(_amount); - collToken.safeTransfer(_account, _amount); + uint256 b = collToken.balanceOf(address(vault)); + uint256 a = collToken.allowance(address(vault), address(this)); + collToken.safeTransferFrom(address(vault), _account, _amount); } function sendCollToDefaultPool(uint256 _amount) external override { @@ -191,7 +199,7 @@ contract ActivePool is Initializable, IActivePool { _accountForReceivedColl(_amount); // Pull Coll tokens from sender - collToken.safeTransferFrom(msg.sender, address(this), _amount); + collToken.safeTransferFrom(msg.sender, address(vault), _amount); } function accountForReceivedColl(uint256 _amount) public { diff --git a/contracts/src/AddressesRegistry.sol b/contracts/src/AddressesRegistry.sol index 659c94145..9d6470e8f 100644 --- a/contracts/src/AddressesRegistry.sol +++ b/contracts/src/AddressesRegistry.sol @@ -25,7 +25,8 @@ contract AddressesRegistry is Ownable2StepUpgradeable, IAddressesRegistry { IBoldToken public boldToken; IWETH public WETH; IParameters public parameters; - + ICollateralVault public collateralVault; + event CollTokenAddressChanged(address _collTokenAddress); event BorrowerOperationsAddressChanged(address _borrowerOperationsAddress); event TroveManagerAddressChanged(address _troveManagerAddress); @@ -45,6 +46,7 @@ contract AddressesRegistry is Ownable2StepUpgradeable, IAddressesRegistry { event BoldTokenAddressChanged(address _boldTokenAddress); event WETHAddressChanged(address _wethAddress); event ParametersAddressChanged(address _parameters); + event CollateralVaultChanged(address _collateralVault); function initialize( address _owner @@ -75,6 +77,7 @@ contract AddressesRegistry is Ownable2StepUpgradeable, IAddressesRegistry { boldToken = _vars.boldToken; WETH = _vars.WETH; parameters = _vars.parameters; + collateralVault = _vars.collateralVault; emit CollTokenAddressChanged(address(_vars.collToken)); emit BorrowerOperationsAddressChanged(address(_vars.borrowerOperations)); @@ -95,5 +98,6 @@ contract AddressesRegistry is Ownable2StepUpgradeable, IAddressesRegistry { emit BoldTokenAddressChanged(address(_vars.boldToken)); emit WETHAddressChanged(address(_vars.WETH)); emit ParametersAddressChanged(address(_vars.parameters)); + emit CollateralVaultChanged(address(_vars.collateralVault)); } } diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index f62e83967..f2dbc6fc5 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -1318,7 +1318,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio function _pullCollAndSendToActivePool(IActivePool _activePool, uint256 _amount) internal { // Send Coll tokens from sender to active pool - collToken.safeTransferFrom(msg.sender, address(_activePool), _amount); + collToken.safeTransferFrom(msg.sender, address(_activePool.vault()), _amount); // Make sure Active Pool accountancy is right _activePool.accountForReceivedColl(_amount); } diff --git a/contracts/src/CollateralVault.sol b/contracts/src/CollateralVault.sol new file mode 100644 index 000000000..8cb1c872b --- /dev/null +++ b/contracts/src/CollateralVault.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.24; + +import "openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/utils/SafeERC20Upgradeable.sol"; + +import "./Interfaces/IAddressesRegistry.sol"; +import "./Interfaces/ICollateralVault.sol"; + +contract CollateralVault is Ownable2StepUpgradeable, ICollateralVault { + using SafeERC20Upgradeable for IERC20Upgradeable; + + IERC20Upgradeable collToken; + // debt is negative when owner has returned more than withdrawed before + int256 public ownerDebt; + + function initialize( + address _owner, + IAddressesRegistry _addressesRegistry + ) external initializer { + __Ownable2Step_init(); + _transferOwnership(_owner); + + collToken = _addressesRegistry.collToken(); + + // Allow funds movements between Liquity contracts + address activePool = address(_addressesRegistry.activePool()); + collToken.approve(activePool, type(uint256).max); + address borrowerOps = address(_addressesRegistry.borrowerOperations()); + collToken.approve(borrowerOps, type(uint256).max); + address defaultPool = address(_addressesRegistry.defaultPool()); + collToken.approve(defaultPool, type(uint256).max); + } + + // only owner can withdraw + function withdraw(uint256 amount) external onlyOwner { + _requireBalanceIsEnough(amount); + ownerDebt += int256(amount); + collToken.approve(owner(), amount); + collToken.safeTransfer(owner(), amount); + } + + // anyone can top up + function topUp(uint256 amount) external { + ownerDebt -= int256(amount); + collToken.safeTransferFrom(msg.sender, address(this), amount); + } + + function _requireBalanceIsEnough(uint256 amount) internal view { + require(amount <= collToken.balanceOf(address(this)), "CollateralVault: Too big withdraw"); + } +} \ No newline at end of file diff --git a/contracts/src/DefaultPool.sol b/contracts/src/DefaultPool.sol index 9d393cc68..0c9453c5e 100644 --- a/contracts/src/DefaultPool.sol +++ b/contracts/src/DefaultPool.sol @@ -24,11 +24,13 @@ contract DefaultPool is Initializable, IDefaultPool { IERC20Upgradeable public collToken; address public troveManagerAddress; address public activePoolAddress; + address public activePoolVaultAddress; uint256 internal collBalance; // deposited Coll tracker uint256 internal BoldDebt; // debt event CollTokenAddressChanged(address _newCollTokenAddress); event ActivePoolAddressChanged(address _newActivePoolAddress); + event ActivePoolVaultAddressChanged(address _newActivePoolAddress); event TroveManagerAddressChanged(address _newTroveManagerAddress); event DefaultPoolBoldDebtUpdated(uint256 _boldDebt); event DefaultPoolCollBalanceUpdated(uint256 _collBalance); @@ -37,10 +39,12 @@ contract DefaultPool is Initializable, IDefaultPool { collToken = _addressesRegistry.collToken(); troveManagerAddress = address(_addressesRegistry.troveManager()); activePoolAddress = address(_addressesRegistry.activePool()); + activePoolVaultAddress = address(_addressesRegistry.collateralVault()); emit CollTokenAddressChanged(address(collToken)); emit TroveManagerAddressChanged(troveManagerAddress); emit ActivePoolAddressChanged(activePoolAddress); + emit ActivePoolVaultAddressChanged(activePoolVaultAddress); // Allow funds movements between Liquity contracts collToken.approve(activePoolAddress, type(uint256).max); @@ -79,8 +83,8 @@ contract DefaultPool is Initializable, IDefaultPool { uint256 newCollBalance = collBalance + _amount; collBalance = newCollBalance; - // Pull Coll tokens from ActivePool - collToken.safeTransferFrom(msg.sender, address(this), _amount); + // Pull Coll tokens from ActivePool vault + collToken.safeTransferFrom(activePoolVaultAddress, address(this), _amount); emit DefaultPoolCollBalanceUpdated(newCollBalance); } diff --git a/contracts/src/Interfaces/IActivePool.sol b/contracts/src/Interfaces/IActivePool.sol index c6f663cbe..242e10240 100644 --- a/contracts/src/Interfaces/IActivePool.sol +++ b/contracts/src/Interfaces/IActivePool.sol @@ -10,6 +10,7 @@ interface IActivePool { function defaultPoolAddress() external view returns (address); function borrowerOperationsAddress() external view returns (address); function troveManagerAddress() external view returns (address); + function vault() external view returns (address); function interestRouter() external view returns (IInterestRouter); // We avoid IStabilityPool here in order to prevent creating a dependency cycle that would break flattening function stabilityPool() external view returns (IBoldRewardsReceiver); diff --git a/contracts/src/Interfaces/IAddressesRegistry.sol b/contracts/src/Interfaces/IAddressesRegistry.sol index cf8cd23ff..86d1cbccb 100644 --- a/contracts/src/Interfaces/IAddressesRegistry.sol +++ b/contracts/src/Interfaces/IAddressesRegistry.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.0; import "./IActivePool.sol"; import "./IBoldToken.sol"; import "./IBorrowerOperations.sol"; +import "./ICollateralVault.sol"; import "./ICollSurplusPool.sol"; import "./IDefaultPool.sol"; import "./IHintHelpers.sol"; @@ -40,6 +41,7 @@ interface IAddressesRegistry { IBoldToken boldToken; IWETH WETH; IParameters parameters; + ICollateralVault collateralVault; } function collToken() external view returns (IERC20MetadataUpgradeable); @@ -61,6 +63,7 @@ interface IAddressesRegistry { function boldToken() external view returns (IBoldToken); function WETH() external returns (IWETH); function parameters() external returns (IParameters); + function collateralVault() external view returns(ICollateralVault); function setAddresses(AddressVars memory _vars) external; } diff --git a/contracts/src/Interfaces/ICollateralVault.sol b/contracts/src/Interfaces/ICollateralVault.sol new file mode 100644 index 000000000..78d389f29 --- /dev/null +++ b/contracts/src/Interfaces/ICollateralVault.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface ICollateralVault { +} \ No newline at end of file diff --git a/contracts/src/Parameters.sol b/contracts/src/Parameters.sol index f7c5bebb3..93e94f6db 100644 --- a/contracts/src/Parameters.sol +++ b/contracts/src/Parameters.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.24; import "openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; -import "./interfaces/IParameters.sol"; +import "./Interfaces/IParameters.sol"; import "./Dependencies/Constants.sol"; // For comments please refer to (https://github.com/liquity/bold/blob/main/contracts/src/Dependencies/Constants.sol) diff --git a/contracts/test/TestContracts/BaseTest.sol b/contracts/test/TestContracts/BaseTest.sol index b121d309b..d9d97e927 100644 --- a/contracts/test/TestContracts/BaseTest.sol +++ b/contracts/test/TestContracts/BaseTest.sol @@ -55,6 +55,7 @@ contract BaseTest is TestAccounts, Logging, TroveId { GasPool gasPool; IInterestRouter mockInterestRouter; IERC20Upgradeable collToken; + ICollateralVault vault; HintHelpers hintHelpers; IWETH WETH; // used for gas compensation WETHZapper wethZapper; @@ -573,6 +574,7 @@ contract BaseTest is TestAccounts, Logging, TroveId { console.log("StabilityPool addr: ", address(stabilityPool)); console.log("TroveManager addr: ", address(troveManager)); console.log("BoldToken addr: ", address(boldToken)); + console.log("CollateralVault addr: ", address(vault)); } function abs(uint256 x, uint256 y) public pure returns (uint256) { diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 3d601e5c8..4d5eb6afe 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -7,6 +7,7 @@ import "src/ActivePool.sol"; import "src/BoldToken.sol"; import "src/BorrowerOperations.sol"; import "src/CollSurplusPool.sol"; +import "src/CollateralVault.sol"; import "src/DefaultPool.sol"; import "src/GasPool.sol"; import "src/HintHelpers.sol"; @@ -95,6 +96,7 @@ contract TestDeployer is MetadataDeployment { IInterestRouter interestRouter; IERC20MetadataUpgradeable collToken; LiquityContractsDevPools pools; + ICollateralVault collateralVault; } struct LiquityContracts { @@ -111,6 +113,7 @@ contract TestDeployer is MetadataDeployment { GasPool gasPool; IInterestRouter interestRouter; IERC20MetadataUpgradeable collToken; + ICollateralVault collateralVault; } struct Zappers { @@ -134,6 +137,7 @@ contract TestDeployer is MetadataDeployment { address priceFeed; address gasPool; address interestRouter; + address collateralVault; } struct TroveManagerParams { @@ -463,6 +467,9 @@ contract TestDeployer is MetadataDeployment { addresses.sortedTroves = getAddress( address(this), abi.encodePacked(type(SortedTroves).creationCode), bSALT ); + addresses.collateralVault = getAddress( + address(this), abi.encodePacked(type(CollateralVault).creationCode), bSALT + ); // Deploy contracts IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ @@ -484,7 +491,8 @@ contract TestDeployer is MetadataDeployment { collateralRegistry: _collateralRegistry, boldToken: _boldToken, WETH: _weth, - parameters: _collateralRegistry.parameters() + parameters: _collateralRegistry.parameters(), + collateralVault: ICollateralVault(addresses.collateralVault) }); contracts.addressesRegistry.setAddresses(addressVars); @@ -506,6 +514,8 @@ contract TestDeployer is MetadataDeployment { CollSurplusPool(address(contracts.pools.collSurplusPool)).initialize(contracts.addressesRegistry); contracts.sortedTroves = new SortedTroves{salt: bSALT}(); SortedTroves(address(contracts.sortedTroves)).initialize(contracts.addressesRegistry); + contracts.collateralVault = new CollateralVault{salt: bSALT}(); + CollateralVault(address(contracts.collateralVault)).initialize(address(this), contracts.addressesRegistry); assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); assert(address(contracts.troveManager) == addresses.troveManager); @@ -706,6 +716,10 @@ contract TestDeployer is MetadataDeployment { addresses.sortedTroves = getAddress( address(this), abi.encodePacked(type(SortedTroves).creationCode), bSALT ); + addresses.collateralVault = getAddress( + address(this), abi.encodePacked(type(CollateralVault).creationCode), SALT + ); + contracts.priceFeed = _deployPriceFeed(_params.branch, _externalAddresses, _oracleParams, addresses.borrowerOperations); @@ -730,8 +744,8 @@ contract TestDeployer is MetadataDeployment { collateralRegistry: _params.collateralRegistry, boldToken: _params.boldToken, WETH: _params.weth, - parameters: _params.collateralRegistry.parameters() - + parameters: _params.collateralRegistry.parameters(), + collateralVault: ICollateralVault(addresses.collateralVault) }); contracts.addressesRegistry.setAddresses(addressVars); @@ -753,6 +767,9 @@ contract TestDeployer is MetadataDeployment { CollSurplusPool(address(contracts.collSurplusPool)).initialize(contracts.addressesRegistry); contracts.sortedTroves = new SortedTroves{salt: bSALT}(); SortedTroves(address(contracts.sortedTroves)).initialize(contracts.addressesRegistry); + contracts.collateralVault = new CollateralVault{salt: SALT}(); + CollateralVault(address(contracts.collateralVault)).initialize(address(this), contracts.addressesRegistry); + assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); assert(address(contracts.troveManager) == addresses.troveManager); diff --git a/contracts/test/TestContracts/DevTestSetup.sol b/contracts/test/TestContracts/DevTestSetup.sol index 9c49796d0..62f90d8f8 100644 --- a/contracts/test/TestContracts/DevTestSetup.sol +++ b/contracts/test/TestContracts/DevTestSetup.sol @@ -64,6 +64,7 @@ contract DevTestSetup is BaseTest { stabilityPool = contracts.stabilityPool; troveManager = contracts.troveManager; troveNFT = contracts.troveNFT; + vault = contracts.collateralVault; metadataNFT = addressesRegistry.metadataNFT(); mockInterestRouter = contracts.interestRouter; wethZapper = zappers.wethZapper; diff --git a/contracts/test/redemptions.t.sol b/contracts/test/redemptions.t.sol index a7586b6cd..50856b126 100644 --- a/contracts/test/redemptions.t.sol +++ b/contracts/test/redemptions.t.sol @@ -190,7 +190,7 @@ contract Redemptions is DevTestSetup { uint256 expectedCollDelta = correspondingColl - predictedCollFee; assertGt(expectedCollDelta, 0); - uint256 activePoolBalBefore = collToken.balanceOf(address(activePool)); + uint256 activePoolBalBefore = collToken.balanceOf(address(vault)); uint256 activePoolCollTrackerBefore = activePool.getCollBalance(); assertGt(activePoolBalBefore, 0); assertGt(activePoolCollTrackerBefore, 0); @@ -198,7 +198,7 @@ contract Redemptions is DevTestSetup { redeem(E, redeemAmount); // Check Active Pool Coll reduced correctly - assertEq(collToken.balanceOf(address(activePool)), activePoolBalBefore - expectedCollDelta); + assertEq(collToken.balanceOf(address(vault)), activePoolBalBefore - expectedCollDelta); assertEq(activePool.getCollBalance(), activePoolCollTrackerBefore - expectedCollDelta); } @@ -267,7 +267,7 @@ contract Redemptions is DevTestSetup { uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; assertGt(expectedCollDelta, 0); - uint256 activePoolBalBefore = collToken.balanceOf(address(activePool)); + uint256 activePoolBalBefore = collToken.balanceOf(address(vault)); uint256 activePoolCollTrackerBefore = activePool.getCollBalance(); assertGt(activePoolBalBefore, 0); assertGt(activePoolCollTrackerBefore, 0); @@ -275,7 +275,7 @@ contract Redemptions is DevTestSetup { redeem(E, totalBoldRedeemAmount); // Check Active Pool Coll reduced correctly - assertApproxEqAbs(collToken.balanceOf(address(activePool)), activePoolBalBefore - expectedCollDelta, 30); + assertApproxEqAbs(collToken.balanceOf(address(vault)), activePoolBalBefore - expectedCollDelta, 30); assertApproxEqAbs(activePool.getCollBalance(), activePoolCollTrackerBefore - expectedCollDelta, 30); }