diff --git a/contracts/Mocks/MockTradeFactory.sol b/contracts/Mocks/MockTradeFactory.sol new file mode 100644 index 0000000..aa76f82 --- /dev/null +++ b/contracts/Mocks/MockTradeFactory.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.18; + +contract MockTradeFactory { + function enable(address, address) external {} + + function disable(address, address) external {} +} diff --git a/contracts/splitter/Dumper.sol b/contracts/splitter/Dumper.sol new file mode 100644 index 0000000..0698eac --- /dev/null +++ b/contracts/splitter/Dumper.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity >=0.8.18; + +import {Governance} from "@periphery/utils/Governance.sol"; +import {TradeFactorySwapper} from "@periphery/swappers/TradeFactorySwapper.sol"; +import {Accountant, ERC20, SafeERC20} from "../accountants/Accountant.sol"; + +contract Dumper is TradeFactorySwapper, Governance { + using SafeERC20 for ERC20; + + Accountant public immutable accountant; + + address public immutable splitter; + + address public splitToken; + + constructor( + address _governance, + address _accountant, + address _splitter, + address _tf, + address _splitToken + ) Governance(_governance) { + require(_accountant != address(0), "ZERO ADDRESS"); + require(_splitter != address(0), "ZERO ADDRESS"); + require(_splitToken != address(0), "ZERO ADDRESS"); + accountant = Accountant(_accountant); + splitter = _splitter; + _setTradeFactory(_tf, _splitToken); + splitToken = _splitToken; + } + + function setTradeFactory(address _tf) external onlyGovernance { + _setTradeFactory(_tf, splitToken); + } + + function addToken(address _tokenFrom) external onlyGovernance { + _addToken(_tokenFrom, splitToken); + } + + function addTokens(address[] calldata _tokens) external onlyGovernance { + address _splitToken = splitToken; + for (uint256 i; i < _tokens.length; ++i) { + _addToken(_tokens[i], _splitToken); + } + } + + function removeTokens(address[] calldata _tokens) external onlyGovernance { + address _splitToken = splitToken; + for (uint256 i; i < _tokens.length; ++i) { + _removeToken(_tokens[i], _splitToken); + } + } + + function setSplitToken(address _splitToken) external onlyGovernance { + require(_splitToken != address(0), "ZERO ADDRESS"); + // Set to same Trade Factory address but new split token + _setTradeFactory(tradeFactory(), _splitToken); + splitToken = _splitToken; + } + + // Send the split token to the Splitter contract. + function distribute() external { + ERC20(splitToken).safeTransfer( + splitter, + ERC20(splitToken).balanceOf(address(this)) - 1 + ); + } + + function _claimRewards() internal override {} + + // Claim the fees from the accountant + function claim(address _token) external onlyGovernance { + accountant.distribute(_token); + } + + function claim(address[] calldata _tokens) external onlyGovernance { + for (uint256 i; i < _tokens.length; ++i) { + accountant.distribute(_tokens[i]); + } + } + + function claim(address _token, uint256 _amount) external onlyGovernance { + accountant.distribute(_token, _amount); + } + + function sweep(address _token) external onlyGovernance { + address daddy = accountant.feeManager(); + ERC20(_token).safeTransfer( + daddy, + ERC20(_token).balanceOf(address(this)) + ); + } +} diff --git a/scripts/Deploy.s.sol b/scripts/Deploy.s.sol index e65bc19..064a9e2 100644 --- a/scripts/Deploy.s.sol +++ b/scripts/Deploy.s.sol @@ -16,8 +16,8 @@ contract Deploy is Script { // Append constructor args to the bytecode bytes memory bytecode = abi.encodePacked( - vm.getCode("registry/ReleaseRegistry.sol:ReleaseRegistry"), - abi.encode(initGov) + vm.getCode("splitter/Dumper.sol:Dumper"), + abi.encode() ); // Use salt of 0. diff --git a/src/splitter/Dumper.sol b/src/splitter/Dumper.sol new file mode 100644 index 0000000..ae33778 --- /dev/null +++ b/src/splitter/Dumper.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity >=0.8.18; + +import {Governance} from "@periphery/utils/Governance.sol"; +import {Accountant, ERC20, SafeERC20} from "../accountants/Accountant.sol"; + +interface IAuction { + function kick(address _token) external returns (uint256); +} + +contract Dumper is Governance { + using SafeERC20 for ERC20; + + modifier onlyAllowed() { + require(msg.sender == governance || allowed[msg.sender], "NOT ALLOWED"); + _; + } + + Accountant public immutable accountant; + + address public immutable splitter; + + address public splitToken; + + address public auction; + + mapping(address => bool) public allowed; + + constructor( + address _governance, + address _accountant, + address _splitter, + address _splitToken + ) Governance(_governance) { + require(_accountant != address(0), "ZERO ADDRESS"); + require(_splitter != address(0), "ZERO ADDRESS"); + require(_splitToken != address(0), "ZERO ADDRESS"); + accountant = Accountant(_accountant); + splitter = _splitter; + splitToken = _splitToken; + } + + // Send the split token to the Splitter contract. + function distribute() external { + ERC20(splitToken).safeTransfer( + splitter, + ERC20(splitToken).balanceOf(address(this)) - 1 + ); + } + + function dumpToken(address _token) external onlyAllowed { + _dumpToken(_token); + } + + function dumpTokens(address[] calldata _tokens) external onlyAllowed { + for (uint256 i; i < _tokens.length; ++i) { + _dumpToken(_tokens[i]); + } + } + + function _dumpToken(address _token) internal { + uint256 accountantBalance = ERC20(_token).balanceOf( + address(accountant) + ); + if (accountantBalance > 0) { + accountant.distribute(_token); + } + ERC20(_token).safeTransfer( + auction, + ERC20(_token).balanceOf(address(this)) - 1 + ); + IAuction(auction).kick(_token); + } + + // Claim the fees from the accountant + function claimToken(address _token) external onlyAllowed { + accountant.distribute(_token); + } + + function claimTokens(address[] calldata _tokens) external onlyAllowed { + for (uint256 i; i < _tokens.length; ++i) { + accountant.distribute(_tokens[i]); + } + } + + function claimToken(address _token, uint256 _amount) external onlyAllowed { + accountant.distribute(_token, _amount); + } + + function sweep(address _token) external onlyGovernance { + ERC20(_token).safeTransfer( + governance, + ERC20(_token).balanceOf(address(this)) + ); + } + + function setSplitToken(address _splitToken) external onlyGovernance { + require(_splitToken != address(0), "ZERO ADDRESS"); + splitToken = _splitToken; + } + + function setAuction(address _auction) external onlyGovernance { + auction = _auction; + } + + function setAllowed( + address _person, + bool _allowed + ) external onlyGovernance { + allowed[_person] = _allowed; + } +} diff --git a/src/test/splitter/TestSplitter.t.sol b/src/test/splitter/TestSplitter.t.sol index d2e5cd8..467858a 100644 --- a/src/test/splitter/TestSplitter.t.sol +++ b/src/test/splitter/TestSplitter.t.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.18; -import {Setup, ISplitter, ISplitterFactory, IVault, MockTokenized} from "../utils/Setup.sol"; +import {Setup, ISplitter, ISplitterFactory, IVault, Accountant, MockTokenized} from "../utils/Setup.sol"; + contract TestSplitter is Setup { event UpdateManagerRecipient(address indexed newManagerRecipient); @@ -12,7 +13,6 @@ contract TestSplitter is Setup { IVault public vault; MockTokenized public mockTokenized; - function setUp() public override { super.setUp(); (splitterFactory, splitter) = setupSplitter(); diff --git a/tests/splitter/test_dumper.py b/tests/splitter/test_dumper.py new file mode 100644 index 0000000..c1385ba --- /dev/null +++ b/tests/splitter/test_dumper.py @@ -0,0 +1,78 @@ +import ape +from ape import project + + +def test_dumper(asset, vault, accountant, daddy, brain, amount): + splitter = daddy + tf = brain.deploy(project.MockTradeFactory) + dumper = brain.deploy( + project.Dumper, brain, accountant.address, splitter, tf, vault + ) + accountant.setFeeRecipient(dumper, sender=daddy) + + assert dumper.governance() == brain + assert dumper.accountant() == accountant + assert dumper.tradeFactory() == tf + assert dumper.splitToken() == vault + assert dumper.rewardTokens() == [] + + with ape.reverts("!governance"): + dumper.addTokens([asset], sender=daddy) + + with ape.reverts("!governance"): + dumper.addToken(asset, sender=daddy) + + dumper.addTokens([asset], sender=brain) + + assert dumper.rewardTokens() == [asset] + + asset.mint(accountant, amount, sender=brain) + + assert asset.balanceOf(accountant) == amount + assert asset.balanceOf(dumper) == 0 + assert asset.balanceOf(splitter) == 0 + + with ape.reverts("!governance"): + dumper.claim([asset], sender=daddy) + + dumper.claim([asset], sender=brain) + + assert asset.balanceOf(accountant) == 0 + assert asset.balanceOf(dumper) == amount + assert vault.balanceOf(dumper) == 0 + assert asset.balanceOf(splitter) == 0 + assert vault.balanceOf(splitter) == 0 + + asset.transferFrom(dumper, tf, amount, sender=tf) + + assert asset.balanceOf(accountant) == 0 + assert asset.balanceOf(dumper) == 0 + assert asset.balanceOf(tf) == amount + + asset.approve(vault, amount, sender=tf) + vault.deposit(amount, dumper, sender=tf) + + assert asset.balanceOf(accountant) == 0 + assert asset.balanceOf(dumper) == 0 + assert vault.balanceOf(dumper) == amount + assert asset.balanceOf(splitter) == 0 + assert vault.balanceOf(splitter) == 0 + + dumper.distribute(sender=daddy) + + assert asset.balanceOf(accountant) == 0 + assert asset.balanceOf(dumper) == 0 + assert vault.balanceOf(dumper) == 1 + assert asset.balanceOf(splitter) == 0 + assert vault.balanceOf(splitter) == amount - 1 + + with ape.reverts("!governance"): + dumper.setSplitToken(asset, sender=daddy) + + dumper.setSplitToken(asset, sender=brain) + + assert dumper.governance() == brain + assert dumper.accountant() == accountant + assert dumper.tradeFactory() == tf + assert dumper.splitToken() == asset + assert dumper.rewardTokens() == [asset]