diff --git a/contracts/vortex/CarbonVortex.sol b/contracts/vortex/CarbonVortex.sol index 379216ae..97d7be44 100644 --- a/contracts/vortex/CarbonVortex.sol +++ b/contracts/vortex/CarbonVortex.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.19; import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import { EnumerableSetUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -36,12 +37,12 @@ import { PPM_RESOLUTION, MAX_GAP } from "../utility/Constants.sol"; contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, Utils { using Address for address payable; using SafeCast for uint256; + using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet; uint128 private constant INITIAL_PRICE_SOURCE_AMOUNT = type(uint128).max; uint128 private constant INITIAL_PRICE_TARGET_AMOUNT = 1e12; - // addresses for token withdrawal - ICarbonController private immutable _carbonController; + // immutable vault address for token withdrawal IVault private immutable _vault; // first token for swapping @@ -89,19 +90,20 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, // address for token collection - collects all swapped target/final target tokens address payable private _transferAddress; + // set of controller addresses from which fees can be withdrawn + EnumerableSetUpgradeable.AddressSet private _controllers; + // upgrade forward-compatibility storage gap - uint256[MAX_GAP - 8] private __gap; + uint256[MAX_GAP - 10] private __gap; /** * @dev used to set immutable state variables */ constructor( - ICarbonController carbonController, IVault vault, Token targetTokenInit, Token finalTargetTokenInit ) validAddress(Token.unwrap(targetTokenInit)) { - _carbonController = carbonController; _vault = vault; _targetToken = targetTokenInit; @@ -112,8 +114,8 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, /** * @dev fully initializes the contract and its parents */ - function initialize(address payable transferAddressInit) public initializer { - __CarbonVortex_init(transferAddressInit); + function initialize(address payable transferAddressInit, address[] calldata controllersInit) public initializer { + __CarbonVortex_init(transferAddressInit, controllersInit); } // solhint-disable func-name-mixedcase @@ -121,17 +123,23 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, /** * @dev initializes the contract and its parents */ - function __CarbonVortex_init(address payable transferAddressInit) internal onlyInitializing { + function __CarbonVortex_init( + address payable transferAddressInit, + address[] calldata controllersInit + ) internal onlyInitializing { __Upgradeable_init(); __ReentrancyGuard_init(); - __CarbonVortex_init_unchained(transferAddressInit); + __CarbonVortex_init_unchained(transferAddressInit, controllersInit); } /** * @dev performs contract-specific initialization */ - function __CarbonVortex_init_unchained(address payable transferAddressInit) internal onlyInitializing { + function __CarbonVortex_init_unchained( + address payable transferAddressInit, + address[] calldata controllersInit + ) internal onlyInitializing { // set rewards PPM to 1000 _setRewardsPPM(1000); // set price reset multiplier to 2x @@ -150,6 +158,8 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, _setMinTokenSaleAmount(_targetToken, uint128(10) * uint128(10) ** _targetToken.decimals()); // set transfer address _setTransferAddress(transferAddressInit); + // set controller addresses + _addControllerAddresses(controllersInit); } /** @@ -304,6 +314,28 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, _setTransferAddress(newTransferAddress); } + /** + * @notice add a controller address + * + * requirements: + * + * - the caller must be the current admin of the contract + */ + function addController(address controller) external onlyAdmin { + _addController(controller); + } + + /** + * @notice remove a controller address + * + * requirements: + * + * - the caller must be the current admin of the contract + */ + function removeController(address controller) external onlyAdmin { + _removeController(controller); + } + /** * @dev withdraws funds held by the contract and sends them to an account * @@ -363,13 +395,24 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, return _transferAddress; } + /** + * @inheritdoc ICarbonVortex + */ + function controllers() external view returns (address[] memory) { + return _controllers.values(); + } + /** * @inheritdoc ICarbonVortex */ function availableTokens(Token token) external view returns (uint256) { uint256 totalFees = 0; - if (address(_carbonController) != address(0)) { - totalFees += _carbonController.accumulatedFees(token); + uint256 controllersLength = _controllers.length(); + if (controllersLength > 0) { + for (uint256 i = 0; i < controllersLength; i = uncheckedInc(i)) { + ICarbonController controller = ICarbonController(_controllers.at(i)); + totalFees += controller.accumulatedFees(token); + } } if (address(_vault) != address(0)) { totalFees += token.balanceOf(address(_vault)); @@ -389,18 +432,30 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, uint256[] memory rewardAmounts = new uint256[](len); // cache rewardsPPM to save gas uint256 rewardsPPMValue = _rewardsPPM; + // cache controllers length to save gas + uint256 controllersLength = _controllers.length(); + address[] memory __controllers; // cache address checks to save gas - bool carbonControllerIsNotZero = address(_carbonController) != address(0); + bool controllersNotEmpty = controllersLength != 0; bool vaultIsNotZero = address(_vault) != address(0); + // cache controllers to save gas + if (controllersNotEmpty) { + __controllers = _controllers.values(); + } + // withdraw fees from carbon vault for (uint256 i = 0; i < len; i = uncheckedInc(i)) { Token token = tokens[i]; // withdraw token fees uint256 totalFeeAmount = 0; - if (carbonControllerIsNotZero) { - totalFeeAmount += _carbonController.withdrawFees(token, type(uint256).max, address(this)); + if (controllersNotEmpty) { + // withdraw fees from all controllers + for (uint256 j = 0; j < controllersLength; j = uncheckedInc(j)) { + ICarbonController controller = ICarbonController(__controllers[j]); + totalFeeAmount += controller.withdrawFees(token, type(uint256).max, address(this)); + } } if (vaultIsNotZero) { // get vault token balance @@ -925,6 +980,32 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, }); } + function _addController(address controller) private { + // add the controller to the set ; revert if it already exists + if (!_controllers.add(controller)) { + revert ControllerAlreadyAdded(); + } + // emit event for controller address added + emit ControllerAdded(controller); + } + + function _removeController(address controller) private { + // remove the controller from the set ; revert if it doesn't exist + if (!_controllers.remove(controller)) { + revert ControllerDoesNotExist(); + } + // emit event for controller address removed + emit ControllerRemoved(controller); + } + + function _addControllerAddresses(address[] calldata __controllers) private { + // add the new controllers + uint256 length = __controllers.length; + for (uint256 i = 0; i < length; i = uncheckedInc(i)) { + _addController(__controllers[i]); + } + } + /** * @dev returns true if the auction price is below or equal to the minimum possible price * @dev check if timeElapsed / priceDecayHalfLife >= 128 diff --git a/contracts/vortex/interfaces/ICarbonVortex.sol b/contracts/vortex/interfaces/ICarbonVortex.sol index c022df36..f805cafd 100644 --- a/contracts/vortex/interfaces/ICarbonVortex.sol +++ b/contracts/vortex/interfaces/ICarbonVortex.sol @@ -20,6 +20,8 @@ interface ICarbonVortex is IUpgradeable { error InsufficientNativeTokenSent(); error InsufficientAmountForTrading(); error UnnecessaryNativeTokenReceived(); + error ControllerAlreadyAdded(); + error ControllerDoesNotExist(); struct Price { uint128 sourceAmount; @@ -101,6 +103,16 @@ interface ICarbonVortex is IUpgradeable { */ event TransferAddressUpdated(address indexed prevTransferAddress, address indexed newTransferAddress); + /** + * @notice triggered when a controller address is added + */ + event ControllerAdded(address indexed controller); + + /** + * @notice triggered when a controller address is removed + */ + event ControllerRemoved(address indexed controller); + /** * @notice returns the rewards ppm */ @@ -203,6 +215,11 @@ interface ICarbonVortex is IUpgradeable { */ function transferAddress() external view returns (address); + /** + * @notice returns the controller addresses + */ + function controllers() external view returns (address[] memory); + /** * @notice trades *targetToken* for *targetAmount* of *token* based on the current token price (trade by target amount) * @notice if token == *targetToken*, trades *finalTargetToken* for amount of *targetToken* and also diff --git a/deploy/tests/mainnet/0004-fee-burner.ts b/deploy/tests/mainnet/0004-fee-burner.ts index d4856031..bd2574b2 100644 --- a/deploy/tests/mainnet/0004-fee-burner.ts +++ b/deploy/tests/mainnet/0004-fee-burner.ts @@ -34,7 +34,7 @@ describeDeployment(__filename, () => { const implementationAddress = await proxyAdmin.getProxyImplementation(carbonVortex.address); const carbonVortexImpl: CarbonVortex = await ethers.getContractAt('CarbonVortex', implementationAddress); // hardcoding gas limit to avoid gas estimation attempts (which get rejected instead of reverted) - const tx = await carbonVortexImpl.initialize(ZERO_ADDRESS, { gasLimit: 6000000 }); + const tx = await carbonVortexImpl.initialize(ZERO_ADDRESS, [], { gasLimit: 6000000 }); await expect(tx.wait()).to.be.reverted; }); }); diff --git a/deploy/tests/mainnet/0006-carbon-vortex-upgrade.ts b/deploy/tests/mainnet/0006-carbon-vortex-upgrade.ts index 213cf39c..ffd9336a 100644 --- a/deploy/tests/mainnet/0006-carbon-vortex-upgrade.ts +++ b/deploy/tests/mainnet/0006-carbon-vortex-upgrade.ts @@ -39,7 +39,7 @@ describeDeployment(__filename, () => { const implementationAddress = await proxyAdmin.getProxyImplementation(carbonVortex.address); const carbonVortexImpl: CarbonVortex = await ethers.getContractAt('CarbonVortex', implementationAddress); // hardcoding gas limit to avoid gas estimation attempts (which get rejected instead of reverted) - const tx = await carbonVortexImpl.initialize(ZERO_ADDRESS, { gasLimit: 6000000 }); + const tx = await carbonVortexImpl.initialize(ZERO_ADDRESS, [], { gasLimit: 6000000 }); await expect(tx.wait()).to.be.reverted; }); }); diff --git a/deploy/tests/mainnet/0012-carbon-vortex-upgrade.ts b/deploy/tests/mainnet/0012-carbon-vortex-upgrade.ts index 0141ff9e..1c2fa72f 100644 --- a/deploy/tests/mainnet/0012-carbon-vortex-upgrade.ts +++ b/deploy/tests/mainnet/0012-carbon-vortex-upgrade.ts @@ -39,7 +39,7 @@ describeDeployment(__filename, () => { const implementationAddress = await proxyAdmin.getProxyImplementation(carbonVortex.address); const carbonVortexImpl: CarbonVortex = await ethers.getContractAt('CarbonVortex', implementationAddress); // hardcoding gas limit to avoid gas estimation attempts (which get rejected instead of reverted) - const tx = await carbonVortexImpl.initialize(ZERO_ADDRESS, { gasLimit: 6000000 }); + const tx = await carbonVortexImpl.initialize(ZERO_ADDRESS, [], { gasLimit: 6000000 }); await expect(tx.wait()).to.be.reverted; }); }); diff --git a/deploy/tests/mainnet/0016-carbon-vortex-upgrade.ts b/deploy/tests/mainnet/0016-carbon-vortex-upgrade.ts index 8b446917..597f923f 100644 --- a/deploy/tests/mainnet/0016-carbon-vortex-upgrade.ts +++ b/deploy/tests/mainnet/0016-carbon-vortex-upgrade.ts @@ -22,7 +22,7 @@ describeDeployment(__filename, () => { const implementationAddress = await proxyAdmin.getProxyImplementation(carbonVortex.address); const carbonControllerImpl: CarbonVortex = await ethers.getContractAt('CarbonVortex', implementationAddress); // hardcoding gas limit to avoid gas estimation attempts (which get rejected instead of reverted) - const tx = await carbonControllerImpl.initialize(ZERO_ADDRESS, { gasLimit: 6000000 }); + const tx = await carbonControllerImpl.initialize(ZERO_ADDRESS, [], { gasLimit: 6000000 }); await expect(tx.wait()).to.be.reverted; }); diff --git a/deploy/tests/mainnet/0017-carbon-vortex-upgrade.ts b/deploy/tests/mainnet/0017-carbon-vortex-upgrade.ts index be2d5ae2..37eb8113 100644 --- a/deploy/tests/mainnet/0017-carbon-vortex-upgrade.ts +++ b/deploy/tests/mainnet/0017-carbon-vortex-upgrade.ts @@ -22,7 +22,7 @@ describeDeployment(__filename, () => { const implementationAddress = await proxyAdmin.getProxyImplementation(carbonVortex.address); const carbonControllerImpl: CarbonVortex = await ethers.getContractAt('CarbonVortex', implementationAddress); // hardcoding gas limit to avoid gas estimation attempts (which get rejected instead of reverted) - const tx = await carbonControllerImpl.initialize(ZERO_ADDRESS, { gasLimit: 6000000 }); + const tx = await carbonControllerImpl.initialize(ZERO_ADDRESS, [], { gasLimit: 6000000 }); await expect(tx.wait()).to.be.reverted; }); diff --git a/test/carbon/accuracy/data/ArbitraryTrade.json b/test/carbon/accuracy/data/ArbitraryTrade.json index d44912f2..db3cb30a 100644 --- a/test/carbon/accuracy/data/ArbitraryTrade.json +++ b/test/carbon/accuracy/data/ArbitraryTrade.json @@ -30799,4 +30799,4 @@ "spec": "138.888888888889" } } -] \ No newline at end of file +] diff --git a/test/carbon/accuracy/data/EthUsdcTrade.json b/test/carbon/accuracy/data/EthUsdcTrade.json index 68d4c9d0..8de6b5ba 100644 --- a/test/carbon/accuracy/data/EthUsdcTrade.json +++ b/test/carbon/accuracy/data/EthUsdcTrade.json @@ -3023,4 +3023,4 @@ "spec": "118349999999.999999999941" } } -] \ No newline at end of file +] diff --git a/test/carbon/accuracy/data/ExtremeSrcTrade.json b/test/carbon/accuracy/data/ExtremeSrcTrade.json index 32397499..36c97200 100644 --- a/test/carbon/accuracy/data/ExtremeSrcTrade.json +++ b/test/carbon/accuracy/data/ExtremeSrcTrade.json @@ -7167,4 +7167,4 @@ "spec": "924248818871128843570750.388118986304" } } -] \ No newline at end of file +] diff --git a/test/carbon/accuracy/data/ExtremeTrgTrade.json b/test/carbon/accuracy/data/ExtremeTrgTrade.json index 1a257df4..ec5464d8 100644 --- a/test/carbon/accuracy/data/ExtremeTrgTrade.json +++ b/test/carbon/accuracy/data/ExtremeTrgTrade.json @@ -9253,4 +9253,4 @@ "spec": "189999993267.999957109613" } } -] \ No newline at end of file +] diff --git a/test/forge/CarbonVortex.t.sol b/test/forge/CarbonVortex.t.sol index 8ee66f1b..1c875daa 100644 --- a/test/forge/CarbonVortex.t.sol +++ b/test/forge/CarbonVortex.t.sol @@ -12,6 +12,9 @@ import { VortexTestCaseParser } from "./VortexTestCaseParser.t.sol"; import { AccessDenied, InvalidAddress, InvalidFee, ZeroValue } from "../../contracts/utility/Utils.sol"; import { PPM_RESOLUTION } from "../../contracts/utility/Constants.sol"; +import { TestCarbonController } from "../../contracts/helpers/TestCarbonController.sol"; +import { TestVoucher } from "../../contracts/helpers/TestVoucher.sol"; + import { ICarbonVortex } from "../../contracts/vortex/interfaces/ICarbonVortex.sol"; import { IVault } from "../../contracts/utility/interfaces/IVault.sol"; @@ -147,6 +150,16 @@ contract CarbonVortexTest is TestFixture { */ event Approval(address indexed owner, address indexed spender, uint256 value); + /** + * @notice triggered when a controller address is added + */ + event ControllerAdded(address indexed controller); + + /** + * @notice triggered when a controller address is removed + */ + event ControllerRemoved(address indexed controller); + /// @dev function to set up state before tests function setUp() public virtual { // Set up tokens and users @@ -175,7 +188,7 @@ contract CarbonVortexTest is TestFixture { function testShouldRevertWhenDeployingWithInvalidTargetToken() public { vm.expectRevert(InvalidAddress.selector); - new CarbonVortex(carbonController, IVault(vault), Token.wrap(address(0)), bnt); + new CarbonVortex(IVault(vault), Token.wrap(address(0)), bnt); } function testShouldBeInitialized() public view { @@ -185,7 +198,9 @@ contract CarbonVortexTest is TestFixture { function testShouldntBeAbleToReinitialize() public { vm.expectRevert("Initializable: contract is already initialized"); - carbonVortex.initialize(payable(0)); + // initialize empty controller address array + address[] memory emptyControllers = new address[](0); + carbonVortex.initialize(payable(0), emptyControllers); } /** @@ -439,9 +454,52 @@ contract CarbonVortexTest is TestFixture { vm.stopPrank(); } - /// @dev test that vortex can be deployed with carbon controller set to 0x0 address and it will be skipped on execute - function testExecuteShouldSkipCarbonControllerDeployedWithZeroAddress() public { - // deploy new vortex with carbon controller set to 0x0 + /// @dev test should withdraw fees from multiple controllers and Vault on calling execute + function testShouldWithdrawFeesOnExecuteForMultipleControllersAndVault() public { + uint256[] memory tokenAmounts = new uint256[](4); + tokenAmounts[0] = 100 ether; + tokenAmounts[1] = 60 ether; + tokenAmounts[2] = 20 ether; + tokenAmounts[3] = 10 ether; + Token[] memory tokens = new Token[](4); + tokens[0] = token1; + tokens[1] = token2; + tokens[2] = targetToken; + tokens[3] = finalTargetToken; + + // deploy new voucher + TestVoucher voucher2 = deployVoucher(); + // deploy new carbon controller + TestCarbonController carbonController2 = deployCarbonController(voucher2); + vm.startPrank(admin); + // add new carbon controller to the vortex + carbonVortex.addController(address(carbonController2)); + // grant fee manager role on carbonController to carbon vortex + carbonController2.grantRole(carbonController2.roleFeesManager(), address(carbonVortex)); + + for (uint256 i = 0; i < 4; ++i) { + carbonController.testSetAccumulatedFees(tokens[i], tokenAmounts[i]); + carbonController2.testSetAccumulatedFees(tokens[i], tokenAmounts[i]); + tokens[i].safeTransfer(address(vault), tokenAmounts[i]); + tokens[i].safeTransfer(address(carbonController2), tokenAmounts[i]); + + vm.expectEmit(); + // carbon controller fees event + emit FeesWithdrawn(tokens[i], address(carbonVortex), tokenAmounts[i], address(carbonVortex)); + vm.expectEmit(); + // carbon controller 2 fees event + emit FeesWithdrawn(tokens[i], address(carbonVortex), tokenAmounts[i], address(carbonVortex)); + vm.expectEmit(); + // vault fees event + emit FundsWithdrawn(tokens[i], address(carbonVortex), address(carbonVortex), tokenAmounts[i]); + carbonVortex.execute(tokens); + } + vm.stopPrank(); + } + + /// @dev test that vortex can be deployed with an empty controller set and it will skip the controller calls + function testShouldSkipEmptyControllerSet() public { + // deploy new vortex with no controllers in the initializer deployCarbonVortex(address(0), vault, transferAddress, targetToken, finalTargetToken); vm.startPrank(admin); @@ -490,7 +548,7 @@ contract CarbonVortexTest is TestFixture { Token[] memory tokens = new Token[](1); tokens[0] = token; // call execute for the target token - // expect two withdraw emits from carbon controller + // expect one withdraw emit from carbon controller vm.expectEmit(); emit FeesWithdrawn(token, address(carbonVortex), accumulatedFees, address(carbonVortex)); // execute @@ -2350,8 +2408,6 @@ contract CarbonVortexTest is TestFixture { /// @dev test that there isn't an incorrect reading of the fees function testShouldReturnTotalFeesAvailableCorrectly() public { - // deploy new vortex - deployCarbonVortex(address(carbonController), address(vault), transferAddress, targetToken, finalTargetToken); vm.startPrank(admin); // set fees uint256 accumulatedFees = 100 ether; @@ -2368,6 +2424,32 @@ contract CarbonVortexTest is TestFixture { assertEq(totalFees, accumulatedFees * 2); } + /// @dev test that available tokens are correctly calculated up for multiple controllers + function testShouldReturnTotalFeesAvailableCorrectlyForMultipleControllers() public { + // deploy a second carbon controller + TestVoucher voucher2 = deployVoucher(); + TestCarbonController carbonController2 = deployCarbonController(voucher2); + + vm.startPrank(admin); + // add the second carbon controller to the carbon vortex + carbonVortex.addController(address(carbonController2)); + + uint256 accumulatedFees = 100 ether; + // increment fees in the carbon controller + carbonController.testSetAccumulatedFees(token1, accumulatedFees); + carbonController2.testSetAccumulatedFees(token1, accumulatedFees); + // transfer fees to vault + token1.safeTransfer(address(vault), accumulatedFees); + + vm.startPrank(user1); + + // get total fees + uint256 totalFees = carbonVortex.availableTokens(token1); + + // assert total fees is correct + assertEq(totalFees, accumulatedFees * 3); + } + /// @dev test should return the correct amount available for trading for the target token function testShouldReturnTheAmountAvailableForTradingForTheTargetToken(uint256 accumulatedFees) public { vm.prank(admin); @@ -2490,6 +2572,27 @@ contract CarbonVortexTest is TestFixture { assertEq(Token.unwrap(carbonVortex.finalTargetToken()), Token.unwrap(finalTargetToken)); } + /// @dev test should return the controllers + function testShouldReturnTheControllers() public { + // assert that there is only one controller for the initial deployment + address[] memory controllers = carbonVortex.controllers(); + assertEq(controllers.length, 1); + assertEq(controllers[0], address(carbonController)); + + // deploy a second carbon controller + TestVoucher voucher2 = deployVoucher(); + TestCarbonController carbonController2 = deployCarbonController(voucher2); + vm.startPrank(admin); + // add the second carbon controller to the carbon vortex + carbonVortex.addController(address(carbonController2)); + vm.stopPrank(); + // assert that there are two controllers after adding the second one + controllers = carbonVortex.controllers(); + assertEq(controllers.length, 2); + assertEq(controllers[0], address(carbonController)); + assertEq(controllers[1], address(carbonController2)); + } + /// @dev test should revert on expected trade input if the target amount is larger than the available balance function testShouldRevertOnExpectedTradeInputIfTheTargetAmountIsLargerThanTheAvailableBalance() public { vm.prank(admin); @@ -3673,6 +3776,74 @@ contract CarbonVortexTest is TestFixture { assertNotEq(price.targetAmount, 0); } + /** + * @dev admin controller add / remove tests + */ + + function testAdminShouldBeAbleToAddControllers() public { + vm.startPrank(admin); + // add controller + address controller1 = makeAddr("controller1"); + vm.expectEmit(); + emit ControllerAdded(controller1); + carbonVortex.addController(controller1); + + // check if controller is added + address[] memory controllers = carbonVortex.controllers(); + for (uint256 i = 0; i < controllers.length; ++i) { + if (controllers[i] == controller1) { + assertTrue(true); + return; + } + } + assertTrue(false); + vm.stopPrank(); + } + + function testAdminShouldBeAbleToRemoveControllers() public { + vm.startPrank(admin); + // add controller + address controller1 = makeAddr("controller1"); + carbonVortex.addController(controller1); + + // remove controller + vm.expectEmit(); + emit ControllerRemoved(controller1); + carbonVortex.removeController(controller1); + + // check if controller is removed + address[] memory controllers = carbonVortex.controllers(); + for (uint256 i = 0; i < controllers.length; ++i) { + if (controllers[i] == controller1) { + assertTrue(false); + return; + } + } + assertTrue(true); + vm.stopPrank(); + } + + function testShouldRevertIfAddingAnAlreadyExistingController() public { + vm.startPrank(admin); + // add controller + address controller1 = makeAddr("controller1"); + carbonVortex.addController(controller1); + + // expect revert when adding same controller again + vm.expectRevert(ICarbonVortex.ControllerAlreadyAdded.selector); + carbonVortex.addController(controller1); + vm.stopPrank(); + } + + function testShouldRevertIfRemovingNonExistentController() public { + vm.startPrank(admin); + address controller1 = makeAddr("controller1"); + // expect revert when removing a nonexistent controller + vm.expectRevert(ICarbonVortex.ControllerDoesNotExist.selector); + carbonVortex.removeController(controller1); + vm.stopPrank(); + } + /** * @dev reentrancy tests */ diff --git a/test/forge/TestFixture.t.sol b/test/forge/TestFixture.t.sol index 2001cb8d..0d372556 100644 --- a/test/forge/TestFixture.t.sol +++ b/test/forge/TestFixture.t.sol @@ -21,7 +21,6 @@ import { TestCarbonController } from "../../contracts/helpers/TestCarbonControll import { CarbonBatcher } from "../../contracts/utility/CarbonBatcher.sol"; import { IVoucher } from "../../contracts/voucher/interfaces/IVoucher.sol"; -import { ICarbonController } from "../../contracts/carbon/interfaces/ICarbonController.sol"; import { IVault } from "../../contracts/utility/interfaces/IVault.sol"; import { Token } from "../../contracts/token/Token.sol"; @@ -133,13 +132,20 @@ contract TestFixture is Test { vm.startPrank(admin); // Deploy Carbon Vortex - carbonVortex = new CarbonVortex( - ICarbonController(_carbonController), - IVault(_vault), - _targetToken, - _finalTargetToken + carbonVortex = new CarbonVortex(IVault(_vault), _targetToken, _finalTargetToken); + // set controllers array + address[] memory controllers = new address[](1); + controllers[0] = _carbonController; + if (_carbonController == address(0)) { + // if carbon controller is 0x0, set empty controllers array + controllers = new address[](0); + } + // set init data + bytes memory vortexInitData = abi.encodeWithSelector( + carbonVortex.initialize.selector, + payable(_fundReceiver), + controllers ); - bytes memory vortexInitData = abi.encodeWithSelector(carbonVortex.initialize.selector, payable(_fundReceiver)); // Deploy Carbon Vortex proxy address carbonVortexProxy = address( new OptimizedTransparentUpgradeableProxy(