diff --git a/contracts/vortex/CarbonVortex.sol b/contracts/vortex/CarbonVortex.sol index e4766ca2..379216ae 100644 --- a/contracts/vortex/CarbonVortex.sol +++ b/contracts/vortex/CarbonVortex.sol @@ -44,9 +44,6 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, ICarbonController private immutable _carbonController; IVault private immutable _vault; - // address for token collection - collects all swapped target/final target tokens - address payable private immutable _transferAddress; - // first token for swapping Token private immutable _targetToken; // second (optional) token for swapping @@ -89,34 +86,34 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, // initial and current target token sale amount - for targetToken->finalTargetToken trades SaleAmount private _targetTokenSaleAmount; + // address for token collection - collects all swapped target/final target tokens + address payable private _transferAddress; + // upgrade forward-compatibility storage gap - uint256[MAX_GAP - 7] private __gap; + uint256[MAX_GAP - 8] private __gap; /** - * @dev used to set immutable state variables and initialize the implementation + * @dev used to set immutable state variables */ constructor( ICarbonController carbonController, IVault vault, - address payable transferAddress, Token targetTokenInit, Token finalTargetTokenInit - ) validAddress(transferAddress) validAddress(Token.unwrap(targetTokenInit)) { + ) validAddress(Token.unwrap(targetTokenInit)) { _carbonController = carbonController; _vault = vault; - _transferAddress = transferAddress; - _targetToken = targetTokenInit; _finalTargetToken = finalTargetTokenInit; - initialize(); + _disableInitializers(); } /** * @dev fully initializes the contract and its parents */ - function initialize() public initializer { - __CarbonVortex_init(); + function initialize(address payable transferAddressInit) public initializer { + __CarbonVortex_init(transferAddressInit); } // solhint-disable func-name-mixedcase @@ -124,17 +121,17 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, /** * @dev initializes the contract and its parents */ - function __CarbonVortex_init() internal onlyInitializing { + function __CarbonVortex_init(address payable transferAddressInit) internal onlyInitializing { __Upgradeable_init(); __ReentrancyGuard_init(); - __CarbonVortex_init_unchained(); + __CarbonVortex_init_unchained(transferAddressInit); } /** * @dev performs contract-specific initialization */ - function __CarbonVortex_init_unchained() internal onlyInitializing { + function __CarbonVortex_init_unchained(address payable transferAddressInit) internal onlyInitializing { // set rewards PPM to 1000 _setRewardsPPM(1000); // set price reset multiplier to 2x @@ -151,6 +148,8 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, _setMaxTargetTokenSaleAmount(uint128(100) * uint128(10) ** _targetToken.decimals()); // set min target token sale amount to 10 eth _setMinTokenSaleAmount(_targetToken, uint128(10) * uint128(10) ** _targetToken.decimals()); + // set transfer address + _setTransferAddress(transferAddressInit); } /** @@ -178,7 +177,7 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, * @inheritdoc Upgradeable */ function version() public pure override(IVersioned, Upgradeable) returns (uint16) { - return 3; + return 4; } /** @@ -294,6 +293,17 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, _setPairDisabled(token, disabled); } + /** + * @notice sets the transfer address + * + * requirements: + * + * - the caller must be the current admin of the contract + */ + function setTransferAddress(address newTransferAddress) external onlyAdmin { + _setTransferAddress(newTransferAddress); + } + /** * @dev withdraws funds held by the contract and sends them to an account * @@ -346,6 +356,13 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, return _finalTargetToken; } + /** + * @inheritdoc ICarbonVortex + */ + function transferAddress() external view returns (address) { + return _transferAddress; + } + /** * @inheritdoc ICarbonVortex */ @@ -402,25 +419,23 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, for (uint256 i = 0; i < len; i = uncheckedInc(i)) { Token token = tokens[i]; uint256 totalFeeAmount = feeAmounts[i]; - // skip the final target token - if (token == _finalTargetToken) { - continue; - } + // get fee and reward amounts + uint256 rewardAmount = rewardAmounts[i]; + uint256 feeAmount = totalFeeAmount - rewardAmount; // skip token if no fees have accumulated or token pair is disabled if (totalFeeAmount == 0 || _disabledPairs[token]) { continue; } - // get fee and reward amounts - uint256 rewardAmount = rewardAmounts[i]; - uint256 feeAmount = totalFeeAmount - rewardAmount; + // transfer proceeds to the transfer address for the final target token + if (token == _finalTargetToken) { + _transferProceeds(token, feeAmount); + continue; + } if (token == _targetToken) { // if _finalTargetToken is not set, directly transfer the fees to the transfer address if (Token.unwrap(_finalTargetToken) == address(0)) { - // safe due to nonReentrant modifier (forwards all gas fees in case of the native token) - _targetToken.unsafeTransfer(_transferAddress, feeAmount); - // increment totalCollected amount - _totalCollected += feeAmount; + _transferProceeds(_targetToken, feeAmount); } else if ( !_tradingEnabled(token) || _amountAvailableForTrading(token) < _minTokenSaleAmounts[token] || @@ -649,10 +664,7 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, // if no final target token is defined, transfer the target token to `transferAddress` if (Token.unwrap(_finalTargetToken) == address(0)) { - // safe due to nonreenrant modifier (forwards all available gas if token is native) - _targetToken.unsafeTransfer(_transferAddress, sourceAmount); - // increment total collected in `transferAddress` - _totalCollected += sourceAmount; + _transferProceeds(_targetToken, sourceAmount); } // if remaining balance is below the min token sale amount, reset the auction @@ -687,34 +699,26 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, if (sourceAmount > maxInput) { revert GreaterThanMaxInput(); } - + // revert if unnecessary native token is received + if (_finalTargetToken != NATIVE_TOKEN && msg.value > 0) { + revert UnnecessaryNativeTokenReceived(); + } // check enough final target token (if final target token is native) has been sent for the trade - if (_finalTargetToken == NATIVE_TOKEN) { - if (msg.value < sourceAmount) { - revert InsufficientNativeTokenSent(); - } - payable(_transferAddress).sendValue(sourceAmount); - } else { - // revert if unnecessary native token is received - if (msg.value > 0) { - revert UnnecessaryNativeTokenReceived(); - } - // transfer the tokens from the user to the _transferAddress - _finalTargetToken.safeTransferFrom(msg.sender, _transferAddress, sourceAmount); + if (_finalTargetToken == NATIVE_TOKEN && msg.value < sourceAmount) { + revert InsufficientNativeTokenSent(); } - + _finalTargetToken.safeTransferFrom(msg.sender, address(this), sourceAmount); // transfer the _targetToken to the user // safe due to nonReentrant modifier (forwards all available gas if native) _targetToken.unsafeTransfer(msg.sender, targetAmount); + _transferProceeds(_finalTargetToken, sourceAmount); + // if final target token is native, refund any excess native token to caller if (_finalTargetToken == NATIVE_TOKEN && msg.value > sourceAmount) { payable(msg.sender).sendValue(msg.value - sourceAmount); } - // increment total collected in _transferAddress - _totalCollected += sourceAmount; - // update the available target token sale amount _targetTokenSaleAmount.current -= targetAmount; @@ -905,6 +909,22 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, emit PairDisabledStatusUpdated(token, prevPairStatus, disabled); } + function _setTransferAddress(address newTransferAddress) private { + address prevTransferAddress = _transferAddress; + + // return if the transfer address is the same + if (prevTransferAddress == newTransferAddress) { + return; + } + + _transferAddress = payable(newTransferAddress); + + emit TransferAddressUpdated({ + prevTransferAddress: prevTransferAddress, + newTransferAddress: newTransferAddress + }); + } + /** * @dev returns true if the auction price is below or equal to the minimum possible price * @dev check if timeElapsed / priceDecayHalfLife >= 128 @@ -1012,6 +1032,17 @@ contract CarbonVortex is ICarbonVortex, Upgradeable, ReentrancyGuardUpgradeable, return price; } + function _transferProceeds(Token token, uint256 amount) private { + // increment totalCollected amount + _totalCollected += amount; + // if transfer address is 0, proceeds stay in the vortex + if (_transferAddress == address(0)) { + return; + } + // safe due to nonReentrant modifier (forwards all available gas in case of ETH) + token.unsafeTransfer(_transferAddress, amount); + } + function uncheckedInc(uint256 i) private pure returns (uint256 j) { unchecked { j = i + 1; diff --git a/contracts/vortex/interfaces/ICarbonVortex.sol b/contracts/vortex/interfaces/ICarbonVortex.sol index cf2808ba..c022df36 100644 --- a/contracts/vortex/interfaces/ICarbonVortex.sol +++ b/contracts/vortex/interfaces/ICarbonVortex.sol @@ -96,6 +96,11 @@ interface ICarbonVortex is IUpgradeable { */ event MinTokenSaleAmountUpdated(Token indexed token, uint128 prevMinTokenSaleAmount, uint128 newMinTokenSaleAmount); + /** + * @notice triggered when the transfer address is updated + */ + event TransferAddressUpdated(address indexed prevTransferAddress, address indexed newTransferAddress); + /** * @notice returns the rewards ppm */ @@ -193,6 +198,11 @@ interface ICarbonVortex is IUpgradeable { */ function finalTargetToken() external view returns (Token); + /** + * @notice returns the transfer address + */ + function transferAddress() external view returns (address); + /** * @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/scripts/mainnet/0017-CarbonVortex-upgrade.ts b/deploy/scripts/mainnet/0017-CarbonVortex-upgrade.ts index 68820cc2..13aeae25 100644 --- a/deploy/scripts/mainnet/0017-CarbonVortex-upgrade.ts +++ b/deploy/scripts/mainnet/0017-CarbonVortex-upgrade.ts @@ -4,8 +4,10 @@ import { DeployedContracts, upgradeProxy, InstanceName, setDeploymentMetadata } import { NATIVE_TOKEN_ADDRESS } from '../../../utils/Constants'; /** - * upgrade carbon vortex 2.0 to v3: + * upgrade carbon vortex 2.0 to v4: * remove the old vortex dependency + * fix final target token execute call to send funds to transfer address + * make transfer address a settable variable */ const func: DeployFunction = async ({ getNamedAccounts }: HardhatRuntimeEnvironment) => { const { deployer, bnt, vault } = await getNamedAccounts(); @@ -14,8 +16,11 @@ const func: DeployFunction = async ({ getNamedAccounts }: HardhatRuntimeEnvironm await upgradeProxy({ name: InstanceName.CarbonVortex, from: deployer, - args: [carbonController.address, vault, bnt, NATIVE_TOKEN_ADDRESS, bnt], - checkVersion: false + args: [carbonController.address, vault, NATIVE_TOKEN_ADDRESS, bnt], + checkVersion: false, + proxy: { + args: [bnt] + } }); return true; diff --git a/deploy/scripts/network/0004-CarbonVortex.ts b/deploy/scripts/network/0004-CarbonVortex.ts index ae32a4a8..e053271c 100644 --- a/deploy/scripts/network/0004-CarbonVortex.ts +++ b/deploy/scripts/network/0004-CarbonVortex.ts @@ -21,7 +21,10 @@ const func: DeployFunction = async ({ getNamedAccounts }: HardhatRuntimeEnvironm await deployProxy({ name: InstanceName.CarbonVortex, from: deployer, - args: [carbonController.address, ZERO_ADDRESS, transferAddress, targetToken, finalTargetToken] + args: [carbonController.address, ZERO_ADDRESS, targetToken, finalTargetToken], + proxy: { + args: [transferAddress] + } }); const carbonVortex = await DeployedContracts.CarbonVortex.deployed(); diff --git a/deploy/tests/mainnet/0004-fee-burner.ts b/deploy/tests/mainnet/0004-fee-burner.ts index 52be1525..d4856031 100644 --- a/deploy/tests/mainnet/0004-fee-burner.ts +++ b/deploy/tests/mainnet/0004-fee-burner.ts @@ -1,5 +1,6 @@ import { CarbonController, CarbonVortex, ProxyAdmin } from '../../../components/Contracts'; import { DeployedContracts, describeDeployment } from '../../../utils/Deploy'; +import { ZERO_ADDRESS } from '../../../utils/Constants'; import { expect } from 'chai'; import { ethers } from 'hardhat'; @@ -33,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({ 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 420e6b9f..213cf39c 100644 --- a/deploy/tests/mainnet/0006-carbon-vortex-upgrade.ts +++ b/deploy/tests/mainnet/0006-carbon-vortex-upgrade.ts @@ -1,3 +1,4 @@ +import { ZERO_ADDRESS } from '../../../utils/Constants'; import { CarbonController, CarbonVortex, ProxyAdmin } from '../../../components/Contracts'; import { DeployedContracts, describeDeployment } from '../../../utils/Deploy'; import { expect } from 'chai'; @@ -38,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({ 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 5628355f..0141ff9e 100644 --- a/deploy/tests/mainnet/0012-carbon-vortex-upgrade.ts +++ b/deploy/tests/mainnet/0012-carbon-vortex-upgrade.ts @@ -1,5 +1,6 @@ import { CarbonController, CarbonVortex, ProxyAdmin } from '../../../components/Contracts'; import { DeployedContracts, describeDeployment } from '../../../utils/Deploy'; +import { ZERO_ADDRESS } from '../../../utils/Constants'; import { expect } from 'chai'; import { ethers } from 'hardhat'; @@ -38,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({ 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 10e0c72b..8b446917 100644 --- a/deploy/tests/mainnet/0016-carbon-vortex-upgrade.ts +++ b/deploy/tests/mainnet/0016-carbon-vortex-upgrade.ts @@ -1,5 +1,6 @@ import { CarbonVortex, ProxyAdmin } from '../../../components/Contracts'; import { DeployedContracts, describeDeployment } from '../../../utils/Deploy'; +import { ZERO_ADDRESS } from '../../../utils/Constants'; import { expect } from 'chai'; import { ethers } from 'hardhat'; @@ -21,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({ 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 8868b5ed..be2d5ae2 100644 --- a/deploy/tests/mainnet/0017-carbon-vortex-upgrade.ts +++ b/deploy/tests/mainnet/0017-carbon-vortex-upgrade.ts @@ -1,5 +1,6 @@ import { CarbonVortex, ProxyAdmin } from '../../../components/Contracts'; import { DeployedContracts, describeDeployment } from '../../../utils/Deploy'; +import { ZERO_ADDRESS } from '../../../utils/Constants'; import { expect } from 'chai'; import { ethers } from 'hardhat'; @@ -21,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({ gasLimit: 6000000 }); + const tx = await carbonControllerImpl.initialize(ZERO_ADDRESS, { gasLimit: 6000000 }); await expect(tx.wait()).to.be.reverted; }); diff --git a/test/forge/CarbonVortex.t.sol b/test/forge/CarbonVortex.t.sol index d1989a3b..8ee66f1b 100644 --- a/test/forge/CarbonVortex.t.sol +++ b/test/forge/CarbonVortex.t.sol @@ -50,6 +50,8 @@ contract CarbonVortexTest is TestFixture { uint128 private constant MIN_TARGET_TOKEN_SALE_AMOUNT_DEFAULT = 10 ether; uint128 private constant MIN_TARGET_TOKEN_SALE_AMOUNT_UPDATED = 15 ether; + address payable private constant TRANSFER_ADDRESS_UPDATED = payable(0); + uint128 private constant INITIAL_PRICE_SOURCE_AMOUNT = type(uint128).max; uint128 private constant INITIAL_PRICE_TARGET_AMOUNT = 1e12; @@ -129,6 +131,11 @@ contract CarbonVortexTest is TestFixture { */ event MinTokenSaleAmountUpdated(Token indexed token, uint128 prevMinTokenSaleAmount, uint128 newMinTokenSaleAmount); + /** + * @notice triggered when the transfer address is updated + */ + event TransferAddressUpdated(address indexed prevTransferAddress, address indexed newTransferAddress); + /** * @dev triggered when fees are withdrawn (CarbonController event) */ @@ -166,24 +173,19 @@ contract CarbonVortexTest is TestFixture { * @dev construction tests */ - function testShouldRevertWhenDeployingWithInvalidTransferAddress() public { - vm.expectRevert(InvalidAddress.selector); - new CarbonVortex(carbonController, IVault(vault), payable(address(0)), NATIVE_TOKEN, bnt); - } - function testShouldRevertWhenDeployingWithInvalidTargetToken() public { vm.expectRevert(InvalidAddress.selector); - new CarbonVortex(carbonController, IVault(vault), transferAddress, Token.wrap(address(0)), bnt); + new CarbonVortex(carbonController, IVault(vault), Token.wrap(address(0)), bnt); } function testShouldBeInitialized() public view { uint16 version = carbonVortex.version(); - assertEq(version, 3); + assertEq(version, 4); } function testShouldntBeAbleToReinitialize() public { vm.expectRevert("Initializable: contract is already initialized"); - carbonVortex.initialize(); + carbonVortex.initialize(payable(0)); } /** @@ -643,7 +645,76 @@ contract CarbonVortexTest is TestFixture { } } - /// @dev test execute shouldnt emit a trade reset event for the target token if the final target token is zero + /// @dev test execute should transfer tokens directly to the transfer address on execute for the final target token + function testShouldTransferTokensDirectlyToTheTransferAddressOnExecuteIfCalledWithFinalTargetToken() public { + vm.startPrank(admin); + + // test with the target token + Token[] memory tokens = new Token[](1); + tokens[0] = finalTargetToken; + + uint256 accumulatedFees = 100 ether; + + // set fees in carbon controller + carbonController.testSetAccumulatedFees(tokens[0], accumulatedFees); + + vm.stopPrank(); + + vm.startPrank(user1); + + // get transfer address balance before + uint256 balanceBefore = finalTargetToken.balanceOf(transferAddress); + + // call execute for the target token + carbonVortex.execute(tokens); + + // get transfer address balance after + uint256 balanceAfter = finalTargetToken.balanceOf(transferAddress); + + // calculate reward amount + uint256 rewardAmount = (accumulatedFees * carbonVortex.rewardsPPM()) / PPM_RESOLUTION; + + // assert receiver address received the fees + assertEq(balanceAfter - balanceBefore, accumulatedFees - rewardAmount); + } + + /// @dev test execute should transfer tokens directly to the transfer address on execute for the final target token + function testShouldLeaveTokensInTheVortexIfTheTransferAddressIsZeroOnExecuteIfCalledWithFinalTargetToken() public { + // Deploy new Carbon Vortex with the transfer address set to the zero address + deployCarbonVortex(address(carbonController), vault, address(0), targetToken, finalTargetToken); + + vm.startPrank(admin); + + // test with the target token + Token[] memory tokens = new Token[](1); + tokens[0] = finalTargetToken; + + uint256 accumulatedFees = 100 ether; + + // set fees in carbon controller + carbonController.testSetAccumulatedFees(tokens[0], accumulatedFees); + + vm.stopPrank(); + + vm.startPrank(user1); + + // get vortex balance before + uint256 balanceBefore = finalTargetToken.balanceOf(address(carbonVortex)); + + // call execute for the target token + carbonVortex.execute(tokens); + + // get vortex balance after + uint256 balanceAfter = finalTargetToken.balanceOf(address(carbonVortex)); + + // calculate reward amount + uint256 rewardAmount = (accumulatedFees * carbonVortex.rewardsPPM()) / PPM_RESOLUTION; + + // assert receiver address received the fees + assertEq(balanceAfter - balanceBefore, accumulatedFees - rewardAmount); + } + + /// @dev test execute should transfer tokens directly to the transfer address if the final target token is zero function testShouldTransferTokensDirectlyToTheTransferAddressOnExecuteIfFinalTargetTokenIsZero() public { // Deploy new Carbon Vortex with the final target token set to the zero address deployCarbonVortex(address(carbonController), vault, transferAddress, targetToken, Token.wrap(address(0))); @@ -715,6 +786,50 @@ contract CarbonVortexTest is TestFixture { assertEq(totalCollectedAfter - totalCollectedBefore, accumulatedFees - rewardAmount); } + /// @dev test execute should transfer tokens directly to the transfer address if the final target token is zero + function testShouldLeaveTokensInTheVortexIfTheTransferAddressIsZeroOnExecuteIfFinalTargetTokenIsZero() public { + // Deploy new Carbon Vortex with the transfer address and final target token set to the zero address + deployCarbonVortex(address(carbonController), vault, address(0), targetToken, Token.wrap(address(0))); + + vm.startPrank(admin); + + // test with the target token + Token[] memory tokens = new Token[](1); + tokens[0] = targetToken; + + uint256 accumulatedFees = 100 ether; + + // set fees in carbon controller + carbonController.testSetAccumulatedFees(tokens[0], accumulatedFees); + + vm.stopPrank(); + + vm.startPrank(user1); + + // get transfer address balance before + uint256 balanceBefore = targetToken.balanceOf(address(carbonVortex)); + + // get total collected before + uint256 totalCollectedBefore = carbonVortex.totalCollected(); + + // call execute for the target token + carbonVortex.execute(tokens); + + // get transfer address balance after + uint256 balanceAfter = targetToken.balanceOf(address(carbonVortex)); + + // get total collected after + uint256 totalCollectedAfter = carbonVortex.totalCollected(); + + // calculate reward amount + uint256 rewardAmount = (accumulatedFees * carbonVortex.rewardsPPM()) / PPM_RESOLUTION; + + // assert receiver address received the fees + assertEq(balanceAfter - balanceBefore, accumulatedFees - rewardAmount); + // assert total collected is updated + assertEq(totalCollectedAfter - totalCollectedBefore, accumulatedFees - rewardAmount); + } + /// @dev test execute should update the current sale amount on first call to execute for the target token function testShouldUpdateTheCurrentSaleAmountOnFirstCallToExecuteForTheTargetToken(uint256 accumulatedFees) public { vm.startPrank(admin); @@ -1196,6 +1311,46 @@ contract CarbonVortexTest is TestFixture { assertEq(balanceGain, expectedSourceAmount); } + function testShouldLeaveFundsInTheVortexIfTransferAddressIsZeroAtEndOfFinalTargetToTargetTokenTrade() public { + // deploy new carbon vortex with transfer address set to zero + deployCarbonVortex(address(carbonController), vault, address(0), targetToken, finalTargetToken); + vm.prank(admin); + // set fees + uint256 accumulatedFees = 100 ether; + carbonController.testSetAccumulatedFees(targetToken, accumulatedFees); + + vm.startPrank(user1); + + // execute + Token[] memory tokens = new Token[](1); + tokens[0] = targetToken; + carbonVortex.execute(tokens); + + // trade target for final target + uint128 targetAmount = 1 ether; + + uint256 finalTargetBalanceBefore = finalTargetToken.balanceOf(address(carbonVortex)); + + // advance time so that the price decays and gets to market price + // market price = 4000 BNT per 1 ETH - 38.5 days + vm.warp(39 days); + + // get the expected trade input for 1 ether of target token + uint128 expectedSourceAmount = carbonVortex.expectedTradeInput(targetToken, targetAmount); + + // approve the source token + finalTargetToken.safeApprove(address(carbonVortex), expectedSourceAmount); + // trade + carbonVortex.trade(targetToken, targetAmount, expectedSourceAmount); + + uint256 finalTargetBalanceAfter = finalTargetToken.balanceOf(address(carbonVortex)); + + uint256 balanceGain = finalTargetBalanceAfter - finalTargetBalanceBefore; + + // assert that `transferAddress` received the final target token + assertEq(balanceGain, expectedSourceAmount); + } + function testTradingTargetTokenForTokenShouldSendTokenBalanceToTheUser() public { vm.prank(admin); Token token = token1; @@ -1438,6 +1593,44 @@ contract CarbonVortexTest is TestFixture { assertEq(balanceGain, sourceAmount); } + /// @dev test trading target token for token should transfer target tokens to + /// @dev transfer address if the final target token is zero + function testTradingTargetTokenForTokenShouldLeaveTargetTokensInContractIfTransferAddressAndFinalTargetTokenAreZero() + public + { + // Deploy new Carbon Vortex with the transfer address and final target token set to the zero address + deployCarbonVortex(address(carbonController), vault, address(0), targetToken, Token.wrap(address(0))); + + vm.prank(admin); + // set fees + uint256 accumulatedFees = 100 ether; + carbonController.testSetAccumulatedFees(token1, accumulatedFees); + + vm.startPrank(user1); + + Token[] memory tokens = new Token[](1); + tokens[0] = token1; + // execute so that the vortex has tokens + carbonVortex.execute(tokens); + + // increase timestamp so that the token is tradeable + vm.warp(46 days); + + uint128 targetAmount = 1 ether; + uint128 sourceAmount = carbonVortex.expectedTradeInput(token1, targetAmount); + + uint256 balanceBefore = targetToken.balanceOf(address(carbonVortex)); + + // trade (send sourceAmount of native token because the target token is native) + carbonVortex.trade{ value: sourceAmount }(token1, targetAmount, sourceAmount); + + uint256 balanceAfter = targetToken.balanceOf(address(carbonVortex)); + + uint256 balanceGain = balanceAfter - balanceBefore; + + assertEq(balanceGain, sourceAmount); + } + /// @dev test that sending any ETH with the transaction /// @dev on target -> token trades should revert if targetToken != NATIVE_TOKEN function testShouldRevertIfUnnecessaryNativeTokenSentOnTargetToFinalTargetTrades() public { @@ -3063,6 +3256,52 @@ contract CarbonVortexTest is TestFixture { vm.stopPrank(); } + /** + * @dev transferAddress tests + */ + + /// @dev test that setTransferAddress should revert when a non admin calls it + function testShouldRevertWhenNonAdminAttemptsToSetTheTransferAddress() public { + vm.prank(user1); + vm.expectRevert(AccessDenied.selector); + carbonVortex.setTransferAddress(TRANSFER_ADDRESS_UPDATED); + } + + /// @dev test that setTransferAddress with the same address should be ignored + function testShouldIgnoreSettingTheSameTransferAddress() public { + // get transfer address before + address transferAddressBefore = carbonVortex.transferAddress(); + vm.prank(admin); + carbonVortex.setTransferAddress(transferAddressBefore); + // get transfer address after + address transferAddressAfter = carbonVortex.transferAddress(); + // assert that the transfer address has not changed + assertEq(transferAddressBefore, transferAddressAfter); + } + + /// @dev test that setTransferAddress with the same address should be ignored (using fail test) + function testFailShouldIgnoreSettingTheSameTransferAddress() public { + vm.prank(admin); + vm.expectEmit(false, false, false, false); + emit TransferAddressUpdated(transferAddress, transferAddress); + carbonVortex.setTransferAddress(transferAddress); + } + + /// @dev test that admin should be able to update the transfer address + function testShouldBeAbleToSetAndUpdateTheTransferAddress() public { + vm.startPrank(admin); + address transferAddressBefore = carbonVortex.transferAddress(); + assertEq(transferAddressBefore, transferAddress); + + vm.expectEmit(); + emit TransferAddressUpdated(transferAddress, TRANSFER_ADDRESS_UPDATED); + carbonVortex.setTransferAddress(TRANSFER_ADDRESS_UPDATED); + + address transferAddressAfter = carbonVortex.transferAddress(); + assertEq(transferAddressAfter, TRANSFER_ADDRESS_UPDATED); + vm.stopPrank(); + } + /** * @dev min target token sale amount tests */ diff --git a/test/forge/TestFixture.t.sol b/test/forge/TestFixture.t.sol index f946a897..d9178b2c 100644 --- a/test/forge/TestFixture.t.sol +++ b/test/forge/TestFixture.t.sol @@ -134,11 +134,10 @@ contract TestFixture is Test { carbonVortex = new CarbonVortex( ICarbonController(_carbonController), IVault(_vault), - payable(_fundReceiver), _targetToken, _finalTargetToken ); - bytes memory vortexInitData = abi.encodeWithSelector(carbonVortex.initialize.selector); + bytes memory vortexInitData = abi.encodeWithSelector(carbonVortex.initialize.selector, payable(_fundReceiver)); // Deploy Carbon Vortex proxy address carbonVortexProxy = address( new OptimizedTransparentUpgradeableProxy(