diff --git a/contracts/pol/CarbonPOL.sol b/contracts/pol/CarbonPOL.sol index 5a01356e..604bfbc3 100644 --- a/contracts/pol/CarbonPOL.sol +++ b/contracts/pol/CarbonPOL.sol @@ -39,8 +39,11 @@ contract CarbonPOL is ICarbonPOL, Upgradeable, ReentrancyGuardUpgradeable, Utils // initial and current eth sale amount - for ETH->BNT trades EthSaleAmount private _ethSaleAmount; + // min eth sale amount - resets the current eth sale amount if below this amount after a trade + uint128 private _minEthSaleAmount; + // upgrade forward-compatibility storage gap - uint256[MAX_GAP - 4] private __gap; + uint256[MAX_GAP - 5] private __gap; /** * @dev used to initialize the implementation @@ -81,6 +84,8 @@ contract CarbonPOL is ICarbonPOL, Upgradeable, ReentrancyGuardUpgradeable, Utils _setPriceDecayHalfLife(10 days); // set initial eth sale amount to 100 eth _setEthSaleAmount(100 ether); + // set min eth sale amount to 10 eth + _setMinEthSaleAmount(10 ether); } /** @@ -148,6 +153,17 @@ contract CarbonPOL is ICarbonPOL, Upgradeable, ReentrancyGuardUpgradeable, Utils _setEthSaleAmount(newEthSaleAmount); } + /** + * @notice sets the min eth sale amount + * + * requirements: + * + * - the caller must be the admin of the contract + */ + function setMinEthSaleAmount(uint128 newMinEthSaleAmount) external onlyAdmin greaterThanZero(newMinEthSaleAmount) { + _setMinEthSaleAmount(newMinEthSaleAmount); + } + /** * @notice enable trading for TKN->ETH and set the initial price * @@ -200,6 +216,13 @@ contract CarbonPOL is ICarbonPOL, Upgradeable, ReentrancyGuardUpgradeable, Utils return _ethSaleAmount; } + /** + * @inheritdoc ICarbonPOL + */ + function minEthSaleAmount() external view returns (uint128) { + return _minEthSaleAmount; + } + /** * @inheritdoc ICarbonPOL */ @@ -323,10 +346,12 @@ contract CarbonPOL is ICarbonPOL, Upgradeable, ReentrancyGuardUpgradeable, Utils // update the available eth sale amount _ethSaleAmount.current -= amount; - // check if below 10% of the initial eth sale amount - if (_ethSaleAmount.current < _ethSaleAmount.initial / 10) { + // check if remaining eth sale amount is below the min eth sale amount + if (_ethSaleAmount.current < _minEthSaleAmount) { // top up the eth sale amount - _ethSaleAmount.current = _ethSaleAmount.initial; + _ethSaleAmount.current = address(this).balance > _ethSaleAmount.initial + ? _ethSaleAmount.initial + : uint128(address(this).balance); // reset the price to double the current one Price memory price = tokenPrice(NATIVE_TOKEN); price.sourceAmount *= _marketPriceMultiply; @@ -391,6 +416,22 @@ contract CarbonPOL is ICarbonPOL, Upgradeable, ReentrancyGuardUpgradeable, Utils emit EthSaleAmountUpdated(prevEthSaleAmount, newEthSaleAmount); } + /** + * @dev set min eth sale amount helper + */ + function _setMinEthSaleAmount(uint128 newMinEthSaleAmount) private { + uint128 prevMinEthSaleAmount = _minEthSaleAmount; + + // return if the min eth sale amount is the same + if (prevMinEthSaleAmount == newMinEthSaleAmount) { + return; + } + + _minEthSaleAmount = newMinEthSaleAmount; + + emit MinEthSaleAmountUpdated(prevMinEthSaleAmount, newMinEthSaleAmount); + } + /** * @dev returns the token amount available for trading */ diff --git a/contracts/pol/interfaces/ICarbonPOL.sol b/contracts/pol/interfaces/ICarbonPOL.sol index c4368c28..58b93080 100644 --- a/contracts/pol/interfaces/ICarbonPOL.sol +++ b/contracts/pol/interfaces/ICarbonPOL.sol @@ -55,6 +55,11 @@ interface ICarbonPOL is IUpgradeable { */ event EthSaleAmountUpdated(uint128 prevEthSaleAmount, uint128 newEthSaleAmount); + /** + * @notice triggered when the min eth sale amount is updated + */ + event MinEthSaleAmountUpdated(uint128 prevMinEthSaleAmount, uint128 newMinEthSaleAmount); + /** * @notice returns the market price multiplier */ @@ -70,6 +75,11 @@ interface ICarbonPOL is IUpgradeable { */ function ethSaleAmount() external view returns (EthSaleAmount memory); + /** + * @notice returns the min eth sale amount - resets the current eth sale amount if below this amount after a trade + */ + function minEthSaleAmount() external view returns (uint128); + /** * @notice returns true if trading is enabled for token */ diff --git a/package.json b/package.json index 9c96b5bd..b184ac70 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "glob": "^10.2.7", "hardhat-contract-sizer": "^2.10.0", "hardhat-dependency-compiler": "^1.1.3", - "hardhat-deploy": "^0.11.34", + "hardhat-deploy": "0.11.34", "hardhat-deploy-tenderly": "^0.2.0", "hardhat-storage-layout": "^0.1.7", "hardhat-watcher": "^2.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 895f1d84..905cf02f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,11 +154,11 @@ devDependencies: specifier: ^1.1.3 version: 1.1.3(hardhat@2.15.0) hardhat-deploy: - specifier: ^0.11.34 - version: 0.11.37 + specifier: 0.11.34 + version: 0.11.34 hardhat-deploy-tenderly: specifier: ^0.2.0 - version: 0.2.0(hardhat-deploy@0.11.37)(hardhat@2.15.0) + version: 0.2.0(hardhat-deploy@0.11.34)(hardhat@2.15.0) hardhat-storage-layout: specifier: ^0.1.7 version: 0.1.7(hardhat@2.15.0) @@ -1638,7 +1638,7 @@ packages: ethers: 5.7.2 fs-extra: 9.1.0 hardhat: 2.15.0(ts-node@10.9.1)(typescript@5.2.2) - hardhat-deploy: 0.11.37 + hardhat-deploy: 0.11.34 js-yaml: 3.14.1 transitivePeerDependencies: - bufferutil @@ -6091,7 +6091,7 @@ packages: hardhat: 2.15.0(ts-node@10.9.1)(typescript@5.2.2) dev: true - /hardhat-deploy-tenderly@0.2.0(hardhat-deploy@0.11.37)(hardhat@2.15.0): + /hardhat-deploy-tenderly@0.2.0(hardhat-deploy@0.11.34)(hardhat@2.15.0): resolution: {integrity: sha512-obJm+Wh7tRsLRFgmZkkUGC1/RU37J2gHQfCt+ndbSswW5XCjSOIu6tkShhYp147HqOPml84X2qX6qZku1ykIZQ==} peerDependencies: hardhat: ^2.0.0 @@ -6099,14 +6099,14 @@ packages: dependencies: axios: 0.24.0 hardhat: 2.15.0(ts-node@10.9.1)(typescript@5.2.2) - hardhat-deploy: 0.11.37 + hardhat-deploy: 0.11.34 js-yaml: 4.1.0 transitivePeerDependencies: - debug dev: true - /hardhat-deploy@0.11.37: - resolution: {integrity: sha512-pohPSEEo/X9Yfv0Fc0kXBQW6JO0LNOILBGCP69Ci1COJvLht1hLjAtXt/hccyvD9qY/uwJAM75fmsf41Y9N7lg==} + /hardhat-deploy@0.11.34: + resolution: {integrity: sha512-N6xcwD8LSMV/IyfEr8TfR2YRbOh9Q4QvitR9MKZRTXQmgQiiMGjX+2efMjKgNMxwCVlmpfnE1tyDxOJOOUseLQ==} dependencies: '@ethersproject/abi': 5.7.0 '@ethersproject/abstract-signer': 5.7.0 diff --git a/test/forge/CarbonPOL.t.sol b/test/forge/CarbonPOL.t.sol index 01516cb3..230ce739 100644 --- a/test/forge/CarbonPOL.t.sol +++ b/test/forge/CarbonPOL.t.sol @@ -28,6 +28,9 @@ contract CarbonPOLTest is TestFixture { uint128 private constant ETH_SALE_AMOUNT_DEFAULT = 100 ether; uint128 private constant ETH_SALE_AMOUNT_UPDATED = 150 ether; + uint128 private constant MIN_ETH_SALE_AMOUNT_DEFAULT = 10 ether; + uint128 private constant MIN_ETH_SALE_AMOUNT_UPDATED = 15 ether; + // Events /** @@ -60,6 +63,11 @@ contract CarbonPOLTest is TestFixture { */ event EthSaleAmountUpdated(uint128 prevEthSaleAmount, uint128 newEthSaleAmount); + /** + * @notice triggered when the min eth sale amount is updated + */ + event MinEthSaleAmountUpdated(uint128 prevMinEthSaleAmount, uint128 newMinEthSaleAmount); + /** * @dev Emitted when the allowance of a `spender` for an `owner` is set by * a call to {approve}. `value` is the new allowance. @@ -221,6 +229,47 @@ contract CarbonPOLTest is TestFixture { vm.stopPrank(); } + /** + * @dev min eth sale amount tests + */ + + /// @dev test that setMinEthSaleAmount should revert when a non-admin calls it + function testShouldRevertWhenNonAdminAttemptsToSetTheMinEthSaleAmount() public { + vm.prank(user1); + vm.expectRevert(AccessDenied.selector); + carbonPOL.setMinEthSaleAmount(MIN_ETH_SALE_AMOUNT_UPDATED); + } + + /// @dev test that setMinEthSaleAmount should revert when setting to an invalid value + function testShouldRevertSettingTheMinEthSaleAmountWithAnInvalidValue() public { + vm.prank(admin); + vm.expectRevert(ZeroValue.selector); + carbonPOL.setMinEthSaleAmount(0); + } + + /// @dev test that setMinEthSaleAmount with the same value should be ignored + function testFailShouldIgnoreSettingTheSameMinEthSaleAmount() public { + vm.prank(admin); + vm.expectEmit(false, false, false, false); + emit MinEthSaleAmountUpdated(MIN_ETH_SALE_AMOUNT_DEFAULT, MIN_ETH_SALE_AMOUNT_DEFAULT); + carbonPOL.setMinEthSaleAmount(MIN_ETH_SALE_AMOUNT_DEFAULT); + } + + /// @dev test that admin should be able to update the min eth sale amount + function testShouldBeAbleToSetAndUpdateTheMinEthSaleAmount() public { + vm.startPrank(admin); + uint128 minEthSaleAmount = carbonPOL.minEthSaleAmount(); + assertEq(minEthSaleAmount, MIN_ETH_SALE_AMOUNT_DEFAULT); + + vm.expectEmit(); + emit MinEthSaleAmountUpdated(MIN_ETH_SALE_AMOUNT_DEFAULT, MIN_ETH_SALE_AMOUNT_UPDATED); + carbonPOL.setMinEthSaleAmount(MIN_ETH_SALE_AMOUNT_UPDATED); + + minEthSaleAmount = carbonPOL.minEthSaleAmount(); + assertEq(minEthSaleAmount, MIN_ETH_SALE_AMOUNT_UPDATED); + vm.stopPrank(); + } + /// @dev test that setting the eth sale amount to an amount below the current eth sale amount reset the current amount function testCurrentEthSaleAmountIsUpdatedWhenAboveTheNewEthSaleAmount() public { vm.startPrank(admin); @@ -788,7 +837,7 @@ contract CarbonPOLTest is TestFixture { vm.stopPrank(); } - /// @dev test trading eth below the 10% * sale amount threshold should reset the price and current eth amount + /// @dev test trading eth below the 10 ether sale amount threshold should reset the price and current eth amount function testTradingETHBelowTheSaleThreshholdShouldResetThePriceAndCurrentEthAmount() public { // enable trading and set price for the native token vm.prank(admin); @@ -825,7 +874,7 @@ contract CarbonPOLTest is TestFixture { // get the price before the threshold trade ICarbonPOL.Price memory prevPrice = carbonPOL.tokenPrice(NATIVE_TOKEN); - // trade 10% more (so that we go below 10% of the max sale amount) + // trade 10% more (so that we go below 10 ether current eth sale amount) amountToSell = uint128((initialSaleAmount * 10) / 100); carbonPOL.trade(token, amountToSell); @@ -847,7 +896,7 @@ contract CarbonPOLTest is TestFixture { vm.stopPrank(); } - /// @dev test trading eth below the 10% * sale amount threshold should emit price updated event + /// @dev test trading eth below the 10 ether sale amount threshold should emit price updated event function testTradingETHBelowTheSaleThreshholdShouldEmitEvent() public { // enable trading and set price for the native token vm.prank(admin); @@ -868,7 +917,7 @@ contract CarbonPOLTest is TestFixture { // assert current and initial eth sale amount are equal assertEq(initialSaleAmount, currentSaleAmount); - // trade 95% of the sale amount + // trade 95% of the sale amount (leaving 5 eth as current amount) uint128 amountToSell = uint128((initialSaleAmount * 95) / 100); // approve bnt for eth -> bnt trades @@ -891,6 +940,52 @@ contract CarbonPOLTest is TestFixture { vm.stopPrank(); } + /// @dev test trading eth below the 10 ether sale amount threshold should reset the current sale amount + /// @dev to contract balance or the initial amount, depending on which is less + function testTradingETHBelowTheSaleThreshholdShouldResetSaleAmountToContractBalanceIfItsLessThanInitialAmount() + public + { + // enable trading and set price for the native token + vm.startPrank(admin); + Token token = NATIVE_TOKEN; + + // set 1 eth = 2000 bnt as initial price + ICarbonPOL.Price memory initialPrice = ICarbonPOL.Price({ sourceAmount: 2000 * 1e18, targetAmount: 1e18 }); + carbonPOL.enableTradingETH(initialPrice); + + // set up current eth sale amount + carbonPOL.setEthSaleAmount(uint128(address(carbonPOL).balance * 2)); + + vm.stopPrank(); + + vm.startPrank(user1); + + // set timestamp to 10 days + vm.warp(10 days); + + uint128 initialSaleAmount = carbonPOL.ethSaleAmount().initial; + uint128 currentSaleAmount = carbonPOL.ethSaleAmount().current; + + // check the initial sale amount is greater than the contract balance + assertGt(initialSaleAmount, address(carbonPOL).balance); + + // trade 95% of the sale amount (leaving 5 eth as current amount) + uint128 amountToSell = uint128((currentSaleAmount * 95) / 100); + + // approve bnt for eth -> bnt trades + bnt.safeApprove(address(carbonPOL), MAX_SOURCE_AMOUNT); + + // trade + carbonPOL.trade(token, amountToSell); + + uint128 currentSaleAmountPostTrade = carbonPOL.ethSaleAmount().current; + + // check that the current sale amount has been reset to the contract's balance + assertEq(currentSaleAmountPostTrade, address(carbonPOL).balance); + + vm.stopPrank(); + } + /// @dev test should revert getting price for tokens for which trading is disabled function testShouldRevertTokenPriceIfTradingIsDisabled(bool isNativeToken) public { Token token = isNativeToken ? NATIVE_TOKEN : token1;