├── .env.example ├── .gitignore ├── .mocharc.json ├── .nvmrc ├── .prettierrc.yaml ├── .solcover.js ├── .solhint.json ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.MD ├── addresses.json ├── audits ├── Code4rena_Nested_Analysis_Report_2021_12.pdf ├── Code4rena_Nested_Analysis_Report_2022_04.pdf ├── Code4rena_Nested_Analysis_Report_2022_06.md ├── PeckShield-Audit-Report-Nested-v1.0.pdf └── Red4Sec_Nested_Security_Audit_Report_v3.pdf ├── contracts ├── FeeSplitter.sol ├── NestedAsset.sol ├── NestedBuybacker.sol ├── NestedFactory.sol ├── NestedRecords.sol ├── NestedReserve.sol ├── OperatorResolver.sol ├── Withdrawer.sol ├── abstracts │ ├── MixinOperatorResolver.sol │ ├── OwnableFactoryHandler.sol │ └── OwnableProxyDelegation.sol ├── governance │ ├── OwnerProxy.sol │ ├── TimelockControllerEmergency.sol │ └── scripts │ │ ├── OperatorScripts.sol │ │ ├── SingleCall.sol │ │ └── UpdateFees.sol ├── interfaces │ ├── INestedFactory.sol │ ├── IOperatorResolver.sol │ └── external │ │ ├── IBeefyVaultV6.sol │ │ ├── IBiswapPair.sol │ │ ├── IBiswapRouter02.sol │ │ ├── ICurvePool │ │ ├── ICurvePool.sol │ │ ├── ICurvePoolETH.sol │ │ └── ICurvePoolNonETH.sol │ │ ├── INestedToken.sol │ │ ├── IStakingVault │ │ ├── IStakeDaoStrategy.sol │ │ ├── IStakingVault.sol │ │ └── IYearnVault.sol │ │ ├── ITransparentUpgradeableProxy.sol │ │ └── IWETH.sol ├── libraries │ ├── CurveHelpers │ │ ├── CurveHelpers.sol │ │ └── README.md │ ├── ExchangeHelpers.sol │ ├── OperatorHelpers.sol │ └── StakingLPVaultHelpers.sol ├── mocks │ ├── AugustusSwapper.sol │ ├── DeflationaryMockERC20.sol │ ├── DummyRouter.sol │ ├── FailedDeploy.sol │ ├── MockERC20.sol │ ├── MockSmartChef.sol │ ├── TestableMixingOperatorResolver.sol │ ├── TestableOperatorCaller.sol │ ├── TokenTransferProxy.sol │ └── WETHMock.sol ├── operators │ ├── Beefy │ │ ├── BeefyVaultOperator.sol │ │ ├── BeefyVaultStorage.sol │ │ └── lp │ │ │ ├── BeefyZapBiswapLPVaultOperator.sol │ │ │ ├── BeefyZapUniswapLPVaultOperator.sol │ │ │ └── README.md │ ├── Flat │ │ ├── FlatOperator.sol │ │ ├── IFlatOperator.sol │ │ └── README.md │ ├── Paraswap │ │ ├── IParaswapOperator.sol │ │ └── ParaswapOperator.sol │ ├── StakeDAO │ │ ├── StakeDaoCurveStrategyOperator.sol │ │ └── StakeDaoStrategyStorage.sol │ ├── Yearn │ │ ├── YearnCurveVaultOperator.sol │ │ └── YearnVaultStorage.sol │ └── ZeroEx │ │ ├── IZeroExOperator.sol │ │ ├── README.md │ │ ├── ZeroExOperator.sol │ │ └── ZeroExStorage.sol └── utils │ └── NestedAssetBatcher.sol ├── hardhat.config.ts ├── package.json ├── scripts ├── deployAll.ts ├── deployAllWithoutProxy.ts ├── deployFactory.ts ├── deployFullFactory.ts ├── deployNestedAssetBatcher.ts ├── deployOwnerProxy.ts ├── deployTimelockWithEmergency.ts ├── op_scripts │ ├── deployOperatorScripts.ts │ ├── removeOperator.ts │ ├── setUnrevealedTokenUri.ts │ ├── transferOwnership.ts │ └── upgradeProxy.ts ├── operators │ ├── FlatOperator │ │ └── generateCalldata.ts │ └── ParaswapOperator │ │ ├── generateCalldata.ts │ │ └── verify.ts ├── reachNonce.ts ├── setEntryAndExitFees.ts ├── tenderly.ts └── utils.ts ├── static ├── input-orders.png ├── output-orders.png ├── ownership.png └── processInputOrders.png ├── test ├── helpers.ts ├── shared │ ├── actors.ts │ ├── fixtures.ts │ ├── impersonnate.ts │ └── provider.ts ├── types.ts └── unit │ ├── BeefyVaultOperator.unit.ts │ ├── BeefyZapBiswapLPVaultOperator.ts │ ├── BeefyZapUniswapLPVaultOperator.ts │ ├── ExchangeHelpers.unit.ts │ ├── FeeSplitter.unit.ts │ ├── FlatOperator.unit.ts │ ├── NestedAsset.unit.ts │ ├── NestedAssetBatcher.unit.ts │ ├── NestedBuyBacker.unit.ts │ ├── NestedFactory.unit.ts │ ├── NestedRecords.unit.ts │ ├── NestedReserve.unit.ts │ ├── OperatorResolver.unit.ts │ ├── OwnableFactoryHandler.unit.ts │ ├── OwnerProxy.unit.ts │ ├── ParaswapOperator.unit.ts │ ├── StakeDaoCurveStrategyOperator.unit.ts │ ├── YearnCurveVaultOperator.unit.ts │ └── ZeroExOperator.unit.ts ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | ALCHEMY_ROPSTEN_API_KEY="" 2 | ALCHEMY_KOVAN_API_KEY="" 3 | ALCHEMY_MAINNET_API_KEY="" 4 | ACCOUNT_PRIVATE_KEY="" 5 | MNEMONIC="" 6 | FORKING="true" 7 | COINMARKETCAP_API_KEY="" 8 | REPORT_GAS="false" 9 | ETHERSCAN_API_KEY="" 10 | 11 | FORKING="false" 12 | 13 | # BSC fork config 14 | FORK_CHAINID="56" 15 | FORK_URL="https://bsc-dataseed.binance.org/" 16 | 17 | # ETH fork config 18 | # FORK_CHAINID="1" 19 | # FORK_URL="" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn-error.log 3 | .env 4 | .DS_Store 5 | .idea 6 | 7 | coverage.json 8 | coverage/ 9 | artifacts/ 10 | cache/ 11 | typechain/ 12 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "hardhat/register", 3 | "timeout": 30000 4 | } -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/erbium 2 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | singleQuote: false 3 | trailingComma: all 4 | arrowParens: avoid 5 | tabWidth: 4 6 | bracketSpacing: true 7 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mocha: { 3 | timeout: 30000, 4 | }, 5 | skipFiles: ["contracts/mocks/", "contracts/interfaces/"], 6 | } 7 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "avoid-suicide": "error", 6 | "avoid-sha3": "warn", 7 | "code-complexity": ["warn", 7], 8 | "compiler-version": "off", 9 | "max-states-count": ["error", 18], 10 | "max-line-length": ["warn", 145], 11 | "not-rely-on-time": "warn", 12 | "quotes": ["warn", "double"], 13 | "prettier/prettier": "off", 14 | "func-visibility": ["warn",{"ignoreConstructors":true}] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Debug Mocha Tests", 5 | "type": "node", 6 | "request": "attach", 7 | "port": 9287, 8 | "protocol": "inspector", 9 | "timeout": 30000, 10 | "smartStep": false, 11 | "sourceMaps": true, 12 | "sourceMapPathOverrides": { 13 | "webpack-internal:///./*": "${workspaceRoot}/*", 14 | "webpack-internal:///*": "*", 15 | "webpack:///./~/*": "${workspaceRoot}/node_modules/*", 16 | "webpack:///./*": "${workspaceRoot}/*", 17 | "webpack:///*": "*" 18 | }, 19 | "skipFiles": [ 20 | "/*", 21 | "/*.js", 22 | "node_modules/**", 23 | "**hbenl.vscode-mocha-test-adapter-**", 24 | "**hbenl.vscode-mocha-test-adapter-**/node_modules", 25 | ] 26 | } 27 | ] 28 | 29 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "mochaExplorer.require": "hardhat/register", 3 | "mochaExplorer.files": "test/**/*.ts", 4 | "mochaExplorer.debuggerConfig": "Debug Mocha Tests", 5 | "mochaExplorer.debuggerPort": 9287, 6 | "editor.formatOnSave": true, 7 | "solidity.formatter": "prettier", 8 | "[solidity]": { 9 | "editor.defaultFormatter": "JuanBlanco.solidity" 10 | } 11 | } -------------------------------------------------------------------------------- /audits/Code4rena_Nested_Analysis_Report_2021_12.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MassDotMoney/nested-core-lego/c1eb3de913bf9b29a09015c10f4690ebd9632c54/audits/Code4rena_Nested_Analysis_Report_2021_12.pdf -------------------------------------------------------------------------------- /audits/Code4rena_Nested_Analysis_Report_2022_04.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MassDotMoney/nested-core-lego/c1eb3de913bf9b29a09015c10f4690ebd9632c54/audits/Code4rena_Nested_Analysis_Report_2022_04.pdf -------------------------------------------------------------------------------- /audits/PeckShield-Audit-Report-Nested-v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MassDotMoney/nested-core-lego/c1eb3de913bf9b29a09015c10f4690ebd9632c54/audits/PeckShield-Audit-Report-Nested-v1.0.pdf -------------------------------------------------------------------------------- /audits/Red4Sec_Nested_Security_Audit_Report_v3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MassDotMoney/nested-core-lego/c1eb3de913bf9b29a09015c10f4690ebd9632c54/audits/Red4Sec_Nested_Security_Audit_Report_v3.pdf -------------------------------------------------------------------------------- /contracts/NestedAsset.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 5 | import "@openzeppelin/contracts/utils/Counters.sol"; 6 | import "./abstracts/OwnableFactoryHandler.sol"; 7 | 8 | /// @title Collection of NestedNFTs used to represent ownership of real assets stored in NestedReserves 9 | /// @dev Only NestedFactory contracts are allowed to call functions that write to storage 10 | contract NestedAsset is ERC721Enumerable, OwnableFactoryHandler { 11 | using Counters for Counters.Counter; 12 | 13 | /* ----------------------------- VARIABLES ----------------------------- */ 14 | 15 | Counters.Counter private _tokenIds; 16 | 17 | /// @dev Base URI (API) 18 | string public baseUri; 19 | 20 | /// @dev Token URI when not revealed 21 | string public unrevealedTokenUri; 22 | 23 | /// @dev NFT contract URI 24 | string public contractUri; 25 | 26 | /// @dev Stores the original asset of each asset 27 | mapping(uint256 => uint256) public originalAsset; 28 | 29 | /// @dev Stores owners of burnt assets 30 | mapping(uint256 => address) public lastOwnerBeforeBurn; 31 | 32 | /// @dev True if revealed, false if not. 33 | bool public isRevealed; 34 | 35 | /* ---------------------------- CONSTRUCTORS --------------------------- */ 36 | 37 | constructor() ERC721("NestedNFT", "NESTED") {} 38 | 39 | /* ----------------------------- MODIFIERS ----------------------------- */ 40 | 41 | /// @dev Reverts the transaction if the address is not the token owner 42 | modifier onlyTokenOwner(address _address, uint256 _tokenId) { 43 | require(_address == ownerOf(_tokenId), "NA: FORBIDDEN_NOT_OWNER"); 44 | _; 45 | } 46 | 47 | /* ------------------------------- VIEWS ------------------------------- */ 48 | 49 | /// @notice Get the Uniform Resource Identifier (URI) for `tokenId` token. 50 | /// @param _tokenId The id of the NestedAsset 51 | /// @return The token Uniform Resource Identifier (URI) 52 | function tokenURI(uint256 _tokenId) public view virtual override returns (string memory) { 53 | require(_exists(_tokenId), "URI query for nonexistent token"); 54 | if (isRevealed) { 55 | return super.tokenURI(_tokenId); 56 | } else { 57 | return unrevealedTokenUri; 58 | } 59 | } 60 | 61 | /// @inheritdoc ERC721 62 | function _baseURI() internal view override returns (string memory) { 63 | return baseUri; 64 | } 65 | 66 | /// @notice Returns the owner of the original token if the token was replicated 67 | /// If the original asset was burnt, the last owner before burn is returned 68 | /// @param _tokenId The asset for which we want to know the original owner 69 | /// @return The owner of the original asset 70 | function originalOwner(uint256 _tokenId) external view returns (address) { 71 | uint256 originalAssetId = originalAsset[_tokenId]; 72 | 73 | if (originalAssetId != 0) { 74 | return _exists(originalAssetId) ? ownerOf(originalAssetId) : lastOwnerBeforeBurn[originalAssetId]; 75 | } 76 | return address(0); 77 | } 78 | 79 | /* ---------------------------- ONLY FACTORY --------------------------- */ 80 | 81 | /// @notice Mints an ERC721 token for the user and stores the original asset used to create the new asset if any 82 | /// @param _owner The account address that signed the transaction 83 | /// @param _replicatedTokenId The token id of the replicated asset, 0 if no replication 84 | /// @return The minted token's id 85 | function mint(address _owner, uint256 _replicatedTokenId) public onlyFactory returns (uint256) { 86 | _tokenIds.increment(); 87 | 88 | uint256 tokenId = _tokenIds.current(); 89 | _safeMint(_owner, tokenId); 90 | 91 | // Stores the first asset of the replication chain as the original 92 | if (_replicatedTokenId == 0) { 93 | return tokenId; 94 | } 95 | 96 | require(_exists(_replicatedTokenId), "NA: NON_EXISTENT_TOKEN_ID"); 97 | require(tokenId != _replicatedTokenId, "NA: SELF_DUPLICATION"); 98 | 99 | uint256 originalTokenId = originalAsset[_replicatedTokenId]; 100 | originalAsset[tokenId] = originalTokenId != 0 ? originalTokenId : _replicatedTokenId; 101 | 102 | return tokenId; 103 | } 104 | 105 | /// @notice Burns an ERC721 token 106 | /// @param _owner The account address that signed the transaction 107 | /// @param _tokenId The id of the NestedAsset 108 | function burn(address _owner, uint256 _tokenId) external onlyFactory onlyTokenOwner(_owner, _tokenId) { 109 | lastOwnerBeforeBurn[_tokenId] = _owner; 110 | _burn(_tokenId); 111 | } 112 | 113 | /* ----------------------------- ONLY OWNER ---------------------------- */ 114 | 115 | /// @notice Update isRevealed to reveal or hide the token URI 116 | function setIsRevealed(bool _isRevealed) external onlyOwner { 117 | isRevealed = _isRevealed; 118 | } 119 | 120 | /// @notice Set the base URI (once revealed) 121 | /// @param _baseUri The new baseURI 122 | function setBaseURI(string memory _baseUri) external onlyOwner { 123 | require(bytes(_baseUri).length != 0, "NA: EMPTY_URI"); 124 | baseUri = _baseUri; 125 | } 126 | 127 | /// @notice Set the unrevealed token URI (fixed) 128 | /// @param _newUri The new unrevealed URI 129 | function setUnrevealedTokenURI(string memory _newUri) external onlyOwner { 130 | require(bytes(_newUri).length != 0, "NA: EMPTY_URI"); 131 | unrevealedTokenUri = _newUri; 132 | } 133 | 134 | /// @notice Set the contract URI 135 | /// @param _newUri The new contract URI 136 | function setContractURI(string memory _newUri) external onlyOwner { 137 | contractUri = _newUri; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /contracts/NestedBuybacker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | import "./interfaces/external/INestedToken.sol"; 8 | import "./FeeSplitter.sol"; 9 | import "./libraries/ExchangeHelpers.sol"; 10 | 11 | /// @title Token sent to this contract are used to purchase NST. 12 | /// @dev Some of it is burned, the rest is sent to a pool that will redistribute 13 | /// to the NST ecosystem and community. 14 | contract NestedBuybacker is Ownable { 15 | /// @dev Emitted when the reserve address is updated 16 | /// @param newReserve The new reserve address 17 | event ReserveUpdated(address newReserve); 18 | 19 | /// @dev Emitted when the fee splitter address is updated 20 | /// @param newFeeSplitter The new FeeSplitter address 21 | event FeeSplitterUpdated(FeeSplitter newFeeSplitter); 22 | 23 | /// @dev Emitted when the burn percentage is updated 24 | /// @param newBurnPart The new burn percentage amount 25 | event BurnPartUpdated(uint256 newBurnPart); 26 | 27 | /// @dev Emitted when the buy back is executed 28 | /// @param forToken sellToken used for the buy back 29 | event BuybackTriggered(IERC20 forToken); 30 | 31 | /// @dev The Nested project token 32 | INestedToken public immutable NST; 33 | 34 | /// @dev Current address where user assets are stored 35 | address public nstReserve; 36 | 37 | /// @dev Current fee splitter address 38 | FeeSplitter public feeSplitter; 39 | 40 | /// @dev Part of the bought tokens to be burned (100% = 1000) 41 | uint256 public burnPercentage; 42 | 43 | receive() external payable {} 44 | 45 | constructor( 46 | address _NST, 47 | address _nstReserve, 48 | address payable _feeSplitter, 49 | uint256 _burnPercentage 50 | ) { 51 | require(_burnPercentage <= 1000, "NB: INVALID_BURN_PART"); 52 | require(_NST != address(0) && _nstReserve != address(0) && _feeSplitter != address(0), "NB: INVALID_ADDRESS"); 53 | burnPercentage = _burnPercentage; 54 | NST = INestedToken(_NST); 55 | feeSplitter = FeeSplitter(_feeSplitter); 56 | nstReserve = _nstReserve; 57 | } 58 | 59 | /// @notice Update the nested reserve address 60 | /// @param _nstReserve New reserve contract address 61 | function setNestedReserve(address _nstReserve) external onlyOwner { 62 | require(_nstReserve != address(0), "NB: INVALID_ADDRESS"); 63 | nstReserve = _nstReserve; 64 | emit ReserveUpdated(nstReserve); 65 | } 66 | 67 | /// @notice Update the fee splitter address 68 | /// @param _feeSplitter The new fee splitter contract address 69 | function setFeeSplitter(FeeSplitter _feeSplitter) external onlyOwner { 70 | require(address(_feeSplitter) != address(0), "NB: INVALID_ADDRESS"); 71 | feeSplitter = _feeSplitter; 72 | emit FeeSplitterUpdated(feeSplitter); 73 | } 74 | 75 | /// @notice Update parts deciding what amount is sent to reserve or burned 76 | /// @param _burnPercentage The new burn percentage 77 | function setBurnPart(uint256 _burnPercentage) external onlyOwner { 78 | require(_burnPercentage <= 1000, "NB: INVALID_BURN_PART"); 79 | burnPercentage = _burnPercentage; 80 | emit BurnPartUpdated(burnPercentage); 81 | } 82 | 83 | /// @notice Triggers the purchase of NST sent to reserve and burn 84 | /// @param _swapCallData Call data provided by 0x to fill quotes 85 | /// @param _swapTarget Target contract for the swap (could be Uniswap router for example) 86 | /// @param _sellToken Token to sell in order to buy NST 87 | function triggerForToken( 88 | bytes calldata _swapCallData, 89 | address payable _swapTarget, 90 | IERC20 _sellToken 91 | ) external onlyOwner { 92 | if (feeSplitter.getAmountDue(address(this), _sellToken) != 0) { 93 | IERC20[] memory tokens = new IERC20[](1); 94 | tokens[0] = _sellToken; 95 | feeSplitter.releaseTokensNoETH(tokens); 96 | } 97 | 98 | require(ExchangeHelpers.fillQuote(_sellToken, _swapTarget, _swapCallData), "NB : FAILED_SWAP"); 99 | trigger(); 100 | emit BuybackTriggered(_sellToken); 101 | } 102 | 103 | /// @dev burns part of the bought NST and send the rest to the reserve 104 | function trigger() internal { 105 | uint256 balance = NST.balanceOf(address(this)); 106 | uint256 toBurn = (balance * burnPercentage) / 1000; 107 | uint256 toSendToReserve = balance - toBurn; 108 | NST.burn(toBurn); 109 | SafeERC20.safeTransfer(NST, nstReserve, toSendToReserve); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /contracts/NestedRecords.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./abstracts/OwnableFactoryHandler.sol"; 5 | 6 | /// @title Tracks data for underlying assets of NestedNFTs 7 | contract NestedRecords is OwnableFactoryHandler { 8 | /* ------------------------------ EVENTS ------------------------------ */ 9 | 10 | /// @dev Emitted when maxHoldingsCount is updated 11 | /// @param maxHoldingsCount The new value 12 | event MaxHoldingsChanges(uint256 maxHoldingsCount); 13 | 14 | /// @dev Emitted when the lock timestamp of an NFT is increased 15 | /// @param nftId The NFT ID 16 | /// @param timestamp The new lock timestamp of the portfolio 17 | event LockTimestampIncreased(uint256 nftId, uint256 timestamp); 18 | 19 | /// @dev Emitted when the reserve is updated for a specific portfolio 20 | /// @param nftId The NFT ID 21 | /// @param newReserve The new reserve address 22 | event ReserveUpdated(uint256 nftId, address newReserve); 23 | 24 | /* ------------------------------ STRUCTS ------------------------------ */ 25 | 26 | /// @dev Store user asset informations 27 | struct NftRecord { 28 | mapping(address => uint256) holdings; 29 | address[] tokens; 30 | address reserve; 31 | uint256 lockTimestamp; 32 | } 33 | 34 | /* ----------------------------- VARIABLES ----------------------------- */ 35 | 36 | /// @dev stores for each NFT ID an asset record 37 | mapping(uint256 => NftRecord) public records; 38 | 39 | /// @dev The maximum number of holdings for an NFT record 40 | uint256 public maxHoldingsCount; 41 | 42 | /* ---------------------------- CONSTRUCTOR ---------------------------- */ 43 | 44 | constructor(uint256 _maxHoldingsCount) { 45 | maxHoldingsCount = _maxHoldingsCount; 46 | } 47 | 48 | /* -------------------------- OWNER FUNCTIONS -------------------------- */ 49 | 50 | /// @notice Sets the maximum number of holdings for an NFT record 51 | /// @param _maxHoldingsCount The new maximum number of holdings 52 | function setMaxHoldingsCount(uint256 _maxHoldingsCount) external onlyOwner { 53 | require(_maxHoldingsCount != 0, "NRC: INVALID_MAX_HOLDINGS"); 54 | maxHoldingsCount = _maxHoldingsCount; 55 | emit MaxHoldingsChanges(maxHoldingsCount); 56 | } 57 | 58 | /* ------------------------- FACTORY FUNCTIONS ------------------------- */ 59 | 60 | /// @notice Update the amount for a specific holding and delete 61 | /// the holding if the amount is zero. 62 | /// @param _nftId The id of the NFT 63 | /// @param _token The token/holding address 64 | /// @param _amount Updated amount for this asset 65 | function updateHoldingAmount( 66 | uint256 _nftId, 67 | address _token, 68 | uint256 _amount 69 | ) public onlyFactory { 70 | if (_amount == 0) { 71 | uint256 tokenIndex = 0; 72 | address[] memory tokens = getAssetTokens(_nftId); 73 | while (tokenIndex < tokens.length) { 74 | if (tokens[tokenIndex] == _token) { 75 | deleteAsset(_nftId, tokenIndex); 76 | break; 77 | } 78 | tokenIndex++; 79 | } 80 | } else { 81 | records[_nftId].holdings[_token] = _amount; 82 | } 83 | } 84 | 85 | /// @notice Fully delete a holding record for an NFT 86 | /// @param _nftId The id of the NFT 87 | /// @param _tokenIndex The token index in holdings array 88 | function deleteAsset(uint256 _nftId, uint256 _tokenIndex) public onlyFactory { 89 | address[] storage tokens = records[_nftId].tokens; 90 | address token = tokens[_tokenIndex]; 91 | 92 | require(records[_nftId].holdings[token] != 0, "NRC: HOLDING_INACTIVE"); 93 | 94 | delete records[_nftId].holdings[token]; 95 | tokens[_tokenIndex] = tokens[tokens.length - 1]; 96 | tokens.pop(); 97 | } 98 | 99 | /// @notice Delete a holding item in holding mapping. Does not remove token in NftRecord.tokens array 100 | /// @param _nftId NFT's identifier 101 | /// @param _token Token address for holding to remove 102 | function freeHolding(uint256 _nftId, address _token) public onlyFactory { 103 | delete records[_nftId].holdings[_token]; 104 | } 105 | 106 | /// @notice Helper function that creates a record or add the holding if record already exists 107 | /// @param _nftId The NFT's identifier 108 | /// @param _token The token/holding address 109 | /// @param _amount Amount to add for this asset 110 | /// @param _reserve Reserve address 111 | function store( 112 | uint256 _nftId, 113 | address _token, 114 | uint256 _amount, 115 | address _reserve 116 | ) external onlyFactory { 117 | NftRecord storage _nftRecord = records[_nftId]; 118 | 119 | uint256 amount = records[_nftId].holdings[_token]; 120 | require(_amount != 0, "NRC: INVALID_AMOUNT"); 121 | if (amount != 0) { 122 | require(_nftRecord.reserve == _reserve, "NRC: RESERVE_MISMATCH"); 123 | updateHoldingAmount(_nftId, _token, amount + _amount); 124 | return; 125 | } 126 | require(_nftRecord.tokens.length < maxHoldingsCount, "NRC: TOO_MANY_TOKENS"); 127 | require( 128 | _reserve != address(0) && (_reserve == _nftRecord.reserve || _nftRecord.reserve == address(0)), 129 | "NRC: INVALID_RESERVE" 130 | ); 131 | 132 | _nftRecord.holdings[_token] = _amount; 133 | _nftRecord.tokens.push(_token); 134 | _nftRecord.reserve = _reserve; 135 | } 136 | 137 | /// @notice The factory can update the lock timestamp of a NFT record 138 | /// The new timestamp must be greater than the records lockTimestamp 139 | // if block.timestamp > actual lock timestamp 140 | /// @param _nftId The NFT id to get the record 141 | /// @param _timestamp The new timestamp 142 | function updateLockTimestamp(uint256 _nftId, uint256 _timestamp) external onlyFactory { 143 | require(_timestamp > records[_nftId].lockTimestamp, "NRC: LOCK_PERIOD_CANT_DECREASE"); 144 | records[_nftId].lockTimestamp = _timestamp; 145 | emit LockTimestampIncreased(_nftId, _timestamp); 146 | } 147 | 148 | /// @notice Delete from mapping assetTokens 149 | /// @param _nftId The id of the NFT 150 | function removeNFT(uint256 _nftId) external onlyFactory { 151 | delete records[_nftId]; 152 | } 153 | 154 | /// @notice Set the reserve where assets are stored 155 | /// @param _nftId The NFT ID to update 156 | /// @param _nextReserve Address for the new reserve 157 | function setReserve(uint256 _nftId, address _nextReserve) external onlyFactory { 158 | records[_nftId].reserve = _nextReserve; 159 | emit ReserveUpdated(_nftId, _nextReserve); 160 | } 161 | 162 | /* ------------------------------- VIEWS ------------------------------- */ 163 | 164 | /// @notice Get content of assetTokens mapping 165 | /// @param _nftId The id of the NFT 166 | /// @return Array of token addresses 167 | function getAssetTokens(uint256 _nftId) public view returns (address[] memory) { 168 | return records[_nftId].tokens; 169 | } 170 | 171 | /// @notice Get reserve the assets are stored in 172 | /// @param _nftId The NFT ID 173 | /// @return The reserve address these assets are stored in 174 | function getAssetReserve(uint256 _nftId) external view returns (address) { 175 | return records[_nftId].reserve; 176 | } 177 | 178 | /// @notice Get how many tokens are in a portfolio/NFT 179 | /// @param _nftId NFT ID to examine 180 | /// @return The array length 181 | function getAssetTokensLength(uint256 _nftId) external view returns (uint256) { 182 | return records[_nftId].tokens.length; 183 | } 184 | 185 | /// @notice Get holding amount for a given nft id 186 | /// @param _nftId The id of the NFT 187 | /// @param _token The address of the token 188 | /// @return The holding amount 189 | function getAssetHolding(uint256 _nftId, address _token) public view returns (uint256) { 190 | return records[_nftId].holdings[_token]; 191 | } 192 | 193 | /// @notice Returns the holdings associated to a NestedAsset 194 | /// @param _nftId the id of the NestedAsset 195 | /// @return Two arrays with the same length : 196 | /// - The token addresses in the portfolio 197 | /// - The respective amounts 198 | function tokenHoldings(uint256 _nftId) external view returns (address[] memory, uint256[] memory) { 199 | address[] memory tokens = getAssetTokens(_nftId); 200 | uint256 tokensCount = tokens.length; 201 | uint256[] memory amounts = new uint256[](tokensCount); 202 | 203 | for (uint256 i = 0; i < tokensCount; i++) { 204 | amounts[i] = getAssetHolding(_nftId, tokens[i]); 205 | } 206 | return (tokens, amounts); 207 | } 208 | 209 | /// @notice Get the lock timestamp of a portfolio/NFT 210 | /// @param _nftId The NFT ID 211 | /// @return The lock timestamp from the NftRecord 212 | function getLockTimestamp(uint256 _nftId) external view returns (uint256) { 213 | return records[_nftId].lockTimestamp; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /contracts/NestedReserve.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import "./abstracts/OwnableFactoryHandler.sol"; 6 | 7 | /// @title Stores underlying assets of NestedNFTs. 8 | /// @notice The factory itself can only trigger a transfer after verification that the user 9 | /// holds funds present in this contract. Only the factory can withdraw/transfer assets. 10 | contract NestedReserve is OwnableFactoryHandler { 11 | /// @notice Release funds to a recipient 12 | /// @param _recipient The receiver 13 | /// @param _token The token to transfer 14 | /// @param _amount The amount to transfer 15 | function transfer( 16 | address _recipient, 17 | IERC20 _token, 18 | uint256 _amount 19 | ) external onlyFactory { 20 | require(_recipient != address(0), "NRS: INVALID_ADDRESS"); 21 | SafeERC20.safeTransfer(_token, _recipient, _amount); 22 | } 23 | 24 | /// @notice Release funds to the factory 25 | /// @param _token The ERC20 to transfer 26 | /// @param _amount The amount to transfer 27 | function withdraw(IERC20 _token, uint256 _amount) external onlyFactory { 28 | SafeERC20.safeTransfer(_token, msg.sender, _amount); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/OperatorResolver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./interfaces/IOperatorResolver.sol"; 5 | import "./abstracts/MixinOperatorResolver.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | /// @title Operator Resolver implementation 9 | /// @notice Resolve the operators address 10 | contract OperatorResolver is IOperatorResolver, Ownable { 11 | /// @dev Operators map of the name and address 12 | mapping(bytes32 => Operator) public operators; 13 | 14 | /// @inheritdoc IOperatorResolver 15 | function getOperator(bytes32 name) external view override returns (Operator memory) { 16 | return operators[name]; 17 | } 18 | 19 | /// @inheritdoc IOperatorResolver 20 | function requireAndGetOperator(bytes32 name, string calldata reason) 21 | external 22 | view 23 | override 24 | returns (Operator memory) 25 | { 26 | Operator memory _foundOperator = operators[name]; 27 | require(_foundOperator.implementation != address(0), reason); 28 | return _foundOperator; 29 | } 30 | 31 | /// @inheritdoc IOperatorResolver 32 | function areOperatorsImported(bytes32[] calldata names, Operator[] calldata destinations) 33 | external 34 | view 35 | override 36 | returns (bool) 37 | { 38 | uint256 namesLength = names.length; 39 | require(namesLength == destinations.length, "OR: INPUTS_LENGTH_MUST_MATCH"); 40 | for (uint256 i = 0; i < namesLength; i++) { 41 | if ( 42 | operators[names[i]].implementation != destinations[i].implementation || 43 | operators[names[i]].selector != destinations[i].selector 44 | ) { 45 | return false; 46 | } 47 | } 48 | return true; 49 | } 50 | 51 | /// @inheritdoc IOperatorResolver 52 | function importOperators( 53 | bytes32[] calldata names, 54 | Operator[] calldata operatorsToImport, 55 | MixinOperatorResolver[] calldata destinations 56 | ) external override onlyOwner { 57 | require(names.length == operatorsToImport.length, "OR: INPUTS_LENGTH_MUST_MATCH"); 58 | bytes32 name; 59 | Operator calldata destination; 60 | for (uint256 i = 0; i < names.length; i++) { 61 | name = names[i]; 62 | destination = operatorsToImport[i]; 63 | operators[name] = destination; 64 | emit OperatorImported(name, destination); 65 | } 66 | 67 | // rebuild caches atomically 68 | // see. https://github.com/code-423n4/2021-11-nested-findings/issues/217 69 | rebuildCaches(destinations); 70 | } 71 | 72 | /// @notice rebuild the caches of mixin smart contracts 73 | /// @param destinations The list of mixinOperatorResolver to rebuild 74 | function rebuildCaches(MixinOperatorResolver[] calldata destinations) public onlyOwner { 75 | for (uint256 i = 0; i < destinations.length; i++) { 76 | destinations[i].rebuildCache(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /contracts/Withdrawer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./interfaces/external/IWETH.sol"; 5 | import "@openzeppelin/contracts/utils/Address.sol"; 6 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 7 | 8 | /// @title Native token withdrawer 9 | /// @dev Withdraw native token from the wrapper contract on behalf 10 | /// of the sender. Upgradeable proxy contracts are not able to receive 11 | /// native tokens from contracts via `transfer` (EIP1884), they need a 12 | /// middleman forwarding all available gas and reverting on errors. 13 | contract Withdrawer is ReentrancyGuard { 14 | IWETH public immutable weth; 15 | 16 | constructor(IWETH _weth) { 17 | weth = _weth; 18 | } 19 | 20 | receive() external payable { 21 | require(msg.sender == address(weth), "WD: ETH_SENDER_NOT_WETH"); 22 | } 23 | 24 | /// @notice Withdraw native token from wrapper contract 25 | /// @param amount The amount to withdraw 26 | function withdraw(uint256 amount) external nonReentrant { 27 | weth.transferFrom(msg.sender, address(this), amount); 28 | weth.withdraw(amount); 29 | Address.sendValue(payable(msg.sender), amount); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/abstracts/MixinOperatorResolver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "../OperatorResolver.sol"; 5 | import "../interfaces/IOperatorResolver.sol"; 6 | import "../interfaces/INestedFactory.sol"; 7 | 8 | /// @title Mixin operator resolver 9 | /// @notice Store in cache operators name and address/selector 10 | abstract contract MixinOperatorResolver { 11 | /// @notice Emitted when cache is updated 12 | /// @param name The operator name 13 | /// @param destination The operator address 14 | event CacheUpdated(bytes32 name, IOperatorResolver.Operator destination); 15 | 16 | /// @dev The OperatorResolver used to build the cache 17 | OperatorResolver public immutable resolver; 18 | 19 | /// @dev Cache operators map of the name and Operator struct (address/selector) 20 | mapping(bytes32 => IOperatorResolver.Operator) internal operatorCache; 21 | 22 | constructor(address _resolver) { 23 | require(_resolver != address(0), "MOR: INVALID_ADDRESS"); 24 | resolver = OperatorResolver(_resolver); 25 | } 26 | 27 | /// @dev This function is public not external in order for it to be overridden and 28 | /// invoked via super in subclasses 29 | function resolverOperatorsRequired() public view virtual returns (bytes32[] memory) {} 30 | 31 | /// @notice Rebuild the operatorCache 32 | function rebuildCache() public { 33 | bytes32[] memory requiredOperators = resolverOperatorsRequired(); 34 | bytes32 name; 35 | IOperatorResolver.Operator memory destination; 36 | // The resolver must call this function whenever it updates its state 37 | for (uint256 i = 0; i < requiredOperators.length; i++) { 38 | name = requiredOperators[i]; 39 | // Note: can only be invoked once the resolver has all the targets needed added 40 | destination = resolver.getOperator(name); 41 | if (destination.implementation != address(0)) { 42 | operatorCache[name] = destination; 43 | } else { 44 | delete operatorCache[name]; 45 | } 46 | emit CacheUpdated(name, destination); 47 | } 48 | } 49 | 50 | /// @notice Check the state of operatorCache 51 | function isResolverCached() external view returns (bool) { 52 | bytes32[] memory requiredOperators = resolverOperatorsRequired(); 53 | bytes32 name; 54 | IOperatorResolver.Operator memory cacheTmp; 55 | IOperatorResolver.Operator memory actualValue; 56 | for (uint256 i = 0; i < requiredOperators.length; i++) { 57 | name = requiredOperators[i]; 58 | cacheTmp = operatorCache[name]; 59 | actualValue = resolver.getOperator(name); 60 | // false if our cache is invalid or if the resolver doesn't have the required address 61 | if ( 62 | actualValue.implementation != cacheTmp.implementation || 63 | actualValue.selector != cacheTmp.selector || 64 | cacheTmp.implementation == address(0) 65 | ) { 66 | return false; 67 | } 68 | } 69 | return true; 70 | } 71 | 72 | /// @dev Get operator address in cache and require (if exists) 73 | /// @param name The operator name 74 | /// @return The operator address 75 | function requireAndGetAddress(bytes32 name) internal view returns (IOperatorResolver.Operator memory) { 76 | IOperatorResolver.Operator memory _foundAddress = operatorCache[name]; 77 | require(_foundAddress.implementation != address(0), string(abi.encodePacked("MOR: MISSING_OPERATOR: ", name))); 78 | return _foundAddress; 79 | } 80 | 81 | /// @dev Build the calldata (with safe datas) and call the Operator 82 | /// @param _order The order to execute 83 | /// @param _inputToken The input token address 84 | /// @param _outputToken The output token address 85 | /// @return success If the operator call is successful 86 | /// @return amounts The amounts from the execution (used and received) 87 | /// - amounts[0] : The amount of output token 88 | /// - amounts[1] : The amount of input token USED by the operator (can be different than expected) 89 | function callOperator( 90 | INestedFactory.Order calldata _order, 91 | address _inputToken, 92 | address _outputToken 93 | ) internal returns (bool success, uint256[] memory amounts) { 94 | IOperatorResolver.Operator memory _operator = requireAndGetAddress(_order.operator); 95 | // Parameters are concatenated and padded to 32 bytes. 96 | // We are concatenating the selector + given params 97 | bytes memory data; 98 | (success, data) = _operator.implementation.delegatecall(bytes.concat(_operator.selector, _order.callData)); 99 | 100 | if (success) { 101 | address[] memory tokens; 102 | (amounts, tokens) = abi.decode(data, (uint256[], address[])); 103 | require(tokens[0] == _outputToken, "MOR: INVALID_OUTPUT_TOKEN"); 104 | require(tokens[1] == _inputToken, "MOR: INVALID_INPUT_TOKEN"); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /contracts/abstracts/OwnableFactoryHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | /// @title Asbtract "Ownable" contract managing a whitelist of factories 7 | abstract contract OwnableFactoryHandler is Ownable { 8 | /// @dev Emitted when a new factory is added 9 | /// @param newFactory Address of the new factory 10 | event FactoryAdded(address newFactory); 11 | 12 | /// @dev Emitted when a factory is removed 13 | /// @param oldFactory Address of the removed factory 14 | event FactoryRemoved(address oldFactory); 15 | 16 | /// @dev Supported factories to interact with 17 | mapping(address => bool) public supportedFactories; 18 | 19 | /// @dev Reverts the transaction if the caller is a supported factory 20 | modifier onlyFactory() { 21 | require(supportedFactories[msg.sender], "OFH: FORBIDDEN"); 22 | _; 23 | } 24 | 25 | /// @notice Add a supported factory 26 | /// @param _factory The address of the new factory 27 | function addFactory(address _factory) external onlyOwner { 28 | require(_factory != address(0), "OFH: INVALID_ADDRESS"); 29 | supportedFactories[_factory] = true; 30 | emit FactoryAdded(_factory); 31 | } 32 | 33 | /// @notice Remove a supported factory 34 | /// @param _factory The address of the factory to remove 35 | function removeFactory(address _factory) external onlyOwner { 36 | require(supportedFactories[_factory], "OFH: NOT_SUPPORTED"); 37 | supportedFactories[_factory] = false; 38 | emit FactoryRemoved(_factory); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/abstracts/OwnableProxyDelegation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/utils/Context.sol"; 5 | import "@openzeppelin/contracts/utils/StorageSlot.sol"; 6 | 7 | /// @notice Ownable re-implementation to initialize the owner in the 8 | /// proxy storage after an "upgradeToAndCall()" (delegatecall). 9 | /// @dev The implementation contract owner will be address zero (by removing the constructor) 10 | abstract contract OwnableProxyDelegation is Context { 11 | /// @dev The contract owner 12 | address private _owner; 13 | 14 | /// @dev Storage slot with the proxy admin (see TransparentUpgradeableProxy from OZ) 15 | bytes32 internal constant _ADMIN_SLOT = bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1); 16 | 17 | /// @dev True if the owner is setted 18 | bool public initialized; 19 | 20 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 21 | 22 | /// @notice Initialize the owner (by the proxy admin) 23 | /// @param ownerAddr The owner address 24 | function initialize(address ownerAddr) external { 25 | require(ownerAddr != address(0), "OPD: INVALID_ADDRESS"); 26 | require(!initialized, "OPD: INITIALIZED"); 27 | require(StorageSlot.getAddressSlot(_ADMIN_SLOT).value == msg.sender, "OPD: FORBIDDEN"); 28 | 29 | _setOwner(ownerAddr); 30 | 31 | initialized = true; 32 | } 33 | 34 | /// @dev Returns the address of the current owner. 35 | function owner() public view virtual returns (address) { 36 | return _owner; 37 | } 38 | 39 | /// @dev Throws if called by any account other than the owner. 40 | modifier onlyOwner() { 41 | require(owner() == _msgSender(), "OPD: NOT_OWNER"); 42 | _; 43 | } 44 | 45 | /// @dev Leaves the contract without owner. It will not be possible to call 46 | /// `onlyOwner` functions anymore. Can only be called by the current owner. 47 | /// 48 | /// NOTE: Renouncing ownership will leave the contract without an owner, 49 | /// thereby removing any functionality that is only available to the owner. 50 | function renounceOwnership() public virtual onlyOwner { 51 | _setOwner(address(0)); 52 | } 53 | 54 | /// @dev Transfers ownership of the contract to a new account (`newOwner`). 55 | /// Can only be called by the current owner. 56 | function transferOwnership(address newOwner) public virtual onlyOwner { 57 | require(newOwner != address(0), "OPD: INVALID_ADDRESS"); 58 | _setOwner(newOwner); 59 | } 60 | 61 | /// @dev Update the owner address 62 | /// @param newOwner The new owner address 63 | function _setOwner(address newOwner) private { 64 | address oldOwner = _owner; 65 | _owner = newOwner; 66 | emit OwnershipTransferred(oldOwner, newOwner); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /contracts/governance/OwnerProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | /// @title Owner proxy to run atomic actions. 7 | /// @notice DSProxy-like contract without a cache to simply run 8 | /// a sequence of atomic actions. 9 | contract OwnerProxy is Ownable { 10 | /// @notice Execute atomic actions. Only the owner can call this function (e.g. the timelock) 11 | /// @param _target Address of the "script" to perform a delegatecall 12 | /// @param _data The bytes calldata 13 | /// @return response The delegatecall response 14 | /// @dev Fork from https://github.com/dapphub/ds-proxy/blob/e17a2526ad5c9877ba925ff25c1119f519b7369b/src/proxy.sol#L53 15 | /// @dev bytes4 selector must be included in the calldata (_data) 16 | function execute(address _target, bytes memory _data) public payable onlyOwner returns (bytes memory response) { 17 | require(_target != address(0), "OP: INVALID_TARGET"); 18 | 19 | // call contract in current context 20 | assembly { 21 | let succeeded := delegatecall(sub(gas(), 5000), _target, add(_data, 0x20), mload(_data), 0, 0) 22 | let size := returndatasize() 23 | 24 | response := mload(0x40) 25 | mstore(0x40, add(response, and(add(add(size, 0x20), 0x1f), not(0x1f)))) 26 | mstore(response, size) 27 | returndatacopy(add(response, 0x20), 0, size) 28 | 29 | switch iszero(succeeded) 30 | case 1 { 31 | // throw if delegatecall failed 32 | revert(add(response, 0x20), size) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/governance/scripts/OperatorScripts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "../../interfaces/INestedFactory.sol"; 5 | import "../../interfaces/IOperatorResolver.sol"; 6 | import "../../abstracts/MixinOperatorResolver.sol"; 7 | import "../../interfaces/external/ITransparentUpgradeableProxy.sol"; 8 | 9 | contract OperatorScripts { 10 | struct tupleOperator { 11 | bytes32 name; 12 | bytes4 selector; 13 | } 14 | 15 | address public immutable nestedFactory; 16 | address public immutable resolver; 17 | 18 | constructor(address _nestedFactory, address _resolver) { 19 | require(_nestedFactory != address(0), "AO-SCRIPT: INVALID_FACTORY_ADDR"); 20 | require(_resolver != address(0), "AO-SCRIPT: INVALID_RESOLVER_ADDR"); 21 | nestedFactory = _nestedFactory; 22 | resolver = _resolver; 23 | } 24 | 25 | /// @notice Call NestedFactory and OperatorResolver to add an operator. 26 | /// @param operator The operator to add 27 | /// @param name The operator bytes32 name 28 | function addOperator(IOperatorResolver.Operator memory operator, bytes32 name) external { 29 | require(operator.implementation != address(0), "AO-SCRIPT: INVALID_IMPL_ADDRESS"); 30 | 31 | // Init arrays with length 1 (only one operator to import) 32 | bytes32[] memory names = new bytes32[](1); 33 | IOperatorResolver.Operator[] memory operatorsToImport = new IOperatorResolver.Operator[](1); 34 | MixinOperatorResolver[] memory destinations = new MixinOperatorResolver[](1); 35 | 36 | names[0] = name; 37 | operatorsToImport[0] = operator; 38 | destinations[0] = MixinOperatorResolver(nestedFactory); 39 | 40 | IOperatorResolver(resolver).importOperators(names, operatorsToImport, destinations); 41 | 42 | ITransparentUpgradeableProxy(nestedFactory).upgradeToAndCall( 43 | ITransparentUpgradeableProxy(nestedFactory).implementation(), 44 | abi.encodeWithSelector(INestedFactory.addOperator.selector, name) 45 | ); 46 | } 47 | 48 | /// @notice Deploy and add operators 49 | /// @dev One address and multiple selectors/names 50 | /// @param bytecode Operator implementation bytecode 51 | /// @param operators Array of tuples => bytes32/bytes4 (name and selector) 52 | function deployAddOperators(bytes memory bytecode, tupleOperator[] memory operators) external { 53 | uint256 operatorLength = operators.length; 54 | require(operatorLength != 0, "DAO-SCRIPT: INVALID_OPERATOR_LEN"); 55 | require(bytecode.length != 0, "DAO-SCRIPT: BYTECODE_ZERO"); 56 | 57 | address deployedAddress; 58 | assembly { 59 | deployedAddress := create(0, add(bytecode, 0x20), mload(bytecode)) 60 | } 61 | require(deployedAddress != address(0), "DAO-SCRIPT: FAILED_DEPLOY"); 62 | 63 | // Init arrays 64 | bytes32[] memory names = new bytes32[](operatorLength); 65 | IOperatorResolver.Operator[] memory operatorsToImport = new IOperatorResolver.Operator[](operatorLength); 66 | 67 | for (uint256 i; i < operatorLength; i++) { 68 | names[i] = operators[i].name; 69 | operatorsToImport[i] = IOperatorResolver.Operator(deployedAddress, operators[i].selector); 70 | } 71 | 72 | // Only the NestedFactory as destination 73 | MixinOperatorResolver[] memory destinations = new MixinOperatorResolver[](1); 74 | destinations[0] = MixinOperatorResolver(nestedFactory); 75 | 76 | // Start importing operators 77 | IOperatorResolver(resolver).importOperators(names, operatorsToImport, destinations); 78 | 79 | // Add all the operators to the factory 80 | for (uint256 i; i < operatorLength; i++) { 81 | ITransparentUpgradeableProxy(nestedFactory).upgradeToAndCall( 82 | ITransparentUpgradeableProxy(nestedFactory).implementation(), 83 | abi.encodeWithSelector(INestedFactory.addOperator.selector, operators[i].name) 84 | ); 85 | } 86 | } 87 | 88 | /// @notice Call NestedFactory and OperatorResolver to remove an operator. 89 | /// @param name The operator bytes32 name 90 | function removeOperator(bytes32 name) external { 91 | ITransparentUpgradeableProxy(nestedFactory).upgradeToAndCall( 92 | ITransparentUpgradeableProxy(nestedFactory).implementation(), 93 | abi.encodeWithSelector(INestedFactory.removeOperator.selector, name) 94 | ); 95 | 96 | // Init arrays with length 1 (only one operator to remove) 97 | bytes32[] memory names = new bytes32[](1); 98 | IOperatorResolver.Operator[] memory operatorsToImport = new IOperatorResolver.Operator[](1); 99 | MixinOperatorResolver[] memory destinations = new MixinOperatorResolver[](1); 100 | 101 | names[0] = name; 102 | operatorsToImport[0] = IOperatorResolver.Operator({ implementation: address(0), selector: bytes4(0) }); 103 | destinations[0] = MixinOperatorResolver(nestedFactory); 104 | 105 | IOperatorResolver(resolver).importOperators(names, operatorsToImport, destinations); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /contracts/governance/scripts/SingleCall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | contract SingleCall { 5 | function call(address _target, bytes memory _data) external payable returns (bool success, bytes memory data) { 6 | require(_target != address(0), "INVALID_TARGET"); 7 | (success, data) = _target.call{ value: msg.value }(_data); 8 | require(success, "CALL_ERROR"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/governance/scripts/UpdateFees.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "../../interfaces/INestedFactory.sol"; 5 | import "../../interfaces/external/ITransparentUpgradeableProxy.sol"; 6 | 7 | contract UpdateFees { 8 | /// @notice Update atomically the entryFees and exitFees 9 | /// @param nestedFactory The nestedFactory address 10 | /// @param entryFees The entry fees 11 | /// @param exitFees The exit fees 12 | /// @dev Called using delegatecall by the NestedFactory owner 13 | function updateFees( 14 | ITransparentUpgradeableProxy nestedFactory, 15 | uint256 entryFees, 16 | uint256 exitFees 17 | ) external { 18 | nestedFactory.upgradeToAndCall( 19 | nestedFactory.implementation(), 20 | abi.encodeWithSelector(INestedFactory.setEntryFees.selector, entryFees) 21 | ); 22 | 23 | nestedFactory.upgradeToAndCall( 24 | nestedFactory.implementation(), 25 | abi.encodeWithSelector(INestedFactory.setExitFees.selector, exitFees) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/interfaces/INestedFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "../NestedReserve.sol"; 6 | import "../FeeSplitter.sol"; 7 | 8 | /// @title NestedFactory interface 9 | interface INestedFactory { 10 | /* ------------------------------ EVENTS ------------------------------ */ 11 | 12 | /// @dev Emitted when the feeSplitter is updated 13 | /// @param feeSplitter The new feeSplitter address 14 | event FeeSplitterUpdated(address feeSplitter); 15 | 16 | /// @dev Emitted when the entryFees is updated 17 | /// @param entryFees The new entryFees amount 18 | event EntryFeesUpdated(uint256 entryFees); 19 | 20 | /// @dev Emitted when the exitFees is updated 21 | /// @param exitFees The new exitFees amount 22 | event ExitFeesUpdated(uint256 exitFees); 23 | 24 | /// @dev Emitted when the reserve is updated 25 | /// @param reserve The new reserve address 26 | event ReserveUpdated(address reserve); 27 | 28 | /// @dev Emitted when a NFT (portfolio) is created 29 | /// @param nftId The NFT token Id 30 | /// @param originalNftId If replicated, the original NFT token Id 31 | event NftCreated(uint256 indexed nftId, uint256 originalNftId); 32 | 33 | /// @dev Emitted when a NFT (portfolio) is updated 34 | /// @param nftId The NFT token Id 35 | event NftUpdated(uint256 indexed nftId); 36 | 37 | /// @dev Emitted when a new operator is added 38 | /// @param newOperator The new operator bytes name 39 | event OperatorAdded(bytes32 newOperator); 40 | 41 | /// @dev Emitted when an operator is removed 42 | /// @param oldOperator The old operator bytes name 43 | event OperatorRemoved(bytes32 oldOperator); 44 | 45 | /// @dev Emitted when tokens are unlocked (sent to the owner) 46 | /// @param token The unlocked token address 47 | /// @param amount The unlocked amount 48 | event TokensUnlocked(address token, uint256 amount); 49 | 50 | /* ------------------------------ STRUCTS ------------------------------ */ 51 | 52 | /// @dev Represent an order made to the factory when creating/editing an NFT 53 | /// @param operator The bytes32 name of the Operator 54 | /// @param token The expected token address in output/input 55 | /// @param callData The operator parameters (delegatecall) 56 | struct Order { 57 | bytes32 operator; 58 | address token; 59 | bytes callData; 60 | } 61 | 62 | /// @dev Represent multiple input orders for a given token to perform multiple trades. 63 | /// @param inputToken The input token 64 | /// @param amount The amount to transfer (input amount) 65 | /// @param orders The orders to perform using the input token. 66 | /// @param _fromReserve Specify the input token source (true if reserve, false if wallet) 67 | /// Note: fromReserve can be read as "from portfolio" 68 | struct BatchedInputOrders { 69 | IERC20 inputToken; 70 | uint256 amount; 71 | Order[] orders; 72 | bool fromReserve; 73 | } 74 | 75 | /// @dev Represent multiple output orders to receive a given token 76 | /// @param outputToken The output token 77 | /// @param amounts The amount of sell tokens to use 78 | /// @param orders Orders calldata 79 | /// @param toReserve Specify the output token destination (true if reserve, false if wallet) 80 | /// Note: toReserve can be read as "to portfolio" 81 | struct BatchedOutputOrders { 82 | IERC20 outputToken; 83 | uint256[] amounts; 84 | Order[] orders; 85 | bool toReserve; 86 | } 87 | 88 | /* ------------------------------ OWNER FUNCTIONS ------------------------------ */ 89 | 90 | /// @notice Add an operator (name) for building cache 91 | /// @param operator The operator name to add 92 | function addOperator(bytes32 operator) external; 93 | 94 | /// @notice Remove an operator (name) for building cache 95 | /// @param operator The operator name to remove 96 | function removeOperator(bytes32 operator) external; 97 | 98 | /// @notice Sets the address receiving the fees 99 | /// @param _feeSplitter The address of the receiver 100 | function setFeeSplitter(FeeSplitter _feeSplitter) external; 101 | 102 | /// @notice Sets the entry fees amount 103 | /// Where 1 = 0.01% and 10000 = 100% 104 | /// @param _entryFees Entry fees amount 105 | function setEntryFees(uint256 _entryFees) external; 106 | 107 | /// @notice Sets the exit fees amount 108 | /// Where 1 = 0.01% and 10000 = 100% 109 | /// @param _exitFees Exit fees amount 110 | function setExitFees(uint256 _exitFees) external; 111 | 112 | /// @notice The Factory is not storing funds, but some users can make 113 | /// bad manipulations and send tokens to the contract. 114 | /// In response to that, the owner can retrieve the factory balance of a given token 115 | /// to later return users funds. 116 | /// @param _token The token to retrieve. 117 | function unlockTokens(IERC20 _token) external; 118 | 119 | /* ------------------------------ USERS FUNCTIONS ------------------------------ */ 120 | 121 | /// @notice Create a portfolio and store the underlying assets from the positions 122 | /// @param _originalTokenId The id of the NFT replicated, 0 if not replicating 123 | /// @param _batchedOrders The order to execute 124 | function create(uint256 _originalTokenId, BatchedInputOrders[] calldata _batchedOrders) external payable; 125 | 126 | /// @notice Process multiple input orders 127 | /// @param _nftId The id of the NFT to update 128 | /// @param _batchedOrders The order to execute 129 | function processInputOrders(uint256 _nftId, BatchedInputOrders[] calldata _batchedOrders) external payable; 130 | 131 | /// @notice Process multiple output orders 132 | /// @param _nftId The id of the NFT to update 133 | /// @param _batchedOrders The order to execute 134 | function processOutputOrders(uint256 _nftId, BatchedOutputOrders[] calldata _batchedOrders) external; 135 | 136 | /// @notice Process multiple input orders and then multiple output orders 137 | /// @param _nftId The id of the NFT to update 138 | /// @param _batchedInputOrders The input orders to execute (first) 139 | /// @param _batchedOutputOrders The output orders to execute (after) 140 | function processInputAndOutputOrders( 141 | uint256 _nftId, 142 | BatchedInputOrders[] calldata _batchedInputOrders, 143 | BatchedOutputOrders[] calldata _batchedOutputOrders 144 | ) external payable; 145 | 146 | /// @notice Burn NFT and exchange all tokens for a specific ERC20 then send it back to the user 147 | /// @dev Will unwrap WETH output to ETH 148 | /// @param _nftId The id of the NFT to destroy 149 | /// @param _buyToken The output token 150 | /// @param _orders Orders calldata 151 | function destroy( 152 | uint256 _nftId, 153 | IERC20 _buyToken, 154 | Order[] calldata _orders 155 | ) external; 156 | 157 | /// @notice Withdraw a token from the reserve and transfer it to the owner without exchanging it 158 | /// @param _nftId NFT token ID 159 | /// @param _tokenIndex Index in array of tokens for this NFT and holding. 160 | function withdraw(uint256 _nftId, uint256 _tokenIndex) external; 161 | 162 | /// @notice Update the lock timestamp of an NFT record. 163 | /// Note: Can only increase the lock timestamp. 164 | /// @param _nftId The NFT id to get the record 165 | /// @param _timestamp The new timestamp. 166 | function updateLockTimestamp(uint256 _nftId, uint256 _timestamp) external; 167 | } 168 | -------------------------------------------------------------------------------- /contracts/interfaces/IOperatorResolver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "../abstracts/MixinOperatorResolver.sol"; 5 | 6 | /// @title Operator address resolver interface 7 | interface IOperatorResolver { 8 | /// @dev Represents an operator definition 9 | /// @param implementation Contract address 10 | /// @param selector Function selector 11 | struct Operator { 12 | address implementation; 13 | bytes4 selector; 14 | } 15 | 16 | /// @notice Emitted when an operator is imported 17 | /// @param name The operator name 18 | /// @param destination The operator definition 19 | event OperatorImported(bytes32 name, Operator destination); 20 | 21 | /// @notice Get an operator (address/selector) for a given name 22 | /// @param name The operator name 23 | /// @return The operator struct (address/selector) 24 | function getOperator(bytes32 name) external view returns (Operator memory); 25 | 26 | /// @notice Get an operator (address/selector) for a given name but require the operator to exist. 27 | /// @param name The operator name 28 | /// @param reason Require message 29 | /// @return The operator struct (address/selector) 30 | function requireAndGetOperator(bytes32 name, string calldata reason) external view returns (Operator memory); 31 | 32 | /// @notice Check if some operators are imported with the right name (and vice versa) 33 | /// @dev The check is performed on the index, make sure that the two arrays match 34 | /// @param names The operator names 35 | /// @param destinations The operator addresses 36 | /// @return True if all the addresses/names are correctly imported, false otherwise 37 | function areOperatorsImported(bytes32[] calldata names, Operator[] calldata destinations) 38 | external 39 | view 40 | returns (bool); 41 | 42 | /// @notice Import/replace operators 43 | /// @dev names and destinations arrays must coincide 44 | /// @param names Hashes of the operators names to register 45 | /// @param operatorsToImport Operators to import 46 | /// @param destinations Destinations to rebuild cache atomically 47 | function importOperators( 48 | bytes32[] calldata names, 49 | Operator[] calldata operatorsToImport, 50 | MixinOperatorResolver[] calldata destinations 51 | ) external; 52 | } 53 | -------------------------------------------------------------------------------- /contracts/interfaces/external/IBeefyVaultV6.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; 6 | 7 | interface IBeefyVaultV6 is IERC20 { 8 | function want() external view returns (address); 9 | 10 | function deposit(uint256 _amount) external; 11 | 12 | function withdraw(uint256 _shares) external; 13 | } 14 | -------------------------------------------------------------------------------- /contracts/interfaces/external/IBiswapPair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; 5 | 6 | interface IBiswapPair is IUniswapV2Pair { 7 | function swapFee() external view returns (uint32); 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interfaces/external/IBiswapRouter02.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; 5 | 6 | interface IBiswapRouter02 is IUniswapV2Router02 { 7 | function getAmountOut( 8 | uint256 amountIn, 9 | uint256 reserveIn, 10 | uint256 reserveOut, 11 | uint256 swapFee 12 | ) external pure returns (uint256 amountOut); 13 | 14 | function getAmountIn( 15 | uint256 amountOut, 16 | uint256 reserveIn, 17 | uint256 reserveOut, 18 | uint256 swapFee 19 | ) external pure returns (uint256 amountIn); 20 | } 21 | -------------------------------------------------------------------------------- /contracts/interfaces/external/ICurvePool/ICurvePool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | /// @title Curve pool interface 5 | interface ICurvePool { 6 | function token() external view returns (address); 7 | 8 | function coins(uint256 index) external view returns (address); 9 | } 10 | -------------------------------------------------------------------------------- /contracts/interfaces/external/ICurvePool/ICurvePoolETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./ICurvePool.sol"; 5 | 6 | /// @title ETH Curve pool interface 7 | /// @notice The difference with non-ETH pools is that ETH pools must have 8 | /// a payable add_liquidity function to allow direct sending of 9 | /// ETH in order to add liquidity with ETH and not an ERC20. 10 | interface ICurvePoolETH is ICurvePool { 11 | function add_liquidity(uint256[2] calldata amounts, uint256 min_mint_amount) external payable; 12 | 13 | function add_liquidity(uint256[3] calldata amounts, uint256 min_mint_amount) external payable; 14 | 15 | function add_liquidity(uint256[4] calldata amounts, uint256 min_mint_amount) external payable; 16 | } 17 | -------------------------------------------------------------------------------- /contracts/interfaces/external/ICurvePool/ICurvePoolNonETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./ICurvePool.sol"; 5 | 6 | /// @title non-ETH Curve pool interface 7 | /// @notice The difference with ETH pools is that ETH pools must have 8 | /// a payable add_liquidity function to allow direct sending of 9 | /// ETH in order to add liquidity with ETH and not an ERC20. 10 | interface ICurvePoolNonETH is ICurvePool { 11 | function add_liquidity(uint256[2] calldata amounts, uint256 min_mint_amount) external; 12 | 13 | function add_liquidity(uint256[3] calldata amounts, uint256 min_mint_amount) external; 14 | 15 | function add_liquidity(uint256[4] calldata amounts, uint256 min_mint_amount) external; 16 | } 17 | -------------------------------------------------------------------------------- /contracts/interfaces/external/INestedToken.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | interface INestedToken is IERC20 { 7 | function burn(uint256 amount) external; 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interfaces/external/IStakingVault/IStakeDaoStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./IStakingVault.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | /// @title StakeDAO strategy interface 8 | /// @dev In the deployed code of StakeDAO, the token() function 9 | /// allows to retrieve the LP token to stake. 10 | /// Note : In the StakeDAO repository, this function has 11 | /// been replaced by want(). 12 | interface IStakeDaoStrategy is IStakingVault { 13 | function token() external view returns (IERC20); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/interfaces/external/IStakingVault/IStakingVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | /// @title Generic staking vault interface 5 | interface IStakingVault { 6 | function deposit(uint256 _amount) external; 7 | 8 | function withdraw(uint256 _shares) external; 9 | } 10 | -------------------------------------------------------------------------------- /contracts/interfaces/external/IStakingVault/IYearnVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./IStakingVault.sol"; 5 | 6 | /// @dev Yearn vault interface 7 | interface IYearnVault is IStakingVault { 8 | function withdraw( 9 | uint256 _shares, 10 | address _recipient, 11 | uint256 _maxLoss 12 | ) external returns (uint256); 13 | } 14 | -------------------------------------------------------------------------------- /contracts/interfaces/external/ITransparentUpgradeableProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | interface ITransparentUpgradeableProxy { 5 | function admin() external returns (address); 6 | 7 | function implementation() external returns (address); 8 | 9 | function changeAdmin(address newAdmin) external; 10 | 11 | function upgradeTo(address newImplementation) external; 12 | 13 | function upgradeToAndCall(address newImplementation, bytes calldata data) external payable; 14 | } 15 | -------------------------------------------------------------------------------- /contracts/interfaces/external/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.14; 3 | 4 | interface IWETH { 5 | function deposit() external payable; 6 | 7 | function withdraw(uint256) external; 8 | 9 | function totalSupply() external view returns (uint256); 10 | 11 | function transfer(address recipient, uint256 amount) external returns (bool); 12 | 13 | function balanceOf(address recipien) external returns (uint256); 14 | 15 | function allowance(address owner, address spender) external view returns (uint256); 16 | 17 | function approve(address spender, uint256 amount) external returns (bool); 18 | 19 | function transferFrom( 20 | address sender, 21 | address recipient, 22 | uint256 amount 23 | ) external returns (bool); 24 | 25 | event Transfer(address indexed from, address indexed to, uint256 value); 26 | 27 | event Approval(address indexed owner, address indexed spender, uint256 value); 28 | } 29 | -------------------------------------------------------------------------------- /contracts/libraries/CurveHelpers/CurveHelpers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./../../interfaces/external/ICurvePool/ICurvePool.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | 7 | /// @notice Library for Curve deposit/withdraw 8 | library CurveHelpers { 9 | using SafeERC20 for IERC20; 10 | 11 | /// @dev Get the array of token amount to send to a 12 | /// Curve 2pool to add liquidity 13 | /// @param pool The curve 2pool 14 | /// @param token The token to remove from the pool 15 | /// @param amount The amount of token to remove from the pool 16 | /// @return amounts Array of 2 token amounts sorted by Curve pool token indexes 17 | function getAmounts2Coins( 18 | ICurvePool pool, 19 | address token, 20 | uint256 amount 21 | ) internal view returns (uint256[2] memory amounts) { 22 | for (uint256 i; i < 2; i++) { 23 | if (token == pool.coins(i)) { 24 | amounts[i] = amount; 25 | return amounts; 26 | } 27 | } 28 | revert("CH: INVALID_INPUT_TOKEN"); 29 | } 30 | 31 | /// @dev Get the array of token amount to send to a 32 | /// Curve 3pool to add liquidity 33 | /// @param pool The curve 3pool 34 | /// @param token The token to remove from the pool 35 | /// @param amount The amount of token to remove from the pool 36 | /// @return amounts Array of 3 token amounts sorted by Curve pool token indexes 37 | function getAmounts3Coins( 38 | ICurvePool pool, 39 | address token, 40 | uint256 amount 41 | ) internal view returns (uint256[3] memory amounts) { 42 | for (uint256 i; i < 3; i++) { 43 | if (token == pool.coins(i)) { 44 | amounts[i] = amount; 45 | return amounts; 46 | } 47 | } 48 | revert("CH: INVALID_INPUT_TOKEN"); 49 | } 50 | 51 | /// @dev Get the array of token amount to send to a 52 | /// Curve 4pool to add liquidity 53 | /// @param pool The curve 4pool 54 | /// @param token The token to remove from the pool 55 | /// @param amount The amount of token to remove from the pool 56 | /// @return amounts Array of 4 token amounts sorted by Curve pool token indexes 57 | function getAmounts4Coins( 58 | ICurvePool pool, 59 | address token, 60 | uint256 amount 61 | ) internal view returns (uint256[4] memory amounts) { 62 | for (uint256 i; i < 4; i++) { 63 | if (token == pool.coins(i)) { 64 | amounts[i] = amount; 65 | return amounts; 66 | } 67 | } 68 | revert("CH: INVALID_INPUT_TOKEN"); 69 | } 70 | 71 | /// @dev Remove liquidity from a Curve pool 72 | /// @param pool The Curve pool to remove liquidity from 73 | /// @param amount The Curve pool LP token to withdraw 74 | /// @param outputToken One of the Curve pool token 75 | /// @param poolCoinAmount The amount of token in the Curve pool 76 | /// @param signature The signature of the remove_liquidity_one_coin 77 | /// function to be used to call to the Curve pool 78 | /// @return success If the call to remove liquidity succeeded 79 | function removeLiquidityOneCoin( 80 | ICurvePool pool, 81 | uint256 amount, 82 | address outputToken, 83 | uint256 poolCoinAmount, 84 | bytes4 signature 85 | ) internal returns (bool success) { 86 | for (uint256 i; i < poolCoinAmount; i++) { 87 | if (outputToken == pool.coins(i)) { 88 | (success, ) = address(pool).call(abi.encodeWithSelector(signature, amount, i, 0)); 89 | return success; 90 | } 91 | } 92 | revert("CH: INVALID_OUTPUT_TOKEN"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /contracts/libraries/CurveHelpers/README.md: -------------------------------------------------------------------------------- 1 | # Curve pools 2 | 3 | Due to the number of Curve pool types, we had to choose which ones to support in the YearnCurveVaultOperator and StakeDaoCurveStrategyOperator. 4 | 5 | There is 12 Curve pool types: 6 | 7 | - [Curve plain pools](https://curve.readthedocs.io/exchange-pools.html#plain-pools) with ETH 8 | - [Curve plain pools](https://curve.readthedocs.io/exchange-pools.html#plain-pools) with WETH 9 | - [Curve plain pools](https://curve.readthedocs.io/exchange-pools.html#plain-pools) without ETH or WETH 10 | - [Curve lending pools](https://curve.readthedocs.io/exchange-pools.html#lending-pools) with ETH 11 | - [Curve lending pools](https://curve.readthedocs.io/exchange-pools.html#lending-pools) with WETH 12 | - [Curve lending pools](https://curve.readthedocs.io/exchange-pools.html#lending-pools) without ETH or WETH 13 | - [Curve metapool](https://curve.readthedocs.io/exchange-pools.html#metapools) pool with ETH 14 | - [Curve metapool](https://curve.readthedocs.io/exchange-pools.html#metapools) pool with WETH 15 | - [Curve metapool](https://curve.readthedocs.io/exchange-pools.html#metapools) pool without ETH or WETH 16 | - [Curve factory pools](https://curve.readthedocs.io/factory-pools.html) - pools pool with ETH 17 | - [Curve factory pools](https://curve.readthedocs.io/factory-pools.html) - pools pool with WETH 18 | - [Curve factory pools](https://curve.readthedocs.io/factory-pools.html) - pools pool without ETH or WETH 19 | 20 | ## Differences betweens Curve pools 21 | 22 | ### int128 & uint256 23 | 24 | Some Curve pools has a different interfaces especially on the `remove_liquidity_one_coin` function. 25 | You can find those two different function selectors: 26 | 27 | ``` 28 | remove_liquidity_one_coin(uint256,int128,uint256) 29 | remove_liquidity_one_coin(uint256,uint256,uint256) 30 | ``` 31 | 32 | These differences are the consequence of the presence of functions `withdraw128()` and `withdraw256()` in the StakeDAO and Yearn operators. 33 | 34 | ### ETH handling 35 | 36 | #### Add liquidity 37 | 38 | Some Curve pools has ETH as liquidity, and require to call `add_liquidity` with a value to use ETH for the liquidity addition. 39 | So to add liquidity to this type of pool using ETH, we must use `depositETH(address,uint256,uint256)` that will call: 40 | 41 | ``` 42 | add_liquidity{value: x}(uint256[],uint256) 43 | ``` 44 | 45 | And to add liquidity in this type of pool using another token, we must call: 46 | 47 | ``` 48 | add_liquidity(uint256[],uint256) 49 | ``` 50 | 51 | #### remove liquidity 52 | 53 | When you remove liquidity from a Curve pool using ETH as a Curve output token, the `NestedFactory` will automaticly convert the received ETH into WETH because the [ETH/WETH nested protocol managment](https://github.com/NestedFi/nested-core-lego#eth-managment) forces WETH conversion on `receive` if the sender is not the whithdrawer, so when you call `withdrawETH(address,uint256,uint256)`, you will receive WETH as output token even if you asked for ETH. 54 | 55 | ## Supported Curve pool types 56 | 57 | - [Curve plain pools](https://curve.readthedocs.io/exchange-pools.html#plain-pools) => 100% supported 58 | - Curve plain pools without ETH :heavy_check_mark: 59 | - Curve plain pools with ETH :heavy_check_mark: 60 | - [Curve lending pools](https://curve.readthedocs.io/exchange-pools.html#lending-pools) => partialy supported 61 | - Curve plain pools without ETH :heavy_check_mark: 62 | - Curve plain pools with ETH 63 | - Deposit :heavy_check_mark: 64 | - withdraw non ETH :heavy_check_mark: 65 | - Withdraw ETH :x: 66 | - [Curve factory - pools](https://curve.readthedocs.io/factory-pools.html) => partialy supported 67 | - Curve pool without LP token :heavy_check_mark: 68 | - Metapool with LP token :x: 69 | - [Curve metapool](https://curve.readthedocs.io/exchange-pools.html#metapools) => not supported :x: 70 | -------------------------------------------------------------------------------- /contracts/libraries/ExchangeHelpers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | 6 | /// @notice Helpers for swapping tokens 7 | library ExchangeHelpers { 8 | using SafeERC20 for IERC20; 9 | 10 | /// @dev Perform a swap between two tokens 11 | /// @param _sellToken Token to exchange 12 | /// @param _swapTarget The address of the contract that swaps tokens 13 | /// @param _swapCallData Call data provided by 0x to fill the quote 14 | /// @return True if the call succeeded, false if not 15 | function fillQuote( 16 | IERC20 _sellToken, 17 | address _swapTarget, 18 | bytes memory _swapCallData 19 | ) internal returns (bool) { 20 | setMaxAllowance(_sellToken, _swapTarget); 21 | // solhint-disable-next-line avoid-low-level-calls 22 | (bool success, ) = _swapTarget.call(_swapCallData); 23 | return success; 24 | } 25 | 26 | /// @dev sets the allowance for a token to the maximum if it is not already at max 27 | /// @param _token The token to use for the allowance setting 28 | /// @param _spender Spender to allow 29 | function setMaxAllowance(IERC20 _token, address _spender) internal { 30 | uint256 _currentAllowance = _token.allowance(address(this), _spender); 31 | if (_currentAllowance != type(uint256).max) { 32 | // Decrease to 0 first for tokens mitigating the race condition 33 | _token.safeDecreaseAllowance(_spender, _currentAllowance); 34 | _token.safeIncreaseAllowance(_spender, type(uint256).max); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /contracts/libraries/OperatorHelpers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./../interfaces/external/ICurvePool/ICurvePool.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | 7 | /// @notice Library for all operators 8 | library OperatorHelpers { 9 | using SafeERC20 for IERC20; 10 | 11 | /// @dev Get the arrays of obtained token and spent token 12 | /// @param inputToken The token spent 13 | /// @param inputTokenBalanceBefore The input token balance before 14 | /// @param expectedInputAmount The expected amount of input token spent 15 | /// @param outputToken The token obtained 16 | /// @param outputTokenBalanceBefore The output token balance before 17 | /// @param minAmountOut The minimum of output token expected 18 | function getOutputAmounts( 19 | IERC20 inputToken, 20 | uint256 inputTokenBalanceBefore, 21 | uint256 expectedInputAmount, 22 | IERC20 outputToken, 23 | uint256 outputTokenBalanceBefore, 24 | uint256 minAmountOut 25 | ) internal view returns (uint256[] memory amounts, address[] memory tokens) { 26 | require( 27 | inputTokenBalanceBefore - inputToken.balanceOf(address(this)) == expectedInputAmount, 28 | "OH: INVALID_AMOUNT_WITHDRAWED" 29 | ); 30 | 31 | uint256 tokenAmount = outputToken.balanceOf(address(this)) - outputTokenBalanceBefore; 32 | require(tokenAmount != 0, "OH: INVALID_AMOUNT_RECEIVED"); 33 | require(tokenAmount >= minAmountOut, "OH: INVALID_AMOUNT_RECEIVED"); 34 | 35 | amounts = new uint256[](2); 36 | tokens = new address[](2); 37 | 38 | // Output amounts 39 | amounts[0] = tokenAmount; 40 | amounts[1] = expectedInputAmount; 41 | 42 | // Output token 43 | tokens[0] = address(outputToken); 44 | tokens[1] = address(inputToken); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /contracts/libraries/StakingLPVaultHelpers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./../Withdrawer.sol"; 5 | import "./../libraries/ExchangeHelpers.sol"; 6 | import "./../libraries/CurveHelpers/CurveHelpers.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "./../interfaces/external/ICurvePool/ICurvePool.sol"; 9 | import "./../interfaces/external/ICurvePool/ICurvePoolETH.sol"; 10 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 11 | import "./../interfaces/external/IStakingVault/IStakingVault.sol"; 12 | import "./../interfaces/external/ICurvePool/ICurvePoolNonETH.sol"; 13 | 14 | /// @notice Library for LP Staking Vaults deposit/withdraw 15 | library StakingLPVaultHelpers { 16 | using SafeERC20 for IERC20; 17 | 18 | /// @dev Add liquidity in a Curve pool with ETH and deposit 19 | /// the LP token in a staking vault 20 | /// @param vault The staking vault address to deposit into 21 | /// @param pool The Curve pool to add liquitiy in 22 | /// @param lpToken The Curve pool LP token 23 | /// @param poolCoinAmount The number of token in the Curve pool 24 | /// @param eth ETH address 25 | /// @param amount ETH amount to add in the Curve pool 26 | function _addLiquidityAndDepositETH( 27 | address vault, 28 | ICurvePoolETH pool, 29 | IERC20 lpToken, 30 | uint256 poolCoinAmount, 31 | address eth, 32 | uint256 amount 33 | ) internal { 34 | uint256 lpTokenBalanceBefore = lpToken.balanceOf(address(this)); 35 | 36 | if (poolCoinAmount == 2) { 37 | pool.add_liquidity{ value: amount }(CurveHelpers.getAmounts2Coins(pool, eth, amount), 0); 38 | } else if (poolCoinAmount == 3) { 39 | pool.add_liquidity{ value: amount }(CurveHelpers.getAmounts3Coins(pool, eth, amount), 0); 40 | } else { 41 | pool.add_liquidity{ value: amount }(CurveHelpers.getAmounts4Coins(pool, eth, amount), 0); 42 | } 43 | 44 | uint256 lpTokenToDeposit = lpToken.balanceOf(address(this)) - lpTokenBalanceBefore; 45 | ExchangeHelpers.setMaxAllowance(lpToken, vault); 46 | IStakingVault(vault).deposit(lpTokenToDeposit); 47 | } 48 | 49 | /// @dev Add liquidity in a Curve pool and deposit 50 | /// the LP token in a staking vault 51 | /// @param vault The staking vault address to deposit into 52 | /// @param pool The Curve pool to add liquitiy in 53 | /// @param lpToken The Curve pool lpToken 54 | /// @param poolCoinAmount The number of token in the Curve pool 55 | /// @param token Token to add in the Curve pool liquidity 56 | /// @param amount Token amount to add in the Curve pool 57 | function _addLiquidityAndDeposit( 58 | address vault, 59 | ICurvePoolNonETH pool, 60 | IERC20 lpToken, 61 | uint256 poolCoinAmount, 62 | address token, 63 | uint256 amount 64 | ) internal { 65 | uint256 lpTokenBalanceBefore = lpToken.balanceOf(address(this)); 66 | ExchangeHelpers.setMaxAllowance(IERC20(token), address(pool)); 67 | 68 | if (poolCoinAmount == 2) { 69 | pool.add_liquidity(CurveHelpers.getAmounts2Coins(pool, token, amount), 0); 70 | } else if (poolCoinAmount == 3) { 71 | pool.add_liquidity(CurveHelpers.getAmounts3Coins(pool, token, amount), 0); 72 | } else { 73 | pool.add_liquidity(CurveHelpers.getAmounts4Coins(pool, token, amount), 0); 74 | } 75 | 76 | uint256 lpTokenToDeposit = lpToken.balanceOf(address(this)) - lpTokenBalanceBefore; 77 | ExchangeHelpers.setMaxAllowance(lpToken, vault); 78 | IStakingVault(vault).deposit(lpTokenToDeposit); 79 | } 80 | 81 | /// @dev Withdraw the LP token from the staking vault and 82 | /// remove the liquidity from the Curve pool 83 | /// @param vault The staking vault address to withdraw from 84 | /// @param amount The amount to withdraw 85 | /// @param pool The Curve pool to remove liquitiy from 86 | /// @param lpToken The Curve pool LP token 87 | /// @param poolCoinAmount The number of token in the Curve pool 88 | /// @param outputToken Output token to receive 89 | function _withdrawAndRemoveLiquidity128( 90 | address vault, 91 | uint256 amount, 92 | ICurvePool pool, 93 | IERC20 lpToken, 94 | uint256 poolCoinAmount, 95 | address outputToken 96 | ) internal { 97 | uint256 lpTokenBalanceBefore = lpToken.balanceOf(address(this)); 98 | IStakingVault(vault).withdraw(amount); 99 | 100 | bool success = CurveHelpers.removeLiquidityOneCoin( 101 | pool, 102 | lpToken.balanceOf(address(this)) - lpTokenBalanceBefore, 103 | outputToken, 104 | poolCoinAmount, 105 | bytes4(keccak256(bytes("remove_liquidity_one_coin(uint256,int128,uint256)"))) 106 | ); 107 | 108 | require(success, "SDCSO: CURVE_RM_LIQUIDITY_FAILED"); 109 | } 110 | 111 | /// @dev Withdraw the LP token from the staking vault and 112 | /// remove the liquidity from the Curve pool 113 | /// @param vault The staking vault address to withdraw from 114 | /// @param amount The amount to withdraw 115 | /// @param pool The Curve pool to remove liquitiy from 116 | /// @param lpToken The Curve pool LP token 117 | /// @param poolCoinAmount The number of token in the Curve pool 118 | /// @param outputToken Output token to receive 119 | function _withdrawAndRemoveLiquidity256( 120 | address vault, 121 | uint256 amount, 122 | ICurvePool pool, 123 | IERC20 lpToken, 124 | uint256 poolCoinAmount, 125 | address outputToken 126 | ) internal { 127 | uint256 lpTokenBalanceBefore = lpToken.balanceOf(address(this)); 128 | IStakingVault(vault).withdraw(amount); 129 | 130 | bool success = CurveHelpers.removeLiquidityOneCoin( 131 | pool, 132 | lpToken.balanceOf(address(this)) - lpTokenBalanceBefore, 133 | outputToken, 134 | poolCoinAmount, 135 | bytes4(keccak256(bytes("remove_liquidity_one_coin(uint256,uint256,uint256)"))) 136 | ); 137 | 138 | require(success, "SDCSO: CURVE_RM_LIQUIDITY_FAILED"); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /contracts/mocks/AugustusSwapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./TokenTransferProxy.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | /// @title Mock contract for the AugustusSwapper (Paraswap) using 8 | /// the TokenTransferProxy for approval (dummyRouter equivalent). 9 | contract AugustusSwapper { 10 | TokenTransferProxy public immutable proxy; 11 | 12 | constructor() { 13 | proxy = new TokenTransferProxy(); 14 | } 15 | 16 | function dummyswapToken( 17 | address _inputToken, 18 | address _outputToken, 19 | uint256 _amount 20 | ) external { 21 | proxy.transferFrom(_inputToken, msg.sender, address(this), _amount); 22 | IERC20(_outputToken).transfer(msg.sender, _amount); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/mocks/DeflationaryMockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.11; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract DeflationaryMockERC20 is ERC20 { 7 | constructor( 8 | string memory _name, 9 | string memory _symbol, 10 | uint256 _initialSupply 11 | ) ERC20(_name, _symbol) { 12 | _mint(msg.sender, _initialSupply); 13 | } 14 | 15 | function mint(address recipient, uint256 amount) external { 16 | _mint(recipient, amount); 17 | } 18 | 19 | function burn(uint256 amount) external { 20 | _burn(msg.sender, amount); 21 | } 22 | 23 | function transferFrom( 24 | address sender, 25 | address recipient, 26 | uint256 amount 27 | ) public override returns (bool) { 28 | _burn(sender, (amount * 5) / 10); 29 | super.transferFrom(sender, recipient, (amount * 5) / 10); 30 | return true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /contracts/mocks/DummyRouter.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.3; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 6 | import "../NestedFactory.sol"; 7 | import "../libraries/ExchangeHelpers.sol"; 8 | import "../interfaces/INestedFactory.sol"; 9 | 10 | contract DummyRouter is IERC721Receiver { 11 | address payable public factory; 12 | INestedFactory.Order[] attackOrders; 13 | IWETH public weth; 14 | 15 | receive() external payable {} 16 | 17 | // send ETH, get the token 18 | function dummyswapETH(IERC20 token) public payable { 19 | // send 1ETH, you get 10 dummy tokens 20 | token.transfer(msg.sender, msg.value * 10); 21 | } 22 | 23 | // send a token, get the token 24 | function dummyswapToken( 25 | IERC20 _inputToken, 26 | IERC20 _outputToken, 27 | uint256 _amount 28 | ) external { 29 | IERC20(_inputToken).transferFrom(msg.sender, address(this), _amount); 30 | _outputToken.transfer(msg.sender, _amount); 31 | } 32 | 33 | function reentrancyAttackForDestroy(uint256 nftId) external { 34 | NestedFactory(factory).destroy(nftId, IERC20(address(weth)), attackOrders); 35 | } 36 | 37 | function setMaxAllowance(IERC20 _token, address _spender) external { 38 | ExchangeHelpers.setMaxAllowance(_token, _spender); 39 | } 40 | 41 | function setAllowance( 42 | IERC20 _token, 43 | address _spender, 44 | uint256 _amount 45 | ) external { 46 | _token.approve(_spender, _amount); 47 | } 48 | 49 | function onERC721Received( 50 | address _operator, 51 | address _from, 52 | uint256 _tokenId, 53 | bytes calldata _data 54 | ) external pure override returns (bytes4) { 55 | _operator; 56 | _from; 57 | _tokenId; 58 | _data; 59 | return 0x150b7a02; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contracts/mocks/FailedDeploy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | contract FailedDeploy { 5 | constructor() { 6 | revert(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /contracts/mocks/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20 { 7 | constructor( 8 | string memory _name, 9 | string memory _symbol, 10 | uint256 _initialSupply 11 | ) ERC20(_name, _symbol) { 12 | _mint(msg.sender, _initialSupply); 13 | } 14 | 15 | function mint(address recipient, uint256 amount) external { 16 | _mint(recipient, amount); 17 | } 18 | 19 | function burn(uint256 amount) external { 20 | _burn(msg.sender, amount); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/mocks/MockSmartChef.sol: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MassDotMoney/nested-core-lego/c1eb3de913bf9b29a09015c10f4690ebd9632c54/contracts/mocks/MockSmartChef.sol -------------------------------------------------------------------------------- /contracts/mocks/TestableMixingOperatorResolver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "../abstracts/MixinOperatorResolver.sol"; 5 | 6 | contract TestableMixinResolver is MixinOperatorResolver { 7 | bytes32 private constant CONTRACT_EXAMPLE_1 = "Example_1"; 8 | bytes32 private constant CONTRACT_EXAMPLE_2 = "Example_2"; 9 | bytes32 private constant CONTRACT_EXAMPLE_3 = "Example_3"; 10 | 11 | bytes32[24] private addressesToCache = [CONTRACT_EXAMPLE_1, CONTRACT_EXAMPLE_2, CONTRACT_EXAMPLE_3]; 12 | 13 | constructor(address _resolver) MixinOperatorResolver(_resolver) {} 14 | 15 | function resolverOperatorsRequired() public pure override returns (bytes32[] memory addresses) { 16 | addresses = new bytes32[](3); 17 | addresses[0] = CONTRACT_EXAMPLE_1; 18 | addresses[1] = CONTRACT_EXAMPLE_2; 19 | addresses[2] = CONTRACT_EXAMPLE_3; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/mocks/TestableOperatorCaller.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | /// @notice Testable contract calling 5 | contract TestableOperatorCaller { 6 | address public operator; 7 | 8 | constructor(address _operator) { 9 | operator = _operator; 10 | } 11 | 12 | function performSwap( 13 | address own, 14 | address sellToken, 15 | address buyToken, 16 | bytes calldata swapCallData 17 | ) external returns (bool) { 18 | (bool success, bytes memory data) = operator.delegatecall( 19 | abi.encodeWithSignature("performSwap(address,address,bytes)", sellToken, buyToken, swapCallData) 20 | ); 21 | require(success, "TestableOperatorCaller::performSwap: Error"); 22 | return true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/mocks/TokenTransferProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/utils/Address.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 8 | 9 | interface ITokenTransferProxy { 10 | function transferFrom( 11 | address token, 12 | address from, 13 | address to, 14 | uint256 amount 15 | ) external; 16 | } 17 | 18 | /** 19 | * @dev Allows owner of the contract to transfer tokens on behalf of user. 20 | * User will need to approve this contract to spend tokens on his/her behalf 21 | * on Paraswap platform 22 | */ 23 | contract TokenTransferProxy is Ownable, ITokenTransferProxy { 24 | using SafeERC20 for IERC20; 25 | using Address for address; 26 | 27 | /** 28 | * @dev Allows owner of the contract to transfer tokens on user's behalf 29 | * @dev Swapper contract will be the owner of this contract 30 | * @param token Address of the token 31 | * @param from Address from which tokens will be transferred 32 | * @param to Receipent address of the tokens 33 | * @param amount Amount of tokens to transfer 34 | */ 35 | function transferFrom( 36 | address token, 37 | address from, 38 | address to, 39 | uint256 amount 40 | ) external override onlyOwner { 41 | require(from == tx.origin || from.isContract(), "Invalid from address"); 42 | IERC20(token).safeTransferFrom(from, to, amount); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /contracts/mocks/WETHMock.sol: -------------------------------------------------------------------------------- 1 | /** 2 | *Submitted for verification at Etherscan.io on 2017-12-12 3 | */ 4 | 5 | // Copyright (C) 2015, 2016, 2017 Dapphub 6 | 7 | // This program is free software: you can redistribute it and/or modify 8 | // it under the terms of the GNU General Public License as published by 9 | // the Free Software Foundation, either version 3 of the License, or 10 | // (at your option) any later version. 11 | 12 | // This program is distributed in the hope that it will be useful, 13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | // GNU General Public License for more details. 16 | 17 | // You should have received a copy of the GNU General Public License 18 | // along with this program. If not, see . 19 | 20 | // SPDX-License-Identifier: UNLICENSED 21 | pragma solidity ^0.8.0; 22 | 23 | contract WETH9 { 24 | string public name = "Wrapped Ether"; 25 | string public symbol = "WETH"; 26 | uint8 public decimals = 18; 27 | 28 | event Approval(address indexed src, address indexed guy, uint256 wad); 29 | event Transfer(address indexed src, address indexed dst, uint256 wad); 30 | event Deposit(address indexed dst, uint256 wad); 31 | event Withdrawal(address indexed src, uint256 wad); 32 | 33 | mapping(address => uint256) public balanceOf; 34 | mapping(address => mapping(address => uint256)) public allowance; 35 | 36 | receive() external payable { 37 | deposit(); 38 | } 39 | 40 | function deposit() public payable { 41 | balanceOf[msg.sender] += msg.value; 42 | emit Deposit(msg.sender, msg.value); 43 | } 44 | 45 | function withdraw(uint256 wad) public { 46 | require(balanceOf[msg.sender] >= wad); 47 | balanceOf[msg.sender] -= wad; 48 | payable(msg.sender).transfer(wad); 49 | emit Withdrawal(msg.sender, wad); 50 | } 51 | 52 | function totalSupply() public view returns (uint256) { 53 | return address(this).balance; 54 | } 55 | 56 | function approve(address guy, uint256 wad) public returns (bool) { 57 | allowance[msg.sender][guy] = wad; 58 | emit Approval(msg.sender, guy, wad); 59 | return true; 60 | } 61 | 62 | function transfer(address dst, uint256 wad) public returns (bool) { 63 | return transferFrom(msg.sender, dst, wad); 64 | } 65 | 66 | function transferFrom( 67 | address src, 68 | address dst, 69 | uint256 wad 70 | ) public returns (bool) { 71 | require(balanceOf[src] >= wad, "INSUFFICIENT_BALANCE"); 72 | 73 | if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { 74 | require(allowance[src][msg.sender] >= wad); 75 | allowance[src][msg.sender] -= wad; 76 | } 77 | 78 | balanceOf[src] -= wad; 79 | balanceOf[dst] += wad; 80 | 81 | emit Transfer(src, dst, wad); 82 | 83 | return true; 84 | } 85 | } 86 | 87 | /* 88 | GNU GENERAL PUBLIC LICENSE 89 | Version 3, 29 June 2007 90 | 91 | Copyright (C) 2007 Free Software Foundation, Inc. 92 | Everyone is permitted to copy and distribute verbatim copies 93 | of this license document, but changing it is not allowed. 94 | 95 | */ 96 | -------------------------------------------------------------------------------- /contracts/operators/Beefy/BeefyVaultOperator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./BeefyVaultStorage.sol"; 5 | import "./../../libraries/ExchangeHelpers.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | /// @title Beefy Single Vault Operator 9 | /// @notice Deposit/Withdraw in a Beefy vault (native or non-native). 10 | contract BeefyVaultOperator { 11 | BeefyVaultStorage public immutable operatorStorage; 12 | 13 | constructor(address[] memory vaults, address[] memory tokens) { 14 | uint256 vaultsLength = vaults.length; 15 | require(vaultsLength == tokens.length, "BVO: INVALID_VAULTS_LENGTH"); 16 | operatorStorage = new BeefyVaultStorage(); 17 | 18 | for (uint256 i; i < vaultsLength; i++) { 19 | operatorStorage.addVault(vaults[i], tokens[i]); 20 | } 21 | 22 | operatorStorage.transferOwnership(msg.sender); 23 | } 24 | 25 | /// @notice Deposit the asset in the Beefy vault and receive 26 | /// the vault token (moo). 27 | /// @param vault The vault address to deposit into 28 | /// @param amount The token amount to deposit 29 | /// @param minVaultAmount The minimum vault token amount expected 30 | /// @return amounts Array of amounts : 31 | /// - [0] : The vault token received amount 32 | /// - [1] : The token deposited amount 33 | /// @return tokens Array of token addresses 34 | /// - [0] : The vault token received address 35 | /// - [1] : The token deposited address 36 | function deposit( 37 | address vault, 38 | uint256 amount, 39 | uint256 minVaultAmount 40 | ) external payable returns (uint256[] memory amounts, address[] memory tokens) { 41 | require(amount != 0, "BVO: INVALID_AMOUNT"); 42 | IERC20 token = IERC20(operatorStorage.vaults(vault)); 43 | require(address(token) != address(0), "BVO: INVALID_VAULT"); 44 | 45 | uint256 vaultBalanceBefore = IERC20(vault).balanceOf(address(this)); 46 | uint256 tokenBalanceBefore = token.balanceOf(address(this)); 47 | 48 | ExchangeHelpers.setMaxAllowance(token, vault); 49 | (bool success, ) = vault.call(abi.encodeWithSignature("deposit(uint256)", amount)); 50 | require(success, "BVO: DEPOSIT_CALL_FAILED"); 51 | 52 | uint256 vaultAmount = IERC20(vault).balanceOf(address(this)) - vaultBalanceBefore; 53 | uint256 tokenAmount = tokenBalanceBefore - token.balanceOf(address(this)); 54 | require(vaultAmount != 0 && vaultAmount >= minVaultAmount, "BVO: INVALID_AMOUNT_RECEIVED"); 55 | require(amount == tokenAmount, "BVO: INVALID_AMOUNT_DEPOSITED"); 56 | 57 | amounts = new uint256[](2); 58 | tokens = new address[](2); 59 | 60 | // Output amounts 61 | amounts[0] = vaultAmount; 62 | amounts[1] = tokenAmount; 63 | 64 | // Output token 65 | tokens[0] = vault; 66 | tokens[1] = address(token); 67 | } 68 | 69 | /// @notice Withdraw the vault token (moo) from Beefy and receive 70 | /// the underlying token. 71 | /// @param vault The vault address to withdraw from 72 | /// @param amount The vault token amount to withdraw 73 | /// @return amounts Array of amounts : 74 | /// - [0] : The token received amount 75 | /// - [1] : The vault token deposited amount 76 | /// @return tokens Array of token addresses 77 | /// - [0] : The token received address 78 | /// - [1] : The vault token deposited address 79 | function withdraw(address vault, uint256 amount) 80 | external 81 | returns (uint256[] memory amounts, address[] memory tokens) 82 | { 83 | require(amount != 0, "BVO: INVALID_AMOUNT"); 84 | IERC20 token = IERC20(operatorStorage.vaults(vault)); 85 | require(address(token) != address(0), "BVO: INVALID_VAULT"); 86 | 87 | uint256 tokenBalanceBefore = token.balanceOf(address(this)); 88 | uint256 vaultBalanceBefore = IERC20(vault).balanceOf(address(this)); 89 | 90 | (bool success, ) = vault.call(abi.encodeWithSignature("withdraw(uint256)", amount)); 91 | require(success, "BVO: WITHDRAW_CALL_FAILED"); 92 | 93 | uint256 tokenAmount = token.balanceOf(address(this)) - tokenBalanceBefore; 94 | uint256 vaultAmount = vaultBalanceBefore - IERC20(vault).balanceOf(address(this)); 95 | require(vaultAmount == amount, "BVO: INVALID_AMOUNT_WITHDRAWED"); 96 | require(tokenAmount != 0, "BVO: INVALID_AMOUNT"); 97 | 98 | amounts = new uint256[](2); 99 | tokens = new address[](2); 100 | 101 | // Output amounts 102 | amounts[0] = tokenAmount; 103 | amounts[1] = amount; 104 | 105 | // Output token 106 | tokens[0] = address(token); 107 | tokens[1] = vault; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /contracts/operators/Beefy/BeefyVaultStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | /// @title BeefyVaultOperator storage contract 8 | contract BeefyVaultStorage is Ownable { 9 | /// @dev Emitted when a vault is added 10 | /// @param vault The vault address 11 | /// @param tokenOrZapper The underlying token address or zapper 12 | event VaultAdded(address vault, address tokenOrZapper); 13 | 14 | /// @dev Emitted when a vault is removed 15 | /// @param vault The removed vault address 16 | event VaultRemoved(address vault); 17 | 18 | /// @dev Map of vault address with underlying token address or zapper 19 | mapping(address => address) public vaults; 20 | 21 | /// @notice Add a beefy single asset vault 22 | /// @param vault The vault address 23 | /// @param tokenOrZapper The underlying token address or zapper (used to deposit) 24 | function addVault(address vault, address tokenOrZapper) external onlyOwner { 25 | require(vault != address(0), "BVS: INVALID_VAULT_ADDRESS"); 26 | require(tokenOrZapper != address(0), "BVS: INVALID_UNDERLYING_ADDRESS"); 27 | require(vaults[vault] == address(0), "BVS: ALREADY_EXISTENT_VAULT"); 28 | vaults[vault] = tokenOrZapper; 29 | emit VaultAdded(vault, tokenOrZapper); 30 | } 31 | 32 | /// @notice Remove a beefy vault 33 | /// @param vault The vault address to remove 34 | function removeVault(address vault) external onlyOwner { 35 | require(vaults[vault] != address(0), "BVS: NON_EXISTENT_VAULT"); 36 | delete vaults[vault]; 37 | emit VaultRemoved(vault); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/operators/Beefy/lp/README.md: -------------------------------------------------------------------------------- 1 | # Beefy Zap Lp Vaul Operators 2 | 3 | ## Optimal swap amount 4 | 5 | To solve the problem of remaining dust after adding liquidity in a Uniswap-like liquidity pool, we need to perform a calculation to minimize the amount of unused tokens by the Uniswap-like router. 6 | 7 | > For this example, we will use an WBNB-USDT Uniswap liquidity pool, and we have 1 WBNB as a total investment. 8 | 9 | ### Compute the optimal amount 10 | 11 | | Name | Value | 12 | | ---- | :-------------------------------------------------- | 13 | | A | Amount of BNB in the liquidity pool | 14 | | B | Amount of USDT in the liquidity pool | 15 | | K | Ratio between **A** and **B** in the liquidity pool | 16 | | f | Platform trading fee | 17 | | a | Amount of BNB that i have | 18 | | b | Amount of USDT that i need | 19 | | s | Amount of BNB to swap | 20 | 21 | > We don't know the values of **b** and **s**. 22 | 23 | #### Find b 24 | 25 | The ratio between **A** and **B** in the liquidity pool must be constant whatever the liquidity additions. 26 | 27 | $$K = AB$$ 28 | 29 | After the swap, this constant must remain the same. 30 | $$K = (A + (1-f)s)(B-b)$$ 31 | 32 | So we can express **b** in terms of **s**: 33 | $$b = {{B(1-f)s} \over {A+(1-f)s}}$$ 34 | 35 | #### Find s 36 | 37 | After the swap, the ratio between **A** and **B** in the liquidity pool is: 38 | $${A+s \over B-b} = {a-s \over b}$$ 39 | 40 | From this equation we can solve for s using [b espression](#find-b) as follow: 41 | (A + s)b - (a - s)(B - b) = 0 42 | Ab + sb - aB + ab + sB - sb = 0 43 | Ab - aB + ab + sB = 0 44 | **(A + a)b - (a - s)B = 0** 45 | 46 | ##### b expression reminder 47 | 48 | b = B(1 - f)s / (A + (1 - f)s) 49 | 50 | ##### Replace b with the value expressed in terms of s 51 | 52 | (A + a)B(1 - f)s / (A + (1 - f)s) - (a - s)B = 0 53 | (A + a)(1 - f)s / (A + (1 - f)s) - (a - s) = 0 54 | (A + a)(1 - f)s - (a - s)(A + (1 - f)s) = 0 55 | (A + a)(1 - f)s - aA - a(1 - f)s +sA + (1 - f)s² = 0 56 | A(1 - f)s aA + sA + (1 - f)s² = 0 57 | A(2 - f)s -aA + (1 - f)s² = 0 58 | 59 | **(1-f)s² + A(2-F)s - aA = 0** 60 | 61 | $$s = {(- (2 - f)A + \sqrt(((2 - f)A)² + 4(1 - f)Aa)) \over (2(1 - f))}$$ 62 | -------------------------------------------------------------------------------- /contracts/operators/Flat/FlatOperator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./IFlatOperator.sol"; 5 | 6 | /// @title The flat operator doesn't execute any logic to an input. 7 | /// @notice The input is the output, and the input amount is the output amount. 8 | /// Usefull to deposit/withdraw a token without swapping in your Orders. 9 | contract FlatOperator is IFlatOperator { 10 | /// @inheritdoc IFlatOperator 11 | function transfer(address token, uint256 amount) 12 | external 13 | payable 14 | override 15 | returns (uint256[] memory amounts, address[] memory tokens) 16 | { 17 | require(amount != 0, "FO: INVALID_AMOUNT"); 18 | 19 | amounts = new uint256[](2); 20 | tokens = new address[](2); 21 | 22 | // Output amounts 23 | amounts[0] = amount; 24 | amounts[1] = amount; 25 | // Output token 26 | tokens[0] = token; 27 | tokens[1] = token; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/operators/Flat/IFlatOperator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | /// @title FlatOperator Operator Interface 7 | interface IFlatOperator { 8 | /// @notice Execute the flat operator... it does nothing ! 9 | /// @param token The token address 10 | /// @param amount The amount 11 | /// @return amounts Array of output amounts 12 | /// @return tokens Array of output tokens 13 | function transfer(address token, uint256 amount) 14 | external 15 | payable 16 | returns (uint256[] memory amounts, address[] memory tokens); 17 | } 18 | -------------------------------------------------------------------------------- /contracts/operators/Flat/README.md: -------------------------------------------------------------------------------- 1 | ## FlatOperator 2 | 3 | We have one main operator called _ZeroExOperator_ and he is responsible for swapping assets with 0x. 4 | However, the users may not want to swap everytime... 5 | 6 | For example, the user wants to create a portfolio with 5 DAI and 5 USDC. But if the input is 10 DAI, he can't "swap 5 DAI for 5 DAI" with the _ZeroExOperator_. It makes no sense and will revert. 7 | 8 | ![image](https://user-images.githubusercontent.com/22816913/137106682-02211ca4-cafd-4dea-a254-c4726e1109f5.png) 9 | 10 | To resolve that, we created an operator "doing nothing", the _FlatOperator_. 11 | In fact, we just want to deposit or withdraw without swapping in some cases. 12 | 13 | ![image](https://user-images.githubusercontent.com/22816913/137106149-217ff4d2-e1df-47ab-b7a4-765d41f48af6.png) 14 | 15 | The FlatOperator will do nothing, and return to the factory that "input = output" (for the amount and token address). This way, it "simulates" a deposit (or withdraw in the case of some factory functions). 16 | 17 | ```javascript 18 | function transfer(address token, uint256 amount) 19 | external 20 | payable 21 | override 22 | returns (uint256[] memory amounts, address[] memory tokens) 23 | { 24 | require(amount != 0, "FO: INVALID_AMOUNT"); 25 | 26 | amounts = new uint256[](2); 27 | tokens = new address[](2); 28 | 29 | // Output amounts 30 | amounts[0] = amount; 31 | amounts[1] = amount; 32 | 33 | // Output token 34 | tokens[0] = token; 35 | tokens[1] = token; 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /contracts/operators/Paraswap/IParaswapOperator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | /// @title Paraswap Operator Interface 7 | interface IParaswapOperator { 8 | /// @notice Execute a swap via Paraswap 9 | /// @param sellToken The token sold 10 | /// @param buyToken The token bought 11 | /// @param swapCallData Paraswap calldata from the API 12 | /// @return amounts Array of output amounts 13 | /// @return tokens Array of output tokens 14 | function performSwap( 15 | IERC20 sellToken, 16 | IERC20 buyToken, 17 | bytes calldata swapCallData 18 | ) external payable returns (uint256[] memory amounts, address[] memory tokens); 19 | } 20 | -------------------------------------------------------------------------------- /contracts/operators/Paraswap/ParaswapOperator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./IParaswapOperator.sol"; 5 | import "../../libraries/ExchangeHelpers.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | /// @title The paraswap operator to execute swap with the aggregator 9 | /// @dev see documentation => https://developers.paraswap.network/smart-contracts 10 | contract ParaswapOperator is IParaswapOperator { 11 | address public immutable tokenTransferProxy; 12 | address public immutable augustusSwapper; 13 | 14 | /// @dev No storage, only immutable 15 | constructor(address _tokenTransferProxy, address _augustusSwapper) { 16 | require(_tokenTransferProxy != address(0) && _augustusSwapper != address(0), "PSO: INVALID_ADDRESS"); 17 | tokenTransferProxy = _tokenTransferProxy; 18 | augustusSwapper = _augustusSwapper; 19 | } 20 | 21 | /// @inheritdoc IParaswapOperator 22 | function performSwap( 23 | IERC20 sellToken, 24 | IERC20 buyToken, 25 | bytes calldata swapCallData 26 | ) external payable override returns (uint256[] memory amounts, address[] memory tokens) { 27 | require(sellToken != buyToken, "PSO: SAME_INPUT_OUTPUT"); 28 | amounts = new uint256[](2); 29 | tokens = new address[](2); 30 | uint256 buyBalanceBeforePurchase = buyToken.balanceOf(address(this)); 31 | uint256 sellBalanceBeforePurchase = sellToken.balanceOf(address(this)); 32 | 33 | ExchangeHelpers.setMaxAllowance(sellToken, tokenTransferProxy); 34 | (bool success, ) = augustusSwapper.call(swapCallData); 35 | require(success, "PSO: SWAP_FAILED"); 36 | 37 | uint256 amountBought = buyToken.balanceOf(address(this)) - buyBalanceBeforePurchase; 38 | uint256 amountSold = sellBalanceBeforePurchase - sellToken.balanceOf(address(this)); 39 | require(amountBought != 0, "PSO: INVALID_AMOUNT_BOUGHT"); 40 | require(amountSold != 0, "PSO: INVALID_AMOUNT_SOLD"); 41 | 42 | // Output amounts 43 | amounts[0] = amountBought; 44 | amounts[1] = amountSold; 45 | // Output token 46 | tokens[0] = address(buyToken); 47 | tokens[1] = address(sellToken); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /contracts/operators/StakeDAO/StakeDaoStrategyStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | /// @dev A Curve pool with its number of coins 7 | /// @param poolAddress The address of the curve pool 8 | /// @param poolCoinAmount The number of coins inside 9 | /// @param lpToken The corresponding pool LP token address 10 | struct CurvePool { 11 | address poolAddress; 12 | uint96 poolCoinAmount; 13 | address lpToken; 14 | } 15 | 16 | /// @title StakeDAO strategy operator's storage contract 17 | contract StakeDaoStrategyStorage is Ownable { 18 | /// @dev Emitted when a strategy is added 19 | /// @param strategy The strategy address 20 | /// @param pool The underlying CurvePool 21 | event StrategyAdded(address strategy, CurvePool pool); 22 | 23 | /// @dev Emitted when a strategy is removed 24 | /// @param strategy The removed strategy address 25 | event StrategyRemoved(address strategy); 26 | 27 | /// @dev Map of strategy address with underlying CurvePool 28 | mapping(address => CurvePool) public strategies; 29 | 30 | /// @notice Add a StakeDAO strategy 31 | /// @param strategy The strategy address 32 | /// @param curvePool The underlying CurvePool (used to deposit) 33 | function addStrategy(address strategy, CurvePool calldata curvePool) external onlyOwner { 34 | require(strategy != address(0), "SDSS: INVALID_STRATEGY_ADDRESS"); 35 | require(curvePool.poolAddress != address(0), "SDSS: INVALID_POOL_ADDRESS"); 36 | require(curvePool.lpToken != address(0), "SDSS: INVALID_TOKEN_ADDRESS"); 37 | require(strategies[strategy].poolAddress == address(0), "SDSS: STRATEGY_ALREADY_HAS_POOL"); 38 | require(strategies[strategy].lpToken == address(0), "SDSS: STRATEGY_ALREADY_HAS_LP"); 39 | strategies[strategy] = curvePool; 40 | emit StrategyAdded(strategy, curvePool); 41 | } 42 | 43 | /// @notice Remove a StakeDAO strategy 44 | /// @param strategy The strategy address to remove 45 | function removeStrategy(address strategy) external onlyOwner { 46 | require(strategies[strategy].poolAddress != address(0), "SDSS: NON_EXISTENT_STRATEGY"); 47 | delete strategies[strategy]; 48 | emit StrategyRemoved(strategy); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /contracts/operators/Yearn/YearnVaultStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | struct CurvePool { 7 | address poolAddress; 8 | uint96 poolCoinAmount; 9 | address lpToken; 10 | } 11 | 12 | /// @title YearnVaultStorage storage contract 13 | contract YearnVaultStorage is Ownable { 14 | /// @dev Emitted when a vault is added 15 | /// @param vault The vault address 16 | /// @param pool The underlying CurvePool 17 | event VaultAdded(address vault, CurvePool pool); 18 | 19 | /// @dev Emitted when a vault is removed 20 | /// @param vault The removed vault address 21 | event VaultRemoved(address vault); 22 | 23 | /// @dev Map of vault address with underlying CurvePool 24 | mapping(address => CurvePool) public vaults; 25 | 26 | /// @notice Add a Yearn Curve vault 27 | /// @param vault The vault address 28 | /// @param curvePool The underlying CurvePool (used to add liquidity) 29 | function addVault(address vault, CurvePool calldata curvePool) external onlyOwner { 30 | require(vault != address(0), "YVS: INVALID_VAULT_ADDRESS"); 31 | require(curvePool.poolAddress != address(0), "YVS: INVALID_POOL_ADDRESS"); 32 | require(curvePool.lpToken != address(0), "YVS: INVALID_TOKEN_ADDRESS"); 33 | require(vaults[vault].poolAddress == address(0), "YVS: VAULT_ALREADY_HAS_POOL"); 34 | require(vaults[vault].lpToken == address(0), "YVS: VAULT_ALREADY_HAS_LP"); 35 | vaults[vault] = curvePool; 36 | emit VaultAdded(vault, curvePool); 37 | } 38 | 39 | /// @notice Remove a Yearn vault 40 | /// @param vault The vault address to remove 41 | function removeVault(address vault) external onlyOwner { 42 | require(vaults[vault].poolAddress != address(0), "YVS: NON_EXISTENT_VAULT"); 43 | delete vaults[vault]; 44 | emit VaultRemoved(vault); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /contracts/operators/ZeroEx/IZeroExOperator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | /// @title ZeroEx Operator Interface 7 | interface IZeroExOperator { 8 | /// @notice Execute a swap via 0x 9 | /// @param sellToken The token sold 10 | /// @param buyToken The token bought 11 | /// @param swapCallData 0x calldata from the API 12 | /// @return amounts Array of output amounts 13 | /// @return tokens Array of output tokens 14 | function performSwap( 15 | IERC20 sellToken, 16 | IERC20 buyToken, 17 | bytes calldata swapCallData 18 | ) external payable returns (uint256[] memory amounts, address[] memory tokens); 19 | } 20 | -------------------------------------------------------------------------------- /contracts/operators/ZeroEx/README.md: -------------------------------------------------------------------------------- 1 | ## ZeroExOperator 2 | 3 | This is the main Operator. Interacting with [0x](https://0x.org/) (AMM aggregator). 4 | 5 | 0x creates off-chain orders, and settles on chain. This way, the ZeroExOperator expects to receive calldata to forwarded to 0x contracts : 6 | 7 | ```javascript 8 | /// @param swapCallData 0x calldata from the API 9 | ``` 10 | 11 | The route used to get such information follows this pattern: `https://api.0x.org/swap/v1/quote?buyToken=SNX&sellToken=LINK&buyAmount=7160000000000000000000` 12 | -------------------------------------------------------------------------------- /contracts/operators/ZeroEx/ZeroExOperator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "./IZeroExOperator.sol"; 5 | import "./ZeroExStorage.sol"; 6 | import "../../libraries/ExchangeHelpers.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | 9 | /// @title The 0x protocol operator to execute swap with the aggregator 10 | contract ZeroExOperator is IZeroExOperator { 11 | ZeroExStorage public immutable operatorStorage; 12 | 13 | /// @dev Deploy with the storage contract 14 | constructor(address swapTarget) { 15 | operatorStorage = new ZeroExStorage(); 16 | ZeroExStorage(operatorStorage).updatesSwapTarget(swapTarget); 17 | ZeroExStorage(operatorStorage).transferOwnership(msg.sender); 18 | } 19 | 20 | /// @inheritdoc IZeroExOperator 21 | function performSwap( 22 | IERC20 sellToken, 23 | IERC20 buyToken, 24 | bytes calldata swapCallData 25 | ) external payable override returns (uint256[] memory amounts, address[] memory tokens) { 26 | require(sellToken != buyToken, "ZEO: SAME_INPUT_OUTPUT"); 27 | amounts = new uint256[](2); 28 | tokens = new address[](2); 29 | uint256 buyBalanceBeforePurchase = buyToken.balanceOf(address(this)); 30 | uint256 sellBalanceBeforePurchase = sellToken.balanceOf(address(this)); 31 | 32 | bool success = ExchangeHelpers.fillQuote(sellToken, operatorStorage.swapTarget(), swapCallData); 33 | require(success, "ZEO: SWAP_FAILED"); 34 | 35 | uint256 amountBought = buyToken.balanceOf(address(this)) - buyBalanceBeforePurchase; 36 | uint256 amountSold = sellBalanceBeforePurchase - sellToken.balanceOf(address(this)); 37 | require(amountBought != 0, "ZEO: INVALID_AMOUNT_BOUGHT"); 38 | require(amountSold != 0, "ZEO: INVALID_AMOUNT_SOLD"); 39 | 40 | // Output amounts 41 | amounts[0] = amountBought; 42 | amounts[1] = amountSold; 43 | // Output token 44 | tokens[0] = address(buyToken); 45 | tokens[1] = address(sellToken); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/operators/ZeroEx/ZeroExStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | /// @title ZeroExOperator storage contract 7 | contract ZeroExStorage is Ownable { 8 | address public swapTarget; 9 | 10 | /// @notice Update the address of 0x swaptarget 11 | function updatesSwapTarget(address swapTargetValue) external onlyOwner { 12 | swapTarget = swapTargetValue; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/utils/NestedAssetBatcher.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity 0.8.14; 3 | 4 | import "@openzeppelin/contracts/interfaces/IERC721Enumerable.sol"; 5 | 6 | interface INestedAsset is IERC721Enumerable { 7 | function tokenURI(uint256 _tokenId) external view returns (string memory); 8 | 9 | function lastOwnerBeforeBurn(uint256 _tokenId) external view returns (address); 10 | } 11 | 12 | interface INestedRecords { 13 | function tokenHoldings(uint256 _nftId) external view returns (address[] memory, uint256[] memory); 14 | } 15 | 16 | /// @title Batcher for NestedAsset 17 | /// @notice Front-end batch calls to minimize interactions. 18 | contract NestedAssetBatcher { 19 | INestedAsset public immutable nestedAsset; 20 | INestedRecords public immutable nestedRecords; 21 | 22 | struct Nft { 23 | uint256 id; 24 | Asset[] assets; 25 | } 26 | 27 | struct Asset { 28 | address token; 29 | uint256 qty; 30 | } 31 | 32 | constructor(INestedAsset _nestedAsset, INestedRecords _nestedRecords) { 33 | nestedAsset = _nestedAsset; 34 | nestedRecords = _nestedRecords; 35 | } 36 | 37 | /// @notice Get all NestedAsset tokenURIs owned by a user 38 | /// @param user The address of the user 39 | /// @return String array of all tokenURIs 40 | function getURIs(address user) external view returns (string[] memory) { 41 | unchecked { 42 | uint256 numTokens = nestedAsset.balanceOf(user); 43 | string[] memory uriList = new string[](numTokens); 44 | 45 | for (uint256 i; i < numTokens; i++) { 46 | uriList[i] = nestedAsset.tokenURI(nestedAsset.tokenOfOwnerByIndex(user, i)); 47 | } 48 | 49 | return (uriList); 50 | } 51 | } 52 | 53 | /// @notice Get all NestedAsset IDs owned by a user 54 | /// @param user The address of the user 55 | /// @return Array of all IDs 56 | function getIds(address user) external view returns (uint256[] memory) { 57 | unchecked { 58 | uint256 numTokens = nestedAsset.balanceOf(user); 59 | uint256[] memory ids = new uint256[](numTokens); 60 | for (uint256 i; i < numTokens; i++) { 61 | ids[i] = nestedAsset.tokenOfOwnerByIndex(user, i); 62 | } 63 | return (ids); 64 | } 65 | } 66 | 67 | /// @notice Get all NFTs (with tokens and quantities) owned by a user 68 | /// @param user The address of the user 69 | /// @return Array of all NFTs (struct Nft) 70 | function getNfts(address user) external view returns (Nft[] memory) { 71 | unchecked { 72 | uint256 numTokens = nestedAsset.balanceOf(user); 73 | Nft[] memory nfts = new Nft[](numTokens); 74 | for (uint256 i; i < numTokens; i++) { 75 | uint256 nftId = nestedAsset.tokenOfOwnerByIndex(user, i); 76 | (address[] memory tokens, uint256[] memory amounts) = nestedRecords.tokenHoldings(nftId); 77 | uint256 tokenLength = tokens.length; 78 | Asset[] memory nftAssets = new Asset[](tokenLength); 79 | for (uint256 j; j < tokenLength; j++) { 80 | nftAssets[j] = Asset({ token: tokens[j], qty: amounts[j] }); 81 | } 82 | nfts[i] = Nft({ id: nftId, assets: nftAssets }); 83 | } 84 | return (nfts); 85 | } 86 | } 87 | 88 | /// @notice Require the given tokenID to haven been created and call tokenHoldings. 89 | /// @param _nftId The token id 90 | /// @return tokenHoldings returns 91 | function requireTokenHoldings(uint256 _nftId) external view returns (address[] memory, uint256[] memory) { 92 | try nestedAsset.ownerOf(_nftId) {} catch { 93 | // owner == address(0) 94 | require(nestedAsset.lastOwnerBeforeBurn(_nftId) != address(0), "NAB: NEVER_CREATED"); 95 | } 96 | return nestedRecords.tokenHoldings(_nftId); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import "solidity-coverage"; 3 | import "@nomiclabs/hardhat-waffle"; 4 | import "hardhat-deploy"; 5 | import "hardhat-deploy-ethers"; 6 | import "@typechain/hardhat"; 7 | import "hardhat-gas-reporter"; 8 | import "@nomiclabs/hardhat-solhint"; 9 | import "hardhat-contract-sizer"; 10 | import "hardhat-dependency-compiler"; 11 | import "@nomiclabs/hardhat-etherscan"; 12 | import "@tenderly/hardhat-tenderly"; 13 | 14 | import { HardhatUserConfig } from "hardhat/config"; 15 | 16 | const accounts = { 17 | mnemonic: process.env.MNEMONIC, 18 | initialIndex: parseInt(process.env.ACCOUNT_INDEX ?? "0"), 19 | count: 20, 20 | accountsBalance: "99000000000000000000000", 21 | }; 22 | 23 | /** 24 | * Go to https://hardhat.org/config/ to learn more 25 | * @type import('hardhat/config').HardhatUserConfig 26 | */ 27 | const config: HardhatUserConfig = { 28 | defaultNetwork: "hardhat", 29 | networks: { 30 | hardhat: { 31 | live: false, 32 | saveDeployments: true, 33 | tags: ["test", "local"], 34 | accounts: accounts, 35 | // This is because MetaMask mistakenly assumes all networks in http://localhost:8545 to have a chain id of 1337 36 | // but Hardhat uses a different number by default. Please voice your support for MetaMask to fix this: 37 | // https://github.com/MetaMask/metamask-extension/issues/9827 38 | chainId: 1337, 39 | forking: { 40 | url: process.env.FORK_URL, 41 | enabled: process.env.FORKING === "true" 42 | } 43 | }, 44 | ropsten: { 45 | url: `https://eth-ropsten.alchemyapi.io/v2/${process.env.ALCHEMY_ROPSTEN_API_KEY}`, 46 | accounts: accounts, 47 | }, 48 | kovan: { 49 | url: `https://eth-kovan.alchemyapi.io/v2/${process.env.ALCHEMY_KOVAN_API_KEY}`, 50 | accounts: accounts, 51 | }, 52 | mainnet: { 53 | url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_MAINNET_API_KEY}`, 54 | chainId: 1, 55 | accounts: accounts, 56 | }, 57 | optimism: { 58 | url: "https://mainnet.optimism.io/", 59 | chainId: 10, 60 | accounts: accounts, 61 | }, 62 | bsc: { 63 | url: "https://bsc-dataseed.binance.org/", 64 | chainId: 56, 65 | accounts: accounts, 66 | }, 67 | polygon: { 68 | url: "https://polygon-rpc.com/", 69 | chainId: 137, 70 | accounts: accounts, 71 | }, 72 | fantom: { 73 | url: "https://rpc.ftm.tools/", 74 | chainId: 250, 75 | accounts: accounts, 76 | }, 77 | arbitrum: { 78 | url: "https://rpc.ankr.com/arbitrum", 79 | chainId: 42161, 80 | accounts: accounts, 81 | }, 82 | celo: { 83 | url: "https://forno.celo.org/", 84 | chainId: 42220, 85 | accounts: accounts, 86 | }, 87 | avalanche: { 88 | url: "https://api.avax.network/ext/bc/C/rpc", 89 | chainId: 43114, 90 | accounts: accounts, 91 | } 92 | }, 93 | solidity: { 94 | version: "0.8.14", 95 | settings: { 96 | optimizer: { 97 | enabled: true, 98 | runs: 5000, 99 | }, 100 | outputSelection: { 101 | "*": { 102 | "*": ["storageLayout"], 103 | }, 104 | }, 105 | }, 106 | }, 107 | typechain: { 108 | outDir: "typechain", 109 | target: "ethers-v5", 110 | }, 111 | gasReporter: { 112 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 113 | currency: "USD", 114 | enabled: process.env.REPORT_GAS === "true", 115 | excludeContracts: ["contracts/mocks/", "contracts/libraries/", "contracts/interfaces/"], 116 | }, 117 | etherscan: { 118 | apiKey: process.env.ETHERSCAN_API_KEY, 119 | }, 120 | dependencyCompiler: { 121 | paths: [ 122 | "@openzeppelin/contracts/governance/TimelockController.sol", 123 | "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol" 124 | ], 125 | }, 126 | tenderly: { 127 | project: "", 128 | username: "", 129 | } 130 | }; 131 | 132 | export default config; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nested-finance", 3 | "version": "1.0.0", 4 | "license": "GPL-3.0-or-later", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "console": "hardhat console", 8 | "run": "npx hardhat node", 9 | "export": "hardhat export --export-all exports/deployments.json", 10 | "mainnet:deploy": "hardhat run scripts/deployAll.ts --network mainnet", 11 | "test": "hardhat test", 12 | "test:coverage": "cross-env FORKING=false hardhat coverage", 13 | "test:fork": " hardhat coverage", 14 | "test:gas": "cross-env FORKING=false REPORT_GAS=true hardhat test", 15 | "test:fork:gas": "REPORT_GAS=true hardhat test", 16 | "prettier": "prettier --write test/**/*.[jt]s && prettier --write test/*.[jt]s && prettier --write contracts/* && prettier --write scripts/*", 17 | "lint": "yarn prettier && solhint -c .solhint.json contracts/**/*.sol", 18 | "typechain": "hardhat typechain" 19 | }, 20 | "devDependencies": { 21 | "@codechecks/client": "^0.1.11", 22 | "@defi-wonderland/smock": "^2.0.1", 23 | "@ethersproject/abi": "^5.4.1", 24 | "@ethersproject/abstract-provider": "^5.4.1", 25 | "@ethersproject/abstract-signer": "^5.4.1", 26 | "@ethersproject/bytes": "^5.4.0", 27 | "@ethersproject/providers": "^5.4.5", 28 | "@nomiclabs/ethereumjs-vm": "^4.2.2", 29 | "@nomiclabs/hardhat-ethers": "^2.0.2", 30 | "@nomiclabs/hardhat-etherscan": "^3.0.1", 31 | "@nomiclabs/hardhat-solhint": "^2.0.0", 32 | "@nomiclabs/hardhat-waffle": "^2.0.1", 33 | "@typechain/ethers-v5": "^7.0.1", 34 | "@typechain/hardhat": "^2.3.0", 35 | "@types/chai": "^4.2.21", 36 | "@types/inquirer": "^7.3.1", 37 | "@types/mocha": "^8.2.2", 38 | "@types/node": "^15.0.1", 39 | "@uniswap/v2-core": "^1.0.1", 40 | "@uniswap/v2-periphery": "^1.1.0-beta.0", 41 | "chai": "^4.3.4", 42 | "cross-env": "^7.0.3", 43 | "dotenv": "^8.2.0", 44 | "ethereum-waffle": "^3.3.0", 45 | "ethers": "^5.4.6", 46 | "hardhat": "^2.9.0", 47 | "hardhat-contract-sizer": "^2.0.3", 48 | "hardhat-dependency-compiler": "^1.1.1", 49 | "hardhat-deploy": "^0.7.0-beta.48", 50 | "hardhat-deploy-ethers": "^0.3.0-beta.7", 51 | "hardhat-gas-reporter": "^1.0.4", 52 | "lodash": "^4.17.21", 53 | "mocha": "^9.1.1", 54 | "prettier": "^2.4.1", 55 | "prettier-plugin-solidity": "^1.0.0-beta.18", 56 | "solhint": "^3.3.4", 57 | "solhint-plugin-prettier": "^0.0.5", 58 | "solidity-coverage": "0.7.18", 59 | "synthetix": "^2.50.0-ovm-alpha", 60 | "ts-node": "^9.1.1", 61 | "typechain": "^5.1.2", 62 | "typescript": "^4.2.4" 63 | }, 64 | "dependencies": { 65 | "@openzeppelin/contracts": "^4.3.2", 66 | "@tenderly/hardhat-tenderly": "^1.0.13", 67 | "inquirer": "^8.1.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /scripts/deployAllWithoutProxy.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers, network } from "hardhat"; 2 | import { Contract } from "ethers"; 3 | import addresses from "../addresses.json"; 4 | import { importOperators, registerFlat, registerZeroEx } from "./utils"; 5 | 6 | interface Deployment { 7 | name: string; 8 | address: string; 9 | } 10 | 11 | // Used to add delay between deployment and etherscan verification 12 | const delay = async (ms: number) => new Promise(res => setTimeout(res, ms)); 13 | 14 | const chainId: string = network.config.chainId.toString(); 15 | const context = JSON.parse(JSON.stringify(addresses)); 16 | 17 | // True if you want to enable the etherscan verification 18 | const etherscan = false; 19 | 20 | // Configuration variables 21 | const maxHoldingsCount = context[chainId].config.maxHoldingsCount; 22 | const zeroExSwapTarget = context[chainId].config.zeroExSwapTarget; 23 | const WETH = context[chainId].config.WETH; 24 | const nestedTreasury = context[chainId].config.nestedTreasury; 25 | 26 | let deployments: Deployment[] = []; 27 | 28 | async function main(): Promise { 29 | console.log("Deploy All : "); 30 | 31 | // Get Factories 32 | const feeSplitterFactory = await ethers.getContractFactory("FeeSplitter"); 33 | const nestedAssetFactory = await ethers.getContractFactory("NestedAsset"); 34 | const nestedRecordsFactory = await ethers.getContractFactory("NestedRecords"); 35 | const operatorResolverFactory = await ethers.getContractFactory("OperatorResolver"); 36 | const flatOperatorFactory = await ethers.getContractFactory("FlatOperator"); 37 | const zeroExOperatorFactory = await ethers.getContractFactory("ZeroExOperator"); 38 | const nestedFactoryFactory = await ethers.getContractFactory("NestedFactory"); 39 | const nestedReserveFactory = await ethers.getContractFactory("NestedReserve"); 40 | const withdrawerFactory = await ethers.getContractFactory("Withdrawer"); 41 | 42 | // Deploy FeeSplitter 43 | const feeSplitter = await feeSplitterFactory.deploy([nestedTreasury], [80], 20, WETH); 44 | await verify("FeeSplitter", feeSplitter, [[nestedTreasury], [80], 20, WETH]); 45 | console.log("FeeSplitter deployed : ", feeSplitter.address); 46 | 47 | // Deploy NestedAsset 48 | const nestedAsset = await nestedAssetFactory.deploy(); 49 | await verify("NestedAsset", nestedAsset, []); 50 | console.log("NestedAsset deployed : ", nestedAsset.address); 51 | 52 | // Deploy NestedRecords 53 | const nestedRecords = await nestedRecordsFactory.deploy(maxHoldingsCount); 54 | await verify("NestedRecords", nestedRecords, [maxHoldingsCount]); 55 | console.log("NestedRecords deployed : ", nestedRecords.address); 56 | 57 | // Deploy NestedReserve 58 | const nestedReserve = await nestedReserveFactory.deploy(); 59 | await verify("NestedReserve", nestedReserve, []); 60 | console.log("NestedReserve deployed : ", nestedReserve.address); 61 | 62 | // Deploy OperatorResolver 63 | const operatorResolver = await operatorResolverFactory.deploy(); 64 | await verify("OperatorResolver", operatorResolver, []); 65 | console.log("OperatorResolver deployed : ", operatorResolver.address); 66 | 67 | // Deploy ZeroExOperator 68 | const zeroExOperator = await zeroExOperatorFactory.deploy(zeroExSwapTarget); 69 | await verify("ZeroExOperator", zeroExOperator, [zeroExSwapTarget]); 70 | console.log("ZeroExOperator deployed : ", zeroExOperator.address); 71 | 72 | // Add ZeroExStorage address 73 | deployments.push({ name: "ZeroExStorage", address: await zeroExOperator.operatorStorage() }); 74 | 75 | // Deploy FlatOperator 76 | const flatOperator = await flatOperatorFactory.deploy(); 77 | await verify("FlatOperator", flatOperator, []); 78 | console.log("FlatOperator deployed : ", flatOperator.address); 79 | 80 | // Deploy Withdrawer 81 | const withdrawer = await withdrawerFactory.deploy(WETH); 82 | await verify("Withdrawer", withdrawer, []); 83 | console.log("Withdrawer deployed : ", withdrawer.address); 84 | 85 | // Deploy NestedFactory 86 | const nestedFactory = await nestedFactoryFactory.deploy( 87 | nestedAsset.address, 88 | nestedRecords.address, 89 | nestedReserve.address, 90 | feeSplitter.address, 91 | WETH, 92 | operatorResolver.address, 93 | withdrawer.address, 94 | ); 95 | await verify("NestedFactory", nestedFactory, [ 96 | nestedAsset.address, 97 | nestedRecords.address, 98 | nestedReserve.address, 99 | feeSplitter.address, 100 | WETH, 101 | operatorResolver.address, 102 | ]); 103 | console.log("NestedFactory deployed : ", nestedFactory.address); 104 | 105 | // Set factory to asset, records and reserve 106 | let tx = await nestedAsset.addFactory(nestedFactory.address); 107 | await tx.wait(); 108 | tx = await nestedRecords.addFactory(nestedFactory.address); 109 | await tx.wait(); 110 | tx = await nestedReserve.addFactory(nestedFactory.address); 111 | await tx.wait(); 112 | 113 | // Add operators to OperatorResolver 114 | 115 | // Add operators to OperatorResolver 116 | await importOperators( 117 | operatorResolver, 118 | [registerFlat(flatOperator), registerZeroEx(zeroExOperator)], 119 | nestedFactory, 120 | ); 121 | 122 | // Convert JSON object to string 123 | const data = JSON.stringify(deployments); 124 | console.log(data); 125 | } 126 | 127 | async function verify(name: string, contract: Contract, params: any[]) { 128 | await contract.deployed(); 129 | if (etherscan) { 130 | // wait 1 minute (recommended) 131 | await delay(60000); 132 | await hre.run("verify:verify", { 133 | address: contract.address, 134 | constructorArguments: params, 135 | }); 136 | } 137 | deployments.push({ name: name, address: contract.address }); 138 | } 139 | 140 | main() 141 | .then(() => process.exit(0)) 142 | .catch((error: Error) => { 143 | console.log(JSON.stringify(deployments)); 144 | console.error(error); 145 | process.exit(1); 146 | }); 147 | -------------------------------------------------------------------------------- /scripts/deployFactory.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers, network } from "hardhat"; 2 | import addresses from "../addresses.json"; 3 | 4 | // Used to add delay between deployment and etherscan verification 5 | const delay = async (ms: number) => new Promise(res => setTimeout(res, ms)); 6 | 7 | const chainId: string = network.config.chainId.toString(); 8 | const context = JSON.parse(JSON.stringify(addresses)); 9 | 10 | // True if you want to enable the etherscan verification 11 | const etherscan = true; 12 | 13 | // Configuration variables 14 | const WETH = context[chainId].config.WETH; 15 | 16 | async function main(): Promise { 17 | // Add new proxy/factory with new operators 18 | const feeSplitterFactory = await ethers.getContractFactory("FeeSplitter"); 19 | const nestedAssetFactory = await ethers.getContractFactory("NestedAsset"); 20 | const nestedRecordsFactory = await ethers.getContractFactory("NestedRecords"); 21 | const operatorResolverFactory = await ethers.getContractFactory("OperatorResolver"); 22 | const nestedFactoryFactory = await ethers.getContractFactory("NestedFactory"); 23 | const nestedReserveFactory = await ethers.getContractFactory("NestedReserve"); 24 | const withdrawerFactory = await ethers.getContractFactory("Withdrawer"); 25 | 26 | const feeSplitter = await feeSplitterFactory.attach(""); 27 | const nestedAsset = await nestedAssetFactory.attach(""); 28 | const nestedRecords = await nestedRecordsFactory.attach(""); 29 | const nestedReserve = await nestedReserveFactory.attach(""); 30 | const operatorResolver = await operatorResolverFactory.attach(""); 31 | const withdrawer = await withdrawerFactory.attach(""); 32 | 33 | // Deploy NestedFactory 34 | const nestedFactory = await nestedFactoryFactory.deploy( 35 | nestedAsset.address, 36 | nestedRecords.address, 37 | nestedReserve.address, 38 | feeSplitter.address, 39 | WETH, 40 | operatorResolver.address, 41 | withdrawer.address, 42 | ); 43 | 44 | await nestedFactory.deployed(); 45 | 46 | console.log("NestedFactory deployed : ", nestedFactory.address); 47 | 48 | // verify Tenderly 49 | const contracts = [ 50 | { 51 | name: "NestedFactory", 52 | address: nestedFactory.address, 53 | }, 54 | ]; 55 | await hre.tenderly.verify(...contracts); 56 | 57 | // verify etherscan 58 | if (etherscan) { 59 | // wait 1 minute (recommended) 60 | await delay(60000); 61 | 62 | await hre.run("verify:verify", { 63 | address: nestedFactory.address, 64 | constructorArguments: [ 65 | nestedAsset.address, 66 | nestedRecords.address, 67 | nestedReserve.address, 68 | feeSplitter.address, 69 | WETH, 70 | operatorResolver.address, 71 | withdrawer.address, 72 | ], 73 | }); 74 | } 75 | } 76 | 77 | main() 78 | .then(() => process.exit(0)) 79 | .catch((error: Error) => { 80 | console.error(error); 81 | process.exit(1); 82 | }); 83 | -------------------------------------------------------------------------------- /scripts/deployFullFactory.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers, network } from "hardhat"; 2 | import { Contract } from "ethers"; 3 | import addresses from "../addresses.json"; 4 | import { importOperators, registerFlat, registerZeroEx } from "./utils"; 5 | 6 | interface Deployment { 7 | name: string; 8 | address: string; 9 | } 10 | 11 | // Used to add delay between deployment and etherscan verification 12 | const delay = async (ms: number) => new Promise(res => setTimeout(res, ms)); 13 | 14 | const chainId: string = network.config.chainId.toString(); 15 | const context = JSON.parse(JSON.stringify(addresses)); 16 | 17 | // True if you want to enable the etherscan verification 18 | const etherscan = false; 19 | 20 | // Configuration variables 21 | const zeroExSwapTarget = context[chainId].config.zeroExSwapTarget; 22 | const WETH = context[chainId].config.WETH; 23 | const multisig = context[chainId].config.multisig; 24 | 25 | let deployments: Deployment[] = []; 26 | 27 | async function main(): Promise { 28 | // Add new proxy/factory with new operators 29 | 30 | const feeSplitterFactory = await ethers.getContractFactory("FeeSplitter"); 31 | const nestedAssetFactory = await ethers.getContractFactory("NestedAsset"); 32 | const nestedRecordsFactory = await ethers.getContractFactory("NestedRecords"); 33 | const operatorResolverFactory = await ethers.getContractFactory("OperatorResolver"); 34 | const flatOperatorFactory = await ethers.getContractFactory("FlatOperator"); 35 | const zeroExOperatorFactory = await ethers.getContractFactory("ZeroExOperator"); 36 | const nestedFactoryFactory = await ethers.getContractFactory("NestedFactory"); 37 | const nestedReserveFactory = await ethers.getContractFactory("NestedReserve"); 38 | const transparentUpgradeableProxyFactory = await ethers.getContractFactory("TransparentUpgradeableProxy"); 39 | const withdrawerFactory = await ethers.getContractFactory("Withdrawer"); 40 | 41 | const feeSplitter = await feeSplitterFactory.attach(""); 42 | const nestedAsset = await nestedAssetFactory.attach(""); 43 | const nestedRecords = await nestedRecordsFactory.attach(""); 44 | const nestedReserve = await nestedReserveFactory.attach(""); 45 | 46 | // Deploy ZeroExOperator 47 | const zeroExOperator = await zeroExOperatorFactory.deploy(zeroExSwapTarget); 48 | await verify("ZeroExOperator", zeroExOperator, [zeroExSwapTarget]); 49 | console.log("ZeroExOperator deployed : ", zeroExOperator.address); 50 | 51 | // Add ZeroExStorage address 52 | deployments.push({ name: "ZeroExStorage", address: await zeroExOperator.operatorStorage() }); 53 | 54 | // Deploy FlatOperator 55 | const flatOperator = await flatOperatorFactory.deploy(); 56 | await verify("FlatOperator", flatOperator, []); 57 | console.log("FlatOperator deployed : ", flatOperator.address); 58 | 59 | const operatorResolver = await operatorResolverFactory.deploy(); 60 | await verify("OperatorResolver", operatorResolver, []); 61 | console.log("OperatorResolver deployed : ", operatorResolver.address); 62 | 63 | // Deploy Withdrawer 64 | const withdrawer = await withdrawerFactory.deploy(WETH); 65 | await verify("Withdrawer", withdrawer, []); 66 | console.log("Withdrawer deployed : ", withdrawer.address); 67 | 68 | // Deploy NestedFactory 69 | const nestedFactory = await nestedFactoryFactory.deploy( 70 | nestedAsset.address, 71 | nestedRecords.address, 72 | nestedReserve.address, 73 | feeSplitter.address, 74 | WETH, 75 | operatorResolver.address, 76 | withdrawer.address, 77 | ); 78 | await verify("NestedFactory", nestedFactory, [ 79 | nestedAsset.address, 80 | nestedRecords.address, 81 | nestedReserve.address, 82 | feeSplitter.address, 83 | WETH, 84 | operatorResolver.address, 85 | ]); 86 | console.log("New NestedFactory deployed : ", nestedFactory.address); 87 | 88 | const owner = await nestedRecords.owner(); 89 | 90 | // Deploy FactoryProxy 91 | const factoryProxy = await transparentUpgradeableProxyFactory.deploy(nestedFactory.address, owner, []); 92 | await verify("FactoryProxy", factoryProxy, [nestedFactory.address, owner, []]); 93 | console.log("FactoryProxy deployed : ", factoryProxy.address); 94 | 95 | // Set factory to asset, records and reserve 96 | let tx = await nestedAsset.addFactory(factoryProxy.address); 97 | await tx.wait(); 98 | tx = await nestedRecords.addFactory(factoryProxy.address); 99 | await tx.wait(); 100 | tx = await nestedReserve.addFactory(factoryProxy.address); 101 | await tx.wait(); 102 | 103 | // Initialize the owner in proxy storage by calling upgradeToAndCall 104 | // It will upgrade with the same address (no side effects) 105 | const initData = await nestedFactory.interface.encodeFunctionData("initialize", [owner]); 106 | tx = await factoryProxy.upgradeToAndCall(nestedFactory.address, initData); 107 | await tx.wait(); 108 | 109 | // Set multisig as admin of proxy, so we can call the implementation as owner 110 | tx = await factoryProxy.changeAdmin(multisig); 111 | await tx.wait(); 112 | 113 | // Attach factory impl to proxy address 114 | const proxyImpl = await nestedFactoryFactory.attach(factoryProxy.address); 115 | 116 | // Reset feeSplitter in proxy storage 117 | tx = await proxyImpl.setFeeSplitter(feeSplitter.address); 118 | await tx.wait(); 119 | 120 | // Set entry fees in proxy storage 121 | tx = await proxyImpl.setEntryFees(30); 122 | await tx.wait(); 123 | 124 | // Set exit fees in proxy storage 125 | tx = await proxyImpl.setExitFees(80); 126 | await tx.wait(); 127 | 128 | await importOperators(operatorResolver, [registerFlat(flatOperator), registerZeroEx(zeroExOperator)], proxyImpl); 129 | 130 | // Convert JSON object to string 131 | const data = JSON.stringify(deployments); 132 | console.log(data); 133 | } 134 | 135 | async function verify(name: string, contract: Contract, params: any[]) { 136 | await contract.deployed(); 137 | if (etherscan) { 138 | // wait 1 minute (recommended) 139 | await delay(60000); 140 | await hre.run("verify:verify", { 141 | address: contract.address, 142 | constructorArguments: params, 143 | }); 144 | } 145 | deployments.push({ name: name, address: contract.address }); 146 | } 147 | 148 | main() 149 | .then(() => process.exit(0)) 150 | .catch((error: Error) => { 151 | console.log(JSON.stringify(deployments)); 152 | console.error(error); 153 | process.exit(1); 154 | }); 155 | -------------------------------------------------------------------------------- /scripts/deployNestedAssetBatcher.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers, network } from "hardhat"; 2 | import addresses from "../addresses.json"; 3 | 4 | // Used to add delay between deployment and etherscan verification 5 | const delay = async (ms: number) => new Promise(res => setTimeout(res, ms)); 6 | 7 | const chainId: string = network.config.chainId.toString(); 8 | const context = JSON.parse(JSON.stringify(addresses)); 9 | 10 | async function main(): Promise { 11 | console.log("Deploy NestedAssetBatcher : "); 12 | 13 | // Get Addresses 14 | const nestedAssetAddress = context[chainId].NestedAsset; 15 | const nestedRecordsAddress = context[chainId].NestedRecords; 16 | 17 | // Get Factories 18 | const nestedAssetBatcherFactory = await ethers.getContractFactory("NestedAssetBatcher"); 19 | const nestedAssetBatcher = await nestedAssetBatcherFactory.deploy(nestedAssetAddress, nestedRecordsAddress); 20 | await nestedAssetBatcher.deployed(); 21 | 22 | console.log("NestedAssetBatcher Deployer : ", nestedAssetBatcher.address); 23 | 24 | await delay(60000); 25 | 26 | await hre.run("verify:verify", { 27 | address: nestedAssetBatcher.address, 28 | constructorArguments: [nestedAssetAddress, nestedRecordsAddress], 29 | }); 30 | } 31 | 32 | main() 33 | .then(() => process.exit(0)) 34 | .catch((error: Error) => { 35 | console.error(error); 36 | process.exit(1); 37 | }); 38 | -------------------------------------------------------------------------------- /scripts/deployOwnerProxy.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers, network } from "hardhat"; 2 | import addresses from "../addresses.json"; 3 | 4 | // Used to add delay between deployment and etherscan verification 5 | const delay = async (ms: number) => new Promise(res => setTimeout(res, ms)); 6 | 7 | const chainId: string = network.config.chainId.toString(); 8 | const context = JSON.parse(JSON.stringify(addresses)); 9 | 10 | async function main(): Promise { 11 | const timelockAddress = context[chainId].Timelock; 12 | const feeSplitterAddress = context[chainId].FeeSplitter; 13 | const nestedAssetAddress = context[chainId].NestedAsset; 14 | const nestedRecordsAddress = context[chainId].NestedRecords; 15 | const nestedReserveAddress = context[chainId].NestedReserve; 16 | const operatorResolverAddress = context[chainId].OperatorResolver; 17 | const nestedFactoryAddress = context[chainId].NestedFactoryProxy; 18 | const zeroExStorageAddress = context[chainId].ZeroExStorage; 19 | 20 | // Get factories 21 | const ownerProxyFactory = await ethers.getContractFactory("OwnerProxy"); 22 | const feeSplitterFactory = await ethers.getContractFactory("FeeSplitter"); 23 | const nestedAssetFactory = await ethers.getContractFactory("NestedAsset"); 24 | const nestedRecordsFactory = await ethers.getContractFactory("NestedRecords"); 25 | const nestedReserveFactory = await ethers.getContractFactory("NestedReserve"); 26 | const operatorResolverFactory = await ethers.getContractFactory("OperatorResolver"); 27 | const nestedFactoryFactory = await ethers.getContractFactory("NestedFactory"); 28 | const zeroExStorageFactory = await ethers.getContractFactory("ZeroExStorage"); 29 | 30 | // Get contracts 31 | const feeSplitter = await feeSplitterFactory.attach(feeSplitterAddress); 32 | const nestedAsset = await nestedAssetFactory.attach(nestedAssetAddress); 33 | const nestedRecords = await nestedRecordsFactory.attach(nestedRecordsAddress); 34 | const nestedReserve = await nestedReserveFactory.attach(nestedReserveAddress); 35 | const operatorResolver = await operatorResolverFactory.attach(operatorResolverAddress); 36 | const nestedFactory = await nestedFactoryFactory.attach(nestedFactoryAddress); 37 | const zeroExStorage = await zeroExStorageFactory.attach(zeroExStorageAddress); 38 | 39 | console.log("Deploy OwnerProxy : "); 40 | 41 | // Deploy OwnerProxy 42 | const ownerProxy = await ownerProxyFactory.deploy(); 43 | await ownerProxy.deployed(); 44 | console.log("OwnerProxy Deployed : ", ownerProxy.address); 45 | 46 | // Transfer ownerships 47 | let tx = await ownerProxy.transferOwnership(timelockAddress); 48 | await tx.wait(); 49 | console.log("OwnerProxy ownership transfered to Timelock"); 50 | 51 | tx = await feeSplitter.transferOwnership(ownerProxy.address); 52 | await tx.wait(); 53 | console.log("FeeSplitter ownership transfered to OwnerProxy"); 54 | 55 | tx = await nestedAsset.transferOwnership(ownerProxy.address); 56 | await tx.wait(); 57 | console.log("NestedAsset ownership transfered to OwnerProxy"); 58 | 59 | tx = await nestedRecords.transferOwnership(ownerProxy.address); 60 | await tx.wait(); 61 | console.log("NestedRecords ownership transfered to OwnerProxy"); 62 | 63 | tx = await nestedReserve.transferOwnership(ownerProxy.address); 64 | await tx.wait(); 65 | console.log("NestedReserve ownership transfered to OwnerProxy"); 66 | 67 | tx = await operatorResolver.transferOwnership(ownerProxy.address); 68 | await tx.wait(); 69 | console.log("OperatorResolver ownership transfered to OwnerProxy"); 70 | 71 | tx = await nestedFactory.transferOwnership(ownerProxy.address); 72 | await tx.wait(); 73 | console.log("NestedFactory ownership transfered to OwnerProxy"); 74 | 75 | tx = await zeroExStorage.transferOwnership(ownerProxy.address); 76 | await tx.wait(); 77 | console.log("ZeroExStorage ownership transfered to OwnerProxy"); 78 | 79 | // Verify OwnerProxy on etherscan 80 | await delay(60000); 81 | await hre.run("verify:verify", { 82 | address: ownerProxy.address, 83 | constructorArguments: [], 84 | }); 85 | } 86 | 87 | main() 88 | .then(() => process.exit(0)) 89 | .catch((error: Error) => { 90 | console.error(error); 91 | process.exit(1); 92 | }); 93 | -------------------------------------------------------------------------------- /scripts/deployTimelockWithEmergency.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers, network } from "hardhat"; 2 | import addresses from "../addresses.json"; 3 | 4 | // Used to add delay between deployment and etherscan verification 5 | const delay = async (ms: number) => new Promise(res => setTimeout(res, ms)); 6 | 7 | const chainId: string = network.config.chainId.toString(); 8 | const context = JSON.parse(JSON.stringify(addresses)); 9 | 10 | async function main(): Promise { 11 | console.log("Deploy TimelockControllerEmergency : "); 12 | 13 | // Get Addresses 14 | const multisig = context[chainId].config.multisig; 15 | const emergencyMultisig = context[chainId].config.emergencyMultisig; 16 | 17 | // Get Factories 18 | const timelockControllerEmergencyFactory = await ethers.getContractFactory("TimelockControllerEmergency"); 19 | const timelockControllerEmergency = await timelockControllerEmergencyFactory.deploy( 20 | 21600, 21 | [multisig, emergencyMultisig], 22 | [multisig, emergencyMultisig], 23 | emergencyMultisig, 24 | ); 25 | await timelockControllerEmergency.deployed(); 26 | 27 | console.log("TimelockControllerEmergency Deployed : ", timelockControllerEmergency.address); 28 | 29 | await delay(60000); 30 | 31 | await hre.run("verify:verify", { 32 | address: timelockControllerEmergency.address, 33 | constructorArguments: [21600, [multisig, emergencyMultisig], [multisig, emergencyMultisig], emergencyMultisig], 34 | }); 35 | } 36 | 37 | main() 38 | .then(() => process.exit(0)) 39 | .catch((error: Error) => { 40 | console.error(error); 41 | process.exit(1); 42 | }); 43 | -------------------------------------------------------------------------------- /scripts/op_scripts/deployOperatorScripts.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | 3 | // Used to add delay between deployment and etherscan verification 4 | const delay = async (ms: number) => new Promise(res => setTimeout(res, ms)); 5 | 6 | async function main(): Promise { 7 | console.log("Deploy OperatorScripts : "); 8 | 9 | const nestedFactoryAddr = ""; 10 | const operatorResolverAddr = ""; 11 | 12 | // Get Factories 13 | const operatorScriptsFactory = await ethers.getContractFactory("OperatorScripts"); 14 | const operatorScripts = await operatorScriptsFactory.deploy(nestedFactoryAddr, operatorResolverAddr); 15 | await operatorScripts.deployed(); 16 | 17 | console.log("OperatorScripts Deployed : ", operatorScripts.address); 18 | 19 | await delay(60000); 20 | 21 | await hre.run("verify:verify", { 22 | address: operatorScripts.address, 23 | constructorArguments: [nestedFactoryAddr, operatorResolverAddr], 24 | }); 25 | } 26 | 27 | main() 28 | .then(() => process.exit(0)) 29 | .catch((error: Error) => { 30 | console.error(error); 31 | process.exit(1); 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/op_scripts/removeOperator.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network } from "hardhat"; 2 | import { toBytes32 } from "../utils"; 3 | 4 | async function main(): Promise { 5 | // Factories 6 | const operatorScriptsFactory = await ethers.getContractFactory("OperatorScripts"); 7 | const ownerProxyFactory = await ethers.getContractFactory("OwnerProxy"); 8 | 9 | // Addresses 10 | const operator = toBytes32("Paraswap"); 11 | const operatorScript = ""; 12 | 13 | // Generate OperatorScripts script calldata 14 | const calldata = operatorScriptsFactory.interface.encodeFunctionData("removeOperator", [ 15 | operator 16 | ]); 17 | 18 | const finalCalldata = ownerProxyFactory.interface.encodeFunctionData("execute", [ 19 | operatorScript, 20 | calldata, 21 | ]); 22 | console.log("Calldata for OwnerProxy => ", finalCalldata); 23 | } 24 | 25 | main() 26 | .then(() => process.exit(0)) 27 | .catch((error: Error) => { 28 | console.error(error); 29 | process.exit(1); 30 | }); 31 | -------------------------------------------------------------------------------- /scripts/op_scripts/setUnrevealedTokenUri.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | 3 | // Used to add delay between deployment and etherscan verification 4 | const delay = async (ms: number) => new Promise(res => setTimeout(res, ms)); 5 | 6 | async function main(): Promise { 7 | console.log("Generate calldata : "); 8 | 9 | let nestedAssetAddr = ""; 10 | let singleCallAddr = ""; 11 | let ownerProxyAddr = ""; 12 | 13 | const nestedAssetFactory = await ethers.getContractFactory("NestedAsset"); 14 | const singleCallFactory = await ethers.getContractFactory("SingleCall"); 15 | const ownerProxyFactory = await ethers.getContractFactory("OwnerProxy"); 16 | 17 | const nestedAsset = await nestedAssetFactory.attach(nestedAssetAddr); 18 | const singleCall = await singleCallFactory.attach(singleCallAddr); 19 | const ownerProxy = await ownerProxyFactory.attach(ownerProxyAddr); 20 | 21 | let setURICalldata = await nestedAsset.interface.encodeFunctionData("setUnrevealedTokenURI", ["ipfs://"]); 22 | let singleCalldata = await singleCall.interface.encodeFunctionData("call", [nestedAsset.address, setURICalldata]); 23 | 24 | console.log(singleCalldata); 25 | 26 | let executeCalldata = await ownerProxy.interface.encodeFunctionData("execute", [singleCall.address, singleCalldata]); 27 | 28 | console.log(executeCalldata); 29 | } 30 | 31 | main() 32 | .then(() => process.exit(0)) 33 | .catch((error: Error) => { 34 | console.error(error); 35 | process.exit(1); 36 | }); 37 | -------------------------------------------------------------------------------- /scripts/op_scripts/transferOwnership.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | 3 | async function main(): Promise { 4 | console.log("Generate calldata : "); 5 | 6 | let nestedAssetAddr = ""; 7 | let singleCallAddr = ""; 8 | let ownerProxyAddr = ""; 9 | let newOwner = ""; 10 | 11 | const nestedAssetFactory = await ethers.getContractFactory("NestedAsset"); 12 | const singleCallFactory = await ethers.getContractFactory("SingleCall"); 13 | const ownerProxyFactory = await ethers.getContractFactory("OwnerProxy"); 14 | 15 | const nestedAsset = await nestedAssetFactory.attach(nestedAssetAddr); 16 | const singleCall = await singleCallFactory.attach(singleCallAddr); 17 | const ownerProxy = await ownerProxyFactory.attach(ownerProxyAddr); 18 | 19 | let transferOwnershipCalldata = await nestedAsset.interface.encodeFunctionData("transferOwnership", [newOwner]); 20 | let singleCalldata = await singleCall.interface.encodeFunctionData("call", [nestedAsset.address, transferOwnershipCalldata]); 21 | 22 | console.log(singleCalldata); 23 | 24 | let executeCalldata = await ownerProxy.interface.encodeFunctionData("execute", [singleCall.address, singleCalldata]); 25 | 26 | console.log(executeCalldata); 27 | } 28 | 29 | main() 30 | .then(() => process.exit(0)) 31 | .catch((error: Error) => { 32 | console.error(error); 33 | process.exit(1); 34 | }); 35 | -------------------------------------------------------------------------------- /scripts/op_scripts/upgradeProxy.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | 3 | // Used to add delay between deployment and etherscan verification 4 | const delay = async (ms: number) => new Promise(res => setTimeout(res, ms)); 5 | 6 | async function main(): Promise { 7 | console.log("Generate calldata : "); 8 | 9 | let proxyAddr = ""; 10 | let singleCallAddr = ""; 11 | let ownerProxyAddr = ""; 12 | let factoryAddr = ""; 13 | 14 | const proxyFactory = await ethers.getContractFactory("TransparentUpgradeableProxy"); 15 | const singleCallFactory = await ethers.getContractFactory("SingleCall"); 16 | const ownerProxyFactory = await ethers.getContractFactory("OwnerProxy"); 17 | 18 | const proxy = await proxyFactory.attach(proxyAddr); 19 | const singleCall = await singleCallFactory.attach(singleCallAddr); 20 | const ownerProxy = await ownerProxyFactory.attach(ownerProxyAddr); 21 | 22 | let upgradeToCalldata = await proxy.interface.encodeFunctionData("upgradeTo", [factoryAddr]); 23 | 24 | let singleCalldata = await singleCall.interface.encodeFunctionData("call", [proxyAddr, upgradeToCalldata]); 25 | 26 | console.log(singleCalldata); 27 | 28 | let executeCalldata = await ownerProxy.interface.encodeFunctionData("execute", [singleCall.address, singleCalldata]); 29 | 30 | console.log(executeCalldata); 31 | } 32 | 33 | main() 34 | .then(() => process.exit(0)) 35 | .catch((error: Error) => { 36 | console.error(error); 37 | process.exit(1); 38 | }); 39 | -------------------------------------------------------------------------------- /scripts/operators/FlatOperator/generateCalldata.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network } from "hardhat"; 2 | import addresses from "../../../addresses.json"; 3 | import { toBytes32 } from "../../utils"; 4 | 5 | const chainId: string = network.config.chainId.toString(); 6 | const context = JSON.parse(JSON.stringify(addresses)); 7 | 8 | async function main(): Promise { 9 | // Factories 10 | const flatOperatorFactory = await ethers.getContractFactory("FlatOperator"); 11 | const operatorScriptsFactory = await ethers.getContractFactory("OperatorScripts"); 12 | const ownerProxyFactory = await ethers.getContractFactory("OwnerProxy"); 13 | 14 | // Generate OperatorScripts script calldata 15 | const calldata = operatorScriptsFactory.interface.encodeFunctionData("deployAddOperators", [ 16 | flatOperatorFactory.bytecode, 17 | [ 18 | { 19 | name: toBytes32("Flat"), 20 | selector: flatOperatorFactory.interface.getSighash("transfer(address,uint256)"), 21 | }, 22 | ], 23 | ]); 24 | 25 | const finalCalldata = ownerProxyFactory.interface.encodeFunctionData("execute", [ 26 | context[chainId].scripts.OperatorScripts, 27 | calldata, 28 | ]); 29 | console.log("Calldata for OwnerProxy => ", finalCalldata); 30 | } 31 | 32 | main() 33 | .then(() => process.exit(0)) 34 | .catch((error: Error) => { 35 | console.error(error); 36 | process.exit(1); 37 | }); 38 | -------------------------------------------------------------------------------- /scripts/operators/ParaswapOperator/generateCalldata.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network } from "hardhat"; 2 | import addresses from "../../../addresses.json"; 3 | import { abiCoder, toBytes32 } from "../../utils"; 4 | 5 | const chainId: string = network.config.chainId.toString(); 6 | const context = JSON.parse(JSON.stringify(addresses)); 7 | 8 | async function main(): Promise { 9 | // Factories 10 | const paraswapOperatorFactory = await ethers.getContractFactory("ParaswapOperator"); 11 | const operatorScriptsFactory = await ethers.getContractFactory("OperatorScripts"); 12 | const ownerProxyFactory = await ethers.getContractFactory("OwnerProxy"); 13 | 14 | // Addresses 15 | const tokenTransferProxy = ""; 16 | const augustusSwapper = ""; 17 | 18 | // Concat deploy bytecode + args 19 | // We are using slice(2) to remove the "0x" from the encodeDeploy (args) 20 | const deployCalldata = 21 | paraswapOperatorFactory.bytecode + 22 | paraswapOperatorFactory.interface.encodeDeploy([tokenTransferProxy, augustusSwapper]).slice(2); 23 | 24 | // Generate OperatorScripts script calldata 25 | const calldata = operatorScriptsFactory.interface.encodeFunctionData("deployAddOperators", [ 26 | deployCalldata, 27 | [ 28 | { 29 | name: toBytes32("Paraswap"), 30 | selector: paraswapOperatorFactory.interface.getSighash("performSwap(address,address,bytes)"), 31 | }, 32 | ], 33 | ]); 34 | 35 | const finalCalldata = ownerProxyFactory.interface.encodeFunctionData("execute", [ 36 | context[chainId].scripts.OperatorScripts, 37 | calldata, 38 | ]); 39 | console.log("Calldata for OwnerProxy => ", finalCalldata); 40 | } 41 | 42 | main() 43 | .then(() => process.exit(0)) 44 | .catch((error: Error) => { 45 | console.error(error); 46 | process.exit(1); 47 | }); 48 | -------------------------------------------------------------------------------- /scripts/operators/ParaswapOperator/verify.ts: -------------------------------------------------------------------------------- 1 | import hre from "hardhat"; 2 | 3 | async function main(): Promise { 4 | // Factories 5 | const paraswapOperatorAddr = ""; 6 | 7 | const tokenTransferProxy = ""; 8 | const augustusSwapper = ""; 9 | 10 | await hre.run("verify:verify", { 11 | address: paraswapOperatorAddr, 12 | constructorArguments: [tokenTransferProxy, augustusSwapper], 13 | }); 14 | } 15 | 16 | main() 17 | .then(() => process.exit(0)) 18 | .catch((error: Error) => { 19 | console.error(error); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /scripts/reachNonce.ts: -------------------------------------------------------------------------------- 1 | import hre from "hardhat"; 2 | import { getSigners } from "hardhat-deploy-ethers/dist/src/helpers"; 3 | 4 | async function main(): Promise { 5 | const nonceToReach = 139; // to set 6 | 7 | const signers = await getSigners(hre); 8 | const nextNonce = await hre.ethers.provider.getTransactionCount(signers[0].address); 9 | 10 | if (nonceToReach < nextNonce) { 11 | console.log("Nonce already reached"); 12 | return; 13 | } 14 | 15 | for (let i = nextNonce; i <= nonceToReach; i++) { 16 | let txSent = await signers[0].sendTransaction({ 17 | to: signers[0].address, 18 | value: 0, 19 | }); 20 | await txSent.wait(); 21 | console.log("Reach ", i); 22 | } 23 | } 24 | 25 | main() 26 | .then(() => process.exit(0)) 27 | .catch((error: Error) => { 28 | console.error(error); 29 | process.exit(1); 30 | }); 31 | -------------------------------------------------------------------------------- /scripts/setEntryAndExitFees.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | async function main(): Promise { 4 | const nestedFactoryFactory = await ethers.getContractFactory("NestedFactory"); 5 | 6 | console.log("Set Entry Fees : "); 7 | const nestedFactory = await nestedFactoryFactory.attach(""); // Set address 8 | let tx = await nestedFactory.setEntryFees(30); // 0.3% 9 | await tx.wait(); 10 | console.log("EntryFees setted"); 11 | 12 | console.log("Set Exit Fees : "); 13 | tx = await nestedFactory.setExitFees(80); // 0.8% 14 | await tx.wait(); 15 | console.log("Exit Fees setted"); 16 | } 17 | 18 | main() 19 | .then(() => process.exit(0)) 20 | .catch((error: Error) => { 21 | console.error(error); 22 | process.exit(1); 23 | }); 24 | -------------------------------------------------------------------------------- /scripts/tenderly.ts: -------------------------------------------------------------------------------- 1 | import hre from "hardhat"; 2 | 3 | async function main(): Promise { 4 | const contracts = [ 5 | { 6 | name: "contract_name", 7 | address: "contract_address", 8 | }, 9 | ]; 10 | await hre.tenderly.verify(...contracts); 11 | } 12 | 13 | main() 14 | .then(() => process.exit(0)) 15 | .catch((error: Error) => { 16 | console.error(error); 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /static/input-orders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MassDotMoney/nested-core-lego/c1eb3de913bf9b29a09015c10f4690ebd9632c54/static/input-orders.png -------------------------------------------------------------------------------- /static/output-orders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MassDotMoney/nested-core-lego/c1eb3de913bf9b29a09015c10f4690ebd9632c54/static/output-orders.png -------------------------------------------------------------------------------- /static/ownership.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MassDotMoney/nested-core-lego/c1eb3de913bf9b29a09015c10f4690ebd9632c54/static/ownership.png -------------------------------------------------------------------------------- /static/processInputOrders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MassDotMoney/nested-core-lego/c1eb3de913bf9b29a09015c10f4690ebd9632c54/static/processInputOrders.png -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { BigNumber, Wallet } from "ethers"; 3 | import { string } from "hardhat/internal/core/params/argumentTypes"; 4 | const w3utils = require("web3-utils"); 5 | const abiCoder = new ethers.utils.AbiCoder(); 6 | 7 | export const appendDecimals = (amount: number) => ethers.utils.parseEther(amount.toString()); 8 | export const append6Decimals = (amount: number) => { return BigNumber.from(amount).mul(10 ** 6) }; // needed for EURT that has 6 decimals 9 | 10 | export const getETHSpentOnGas = async (tx: any) => { 11 | const receipt = await tx.wait(); 12 | return receipt.gasUsed.mul(tx.gasPrice); 13 | }; 14 | 15 | export const getTokenName = (address: string, tokens: Record) => 16 | Object.entries(tokens).find(([_, value]) => value === address)?.[0] || "???"; 17 | 18 | export const BIG_NUMBER_ZERO = BigNumber.from(0); 19 | export const UINT256_MAX = BigNumber.from(2).pow(256).sub(1); 20 | 21 | export const toBytes32 = (key: string) => w3utils.rightPad(w3utils.asciiToHex(key), 64); 22 | export const fromBytes32 = (key: string) => w3utils.hexToAscii(key); 23 | 24 | export function getExpectedFees(amount: BigNumber) { 25 | return amount.div(100); 26 | } 27 | 28 | export const setAllowance = async (signer: Wallet, contract: string, spender: string, amount: BigNumber) => { 29 | const data = 30 | ethers.utils.keccak256( 31 | ethers.utils.toUtf8Bytes("approve(address,uint256)") 32 | ).slice(0, 10) + 33 | abiCoder.encode( 34 | ["address", "uint256"], 35 | [spender, amount] 36 | ).slice(2, 130) 37 | 38 | await signer.sendTransaction({ 39 | to: contract, 40 | data: data 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /test/shared/actors.ts: -------------------------------------------------------------------------------- 1 | import { MockProvider } from "ethereum-waffle"; 2 | import { Wallet } from "ethers"; 3 | 4 | export const WALLET_USER_INDEXES = { 5 | ADDRESS_RESOLVER_OWNER: 1, 6 | USER_1: 2, 7 | OWNABLE_OPERATOR_OWNER: 3, 8 | ZERO_EX_OPERATOR_OWNER: 4, 9 | MASTER_DEPLOYER: 5, 10 | SHAREHOLDER_1: 6, 11 | SHAREHOLDER_2: 7, 12 | PROXY_ADMIN: 8, 13 | }; 14 | 15 | export class ActorFixture { 16 | wallets: Array; 17 | provider: MockProvider; 18 | 19 | constructor(wallets: Wallet[], provider: MockProvider) { 20 | this.wallets = wallets; 21 | this.provider = provider; 22 | } 23 | 24 | addressResolverOwner() { 25 | return this._getActor(WALLET_USER_INDEXES.ADDRESS_RESOLVER_OWNER); 26 | } 27 | 28 | user1() { 29 | return this._getActor(WALLET_USER_INDEXES.USER_1); 30 | } 31 | 32 | ownableOperatorOwner() { 33 | return this._getActor(WALLET_USER_INDEXES.OWNABLE_OPERATOR_OWNER); 34 | } 35 | 36 | zeroExOperatorOwner() { 37 | return this._getActor(WALLET_USER_INDEXES.ZERO_EX_OPERATOR_OWNER); 38 | } 39 | 40 | /* 41 | * The master deployer, is responsible to deploy all the contracts (in one fixture) 42 | * Instead of different deployers/owners for every contracts. Usefull for the Factory 43 | * unit tests and integration tests. 44 | */ 45 | masterDeployer() { 46 | return this._getActor(WALLET_USER_INDEXES.MASTER_DEPLOYER); 47 | } 48 | 49 | shareHolder1() { 50 | return this._getActor(WALLET_USER_INDEXES.SHAREHOLDER_1); 51 | } 52 | 53 | shareHolder2() { 54 | return this._getActor(WALLET_USER_INDEXES.SHAREHOLDER_2); 55 | } 56 | 57 | proxyAdmin() { 58 | return this._getActor(WALLET_USER_INDEXES.PROXY_ADMIN); 59 | } 60 | 61 | private _getActor(index: number): Wallet { 62 | /* Actual logic for fetching the wallet */ 63 | if (!index) { 64 | throw new Error(`Invalid index: ${index}`); 65 | } 66 | const account = this.wallets[index]; 67 | if (!account) { 68 | throw new Error(`Account ID ${index} could not be loaded`); 69 | } 70 | return account; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/shared/impersonnate.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, Wallet } from "ethers"; 2 | import { ethers, network } from "hardhat"; 3 | 4 | const abiCoder = new ethers.utils.AbiCoder(); 5 | 6 | 7 | export const impersonnate = async (address: string) => { 8 | await network.provider.request({ 9 | method: "hardhat_impersonateAccount", 10 | params: [address], 11 | }) 12 | 13 | return await ethers.getSigner(address) 14 | } 15 | 16 | export const addBscUsdcBalanceTo = async (receiver: Wallet, amount: BigNumber) => { 17 | const usdcWhale: string = "0xf977814e90da44bfa03b6295a0616a897441acec" 18 | const usdcContract: string = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" 19 | const signer = await impersonnate(usdcWhale) 20 | 21 | const data = 22 | ethers.utils.keccak256( 23 | ethers.utils.toUtf8Bytes("transfer(address,uint256)") 24 | ).slice(0, 10) + 25 | abiCoder.encode( 26 | ["address", "uint256"], 27 | [receiver.address, amount] 28 | ).slice(2, 130) 29 | 30 | await signer.sendTransaction({ 31 | from: signer.address, 32 | to: usdcContract, 33 | data: data 34 | }) 35 | } 36 | 37 | export const addEthEurtBalanceTo = async (receiver: Wallet, amount: BigNumber) => { 38 | const eurtWhale: string = "0x5754284f345afc66a98fbb0a0afe71e0f007b949" 39 | const eurtContract: string = "0xC581b735A1688071A1746c968e0798D642EDE491" 40 | const signer = await impersonnate(eurtWhale) 41 | 42 | const data = 43 | ethers.utils.keccak256( 44 | ethers.utils.toUtf8Bytes("transfer(address,uint256)") 45 | ).slice(0, 10) + 46 | abiCoder.encode( 47 | ["address", "uint256"], 48 | [receiver.address, amount] 49 | ).slice(2, 130) 50 | await signer.sendTransaction({ 51 | from: signer.address, 52 | to: eurtContract, 53 | data: data 54 | }) 55 | } -------------------------------------------------------------------------------- /test/shared/provider.ts: -------------------------------------------------------------------------------- 1 | import { waffle } from "hardhat"; 2 | import { smock } from "@defi-wonderland/smock"; 3 | import { config } from "hardhat"; 4 | import process from "process"; 5 | 6 | export const provider = waffle.provider; 7 | export const createFixtureLoader = waffle.createFixtureLoader; 8 | 9 | const chai = require("chai"); 10 | chai.use(smock.matchers); 11 | export const expect = chai.expect; 12 | export const assert = chai.assert; 13 | 14 | export const describeOnBscFork = 15 | config.networks.hardhat.forking.enabled && process.env.FORK_CHAINID === "56" ? describe : describe.skip; 16 | 17 | export const describeOnEthFork = 18 | config.networks.hardhat.forking.enabled && process.env.FORK_CHAINID === "1" ? describe : describe.skip; 19 | 20 | export const describeWithoutFork = config.networks.hardhat.forking.enabled ? describe.skip : describe; -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import { createFixtureLoader } from "./shared/provider"; 2 | 3 | export type LoadFixtureFunction = ReturnType; 4 | -------------------------------------------------------------------------------- /test/unit/ExchangeHelpers.unit.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { expect } from "chai"; 3 | import { ethers } from "hardhat"; 4 | import { appendDecimals, BIG_NUMBER_ZERO, UINT256_MAX } from "../helpers"; 5 | import { DummyRouter, MockERC20 } from "../../typechain"; 6 | import { describeWithoutFork } from "../shared/provider"; 7 | 8 | describeWithoutFork("ExchangeHelpers", () => { 9 | let dummyRouter: DummyRouter, mockERC20: MockERC20; 10 | let bob: SignerWithAddress, randomContract: SignerWithAddress; 11 | 12 | before(async () => { 13 | const signers = await ethers.getSigners(); 14 | bob = signers[0] as any; 15 | randomContract = signers[1] as any; 16 | }); 17 | 18 | beforeEach(async () => { 19 | const dummyRouterFactory = await ethers.getContractFactory("DummyRouter"); 20 | dummyRouter = await dummyRouterFactory.deploy(); 21 | 22 | const mockERC20Factory = await ethers.getContractFactory("MockERC20"); 23 | mockERC20 = await mockERC20Factory.deploy("Mocked ERC20", "MOCK20", appendDecimals(3000000)); 24 | }); 25 | 26 | /* 27 | * The dummyRouter has these two functions : 28 | * - setMaxAllowance(IERC20 _token, address _spender), using the ExchangeHelper 29 | * - setAllowance(IERC20 _token, address _spender, uint256 _amount) 30 | */ 31 | describe("#setMaxAllowance", () => { 32 | it("should sets allowance to type(uint256).max", async () => { 33 | let currentAllowance = await mockERC20.allowance(dummyRouter.address, randomContract.address); 34 | expect(currentAllowance).to.equal(BIG_NUMBER_ZERO); 35 | 36 | await dummyRouter.setMaxAllowance(mockERC20.address, randomContract.address); 37 | currentAllowance = await mockERC20.allowance(dummyRouter.address, randomContract.address); 38 | expect(currentAllowance).to.equal(UINT256_MAX); 39 | }); 40 | 41 | it("should increase allowance to type(uint256).max", async () => { 42 | let allowance = appendDecimals(100); 43 | await dummyRouter.setAllowance(mockERC20.address, randomContract.address, allowance); 44 | let currentAllowance = await mockERC20.allowance(dummyRouter.address, randomContract.address); 45 | expect(currentAllowance).to.equal(allowance); 46 | 47 | await dummyRouter.setMaxAllowance(mockERC20.address, randomContract.address); 48 | currentAllowance = await mockERC20.allowance(dummyRouter.address, randomContract.address); 49 | expect(currentAllowance).to.equal(UINT256_MAX); 50 | }); 51 | 52 | it("should keep allowance to type(uint256).max", async () => { 53 | await dummyRouter.setAllowance(mockERC20.address, randomContract.address, UINT256_MAX); 54 | let currentAllowance = await mockERC20.allowance(dummyRouter.address, randomContract.address); 55 | expect(currentAllowance).to.equal(UINT256_MAX); 56 | 57 | await dummyRouter.setMaxAllowance(mockERC20.address, randomContract.address); 58 | currentAllowance = await mockERC20.allowance(dummyRouter.address, randomContract.address); 59 | expect(currentAllowance).to.equal(UINT256_MAX); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/unit/FlatOperator.unit.ts: -------------------------------------------------------------------------------- 1 | import { LoadFixtureFunction } from "../types"; 2 | import { factoryAndOperatorsFixture, FactoryAndOperatorsFixture } from "../shared/fixtures"; 3 | import { createFixtureLoader, expect, provider, describeWithoutFork } from "../shared/provider"; 4 | import { appendDecimals, getExpectedFees } from "../helpers"; 5 | import * as utils from "../../scripts/utils"; 6 | 7 | let loadFixture: LoadFixtureFunction; 8 | 9 | interface Order { 10 | operator: string; 11 | token: string; 12 | callData: string | []; 13 | } 14 | 15 | describeWithoutFork("FlatOperator", () => { 16 | let context: FactoryAndOperatorsFixture; 17 | 18 | before("loader", async () => { 19 | loadFixture = createFixtureLoader(provider.getWallets(), provider); 20 | }); 21 | 22 | beforeEach("create fixture loader", async () => { 23 | context = await loadFixture(factoryAndOperatorsFixture); 24 | }); 25 | 26 | it("deploys and has an address", async () => { 27 | expect(context.flatOperator.address).to.be.a.string; 28 | }); 29 | 30 | it("Can't use amount zero", async () => { 31 | // The user add 10 UNI to the portfolio 32 | const totalToBought = appendDecimals(10); 33 | const expectedFee = getExpectedFees(totalToBought); 34 | const totalToSpend = totalToBought.add(expectedFee); 35 | 36 | // Add 0 UNI with FlatOperator 37 | let orders: Order[] = [ 38 | { 39 | operator: context.flatOperatorNameBytes32, 40 | token: context.mockUNI.address, 41 | callData: utils.abiCoder.encode(["address", "uint256"], [context.mockUNI.address, 0]), 42 | }, 43 | ]; 44 | 45 | await expect( 46 | context.nestedFactory.connect(context.user1).create(0, [ 47 | { 48 | inputToken: context.mockUNI.address, 49 | amount: totalToSpend, 50 | orders, 51 | fromReserve: false, 52 | }, 53 | ]), 54 | ).to.revertedWith("NF: OPERATOR_CALL_FAILED"); 55 | }); 56 | 57 | it("Can't use with different input", async () => { 58 | // The user add 10 UNI to the portfolio 59 | const totalToBought = appendDecimals(10); 60 | const expectedFee = getExpectedFees(totalToBought); 61 | const totalToSpend = totalToBought.add(expectedFee); 62 | 63 | // Add 10 DAI with FlatOperator, but input UNI 64 | let orders: Order[] = [ 65 | { 66 | operator: context.flatOperatorNameBytes32, 67 | token: context.mockUNI.address, 68 | callData: utils.abiCoder.encode(["address", "uint256"], [context.mockDAI.address, 10]), 69 | }, 70 | ]; 71 | 72 | // The error is "OUTPUT" because the token in Order is considered as the "right token" (UNI) 73 | // Therefore, the DAI token (output) is considered as invalid. 74 | await expect( 75 | context.nestedFactory.connect(context.user1).create(0, [ 76 | { 77 | inputToken: context.mockUNI.address, 78 | amount: totalToSpend, 79 | orders, 80 | fromReserve: false, 81 | }, 82 | ]), 83 | ).to.revertedWith("MOR: INVALID_OUTPUT_TOKEN"); 84 | }); 85 | 86 | it("Adds token to portfolio when create()", async () => { 87 | // The user add 10 UNI to the portfolio 88 | const uniBought = appendDecimals(10); 89 | const totalToBought = uniBought; 90 | const expectedFee = getExpectedFees(totalToBought); 91 | const totalToSpend = totalToBought.add(expectedFee); 92 | 93 | // Add 10 UNI with FlatOperator 94 | let orders: Order[] = [ 95 | { 96 | operator: context.flatOperatorNameBytes32, 97 | token: context.mockUNI.address, 98 | callData: utils.abiCoder.encode(["address", "uint256"], [context.mockUNI.address, totalToBought]), 99 | }, 100 | ]; 101 | 102 | // User1 creates the portfolio/NFT and emit event NftCreated 103 | await expect( 104 | context.nestedFactory.connect(context.user1).create(0, [ 105 | { 106 | inputToken: context.mockUNI.address, 107 | amount: totalToSpend, 108 | orders, 109 | fromReserve: false, 110 | }, 111 | ]), 112 | ) 113 | .to.emit(context.nestedFactory, "NftCreated") 114 | .withArgs(1, 0); 115 | 116 | // User1 must be the owner of NFT n°1 117 | expect(await context.nestedAsset.ownerOf(1)).to.be.equal(context.user1.address); 118 | 119 | // 10 UNI must be in the reserve 120 | expect(await context.mockUNI.balanceOf(context.nestedReserve.address)).to.be.equal(uniBought); 121 | 122 | /* 123 | * User1 must have the right UNI amount : 124 | * baseAmount - amount spent 125 | */ 126 | expect(await context.mockUNI.balanceOf(context.user1.address)).to.be.equal( 127 | context.baseAmount.sub(totalToSpend), 128 | ); 129 | 130 | // The FeeSplitter must receive the right fee amount 131 | expect(await context.mockUNI.balanceOf(context.feeSplitter.address)).to.be.equal(expectedFee); 132 | 133 | // Must store UNI in the records of the NFT 134 | expect(await context.nestedRecords.getAssetTokens(1).then(value => value.toString())).to.be.equal( 135 | [context.mockUNI.address].toString(), 136 | ); 137 | 138 | // Must have the right amount in the holdings 139 | const holdingsUNIAmount = await context.nestedRecords.getAssetHolding(1, context.mockUNI.address); 140 | expect(holdingsUNIAmount).to.be.equal(uniBought); 141 | }); 142 | 143 | it("remove token from portfolio when destroy()", async () => { 144 | // The user add 10 UNI to the portfolio 145 | const uniBought = appendDecimals(10); 146 | const totalToBought = uniBought; 147 | const expectedFee = getExpectedFees(totalToBought); 148 | const totalToSpend = totalToBought.add(expectedFee); 149 | 150 | // Add 10 UNI with FlatOperator 151 | let orders: Order[] = [ 152 | { 153 | operator: context.flatOperatorNameBytes32, 154 | token: context.mockUNI.address, 155 | callData: utils.abiCoder.encode(["address", "uint256"], [context.mockUNI.address, totalToBought]), 156 | }, 157 | ]; 158 | 159 | // User1 creates the portfolio/NFT and emit event NftCreated 160 | await expect( 161 | context.nestedFactory.connect(context.user1).create(0, [ 162 | { 163 | inputToken: context.mockUNI.address, 164 | amount: totalToSpend, 165 | orders, 166 | fromReserve: false, 167 | }, 168 | ]), 169 | ) 170 | .to.emit(context.nestedFactory, "NftCreated") 171 | .withArgs(1, 0); 172 | 173 | // Remove 10 UNI (with same order) 174 | await context.nestedFactory.connect(context.user1).destroy(1, context.mockUNI.address, orders); 175 | 176 | // UNI from create and from destroy to FeeSplitter (so, two times 1% of 10 UNI) 177 | expect(await context.mockUNI.balanceOf(context.feeSplitter.address)).to.be.equal( 178 | getExpectedFees(uniBought).mul(2), 179 | ); 180 | 181 | // No holdings for NFT 1 182 | expect(await context.nestedRecords.getAssetTokens(1).then(value => value.toString())).to.be.equal( 183 | [].toString(), 184 | ); 185 | 186 | // The NFT is burned 187 | await expect(context.nestedAsset.ownerOf(1)).to.be.revertedWith("ERC721: owner query for nonexistent token"); 188 | }); 189 | 190 | it("remove token from portfolio when destroy() with underspend amount", async () => { 191 | // The user add 10 UNI to the portfolio 192 | const uniBought = appendDecimals(10); 193 | const expectedFee = getExpectedFees(uniBought); 194 | const totalToSpend = uniBought.add(expectedFee); 195 | 196 | // Add 10 UNI with FlatOperator 197 | let orders: Order[] = [ 198 | { 199 | operator: context.flatOperatorNameBytes32, 200 | token: context.mockUNI.address, 201 | callData: utils.abiCoder.encode(["address", "uint256"], [context.mockUNI.address, uniBought]), 202 | }, 203 | ]; 204 | 205 | // User1 creates the portfolio/NFT and emit event NftCreated 206 | await expect( 207 | context.nestedFactory.connect(context.user1).create(0, [ 208 | { 209 | inputToken: context.mockUNI.address, 210 | amount: totalToSpend, 211 | orders, 212 | fromReserve: false, 213 | }, 214 | ]), 215 | ) 216 | .to.emit(context.nestedFactory, "NftCreated") 217 | .withArgs(1, 0); 218 | 219 | // Remove only 1 UNI in the order, hence there is still holdings leading to underspend amount 220 | let orders_underspend: Order[] = [ 221 | { 222 | operator: context.flatOperatorNameBytes32, 223 | token: context.mockUNI.address, 224 | callData: utils.abiCoder.encode(["address", "uint256"], [context.mockUNI.address, appendDecimals(1)]), 225 | }, 226 | ]; 227 | await context.nestedFactory.connect(context.user1).destroy(1, context.mockUNI.address, orders_underspend); 228 | 229 | // UNI from create and from destroy to FeeSplitter (so, two times 1% of 10 UNI) 230 | expect(await context.mockUNI.balanceOf(context.feeSplitter.address)).to.be.equal( 231 | getExpectedFees(uniBought).mul(2), 232 | ); 233 | 234 | // No holdings for NFT 1 235 | expect(await context.nestedRecords.getAssetTokens(1).then(value => value.toString())).to.be.equal( 236 | [].toString(), 237 | ); 238 | 239 | // The NFT is burned 240 | await expect(context.nestedAsset.ownerOf(1)).to.be.revertedWith("ERC721: owner query for nonexistent token"); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /test/unit/NestedAsset.unit.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { expect } from "chai"; 3 | import { ethers } from "hardhat"; 4 | import { NestedAsset, NestedAsset__factory } from "../../typechain"; 5 | import { describeWithoutFork } from "../shared/provider"; 6 | 7 | describeWithoutFork("NestedAsset", () => { 8 | let NestedAsset: NestedAsset__factory, asset: NestedAsset; 9 | let factory: SignerWithAddress, 10 | alice: SignerWithAddress, 11 | bob: SignerWithAddress, 12 | feeToSetter: SignerWithAddress, 13 | feeTo: SignerWithAddress; 14 | 15 | before(async () => { 16 | NestedAsset = await ethers.getContractFactory("NestedAsset"); 17 | 18 | const signers = await ethers.getSigners(); 19 | // All transaction will be sent from the factory unless explicity specified 20 | factory = signers[0]; 21 | alice = signers[1]; 22 | bob = signers[2]; 23 | feeToSetter = signers[3]; 24 | feeTo = signers[4]; 25 | }); 26 | 27 | beforeEach(async () => { 28 | asset = await NestedAsset.deploy(); 29 | await asset.addFactory(factory.address); 30 | await asset.deployed(); 31 | }); 32 | 33 | describe("#mint", () => { 34 | describe("when creating NFTs from scratch", () => { 35 | it("should create ERC-721 tokens with relevant tokenIds", async () => { 36 | await asset.mint(alice.address, 0); 37 | await asset.mint(alice.address, 0); 38 | await asset.mint(bob.address, 0); 39 | expect(await asset.balanceOf(alice.address)).to.equal("2"); 40 | expect(await asset.balanceOf(bob.address)).to.equal("1"); 41 | expect(await asset.tokenOfOwnerByIndex(alice.address, 0)).to.equal("1"); 42 | expect(await asset.tokenOfOwnerByIndex(alice.address, 1)).to.equal("2"); 43 | expect(await asset.tokenOfOwnerByIndex(bob.address, 0)).to.equal("3"); 44 | }); 45 | }); 46 | 47 | describe("when replicating NFTs", () => { 48 | it("should create ERC-721s and store the original asset used for replication", async () => { 49 | await asset.mint(alice.address, 0); 50 | await asset.mint(alice.address, 1); 51 | await asset.mint(bob.address, 2); 52 | expect(await asset.originalAsset(1)).to.equal(0); 53 | expect(await asset.originalAsset(2)).to.equal(1); 54 | expect(await asset.originalAsset(3)).to.equal(1); 55 | }); 56 | 57 | it("should revert if replicate id doesnt exist", async () => { 58 | await expect(asset.mint(alice.address, 1)).to.be.revertedWith("NA: SELF_DUPLICATION"); 59 | await expect(asset.mint(alice.address, 10)).to.be.revertedWith("NA: NON_EXISTENT_TOKEN_ID"); 60 | }); 61 | }); 62 | 63 | it("should revert if the caller is not the factory", async () => { 64 | // Alice tries to mint a token for herself and bypass the factory 65 | await expect(asset.connect(alice).mint(alice.address, 0)).to.be.revertedWith("OFH: FORBIDDEN"); 66 | }); 67 | }); 68 | 69 | describe("#tokenURI", () => { 70 | it("should display NFT metadata", async () => { 71 | await asset.mint(alice.address, 0); 72 | const metadataUriUnrevealed = "unrevealed"; 73 | await asset.setUnrevealedTokenURI(metadataUriUnrevealed); 74 | const tokenId = await asset.tokenOfOwnerByIndex(alice.address, 0); 75 | expect(await asset.tokenURI(tokenId)).to.equal(metadataUriUnrevealed); 76 | 77 | await asset.setBaseURI("revealed/"); 78 | await asset.setIsRevealed(true); 79 | expect(await asset.tokenURI(tokenId)).to.equal("revealed/" + tokenId); 80 | }); 81 | 82 | it("reverts if the token does not exist", async () => { 83 | await expect(asset.tokenURI(1)).to.be.revertedWith("URI query for nonexistent token"); 84 | }); 85 | }); 86 | 87 | describe("#burn", () => { 88 | it("should burn the user's ERC-721 token", async () => { 89 | await asset.mint(alice.address, 0); 90 | expect(await asset.balanceOf(alice.address)).to.equal("1"); 91 | await asset.burn(alice.address, 1); 92 | expect(await asset.balanceOf(alice.address)).to.equal("0"); 93 | expect(await asset.lastOwnerBeforeBurn(1)).to.eq(alice.address); 94 | }); 95 | 96 | it("should delete", async () => { 97 | await asset.mint(alice.address, 0); 98 | expect(await asset.balanceOf(alice.address)).to.equal("1"); 99 | await asset.burn(alice.address, 1); 100 | expect(await asset.balanceOf(alice.address)).to.equal("0"); 101 | expect(await asset.lastOwnerBeforeBurn(1)).to.eq(alice.address); 102 | }); 103 | 104 | it("should revert when burning non existing token", async () => { 105 | await expect(asset.burn(alice.address, 1)).to.be.revertedWith("ERC721: owner query for nonexistent token"); 106 | }); 107 | 108 | it("should revert if the caller is not the factory", async () => { 109 | // Alice tries to burn the token herself and bypass the factory 110 | await expect(asset.connect(alice).burn(alice.address, 1)).to.be.revertedWith("OFH: FORBIDDEN"); 111 | }); 112 | 113 | it("should revert when burning someone else's token", async () => { 114 | await asset.mint(bob.address, 0); 115 | 116 | // Alice asked to burn Bob's token 117 | await expect(asset.burn(alice.address, 1)).to.be.revertedWith("NA: FORBIDDEN_NOT_OWNER"); 118 | }); 119 | }); 120 | 121 | describe("#originalOwner", () => { 122 | beforeEach(async () => { 123 | await asset.mint(alice.address, 0); 124 | await asset.mint(bob.address, 1); 125 | }); 126 | 127 | it("returns the owner address of the original asset", async () => { 128 | expect(await asset.originalOwner(1)).to.eq("0x0000000000000000000000000000000000000000"); 129 | expect(await asset.originalOwner(2)).to.eq(alice.address); 130 | }); 131 | 132 | it("returns the owner address of the original burnt asset", async () => { 133 | await asset.burn(alice.address, 1); 134 | expect(await asset.originalOwner(2)).to.eq(alice.address); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /test/unit/NestedAssetBatcher.unit.ts: -------------------------------------------------------------------------------- 1 | import { LoadFixtureFunction } from "../types"; 2 | import { factoryAndOperatorsFixture, FactoryAndOperatorsFixture } from "../shared/fixtures"; 3 | import { createFixtureLoader, describeWithoutFork, expect, provider } from "../shared/provider"; 4 | import { BigNumber } from "ethers"; 5 | import { appendDecimals, getExpectedFees } from "../helpers"; 6 | import * as utils from "../../scripts/utils"; 7 | 8 | let loadFixture: LoadFixtureFunction; 9 | 10 | describeWithoutFork("NestedAssetBatcher", () => { 11 | let context: FactoryAndOperatorsFixture; 12 | 13 | before("loader", async () => { 14 | loadFixture = createFixtureLoader(provider.getWallets(), provider); 15 | }); 16 | 17 | beforeEach("create fixture loader", async () => { 18 | context = await loadFixture(factoryAndOperatorsFixture); 19 | }); 20 | 21 | describe("Getters", () => { 22 | // Amount already in the portfolio 23 | let baseUniBought = appendDecimals(6); 24 | let baseKncBought = appendDecimals(4); 25 | let baseTotalToBought = baseUniBought.add(baseKncBought); 26 | let baseExpectedFee = getExpectedFees(baseTotalToBought); 27 | let baseTotalToSpend = baseTotalToBought.add(baseExpectedFee); 28 | 29 | beforeEach("Create NFT (id 1)", async () => { 30 | // create nft 1 with UNI and KNC from DAI (use the base amounts) 31 | let orders: utils.OrderStruct[] = utils.getUniAndKncWithDaiOrders(context, baseUniBought, baseKncBought); 32 | await context.nestedFactory 33 | .connect(context.user1) 34 | .create(0, [ 35 | { inputToken: context.mockDAI.address, amount: baseTotalToSpend, orders, fromReserve: true }, 36 | ]); 37 | }); 38 | 39 | it("get all ids", async () => { 40 | expect(await (await context.nestedAssetBatcher.getIds(context.user1.address)).toString()).to.equal( 41 | [BigNumber.from(1)].toString(), 42 | ); 43 | }); 44 | 45 | it("get all NFTs", async () => { 46 | const expectedNfts = [ 47 | { 48 | id: BigNumber.from(1), 49 | assets: [ 50 | { token: context.mockUNI.address, qty: baseUniBought }, 51 | { token: context.mockKNC.address, qty: baseKncBought }, 52 | ], 53 | }, 54 | ]; 55 | 56 | const nfts = await context.nestedAssetBatcher.getNfts(context.user1.address); 57 | 58 | expect(JSON.stringify(utils.cleanResult(nfts))).to.equal(JSON.stringify(utils.cleanResult(expectedNfts))); 59 | }); 60 | 61 | it("require and get TokenHoldings", async () => { 62 | await expect(context.nestedAssetBatcher.requireTokenHoldings(2)).to.be.revertedWith("NAB: NEVER_CREATED"); 63 | await expect(context.nestedAssetBatcher.requireTokenHoldings(1)).to.not.be.reverted; 64 | 65 | let orders: utils.OrderStruct[] = utils.getUsdcWithUniAndKncOrders(context, baseUniBought, baseKncBought); 66 | await context.nestedFactory.connect(context.user1).destroy(1, context.mockUSDC.address, orders); 67 | 68 | // Not reverting after burn 69 | await expect(context.nestedAssetBatcher.requireTokenHoldings(1)).to.not.be.reverted; 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/unit/NestedBuyBacker.unit.ts: -------------------------------------------------------------------------------- 1 | import { Interface } from "@ethersproject/abi"; 2 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 3 | import { appendDecimals } from "../helpers"; 4 | import { ethers } from "hardhat"; 5 | import { expect } from "chai"; 6 | import { DummyRouter, FeeSplitter, MockERC20, NestedBuybacker, WETH9 } from "../../typechain"; 7 | import { describeWithoutFork } from "../shared/provider"; 8 | 9 | describeWithoutFork("NestedBuybacker", () => { 10 | let alice: SignerWithAddress, bob: SignerWithAddress, communityReserve: SignerWithAddress; 11 | let feeSplitter: FeeSplitter, mockWETH: WETH9; 12 | let mockNST: MockERC20, mockUSDT: MockERC20; 13 | let dummyRouter: DummyRouter, buyBacker: NestedBuybacker; 14 | 15 | before(async () => { 16 | const signers = await ethers.getSigners(); 17 | // All transactions will be sent from Alice unless explicity specified 18 | alice = signers[0]; 19 | bob = signers[1]; 20 | communityReserve = signers[2]; 21 | mockNST = await deployMockToken("NST", "NST", alice); 22 | mockUSDT = await deployMockToken("Fake USDT", "TDUS", alice); 23 | }); 24 | 25 | beforeEach(async () => { 26 | const wethFactory = await ethers.getContractFactory("WETH9"); 27 | mockWETH = await wethFactory.deploy(); 28 | 29 | const feeSplitterFactory = await ethers.getContractFactory("FeeSplitter"); 30 | feeSplitter = await feeSplitterFactory.deploy([bob.address], [30], 20, mockWETH.address); 31 | 32 | const NestedBuybackerFactory = await ethers.getContractFactory("NestedBuybacker"); 33 | buyBacker = await NestedBuybackerFactory.deploy( 34 | mockNST.address, 35 | communityReserve.address, 36 | feeSplitter.address, 37 | 250, 38 | ); 39 | 40 | await feeSplitter.setShareholders([bob.address, buyBacker.address], [30, 50]); 41 | 42 | // before each, empty the reserve NST balance 43 | await mockNST.connect(communityReserve).burn(await mockNST.balanceOf(communityReserve.address)); 44 | 45 | const DummyRouterFactory = await ethers.getContractFactory("DummyRouter"); 46 | dummyRouter = await DummyRouterFactory.deploy(); 47 | }); 48 | 49 | it("should revert with INVALID_BURN_PART", async () => { 50 | const NestedBuybackerFactory = await ethers.getContractFactory("NestedBuybacker"); 51 | await expect( 52 | NestedBuybackerFactory.deploy(mockNST.address, communityReserve.address, feeSplitter.address, 1200), 53 | ).to.be.revertedWith("NB: INVALID_BURN_PART"); 54 | }); 55 | 56 | it("sets the nested reserve address", async () => { 57 | expect(await buyBacker.nstReserve()).to.not.equal(bob.address); 58 | await buyBacker.setNestedReserve(bob.address); 59 | expect(await buyBacker.nstReserve()).to.equal(bob.address); 60 | }); 61 | 62 | it("sets the fee splitter address", async () => { 63 | expect(await buyBacker.feeSplitter()).to.not.equal(bob.address); 64 | await buyBacker.setFeeSplitter(bob.address); 65 | expect(await buyBacker.feeSplitter()).to.equal(bob.address); 66 | }); 67 | 68 | it("sends fees as token", async () => { 69 | await mockNST.transfer(dummyRouter.address, ethers.utils.parseEther("100000")); 70 | 71 | const abi = ["function dummyswapToken(address _inputToken, address _outputToken, uint256 _amount)"]; 72 | const iface = new Interface(abi); 73 | const dataUSDT = iface.encodeFunctionData("dummyswapToken", [ 74 | mockUSDT.address, 75 | mockNST.address, 76 | ethers.utils.parseEther("200"), 77 | ]); 78 | 79 | const dataWETH = iface.encodeFunctionData("dummyswapToken", [ 80 | mockWETH.address, 81 | mockNST.address, 82 | ethers.utils.parseEther("10"), 83 | ]); 84 | 85 | // send 16WETH to the fee splitter so that buybacker gets 10WETH (62.5%) 86 | await mockWETH.deposit({ value: appendDecimals(16) }); 87 | await mockWETH.approve(feeSplitter.address, appendDecimals(16)); 88 | await feeSplitter.sendFees(mockWETH.address, appendDecimals(16)); 89 | // also try sending token directly to buybacker (instead of using FeeSplitter) 90 | await mockUSDT.transfer(buyBacker.address, ethers.utils.parseEther("200")); 91 | 92 | await buyBacker.triggerForToken(dataUSDT, dummyRouter.address, mockUSDT.address); 93 | 94 | // we bought 200 NST. Nested reserve should get 75% of that. 95 | expect(await mockNST.balanceOf(communityReserve.address)).to.equal(appendDecimals(150)); 96 | 97 | await buyBacker.triggerForToken(dataWETH, dummyRouter.address, mockWETH.address); 98 | 99 | // we bought 10 WETH. Nested reserve should get 75% of that. 100 | expect(await mockNST.balanceOf(communityReserve.address)).to.equal( 101 | appendDecimals(150).add(ethers.utils.parseEther("7.5")), 102 | ); 103 | 104 | expect(await mockWETH.balanceOf(buyBacker.address)).to.equal(ethers.constants.Zero); 105 | expect(await mockNST.balanceOf(buyBacker.address)).to.equal(ethers.constants.Zero); 106 | expect(await mockUSDT.balanceOf(buyBacker.address)).to.equal(ethers.constants.Zero); 107 | }); 108 | 109 | it("updates the burn percentage", async () => { 110 | await buyBacker.setBurnPart(100); 111 | expect(await buyBacker.burnPercentage()).to.equal(100); 112 | }); 113 | 114 | const deployMockToken = async (name: string, symbol: string, owner: SignerWithAddress) => { 115 | const TokenFactory = await ethers.getContractFactory("MockERC20"); 116 | return TokenFactory.connect(owner).deploy(name, symbol, ethers.utils.parseEther("1000000")); 117 | }; 118 | }); 119 | -------------------------------------------------------------------------------- /test/unit/NestedRecords.unit.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { ethers } from "hardhat"; 3 | import { expect } from "chai"; 4 | import { NestedRecords, NestedRecords__factory } from "../../typechain"; 5 | import { describeWithoutFork } from "../shared/provider"; 6 | 7 | describeWithoutFork("NestedRecords", () => { 8 | let NestedRecords: NestedRecords__factory, nestedRecords: NestedRecords; 9 | let alice: SignerWithAddress, bob: SignerWithAddress; 10 | 11 | before(async () => { 12 | const signers = await ethers.getSigners(); 13 | alice = signers[0] as any; 14 | bob = signers[1] as any; 15 | }); 16 | 17 | beforeEach(async () => { 18 | NestedRecords = await ethers.getContractFactory("NestedRecords"); 19 | nestedRecords = await NestedRecords.deploy(15); 20 | nestedRecords.addFactory(alice.address); 21 | }); 22 | 23 | it("reverts when setting invalid factory", async () => { 24 | await expect(nestedRecords.addFactory(ethers.constants.AddressZero)).to.be.revertedWith("OFH: INVALID_ADDRESS"); 25 | }); 26 | 27 | it("reverts when calling a factory only function when not a factory", async () => { 28 | await expect(nestedRecords.connect(bob).setReserve(0, bob.address)).to.be.revertedWith("OFH: FORBIDDEN"); 29 | }); 30 | 31 | it("reverts when setting a wrong reserve to a NFT", async () => { 32 | await expect(nestedRecords.store(0, bob.address, 20, ethers.constants.AddressZero)).to.be.revertedWith( 33 | "NRC: INVALID_RESERVE", 34 | ); 35 | await nestedRecords.store(0, bob.address, 20, alice.address); 36 | await expect(nestedRecords.store(0, bob.address, 20, bob.address)).to.be.revertedWith("NRC: RESERVE_MISMATCH"); 37 | }); 38 | 39 | it("reverts when calling store with too many orders", async () => { 40 | const maxHoldingsCount = await nestedRecords.maxHoldingsCount(); 41 | const signers = await ethers.getSigners(); 42 | for (let i = 0; i < maxHoldingsCount.toNumber(); i++) { 43 | await nestedRecords.store(0, signers[i + 3].address, 20, alice.address); 44 | } 45 | await expect(nestedRecords.store(0, bob.address, 20, alice.address)).to.be.revertedWith("NRC: TOO_MANY_TOKENS"); 46 | }); 47 | 48 | describe("#setMaxHoldingsCount", () => { 49 | it("reverts when setting an incorrect number of max holdings", async () => { 50 | await expect(nestedRecords.setMaxHoldingsCount(0)).to.be.revertedWith("NRC: INVALID_MAX_HOLDINGS"); 51 | }); 52 | 53 | it("sets max holdings count", async () => { 54 | await nestedRecords.setMaxHoldingsCount(1); 55 | expect(await nestedRecords.maxHoldingsCount()).to.eq(1); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/unit/NestedReserve.unit.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers } from "hardhat"; 3 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 4 | import { appendDecimals } from "../helpers"; 5 | import { MockERC20, MockERC20__factory, NestedReserve, NestedReserve__factory } from "../../typechain"; 6 | import { describeWithoutFork } from "../shared/provider"; 7 | 8 | describeWithoutFork("NestedReserve", () => { 9 | let nestedReserve: NestedReserve__factory, reserve: NestedReserve; 10 | let mockERC20: MockERC20__factory, mockUNI: MockERC20; 11 | let factory: SignerWithAddress, alice: SignerWithAddress, bob: SignerWithAddress; 12 | 13 | const amountToTransfer = appendDecimals(10); 14 | before(async () => { 15 | nestedReserve = await ethers.getContractFactory("NestedReserve"); 16 | mockERC20 = await ethers.getContractFactory("MockERC20"); 17 | 18 | const signers = await ethers.getSigners(); 19 | factory = signers[0] as any; 20 | alice = signers[1] as any; 21 | bob = signers[2] as any; 22 | }); 23 | 24 | beforeEach(async () => { 25 | reserve = await nestedReserve.deploy(); 26 | await reserve.deployed(); 27 | await reserve.addFactory(factory.address); 28 | 29 | mockUNI = await mockERC20.deploy("Mocked UNI", "INU", 0); 30 | await mockUNI.mint(reserve.address, amountToTransfer); 31 | }); 32 | 33 | describe("#transfer", () => { 34 | it("transfer the funds", async () => { 35 | await reserve.transfer(alice.address, mockUNI.address, amountToTransfer); 36 | expect(await mockUNI.balanceOf(alice.address)).to.eq(amountToTransfer); 37 | }); 38 | 39 | it("reverts if insufficient funds", async () => { 40 | await expect(reserve.transfer(alice.address, mockUNI.address, amountToTransfer.add(1))).to.be.revertedWith( 41 | "transfer amount exceeds balance", 42 | ); 43 | }); 44 | 45 | it("reverts if the recipient if unauthorized", async () => { 46 | await expect( 47 | reserve.connect(alice).transfer(alice.address, mockUNI.address, amountToTransfer), 48 | ).to.be.revertedWith("OFH: FORBIDDEN"); 49 | }); 50 | 51 | it("reverts if the token is invalid", async () => { 52 | await expect( 53 | reserve.transfer(alice.address, "0x0000000000000000000000000000000000000000", amountToTransfer), 54 | ).to.be.revertedWith("Address: call to non-contract"); 55 | }); 56 | 57 | it("reverts if the recipient is invalid", async () => { 58 | await expect( 59 | reserve.transfer("0x0000000000000000000000000000000000000000", mockUNI.address, amountToTransfer), 60 | ).to.be.revertedWith("NRS: INVALID_ADDRESS"); 61 | }); 62 | }); 63 | 64 | describe("#withdraw", () => { 65 | it("transfer the funds", async () => { 66 | await reserve.withdraw(mockUNI.address, amountToTransfer); 67 | expect(await mockUNI.balanceOf(factory.address)).to.eq(amountToTransfer); 68 | }); 69 | 70 | it("reverts if insufficient funds", async () => { 71 | await expect(reserve.withdraw(mockUNI.address, amountToTransfer.add(1))).to.be.revertedWith( 72 | "transfer amount exceeds balance", 73 | ); 74 | }); 75 | 76 | it("reverts if the recipient if unauthorized", async () => { 77 | await expect(reserve.connect(alice).withdraw(mockUNI.address, amountToTransfer)).to.be.revertedWith( 78 | "OFH: FORBIDDEN", 79 | ); 80 | }); 81 | 82 | it("reverts if the token is invalid", async () => { 83 | await expect( 84 | reserve.withdraw("0x0000000000000000000000000000000000000000", amountToTransfer), 85 | ).to.be.revertedWith("Address: call to non-contract"); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/unit/OwnableFactoryHandler.unit.ts: -------------------------------------------------------------------------------- 1 | import { LoadFixtureFunction } from "../types"; 2 | import { factoryAndOperatorsFixture, FactoryAndOperatorsFixture } from "../shared/fixtures"; 3 | import { createFixtureLoader, describeWithoutFork, expect, provider } from "../shared/provider"; 4 | import { ethers } from "hardhat"; 5 | import { Wallet } from "ethers"; 6 | 7 | let loadFixture: LoadFixtureFunction; 8 | 9 | describeWithoutFork("OwnableFactoryHandler", () => { 10 | let context: FactoryAndOperatorsFixture; 11 | const otherFactory = Wallet.createRandom(); 12 | 13 | before("loader", async () => { 14 | loadFixture = createFixtureLoader(provider.getWallets(), provider); 15 | }); 16 | 17 | beforeEach("create fixture loader", async () => { 18 | context = await loadFixture(factoryAndOperatorsFixture); 19 | }); 20 | 21 | describe("addFactory()", () => { 22 | it("sets the new factory", async () => { 23 | await expect(context.nestedAsset.connect(context.masterDeployer).addFactory(otherFactory.address)) 24 | .to.emit(context.nestedAsset, "FactoryAdded") 25 | .withArgs(otherFactory.address); 26 | expect(await context.nestedAsset.supportedFactories(otherFactory.address)).to.equal(true); 27 | }); 28 | 29 | it("reverts if unauthorized", async () => { 30 | await expect( 31 | context.nestedAsset.connect(context.user1).addFactory(otherFactory.address), 32 | ).to.be.revertedWith("Ownable: caller is not the owner"); 33 | expect(await context.nestedAsset.supportedFactories(otherFactory.address)).to.equal(false); 34 | }); 35 | 36 | it("reverts if the address is invalid", async () => { 37 | await expect( 38 | context.nestedAsset.connect(context.masterDeployer).addFactory(ethers.constants.AddressZero), 39 | ).to.be.revertedWith("OFH: INVALID_ADDRESS"); 40 | expect(await context.nestedAsset.supportedFactories(otherFactory.address)).to.equal(false); 41 | }); 42 | }); 43 | 44 | describe("removeFactory()", () => { 45 | it("remove a factory", async () => { 46 | await context.nestedAsset.connect(context.masterDeployer).addFactory(otherFactory.address); 47 | expect(await context.nestedAsset.supportedFactories(otherFactory.address)).to.equal(true); 48 | await expect(context.nestedAsset.connect(context.masterDeployer).removeFactory(otherFactory.address)) 49 | .to.emit(context.nestedAsset, "FactoryRemoved") 50 | .withArgs(otherFactory.address); 51 | expect(await context.nestedAsset.supportedFactories(otherFactory.address)).to.equal(false); 52 | }); 53 | 54 | it("reverts if unauthorized", async () => { 55 | await context.nestedAsset.connect(context.masterDeployer).addFactory(otherFactory.address); 56 | await expect( 57 | context.nestedAsset.connect(context.user1).removeFactory(otherFactory.address), 58 | ).to.be.revertedWith("Ownable: caller is not the owner"); 59 | expect(await context.nestedAsset.supportedFactories(otherFactory.address)).to.equal(true); 60 | }); 61 | 62 | it("reverts if already not supported", async () => { 63 | await expect( 64 | context.nestedAsset.connect(context.masterDeployer).removeFactory(otherFactory.address), 65 | ).to.be.revertedWith("OFH: NOT_SUPPORTED"); 66 | 67 | await expect( 68 | context.nestedAsset.connect(context.masterDeployer).removeFactory(ethers.constants.AddressZero), 69 | ).to.be.revertedWith("OFH: NOT_SUPPORTED"); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/unit/ParaswapOperator.unit.ts: -------------------------------------------------------------------------------- 1 | import { LoadFixtureFunction } from "../types"; 2 | import { paraswapOperatorFixture, ParaswapOperatorFixture } from "../shared/fixtures"; 3 | import { ActorFixture } from "../shared/actors"; 4 | import { createFixtureLoader, describeWithoutFork, expect, provider } from "../shared/provider"; 5 | import { BigNumber, Wallet } from "ethers"; 6 | 7 | let loadFixture: LoadFixtureFunction; 8 | 9 | /* 10 | * The operator's in-depth tests are in the factory tests. 11 | */ 12 | describeWithoutFork("ParaswapOperator", () => { 13 | let context: ParaswapOperatorFixture; 14 | const actors = new ActorFixture(provider.getWallets() as Wallet[], provider); 15 | 16 | before("loader", async () => { 17 | loadFixture = createFixtureLoader(provider.getWallets(), provider); 18 | }); 19 | 20 | beforeEach("create fixture loader", async () => { 21 | context = await loadFixture(paraswapOperatorFixture); 22 | }); 23 | 24 | it("deploys and has an address", async () => { 25 | expect(context.paraswapOperator.address).to.be.a.string; 26 | expect(context.augustusSwapper.address).to.be.a.string; 27 | }); 28 | 29 | it("has proxy and swapper", async () => { 30 | expect(context.paraswapOperator.tokenTransferProxy()).to.be.a.string; 31 | expect(context.paraswapOperator.augustusSwapper()).to.be.a.string; 32 | }); 33 | 34 | describe("performSwap()", () => { 35 | it("Swap tokens", async () => { 36 | let initDaiBalance = await context.mockDAI.balanceOf(context.testableOperatorCaller.address); 37 | let initUniBalance = await context.mockUNI.balanceOf(context.testableOperatorCaller.address); 38 | const amount = 1000; 39 | // Calldata swap 1000 DAI against 1000 UNI 40 | let calldata = context.augustusSwapperInterface.encodeFunctionData("dummyswapToken", [ 41 | context.mockDAI.address, 42 | context.mockUNI.address, 43 | amount, 44 | ]); 45 | 46 | // Run swap 47 | await context.testableOperatorCaller 48 | .connect(actors.user1()) 49 | .performSwap( 50 | context.paraswapOperator.address, 51 | context.mockDAI.address, 52 | context.mockUNI.address, 53 | calldata, 54 | ); 55 | 56 | expect(await context.mockDAI.balanceOf(context.testableOperatorCaller.address)).to.be.equal( 57 | initDaiBalance.sub(BigNumber.from(amount)), 58 | ); 59 | expect(await context.mockUNI.balanceOf(context.testableOperatorCaller.address)).to.be.equal( 60 | initUniBalance.add(BigNumber.from(amount)), 61 | ); 62 | }); 63 | 64 | it("Can't swap 0 tokens", async () => { 65 | const amount = 0; 66 | 67 | // Calldata swap 1000 DAI against 1000 UNI 68 | let calldata = context.augustusSwapperInterface.encodeFunctionData("dummyswapToken", [ 69 | context.mockDAI.address, 70 | context.mockUNI.address, 71 | amount, 72 | ]); 73 | 74 | // Run swap 75 | await expect( 76 | context.testableOperatorCaller 77 | .connect(actors.user1()) 78 | .performSwap( 79 | context.paraswapOperator.address, 80 | context.mockDAI.address, 81 | context.mockUNI.address, 82 | calldata, 83 | ), 84 | ).to.be.revertedWith("TestableOperatorCaller::performSwap: Error"); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/unit/ZeroExOperator.unit.ts: -------------------------------------------------------------------------------- 1 | import { LoadFixtureFunction } from "../types"; 2 | import { zeroExOperatorFixture, ZeroExOperatorFixture } from "../shared/fixtures"; 3 | import { ActorFixture } from "../shared/actors"; 4 | import { createFixtureLoader, describeWithoutFork, expect, provider } from "../shared/provider"; 5 | import { BigNumber, Wallet } from "ethers"; 6 | 7 | let loadFixture: LoadFixtureFunction; 8 | 9 | /* 10 | * The operator's in-depth tests are in the factory tests. 11 | */ 12 | describeWithoutFork("ZeroExOperator", () => { 13 | let context: ZeroExOperatorFixture; 14 | const actors = new ActorFixture(provider.getWallets() as Wallet[], provider); 15 | 16 | before("loader", async () => { 17 | loadFixture = createFixtureLoader(provider.getWallets(), provider); 18 | }); 19 | 20 | beforeEach("create fixture loader", async () => { 21 | context = await loadFixture(zeroExOperatorFixture); 22 | }); 23 | 24 | it("deploys and has an address", async () => { 25 | expect(context.zeroExOperator.address).to.be.a.string; 26 | expect(context.dummyRouter.address).to.be.a.string; 27 | }); 28 | 29 | it("has swapTarget (storage)", async () => { 30 | expect(context.zeroExOperator.operatorStorage()).to.be.a.string; 31 | }); 32 | 33 | describe("performSwap()", () => { 34 | it("Swap tokens", async () => { 35 | let initDaiBalance = await context.mockDAI.balanceOf(context.testableOperatorCaller.address); 36 | let initUniBalance = await context.mockUNI.balanceOf(context.testableOperatorCaller.address); 37 | const amount = 1000; 38 | // Calldata swap 1000 DAI against 1000 UNI 39 | let calldata = context.dummyRouterInterface.encodeFunctionData("dummyswapToken", [ 40 | context.mockDAI.address, 41 | context.mockUNI.address, 42 | amount, 43 | ]); 44 | 45 | // Run swap 46 | await context.testableOperatorCaller 47 | .connect(actors.user1()) 48 | .performSwap( 49 | context.zeroExOperator.address, 50 | context.mockDAI.address, 51 | context.mockUNI.address, 52 | calldata, 53 | ); 54 | 55 | expect(await context.mockDAI.balanceOf(context.testableOperatorCaller.address)).to.be.equal( 56 | initDaiBalance.sub(BigNumber.from(amount)), 57 | ); 58 | expect(await context.mockUNI.balanceOf(context.testableOperatorCaller.address)).to.be.equal( 59 | initUniBalance.add(BigNumber.from(amount)), 60 | ); 61 | }); 62 | 63 | it("Can't swap 0 tokens", async () => { 64 | const amount = 0; 65 | 66 | // Calldata swap 1000 DAI against 1000 UNI 67 | let calldata = context.dummyRouterInterface.encodeFunctionData("dummyswapToken", [ 68 | context.mockDAI.address, 69 | context.mockUNI.address, 70 | amount, 71 | ]); 72 | 73 | // Run swap 74 | await expect( 75 | context.testableOperatorCaller 76 | .connect(actors.user1()) 77 | .performSwap( 78 | context.zeroExOperator.address, 79 | context.mockDAI.address, 80 | context.mockUNI.address, 81 | calldata, 82 | ), 83 | ).to.be.revertedWith("TestableOperatorCaller::performSwap: Error"); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "strictNullChecks": false, 7 | "esModuleInterop": true, 8 | "outDir": "dist", 9 | "typeRoots": ["./typechain", "./node_modules/@types", "./types"], 10 | "types": ["@nomiclabs/hardhat-ethers", "@nomiclabs/hardhat-waffle"], 11 | "resolveJsonModule": true 12 | }, 13 | "include": ["./scripts", "./test", "./typechain"], 14 | "files": ["./hardhat.config.ts"] 15 | } 16 | --------------------------------------------------------------------------------