diff --git a/NOTE.md b/NOTE.md new file mode 100644 index 0000000..f01b9f1 --- /dev/null +++ b/NOTE.md @@ -0,0 +1,25 @@ +`InstanceProposer.sol` and `InstanceAdditionProposal.sol` have been rewritten/added. The difference in logic is basically the following: + +## Old + +The old proposal creator could create a multiple denomination addition proposal for only one token address. Ignoring all the dumb stuff like creation fees and parameters, the logic in `InstanceProposalCreator.sol` at line 129 had an issue where the protocol fee would not be checked if a Uniswap Pool instance was found beforehand. + +After above checks this new Proposal would be created, but this new Proposal only supported addition of 4 denominations which is limiting, in my opinion. Meaning one token, 4 denominations. + +## New + +With the new `InstanceProposer.sol`, a user can create as far as gas allows any amount of any token instances of any denomination as long as they fulfill the criteria specified in the contract. + +Most importantly the user MUST specify a variable which represents the exponent of the number 10 taken to this exponent (`10 ** exponent`), because we will be packing the data and need to reduce the size of it to be able to fit all of the data for one instance into one struct (`denomination / (10 ** exponent)`). + +This means that for a token, a user will choose a minimal denomination (with some assisted tooling, automatically, so they won't really be choosing it), and then the length of the trailing zeros is the value of the exponent which we are using. The denomination must be lower than `uint40`. This means that there is a 12 decimals window for the denominations. This means that the denominations can maximally look as such if we take a 4 instances example: + +`[1000000000000, 1000000000, 1000000, 1000]` + +Or to be more precise an example which covers the entire range in a 5 instance example: + +`[1099511627775, 1000000000, 1000000, 1000, 1]` + +As of yet, most important instances have varied their denominations by a factor of 10, so a factor of 1000 seems fine, especially when the only thing required is another proposal to add additional instances. + +In any case the function is understandable enough for a user to enter it over etherscan, although it might be tedious. diff --git a/contracts/InstanceAdditionProposal.sol b/contracts/InstanceAdditionProposal.sol new file mode 100644 index 0000000..0c6d756 --- /dev/null +++ b/contracts/InstanceAdditionProposal.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.7.6; +pragma abicoder v2; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import "./interfaces/IInstanceRegistry.sol"; +import "./interfaces/IInstanceFactory.sol"; + +contract InstanceAdditionProposal { + using SafeMath for uint256; + + IInstanceFactory public immutable instanceFactory; + IInstanceRegistry public immutable instanceRegistry; + + struct InstanceAdditionData { + address tokenAddress; + uint40 smallDenomination; + uint16 base10Exponent; + uint24 uniPoolSwappingFee; + uint16 protocolFee; + } + + InstanceAdditionData[] public toAdd; + + event AddInstanceForRegistry(address instance, address token, uint256 denomination); + + constructor(address _instanceFactory, address _instanceRegistry, InstanceAdditionData[] memory _toAdd) { + instanceFactory = IInstanceFactory(_instanceFactory); + instanceRegistry = IInstanceRegistry(_instanceRegistry); + + // Copying structs is not implemented + for (uint256 i = 0; i < _toAdd.length; i++) { + toAdd.push(_toAdd[i]); + } + } + + function executeProposal() external { + uint256 howMany = toAdd.length; + + for (uint256 i = 0; i < howMany; i++) { + InstanceAdditionData memory data = toAdd[i]; + + uint256 denomination = uint256(data.smallDenomination).mul(10 ** uint256(data.base10Exponent)); + + address instance = instanceFactory.createInstanceClone(denomination, data.tokenAddress); + + IInstanceRegistry.Instance memory newInstanceData = IInstanceRegistry.Instance( + data.tokenAddress != address(0), + IERC20(data.tokenAddress), + IInstanceRegistry.InstanceState.ENABLED, + data.uniPoolSwappingFee, + data.protocolFee + ); + + IInstanceRegistry.Tornado memory tornadoForUpdate = IInstanceRegistry.Tornado(instance, newInstanceData); + + instanceRegistry.updateInstance(tornadoForUpdate); + + emit AddInstanceForRegistry(address(instance), data.tokenAddress, denomination); + } + } +} diff --git a/contracts/InstanceProposalFactory.sol b/contracts/InstanceProposalFactory.sol new file mode 100644 index 0000000..72bb0df --- /dev/null +++ b/contracts/InstanceProposalFactory.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.7.6; +pragma abicoder v2; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts/drafts/IERC20Permit.sol"; +import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import { IUniswapV3PoolState } from "@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolState.sol"; +import { InstanceAdditionProposal } from "./InstanceAdditionProposal.sol"; + +/** + * @notice Contract which creates instance addition proposals. + * @dev In this version, no initializable is needed, and it was also a kind of bad initializable + * in the first place, because the variables which were set, do not have to be pseudo-immutable. + * + * Also in this version, there is no "creationFee", this was a bad idea in the first place by + * whoever implemented it, because anyone would just be able to fork this code in 1 hour as + * I did, and redeploy a free version. + */ +contract InstanceProposalFactory { + using SafeMath for uint256; + using Address for address; + + address public immutable weth; + address public immutable governance; + address public immutable instanceFactory; + address public immutable instanceRegistry; + IUniswapV3Factory public immutable UniswapV3Factory; + + uint16 public minObservationCardinality; + + event NewMinimumObservationCardinalitySet(uint256 newMinObservationCardinality); + event NewGovernanceProposalCreated(address indexed proposal); + + /** + * @dev Throws if called by any account other than the Governance. + */ + modifier onlyGovernance() { + require(governance == msg.sender, "IF: caller is not the Governance"); + _; + } + + /** + * @notice Contract constructor. + * @param _governance The TC governance address. + * @param _instanceFactory The factory address with which created proposals will use to deploy an instance. + * @param _instanceRegistry The registry address with which created proposals will use to register an instance. + * @param _UniswapV3Factory The UNIV3 Factory address with which we check TWAP cardinality. + * @param _wethAddress The address of the Wrapped Ether contract. + * @param _minObservationCardinality The minimum TWAP cardinality for a pool which we will accept. + */ + constructor( + address _governance, + address _instanceFactory, + address _instanceRegistry, + address _UniswapV3Factory, + address _wethAddress, + uint16 _minObservationCardinality + ) { + governance = _governance; + instanceFactory = _instanceFactory; + instanceRegistry = _instanceRegistry; + UniswapV3Factory = IUniswapV3Factory(_UniswapV3Factory); + minObservationCardinality = _minObservationCardinality; + weth = _wethAddress; + } + + /** + * @notice Create an instance addition proposal contract. + * @dev Note that here, Uniswap is being used as an anti-shitcoin oracle. Also, specifically in this version the upper limit on the protocol fee is 3%, I do not consider it very beneficial for anyone to have it above this number. + * The statement of the function: "I have some tokens for which there exist good uniswap pools, for which I would like to create [total number denominations] instances, of denominations which may be divided by a common exponent characteristic for each token's denomination group, and with a protocol fee which is reasonable (under 3%)." + * @param _tokenAddresses ERC20 token addresses to add to the proposal. + * @param _uniswapPoolSwappingFees Swapping fees for each token which are used to determine whether the observation cardinality of the Uniswap Pool is large enough. `3000` means 0.3% fee Uniswap pool. + * @param _denomBase10DivExponents These are the exponents of the (10**exp) divisor which is used to assure that the denomination may be represented as a number below 2^40 - 1. + * @param _denominations The denominations for each token. These will be stored as smallDenom = denomination / (10 ^ exponent) + * @param _protocolFees List of protocol fees for each new instance. `100` means that instance withdrawal fee is 1% of denomination. + * @param _totalNumberDenominations This is the total number of denominations, the sum of the lengths of subarrays of denominations. + */ + function createProposalContract( + address[] calldata _tokenAddresses, + uint24[] calldata _uniswapPoolSwappingFees, + uint16[] calldata _denomBase10DivExponents, + uint256[][] calldata _denominations, + uint16[][] calldata _protocolFees, + uint256 _totalNumberDenominations + ) external returns (address) { + InstanceAdditionProposal.InstanceAdditionData[] memory toAdd = new InstanceAdditionProposal.InstanceAdditionData[]( + _totalNumberDenominations + ); + + uint256 toAddSize = 0; + + require(0 < _tokenAddresses.length, "No tokens specified"); + + // 1️⃣ Each token must be ether, or a contract with a uniswap pool of a minimum cardinality. + + for (uint256 t = 0; t < _tokenAddresses.length; t++) { + address token = _tokenAddresses[t]; + + // 2️⃣ The swapping fees must have an element at t. + // We do not care if there is more swapping fees than tokens + + uint24 swappingFee = _uniswapPoolSwappingFees[t]; + + // 3️⃣ Fails if the token is not a contract or if there is no pool. + + if (token != address(0)) { + require(token.isContract(), "Token must be a contract if not addr(0)"); + + address poolAddress = UniswapV3Factory.getPool(token, weth, swappingFee); + + require(poolAddress != address(0), "A Uniswap V3 pool does not exist for this token."); + + (, , , , uint16 observationCardinalityNext, , ) = IUniswapV3PoolState(poolAddress).slot0(); + + require(minObservationCardinality <= observationCardinalityNext, "Uniswap Pool observation cardinality is low."); + } + + // 4️⃣ Denominations, div exponent and protocol fees must have an element at t(oken). + + uint256[] memory denominations = _denominations[t]; + + uint16 exponent = _denomBase10DivExponents[t]; + + require(0 < exponent, "Exponent is 0."); + + uint16[] memory protocolFees = _protocolFees[t]; + + // 5️⃣ The denominations element must never have 0 length. + + uint256 numDenoms = denominations.length; + + require(0 < numDenoms, "There is no denominations for some token"); + + // 6️⃣ All denominations divided by the exponent must not be 0, and there must be a reasonable protocol fee for each. + + for (uint256 d = 0; d < numDenoms; d++) { + // 7️⃣ protocolFee must have an element at d and be appropriate. + // We do not care if there is more fees than denominations + + uint16 protocolFee = protocolFees[d]; + + require(protocolFee <= 300, "Protocol fee is more than 3%!"); + + // 8️⃣ The denomination must not be 0 post division. + + uint40 smallDenomination = uint40(denominations[d].div(10 ** exponent)); + + require(0 < smallDenomination, "When divided, denom is 0"); + + // 9️⃣ If all of the above are fine, add the packed struct to the memory array. + + toAdd[toAddSize] = InstanceAdditionProposal.InstanceAdditionData({ + tokenAddress: token, + smallDenomination: smallDenomination, + base10Exponent: exponent, + uniPoolSwappingFee: swappingFee, + protocolFee: protocolFee + }); + + toAddSize++; + } + } + + // 🔟 The number of elements added must be _totalNumberDenominations. + + require(toAddSize == _totalNumberDenominations, "Wrong total number of denominations."); + + // 1️⃣1️⃣ Finish and return. + + address proposal = address(new InstanceAdditionProposal(instanceFactory, instanceRegistry, toAdd)); + + emit NewGovernanceProposalCreated(proposal); + + return proposal; + } + + function setMinObservationCardinality(uint16 _newMinObservationCardinality) external onlyGovernance { + minObservationCardinality = _newMinObservationCardinality; + emit NewMinimumObservationCardinalitySet(_newMinObservationCardinality); + } +} diff --git a/contracts/InstanceProposer.sol b/contracts/InstanceProposer.sol deleted file mode 100644 index 301a62d..0000000 --- a/contracts/InstanceProposer.sol +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.7.6; -pragma abicoder v2; - -import { Address } from "@openzeppelin/contracts/utils/Address.sol"; -import "./AddInstanceProposal.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IERC20Permit } from "@openzeppelin/contracts/drafts/IERC20Permit.sol"; -import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; -import { IUniswapV3PoolState } from "@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolState.sol"; - -/** - * @notice Contract which creates instance addition proposals. - * @dev In this version, no initializable is needed, and it was also a kind of bad initializable - * in the first place, because the variables which were set, do not have to be pseudo-immutable. - * - * Also in this version, there is no "creationFee", this was a bad idea in the first place by - * whoever implemented it, because anyone would just be able to fork this code in 1 hour as - * I did, and redeploy a free version. - */ -contract InstanceProposer { - using Address for address; - - address public immutable WETH; - address public immutable governance; - address public immutable instanceFactory; - address public immutable instanceRegistry; - IUniswapV3Factory public immutable UniswapV3Factory; - - uint16 public TWAPSlotsMin; - - event NewCreationFeeSet(uint256 newCreationFee); - event NewTWAPSlotsMinSet(uint256 newTWAPSlotsMin); - event NewGovernanceProposalCreated(address indexed proposal); - - /** - * @dev Throws if called by any account other than the Governance. - */ - modifier onlyGovernance() { - require(governance == msg.sender, "IF: caller is not the Governance"); - _; - } - - /** - * @notice Contract constructor. - * @param _governance The TC governance address. - * @param _instanceFactory The factory address with which created proposals will use to deploy an instance. - * @param _instanceRegistry The registry address with which created proposals will use to register an instance. - * @param _UniswapV3Factory The UNIV3 Factory address with which we check TWAP cardinality. - * @param _WETH The address of the Wrapped Ether contract. - * @param _TWAPSlotsMin The minimum TWAP cardinality for a pool which we will accept. - */ - constructor( - address _governance, - address _instanceFactory, - address _instanceRegistry, - address _UniswapV3Factory, - address _WETH, - uint16 _TWAPSlotsMin - ) { - governance = _governance; - instanceFactory = _instanceFactory; - instanceRegistry = _instanceRegistry; - UniswapV3Factory = IUniswapV3Factory(_UniswapV3Factory); - WETH = _WETH; - TWAPSlotsMin = _TWAPSlotsMin; - } - - /** - * @notice Create an instance addition proposal. - * @dev Note that here, Uniswap is being used as an anti-shitcoin oracle. - * Also, specifically in this version the upper limit on the protocol fee is 1%, I do not - * consider it very beneficial for anyone to have it above this number. - * @param _token address of ERC20 token for a new instance - * @param _uniswapPoolSwappingFee fee value of Uniswap instance which will be used for `TORN/token` - * price determination. `3000` means 0.3% fee Uniswap pool. - * @param _denominations list of denominations for each new instance - * @param _protocolFees list of protocol fees for each new instance. - * `100` means that instance withdrawal fee is 1% of denomination. - */ - function createProposal( - address _token, - uint24 _uniswapPoolSwappingFee, - uint256[] memory _denominations, - uint32[] memory _protocolFees - ) external returns (address) { - require(_token == address(0) || _token.isContract(), "Token is not contract"); - require(_denominations.length > 0, "Empty denominations"); - require(_denominations.length == _protocolFees.length, "Incorrect denominations/fees length"); - - // check Uniswap Pool - for (uint8 i = 0; i < _protocolFees.length; i++) { - require(_protocolFees[i] <= 100, "Protocol fee is more than 1%"); - if (_protocolFees[i] > 0 && _token != address(0)) { - // pool exists - address poolAddr = UniswapV3Factory.getPool(_token, WETH, _uniswapPoolSwappingFee); - require(poolAddr != address(0), "Uniswap pool is not exist"); - // TWAP slots - (, , , , uint16 observationCardinalityNext, , ) = IUniswapV3PoolState(poolAddr).slot0(); - require(observationCardinalityNext >= TWAPSlotsMin, "Uniswap pool TWAP slots number is low"); - break; - } - } - - address proposal = address( - new AddInstanceProposal(instanceFactory, instanceRegistry, _token, _uniswapPoolSwappingFee, _denominations, _protocolFees) - ); - - emit NewGovernanceProposalCreated(proposal); - return proposal; - } - - function setTWAPSlotsMin(uint16 _TWAPSlotsMin) external onlyGovernance { - TWAPSlotsMin = _TWAPSlotsMin; - emit NewTWAPSlotsMinSet(_TWAPSlotsMin); - } -}