Compare commits
	
		
			43 Commits
		
	
	
		
			@0x/contra
			...
			refactor_i
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 46b14b0d62 | ||
|  | c5e25f8b7d | ||
|  | d7dbc0576d | ||
|  | 15fb00e958 | ||
|  | 1d9295cc94 | ||
|  | 79f36cf6fb | ||
|  | b88292c159 | ||
|  | e6d787d60e | ||
|  | 6ce4458a5d | ||
|  | fad6e65c07 | ||
|  | 840c85373e | ||
|  | cda25e4a5d | ||
|  | d0c9c43f7f | ||
|  | 8471bb2908 | ||
|  | 858a95dbe7 | ||
|  | c044e8f534 | ||
|  | 53a289ddd5 | ||
|  | 44fe9159aa | ||
|  | 5997ce3ca3 | ||
|  | eca52ee430 | ||
|  | d314655444 | ||
|  | 0479bb5fe1 | ||
|  | 9ecc31ed54 | ||
|  | eb29abd36c | ||
|  | d202e01522 | ||
|  | e838a6801b | ||
|  | 7439871aa0 | ||
|  | 317f2138c5 | ||
|  | e5834f1901 | ||
|  | 5063446f93 | ||
|  | 0caf495a1a | ||
|  | a20de0fc69 | ||
|  | 9aa0065d2d | ||
|  | c24855e627 | ||
|  | 6bb72dd775 | ||
|  | 77d1ed257c | ||
|  | 5d265360c4 | ||
|  | c9097f6e8b | ||
|  | d3df985a42 | ||
|  | 7267420874 | ||
|  | 17e81432f1 | ||
|  | 57c767c3b1 | ||
|  | dbb1c88ad9 | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -75,8 +75,9 @@ generated_docs/ | ||||
|  | ||||
| TODO.md | ||||
|  | ||||
| # VSCode file | ||||
| # IDE file | ||||
| .vscode | ||||
| .idea | ||||
|  | ||||
| # generated contract artifacts/ | ||||
| contracts/broker/generated-artifacts/ | ||||
|   | ||||
| @@ -1,4 +1,13 @@ | ||||
| [ | ||||
|     { | ||||
|         "timestamp": 1631120757, | ||||
|         "version": "3.3.19", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Dependencies updated" | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "timestamp": 1630459879, | ||||
|         "version": "3.3.18", | ||||
|   | ||||
| @@ -5,6 +5,10 @@ Edit the package's CHANGELOG.json file only. | ||||
|  | ||||
| CHANGELOG | ||||
|  | ||||
| ## v3.3.19 - _September 8, 2021_ | ||||
|  | ||||
|     * Dependencies updated | ||||
|  | ||||
| ## v3.3.18 - _September 1, 2021_ | ||||
|  | ||||
|     * Dependencies updated | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@0x/contracts-erc20", | ||||
|     "version": "3.3.18", | ||||
|     "version": "3.3.19", | ||||
|     "engines": { | ||||
|         "node": ">=6.12" | ||||
|     }, | ||||
| @@ -54,7 +54,7 @@ | ||||
|         "@0x/abi-gen": "^5.6.0", | ||||
|         "@0x/contracts-gen": "^2.0.38", | ||||
|         "@0x/contracts-test-utils": "^5.4.10", | ||||
|         "@0x/contracts-utils": "^4.7.18", | ||||
|         "@0x/contracts-utils": "^4.8.0", | ||||
|         "@0x/dev-utils": "^4.2.7", | ||||
|         "@0x/sol-compiler": "^4.7.3", | ||||
|         "@0x/ts-doc-gen": "^0.0.28", | ||||
|   | ||||
| @@ -1,4 +1,21 @@ | ||||
| [ | ||||
|     { | ||||
|         "version": "1.4.0", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Support cast vote by signature in Treasury" | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "timestamp": 1631120757, | ||||
|         "version": "1.3.5", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Dependencies updated" | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "timestamp": 1630459879, | ||||
|         "version": "1.3.4", | ||||
|   | ||||
| @@ -5,6 +5,10 @@ Edit the package's CHANGELOG.json file only. | ||||
|  | ||||
| CHANGELOG | ||||
|  | ||||
| ## v1.3.5 - _September 8, 2021_ | ||||
|  | ||||
|     * Dependencies updated | ||||
|  | ||||
| ## v1.3.4 - _September 1, 2021_ | ||||
|  | ||||
|     * Dependencies updated | ||||
|   | ||||
| @@ -136,8 +136,9 @@ interface IZrxTreasury { | ||||
|         returns (uint256 proposalId); | ||||
|  | ||||
|     /// @dev Casts a vote for the given proposal. Only callable | ||||
|     ///      during the voting period for that proposal. See | ||||
|     ///      `getVotingPower` for how voting power is computed. | ||||
|     ///      during the voting period for that proposal. | ||||
|     ///      One address can only vote once. | ||||
|     ///      See `getVotingPower` for how voting power is computed. | ||||
|     /// @param proposalId The ID of the proposal to vote on. | ||||
|     /// @param support Whether to support the proposal or not. | ||||
|     /// @param operatedPoolIds The pools operated by `msg.sender`. The | ||||
| @@ -150,6 +151,28 @@ interface IZrxTreasury { | ||||
|     ) | ||||
|         external; | ||||
|  | ||||
|     /// @dev Casts a vote for the given proposal, by signature. | ||||
|     ///      Only callable during the voting period for that proposal. | ||||
|     ///      One address/voter can only vote once. | ||||
|     ///      See `getVotingPower` for how voting power is computed. | ||||
|     /// @param proposalId The ID of the proposal to vote on. | ||||
|     /// @param support Whether to support the proposal or not. | ||||
|     /// @param operatedPoolIds The pools operated by the signer. The | ||||
|     ///        ZRX currently delegated to those pools will be accounted | ||||
|     ///        for in the voting power. | ||||
|     /// @param v the v field of the signature | ||||
|     /// @param r the r field of the signature | ||||
|     /// @param s the s field of the signature | ||||
|     function castVoteBySignature( | ||||
|         uint256 proposalId, | ||||
|         bool support, | ||||
|         bytes32[] memory operatedPoolIds, | ||||
|         uint8 v, | ||||
|         bytes32 r, | ||||
|         bytes32 s | ||||
|     ) | ||||
|         external; | ||||
|  | ||||
|     /// @dev Executes a proposal that has passed and is | ||||
|     ///      currently executable. | ||||
|     /// @param proposalId The ID of the proposal to execute. | ||||
|   | ||||
| @@ -34,11 +34,25 @@ contract ZrxTreasury is | ||||
|     using LibRichErrorsV06 for bytes; | ||||
|     using LibBytesV06 for bytes; | ||||
|  | ||||
|     /// Contract name | ||||
|     string private constant CONTRACT_NAME = "Zrx Treasury"; | ||||
|  | ||||
|     /// Contract version | ||||
|     string private constant CONTRACT_VERSION = "1.0.0"; | ||||
|  | ||||
|     /// The EIP-712 typehash for the contract's domain | ||||
|     bytes32 private constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); | ||||
|  | ||||
|     /// The EIP-712 typehash for the vote struct | ||||
|     bytes32 private constant VOTE_TYPEHASH = keccak256("TreasuryVote(uint256 proposalId,bool support,bytes32[] operatedPoolIds)"); | ||||
|  | ||||
|     // Immutables | ||||
|     IStaking public immutable override stakingProxy; | ||||
|     DefaultPoolOperator public immutable override defaultPoolOperator; | ||||
|     bytes32 public immutable override defaultPoolId; | ||||
|     uint256 public immutable override votingPeriod; | ||||
|     bytes32 immutable domainSeparator; | ||||
|  | ||||
|     uint256 public override proposalThreshold; | ||||
|     uint256 public override quorumThreshold; | ||||
|  | ||||
| @@ -67,6 +81,15 @@ contract ZrxTreasury is | ||||
|         defaultPoolId = params.defaultPoolId; | ||||
|         IStaking.Pool memory defaultPool = stakingProxy_.getStakingPool(params.defaultPoolId); | ||||
|         defaultPoolOperator = DefaultPoolOperator(defaultPool.operator); | ||||
|         domainSeparator = keccak256( | ||||
|             abi.encode( | ||||
|                 DOMAIN_TYPEHASH, | ||||
|                 keccak256(bytes(CONTRACT_NAME)), | ||||
|                 _getChainId(), | ||||
|                 keccak256(bytes(CONTRACT_VERSION)), | ||||
|                 address(this) | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     // solhint-disable | ||||
| @@ -105,7 +128,7 @@ contract ZrxTreasury is | ||||
|     ///        be executed if it passes. Must be at least two epochs | ||||
|     ///        from the current epoch. | ||||
|     /// @param description A text description for the proposal. | ||||
|     /// @param operatedPoolIds The pools operated by `msg.sender`. The | ||||
|     /// @param operatedPoolIds The pools operated by the signer. The | ||||
|     ///        ZRX currently delegated to those pools will be accounted | ||||
|     ///        for in the voting power. | ||||
|     /// @return proposalId The ID of the newly created proposal. | ||||
| @@ -150,8 +173,9 @@ contract ZrxTreasury is | ||||
|     } | ||||
|  | ||||
|     /// @dev Casts a vote for the given proposal. Only callable | ||||
|     ///      during the voting period for that proposal. See | ||||
|     ///      `getVotingPower` for how voting power is computed. | ||||
|     ///      during the voting period for that proposal. | ||||
|     ///      One address can only vote once. | ||||
|     ///      See `getVotingPower` for how voting power is computed. | ||||
|     /// @param proposalId The ID of the proposal to vote on. | ||||
|     /// @param support Whether to support the proposal or not. | ||||
|     /// @param operatedPoolIds The pools operated by `msg.sender`. The | ||||
| @@ -165,43 +189,39 @@ contract ZrxTreasury is | ||||
|         public | ||||
|         override | ||||
|     { | ||||
|         if (proposalId >= proposalCount()) { | ||||
|             revert("castVote/INVALID_PROPOSAL_ID"); | ||||
|         } | ||||
|         if (hasVoted[proposalId][msg.sender]) { | ||||
|             revert("castVote/ALREADY_VOTED"); | ||||
|         } | ||||
|         return _castVote(msg.sender, proposalId, support, operatedPoolIds); | ||||
|     } | ||||
|  | ||||
|         Proposal memory proposal = proposals[proposalId]; | ||||
|         if ( | ||||
|             proposal.voteEpoch != stakingProxy.currentEpoch() || | ||||
|             _hasVoteEnded(proposal.voteEpoch) | ||||
|         ) { | ||||
|             revert("castVote/VOTING_IS_CLOSED"); | ||||
|         } | ||||
|  | ||||
|         uint256 votingPower = getVotingPower(msg.sender, operatedPoolIds); | ||||
|         if (votingPower == 0) { | ||||
|             revert("castVote/NO_VOTING_POWER"); | ||||
|         } | ||||
|  | ||||
|         if (support) { | ||||
|             proposals[proposalId].votesFor = proposals[proposalId].votesFor | ||||
|                 .safeAdd(votingPower); | ||||
|             hasVoted[proposalId][msg.sender] = true; | ||||
|         } else { | ||||
|             proposals[proposalId].votesAgainst = proposals[proposalId].votesAgainst | ||||
|                 .safeAdd(votingPower); | ||||
|             hasVoted[proposalId][msg.sender] = true; | ||||
|         } | ||||
|  | ||||
|         emit VoteCast( | ||||
|             msg.sender, | ||||
|             operatedPoolIds, | ||||
|             proposalId, | ||||
|             support, | ||||
|             votingPower | ||||
|     /// @dev Casts a vote for the given proposal, by signature. | ||||
|     ///      Only callable during the voting period for that proposal. | ||||
|     ///      One address/voter can only vote once. | ||||
|     ///      See `getVotingPower` for how voting power is computed. | ||||
|     /// @param proposalId The ID of the proposal to vote on. | ||||
|     /// @param support Whether to support the proposal or not. | ||||
|     /// @param operatedPoolIds The pools operated by voter. The | ||||
|     ///        ZRX currently delegated to those pools will be accounted | ||||
|     ///        for in the voting power. | ||||
|     /// @param v the v field of the signature | ||||
|     /// @param r the r field of the signature | ||||
|     /// @param s the s field of the signature | ||||
|     function castVoteBySignature( | ||||
|         uint256 proposalId, | ||||
|         bool support, | ||||
|         bytes32[] memory operatedPoolIds, | ||||
|         uint8 v, | ||||
|         bytes32 r, | ||||
|         bytes32 s | ||||
|     ) | ||||
|         public | ||||
|         override | ||||
|     { | ||||
|         bytes32 structHash = keccak256( | ||||
|             abi.encode(VOTE_TYPEHASH, proposalId, support, keccak256(abi.encodePacked(operatedPoolIds))) | ||||
|         ); | ||||
|         bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); | ||||
|         address signatory = ecrecover(digest, v, r, s); | ||||
|  | ||||
|         return _castVote(signatory, proposalId, support, operatedPoolIds); | ||||
|     } | ||||
|  | ||||
|     /// @dev Executes a proposal that has passed and is | ||||
| @@ -373,4 +393,60 @@ contract ZrxTreasury is | ||||
|             .safeAdd(votingPeriod); | ||||
|         return block.timestamp > voteEndTime; | ||||
|     } | ||||
|  | ||||
|     /// @dev Casts a vote for the given proposal. Only callable | ||||
|     ///      during the voting period for that proposal. See | ||||
|     ///      `getVotingPower` for how voting power is computed. | ||||
|     function _castVote( | ||||
|         address voter, | ||||
|         uint256 proposalId, | ||||
|         bool support, | ||||
|         bytes32[] memory operatedPoolIds | ||||
|     ) | ||||
|         private | ||||
|     { | ||||
|         if (proposalId >= proposalCount()) { | ||||
|             revert("_castVote/INVALID_PROPOSAL_ID"); | ||||
|         } | ||||
|         if (hasVoted[proposalId][voter]) { | ||||
|             revert("_castVote/ALREADY_VOTED"); | ||||
|         } | ||||
|  | ||||
|         Proposal memory proposal = proposals[proposalId]; | ||||
|         if ( | ||||
|             proposal.voteEpoch != stakingProxy.currentEpoch() || | ||||
|             _hasVoteEnded(proposal.voteEpoch) | ||||
|         ) { | ||||
|             revert("_castVote/VOTING_IS_CLOSED"); | ||||
|         } | ||||
|  | ||||
|         uint256 votingPower = getVotingPower(voter, operatedPoolIds); | ||||
|         if (votingPower == 0) { | ||||
|             revert("_castVote/NO_VOTING_POWER"); | ||||
|         } | ||||
|  | ||||
|         if (support) { | ||||
|             proposals[proposalId].votesFor = proposals[proposalId].votesFor | ||||
|                 .safeAdd(votingPower); | ||||
|         } else { | ||||
|             proposals[proposalId].votesAgainst = proposals[proposalId].votesAgainst | ||||
|                 .safeAdd(votingPower); | ||||
|         } | ||||
|         hasVoted[proposalId][voter] = true; | ||||
|  | ||||
|         emit VoteCast( | ||||
|             voter, | ||||
|             operatedPoolIds, | ||||
|             proposalId, | ||||
|             support, | ||||
|             votingPower | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /// @dev Gets the Ethereum chain id | ||||
|     function _getChainId() private pure returns (uint256) { | ||||
|         uint256 chainId; | ||||
|         assembly { chainId := chainid() } | ||||
|         return chainId; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@0x/contracts-treasury", | ||||
|     "version": "1.3.4", | ||||
|     "version": "1.3.5", | ||||
|     "engines": { | ||||
|         "node": ">=6.12" | ||||
|     }, | ||||
| @@ -49,7 +49,7 @@ | ||||
|         "@0x/abi-gen": "^5.6.0", | ||||
|         "@0x/contract-addresses": "^6.7.0", | ||||
|         "@0x/contracts-asset-proxy": "^3.7.19", | ||||
|         "@0x/contracts-erc20": "^3.3.18", | ||||
|         "@0x/contracts-erc20": "^3.3.19", | ||||
|         "@0x/contracts-gen": "^2.0.38", | ||||
|         "@0x/contracts-staking": "^2.0.45", | ||||
|         "@0x/contracts-test-utils": "^5.4.10", | ||||
|   | ||||
| @@ -17,8 +17,9 @@ import { | ||||
|     randomAddress, | ||||
|     verifyEventsFromLogs, | ||||
| } from '@0x/contracts-test-utils'; | ||||
| import { BigNumber } from '@0x/utils'; | ||||
| import * as _ from 'lodash'; | ||||
| import { TreasuryVote } from '@0x/protocol-utils'; | ||||
| import { BigNumber, hexUtils } from '@0x/utils'; | ||||
| import * as ethUtil from 'ethereumjs-util'; | ||||
|  | ||||
| import { artifacts } from './artifacts'; | ||||
| import { DefaultPoolOperatorContract, ZrxTreasuryContract, ZrxTreasuryEvents } from './wrappers'; | ||||
| @@ -55,6 +56,8 @@ blockchainTests.resets('Treasury governance', env => { | ||||
|     let nonDefaultPoolId: string; | ||||
|     let poolOperator: string; | ||||
|     let delegator: string; | ||||
|     let relayer: string; | ||||
|     let delegatorPrivateKey: string; | ||||
|     let actions: ProposedAction[]; | ||||
|  | ||||
|     async function deployStakingAsync(): Promise<void> { | ||||
| @@ -105,7 +108,10 @@ blockchainTests.resets('Treasury governance', env => { | ||||
|     } | ||||
|  | ||||
|     before(async () => { | ||||
|         [admin, poolOperator, delegator] = await env.getAccountAddressesAsync(); | ||||
|         const accounts = await env.getAccountAddressesAsync(); | ||||
|         [admin, poolOperator, delegator, relayer] = accounts; | ||||
|         delegatorPrivateKey = hexUtils.toHex(constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(delegator)]); | ||||
|  | ||||
|         zrx = await DummyERC20TokenContract.deployFrom0xArtifactAsync( | ||||
|             erc20Artifacts.DummyERC20Token, | ||||
|             env.provider, | ||||
| @@ -399,7 +405,7 @@ blockchainTests.resets('Treasury governance', env => { | ||||
|             expect(await treasury.proposalCount().callAsync()).to.bignumber.equal(1); | ||||
|         }); | ||||
|     }); | ||||
|     describe('castVote()', () => { | ||||
|     describe('castVote() and castVoteBySignature()', () => { | ||||
|         const VOTE_PROPOSAL_ID = new BigNumber(0); | ||||
|         const DELEGATOR_VOTING_POWER = new BigNumber(420); | ||||
|  | ||||
| @@ -418,17 +424,18 @@ blockchainTests.resets('Treasury governance', env => { | ||||
|                 .propose(actions, currentEpoch.plus(2), PROPOSAL_DESCRIPTION, []) | ||||
|                 .awaitTransactionSuccessAsync({ from: delegator }); | ||||
|         }); | ||||
|         // castVote() | ||||
|         it('Cannot vote on invalid proposalId', async () => { | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             const tx = treasury | ||||
|                 .castVote(INVALID_PROPOSAL_ID, true, []) | ||||
|                 .awaitTransactionSuccessAsync({ from: delegator }); | ||||
|             return expect(tx).to.revertWith('castVote/INVALID_PROPOSAL_ID'); | ||||
|             return expect(tx).to.revertWith('_castVote/INVALID_PROPOSAL_ID'); | ||||
|         }); | ||||
|         it('Cannot vote before voting period starts', async () => { | ||||
|             const tx = treasury.castVote(VOTE_PROPOSAL_ID, true, []).awaitTransactionSuccessAsync({ from: delegator }); | ||||
|             return expect(tx).to.revertWith('castVote/VOTING_IS_CLOSED'); | ||||
|             return expect(tx).to.revertWith('_castVote/VOTING_IS_CLOSED'); | ||||
|         }); | ||||
|         it('Cannot vote after voting period ends', async () => { | ||||
|             await fastForwardToNextEpochAsync(); | ||||
| @@ -436,14 +443,14 @@ blockchainTests.resets('Treasury governance', env => { | ||||
|             await env.web3Wrapper.increaseTimeAsync(TREASURY_PARAMS.votingPeriod.plus(1).toNumber()); | ||||
|             await env.web3Wrapper.mineBlockAsync(); | ||||
|             const tx = treasury.castVote(VOTE_PROPOSAL_ID, true, []).awaitTransactionSuccessAsync({ from: delegator }); | ||||
|             return expect(tx).to.revertWith('castVote/VOTING_IS_CLOSED'); | ||||
|             return expect(tx).to.revertWith('_castVote/VOTING_IS_CLOSED'); | ||||
|         }); | ||||
|         it('Cannot vote twice on same proposal', async () => { | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             await treasury.castVote(VOTE_PROPOSAL_ID, true, []).awaitTransactionSuccessAsync({ from: delegator }); | ||||
|             const tx = treasury.castVote(VOTE_PROPOSAL_ID, false, []).awaitTransactionSuccessAsync({ from: delegator }); | ||||
|             return expect(tx).to.revertWith('castVote/ALREADY_VOTED'); | ||||
|             return expect(tx).to.revertWith('_castVote/ALREADY_VOTED'); | ||||
|         }); | ||||
|         it('Can cast a valid vote', async () => { | ||||
|             await fastForwardToNextEpochAsync(); | ||||
| @@ -465,6 +472,110 @@ blockchainTests.resets('Treasury governance', env => { | ||||
|                 ZrxTreasuryEvents.VoteCast, | ||||
|             ); | ||||
|         }); | ||||
|         // castVoteBySignature() | ||||
|         it('Cannot vote by signature on invalid proposalId', async () => { | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             const vote = new TreasuryVote({ | ||||
|                 proposalId: INVALID_PROPOSAL_ID, | ||||
|                 verifyingContract: admin, | ||||
|             }); | ||||
|             const signature = vote.getSignatureWithKey(delegatorPrivateKey); | ||||
|             const tx = treasury | ||||
|                 .castVoteBySignature(INVALID_PROPOSAL_ID, true, [], signature.v,  signature.r,  signature.s) | ||||
|                 .awaitTransactionSuccessAsync({ from: relayer }); | ||||
|             return expect(tx).to.revertWith('_castVote/INVALID_PROPOSAL_ID'); | ||||
|         }); | ||||
|         it('Cannot vote by signature before voting period starts', async () => { | ||||
|             const vote = new TreasuryVote({ | ||||
|                 proposalId: VOTE_PROPOSAL_ID, | ||||
|                 verifyingContract: admin, | ||||
|             }); | ||||
|             const signature = vote.getSignatureWithKey(delegatorPrivateKey); | ||||
|             const tx = treasury | ||||
|                 .castVoteBySignature(VOTE_PROPOSAL_ID, true, [], signature.v,  signature.r,  signature.s) | ||||
|                 .awaitTransactionSuccessAsync({ from: relayer }); | ||||
|             return expect(tx).to.revertWith('_castVote/VOTING_IS_CLOSED'); | ||||
|         }); | ||||
|         it('Cannot vote by signature after voting period ends', async () => { | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             await env.web3Wrapper.increaseTimeAsync(TREASURY_PARAMS.votingPeriod.plus(1).toNumber()); | ||||
|             await env.web3Wrapper.mineBlockAsync(); | ||||
|  | ||||
|             const vote = new TreasuryVote({ | ||||
|                 proposalId: VOTE_PROPOSAL_ID, | ||||
|                 verifyingContract: admin, | ||||
|             }); | ||||
|             const signature = vote.getSignatureWithKey(delegatorPrivateKey); | ||||
|             const tx = treasury | ||||
|                 .castVoteBySignature(VOTE_PROPOSAL_ID, true, [], signature.v,  signature.r,  signature.s) | ||||
|                 .awaitTransactionSuccessAsync({ from: relayer }); | ||||
|             return expect(tx).to.revertWith('_castVote/VOTING_IS_CLOSED'); | ||||
|         }); | ||||
|         it('Can recover the address from signature correctly', async () => { | ||||
|             const vote = new TreasuryVote({ | ||||
|                 proposalId: VOTE_PROPOSAL_ID, | ||||
|                 verifyingContract: admin, | ||||
|             }); | ||||
|             const signature = vote.getSignatureWithKey(delegatorPrivateKey); | ||||
|             const publicKey = ethUtil.ecrecover( | ||||
|                 ethUtil.toBuffer(vote.getEIP712Hash()), | ||||
|                 signature.v, | ||||
|                 ethUtil.toBuffer(signature.r), | ||||
|                 ethUtil.toBuffer(signature.s), | ||||
|             ); | ||||
|             const address = ethUtil.publicToAddress(publicKey); | ||||
|  | ||||
|             expect(ethUtil.bufferToHex(address)).to.be.equal(delegator); | ||||
|         }); | ||||
|         it('Can cast a valid vote by signature', async () => { | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|  | ||||
|             const vote = new TreasuryVote({ | ||||
|                 proposalId: VOTE_PROPOSAL_ID, | ||||
|                 verifyingContract: treasury.address, | ||||
|                 chainId: 1337, | ||||
|                 support: false, | ||||
|             }); | ||||
|             const signature = vote.getSignatureWithKey(delegatorPrivateKey); | ||||
|             const tx = await treasury | ||||
|                 .castVoteBySignature(VOTE_PROPOSAL_ID, false, [], signature.v,  signature.r,  signature.s) | ||||
|                 .awaitTransactionSuccessAsync({ from: relayer }); | ||||
|  | ||||
|             verifyEventsFromLogs( | ||||
|                 tx.logs, | ||||
|                 [ | ||||
|                     { | ||||
|                         voter: delegator, | ||||
|                         operatedPoolIds: [], | ||||
|                         proposalId: VOTE_PROPOSAL_ID, | ||||
|                         support: vote.support, | ||||
|                         votingPower: DELEGATOR_VOTING_POWER, | ||||
|                     }, | ||||
|                 ], | ||||
|                 ZrxTreasuryEvents.VoteCast, | ||||
|             ); | ||||
|         }); | ||||
|         it('Cannot vote by signature twice on same proposal', async () => { | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             await treasury.castVote(VOTE_PROPOSAL_ID, true, []) | ||||
|                 .awaitTransactionSuccessAsync({ from: delegator }); | ||||
|  | ||||
|             const secondVote = new TreasuryVote({ | ||||
|                 proposalId: VOTE_PROPOSAL_ID, | ||||
|                 verifyingContract: treasury.address, | ||||
|                 chainId: 1337, | ||||
|                 support: false, | ||||
|             }); | ||||
|             const signature = secondVote.getSignatureWithKey(delegatorPrivateKey); | ||||
|             const secondVoteTx = treasury | ||||
|                 .castVoteBySignature(VOTE_PROPOSAL_ID, false, [], signature.v,  signature.r,  signature.s) | ||||
|                 .awaitTransactionSuccessAsync({ from: relayer }); | ||||
|             return expect(secondVoteTx).to.revertWith('_castVote/ALREADY_VOTED'); | ||||
|         }); | ||||
|     }); | ||||
|     describe('execute()', () => { | ||||
|         let passedProposalId: BigNumber; | ||||
| @@ -473,7 +584,7 @@ blockchainTests.resets('Treasury governance', env => { | ||||
|         let ongoingVoteProposalId: BigNumber; | ||||
|  | ||||
|         before(async () => { | ||||
|             // OPerator has enough ZRX to create and pass a proposal | ||||
|             // Operator has enough ZRX to create and pass a proposal | ||||
|             await staking.stake(TREASURY_PARAMS.quorumThreshold).awaitTransactionSuccessAsync({ from: poolOperator }); | ||||
|             await staking | ||||
|                 .moveStake( | ||||
| @@ -549,7 +660,7 @@ blockchainTests.resets('Treasury governance', env => { | ||||
|         }); | ||||
|         it('Cannot execute before or after the execution epoch', async () => { | ||||
|             const tooEarly = treasury.execute(passedProposalId, actions).awaitTransactionSuccessAsync(); | ||||
|             expect(tooEarly).to.revertWith('_assertProposalExecutable/CANNOT_EXECUTE_THIS_EPOCH'); | ||||
|             await expect(tooEarly).to.revertWith('_assertProposalExecutable/CANNOT_EXECUTE_THIS_EPOCH'); | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|             // Proposal 0 is executable here | ||||
|             await fastForwardToNextEpochAsync(); | ||||
|   | ||||
| @@ -1,4 +1,14 @@ | ||||
| [ | ||||
|     { | ||||
|         "version": "4.8.0", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Added FundRecoveryFeature to the 0x EP", | ||||
|                 "pr": 306 | ||||
|             } | ||||
|         ], | ||||
|         "timestamp": 1631120757 | ||||
|     }, | ||||
|     { | ||||
|         "timestamp": 1630459879, | ||||
|         "version": "4.7.18", | ||||
|   | ||||
| @@ -5,6 +5,10 @@ Edit the package's CHANGELOG.json file only. | ||||
|  | ||||
| CHANGELOG | ||||
|  | ||||
| ## v4.8.0 - _September 8, 2021_ | ||||
|  | ||||
|     * Added FundRecoveryFeature to the 0x EP (#306) | ||||
|  | ||||
| ## v4.7.18 - _September 1, 2021_ | ||||
|  | ||||
|     * Dependencies updated | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@0x/contracts-utils", | ||||
|     "version": "4.7.18", | ||||
|     "version": "4.8.0", | ||||
|     "engines": { | ||||
|         "node": ">=6.12" | ||||
|     }, | ||||
|   | ||||
| @@ -1,4 +1,13 @@ | ||||
| [ | ||||
|     { | ||||
|         "timestamp": 1631120757, | ||||
|         "version": "0.28.4", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Dependencies updated" | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "timestamp": 1630459879, | ||||
|         "version": "0.28.3", | ||||
|   | ||||
| @@ -5,6 +5,10 @@ Edit the package's CHANGELOG.json file only. | ||||
|  | ||||
| CHANGELOG | ||||
|  | ||||
| ## v0.28.4 - _September 8, 2021_ | ||||
|  | ||||
|     * Dependencies updated | ||||
|  | ||||
| ## v0.28.3 - _September 1, 2021_ | ||||
|  | ||||
|     * Dependencies updated | ||||
|   | ||||
| @@ -33,6 +33,7 @@ import "./features/interfaces/INativeOrdersFeature.sol"; | ||||
| import "./features/interfaces/IBatchFillNativeOrdersFeature.sol"; | ||||
| import "./features/interfaces/IMultiplexFeature.sol"; | ||||
| import "./features/interfaces/IOtcOrdersFeature.sol"; | ||||
| import "./features/interfaces/IFundRecoveryFeature.sol"; | ||||
|  | ||||
|  | ||||
| /// @dev Interface for a fully featured Exchange Proxy. | ||||
| @@ -48,7 +49,8 @@ interface IZeroEx is | ||||
|     INativeOrdersFeature, | ||||
|     IBatchFillNativeOrdersFeature, | ||||
|     IMultiplexFeature, | ||||
|     IOtcOrdersFeature | ||||
|     IOtcOrdersFeature, | ||||
|     IFundRecoveryFeature | ||||
| { | ||||
|     // solhint-disable state-visibility | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,66 @@ | ||||
| // SPDX-License-Identifier: Apache-2.0 | ||||
| /* | ||||
|   Copyright 2021 ZeroEx Intl. | ||||
|   Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|   you may not use this file except in compliance with the License. | ||||
|   You may obtain a copy of the License at | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|   Unless required by applicable law or agreed to in writing, software | ||||
|   distributed under the License is distributed on an "AS IS" BASIS, | ||||
|   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|   See the License for the specific language governing permissions and | ||||
|   limitations under the License. | ||||
| */ | ||||
|  | ||||
| pragma solidity ^0.6.5; | ||||
| pragma experimental ABIEncoderV2; | ||||
|  | ||||
| import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; | ||||
| import "../migrations/LibMigrate.sol"; | ||||
| import "../fixins/FixinCommon.sol"; | ||||
| import "./interfaces/IFeature.sol"; | ||||
| import "./interfaces/IFundRecoveryFeature.sol"; | ||||
| import "../transformers/LibERC20Transformer.sol"; | ||||
|  | ||||
| contract FundRecoveryFeature is | ||||
|     IFeature, | ||||
|     IFundRecoveryFeature, | ||||
|     FixinCommon | ||||
| { | ||||
|     /// @dev Name of this feature. | ||||
|     string public constant override FEATURE_NAME = "FundRecoveryFeature"; | ||||
|     /// @dev Version of this feature. | ||||
|     uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); | ||||
|  | ||||
|     /// @dev Initialize and register this feature. | ||||
|     ///      Should be delegatecalled by `Migrate.migrate()`. | ||||
|     /// @return success `LibMigrate.SUCCESS` on success. | ||||
|     function migrate() | ||||
|         external | ||||
|         returns (bytes4 success) | ||||
|     { | ||||
|         _registerFeatureFunction(this.transferTrappedTokensTo.selector); | ||||
|         return LibMigrate.MIGRATE_SUCCESS; | ||||
|     } | ||||
|  | ||||
|     /// @dev Recovers ERC20 tokens or ETH from the 0x Exchange Proxy contract | ||||
|     /// @param erc20 ERC20 Token Address. (You can also pass in `0xeeeee...` to indicate ETH) | ||||
|     /// @param amountOut Amount of tokens to withdraw. | ||||
|     /// @param recipientWallet Recipient wallet address. | ||||
|     function transferTrappedTokensTo( | ||||
|         IERC20TokenV06 erc20, | ||||
|         uint256 amountOut, | ||||
|         address payable recipientWallet | ||||
|     ) | ||||
|         external | ||||
|         override | ||||
|         onlyOwner | ||||
|     { | ||||
|         if(amountOut == uint256(-1)) { | ||||
|             amountOut = LibERC20Transformer.getTokenBalanceOf(erc20, address(this)); | ||||
|         } | ||||
|         LibERC20Transformer.transformerTransfer(erc20, recipientWallet, amountOut); | ||||
|     } | ||||
|  | ||||
|      | ||||
| } | ||||
| @@ -0,0 +1,35 @@ | ||||
| // SPDX-License-Identifier: Apache-2.0 | ||||
| /* | ||||
|   Copyright 2020 ZeroEx Intl. | ||||
|   Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|   you may not use this file except in compliance with the License. | ||||
|   You may obtain a copy of the License at | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|   Unless required by applicable law or agreed to in writing, software | ||||
|   distributed under the License is distributed on an "AS IS" BASIS, | ||||
|   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|   See the License for the specific language governing permissions and | ||||
|   limitations under the License. | ||||
| */ | ||||
|  | ||||
| pragma solidity ^0.6.5; | ||||
| pragma experimental ABIEncoderV2; | ||||
|  | ||||
| import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; | ||||
|  | ||||
|  | ||||
| /// @dev Exchange Proxy Recovery Functions | ||||
| interface IFundRecoveryFeature { | ||||
|      | ||||
|     /// @dev calledFrom FundRecoveryFeature.transferTrappedTokensTo() This will be delegatecalled | ||||
|     /// in the context of the Exchange Proxy instance being used. | ||||
|     /// @param erc20 ERC20 Token Address. | ||||
|     /// @param amountOut Amount of tokens to withdraw. | ||||
|     /// @param recipientWallet Recipient wallet address. | ||||
|    function transferTrappedTokensTo( | ||||
|         IERC20TokenV06 erc20, | ||||
|         uint256 amountOut, | ||||
|         address payable recipientWallet | ||||
|     ) | ||||
|         external; | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@0x/contracts-zero-ex", | ||||
|     "version": "0.28.3", | ||||
|     "version": "0.28.4", | ||||
|     "engines": { | ||||
|         "node": ">=6.12" | ||||
|     }, | ||||
| @@ -43,7 +43,7 @@ | ||||
|     "config": { | ||||
|         "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider,BatchFillNativeOrdersFeature,IBatchFillNativeOrdersFeature,MultiplexFeature,IMultiplexFeature,OtcOrdersFeature,IOtcOrdersFeature", | ||||
|         "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", | ||||
|         "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeProtocols|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOtcOrdersFeature|IOwnableFeature|IPancakeSwapFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IUniswapV3Feature|IUniswapV3Pool|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOtcOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBalancerV2|MixinBancor|MixinClipper|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinCurveV2|MixinDodo|MixinDodoV2|MixinKyber|MixinKyberDmm|MixinLido|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinUniswapV3|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|MultiplexLiquidityProvider|MultiplexOtc|MultiplexRfq|MultiplexTransformERC20|MultiplexUniswapV2|MultiplexUniswapV3|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OtcOrdersFeature|OwnableFeature|PancakeSwapFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestMooniswap|TestNativeOrdersFeature|TestNoEthRecipient|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestUniswapV2Factory|TestUniswapV2Pool|TestUniswapV3Factory|TestUniswapV3Feature|TestUniswapV3Pool|TestWeth|TestWethTransformerHost|TestZeroExFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|UniswapV3Feature|WethTransformer|ZeroEx|ZeroExOptimized).json" | ||||
|         "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeProtocols|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|FundRecoveryFeature|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|IFundRecoveryFeature|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOtcOrdersFeature|IOwnableFeature|IPancakeSwapFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IUniswapV3Feature|IUniswapV3Pool|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOtcOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBalancerV2|MixinBancor|MixinClipper|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinCurveV2|MixinDodo|MixinDodoV2|MixinKyber|MixinKyberDmm|MixinLido|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinUniswapV3|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|MultiplexLiquidityProvider|MultiplexOtc|MultiplexRfq|MultiplexTransformERC20|MultiplexUniswapV2|MultiplexUniswapV3|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OtcOrdersFeature|OwnableFeature|PancakeSwapFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestMooniswap|TestNativeOrdersFeature|TestNoEthRecipient|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestUniswapV2Factory|TestUniswapV2Pool|TestUniswapV3Factory|TestUniswapV3Feature|TestUniswapV3Pool|TestWeth|TestWethTransformerHost|TestZeroExFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|UniswapV3Feature|WethTransformer|ZeroEx|ZeroExOptimized).json" | ||||
|     }, | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
| @@ -57,7 +57,7 @@ | ||||
|     "devDependencies": { | ||||
|         "@0x/abi-gen": "^5.6.0", | ||||
|         "@0x/contract-addresses": "^6.7.0", | ||||
|         "@0x/contracts-erc20": "^3.3.18", | ||||
|         "@0x/contracts-erc20": "^3.3.19", | ||||
|         "@0x/contracts-gen": "^2.0.38", | ||||
|         "@0x/contracts-test-utils": "^5.4.10", | ||||
|         "@0x/dev-utils": "^4.2.7", | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import * as FixinReentrancyGuard from '../test/generated-artifacts/FixinReentran | ||||
| import * as FixinTokenSpender from '../test/generated-artifacts/FixinTokenSpender.json'; | ||||
| import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json'; | ||||
| import * as FullMigration from '../test/generated-artifacts/FullMigration.json'; | ||||
| import * as FundRecoveryFeature from '../test/generated-artifacts/FundRecoveryFeature.json'; | ||||
| import * as IBatchFillNativeOrdersFeature from '../test/generated-artifacts/IBatchFillNativeOrdersFeature.json'; | ||||
| import * as IBootstrapFeature from '../test/generated-artifacts/IBootstrapFeature.json'; | ||||
| import * as IBridgeAdapter from '../test/generated-artifacts/IBridgeAdapter.json'; | ||||
| @@ -28,6 +29,7 @@ import * as IERC20Bridge from '../test/generated-artifacts/IERC20Bridge.json'; | ||||
| import * as IERC20Transformer from '../test/generated-artifacts/IERC20Transformer.json'; | ||||
| import * as IFeature from '../test/generated-artifacts/IFeature.json'; | ||||
| import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json'; | ||||
| import * as IFundRecoveryFeature from '../test/generated-artifacts/IFundRecoveryFeature.json'; | ||||
| import * as ILiquidityProvider from '../test/generated-artifacts/ILiquidityProvider.json'; | ||||
| import * as ILiquidityProviderFeature from '../test/generated-artifacts/ILiquidityProviderFeature.json'; | ||||
| import * as ILiquidityProviderSandbox from '../test/generated-artifacts/ILiquidityProviderSandbox.json'; | ||||
| @@ -198,6 +200,7 @@ export const artifacts = { | ||||
|     TransformerDeployer: TransformerDeployer as ContractArtifact, | ||||
|     BatchFillNativeOrdersFeature: BatchFillNativeOrdersFeature as ContractArtifact, | ||||
|     BootstrapFeature: BootstrapFeature as ContractArtifact, | ||||
|     FundRecoveryFeature: FundRecoveryFeature as ContractArtifact, | ||||
|     LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact, | ||||
|     MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, | ||||
|     NativeOrdersFeature: NativeOrdersFeature as ContractArtifact, | ||||
| @@ -211,6 +214,7 @@ export const artifacts = { | ||||
|     IBatchFillNativeOrdersFeature: IBatchFillNativeOrdersFeature as ContractArtifact, | ||||
|     IBootstrapFeature: IBootstrapFeature as ContractArtifact, | ||||
|     IFeature: IFeature as ContractArtifact, | ||||
|     IFundRecoveryFeature: IFundRecoveryFeature as ContractArtifact, | ||||
|     ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact, | ||||
|     IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact, | ||||
|     IMultiplexFeature: IMultiplexFeature as ContractArtifact, | ||||
|   | ||||
							
								
								
									
										96
									
								
								contracts/zero-ex/test/features/fund_recovery_tests.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								contracts/zero-ex/test/features/fund_recovery_tests.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| import { blockchainTests, constants, expect, randomAddress } from '@0x/contracts-test-utils'; | ||||
| import { BigNumber, OwnableRevertErrors } from '@0x/utils'; | ||||
| import { Web3Wrapper } from '@0x/web3-wrapper'; | ||||
|  | ||||
| import { IOwnableFeatureContract, IZeroExContract } from '../../src/wrappers'; | ||||
| import { artifacts } from '../artifacts'; | ||||
| import { FundRecoveryFeatureContract } from '../generated-wrappers/fund_recovery_feature'; | ||||
| import { abis } from '../utils/abis'; | ||||
| import { fullMigrateAsync } from '../utils/migration'; | ||||
| import { TestMintableERC20TokenContract } from '../wrappers'; | ||||
|  | ||||
| blockchainTests('FundRecovery', async env => { | ||||
|     let owner: string; | ||||
|     let zeroEx: IZeroExContract; | ||||
|     let token: TestMintableERC20TokenContract; | ||||
|     before(async () => { | ||||
|         const INITIAL_ERC20_BALANCE = Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18); | ||||
|         [owner] = await env.getAccountAddressesAsync(); | ||||
|         zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {}); | ||||
|         token = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync( | ||||
|             artifacts.TestMintableERC20Token, | ||||
|             env.provider, | ||||
|             env.txDefaults, | ||||
|             {}, | ||||
|         ); | ||||
|         await token.mint(zeroEx.address, INITIAL_ERC20_BALANCE).awaitTransactionSuccessAsync(); | ||||
|         const featureImpl = await FundRecoveryFeatureContract.deployFrom0xArtifactAsync( | ||||
|             artifacts.FundRecoveryFeature, | ||||
|             env.provider, | ||||
|             env.txDefaults, | ||||
|             artifacts, | ||||
|         ); | ||||
|         await new IOwnableFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis) | ||||
|             .migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner) | ||||
|             .awaitTransactionSuccessAsync({ from: owner }); | ||||
|     }); | ||||
|     blockchainTests.resets('Should delegatecall `transferTrappedTokensTo` from the exchange proxy', () => { | ||||
|         const ETH_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; | ||||
|         const recipientAddress = randomAddress(); | ||||
|         it('Tranfers an arbitrary ERC-20 Token', async () => { | ||||
|             const amountOut = Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18); | ||||
|             await zeroEx | ||||
|                 .transferTrappedTokensTo(token.address, amountOut, recipientAddress) | ||||
|                 .awaitTransactionSuccessAsync({ from: owner }); | ||||
|             const recipientAddressBalanceAferTransfer = await token.balanceOf(recipientAddress).callAsync(); | ||||
|             return expect(recipientAddressBalanceAferTransfer).to.bignumber.equal(amountOut); | ||||
|         }); | ||||
|         it('Amount -1 transfers entire balance of ERC-20', async () => { | ||||
|             const balanceOwner = await token.balanceOf(zeroEx.address).callAsync(); | ||||
|             await zeroEx | ||||
|                 .transferTrappedTokensTo(token.address, constants.MAX_UINT256, recipientAddress) | ||||
|                 .awaitTransactionSuccessAsync({ from: owner }); | ||||
|             const recipientAddressBalanceAferTransfer = await token.balanceOf(recipientAddress).callAsync(); | ||||
|             return expect(recipientAddressBalanceAferTransfer).to.bignumber.equal(balanceOwner); | ||||
|         }); | ||||
|         it('Amount -1 transfers entire balance of ETH', async () => { | ||||
|             const amountOut = new BigNumber(20); | ||||
|             await env.web3Wrapper.awaitTransactionMinedAsync( | ||||
|                 await env.web3Wrapper.sendTransactionAsync({ | ||||
|                     from: owner, | ||||
|                     to: zeroEx.address, | ||||
|                     value: amountOut, | ||||
|                 }), | ||||
|             ); | ||||
|             const balanceOwner = await env.web3Wrapper.getBalanceInWeiAsync(zeroEx.address); | ||||
|             await zeroEx | ||||
|                 .transferTrappedTokensTo(ETH_TOKEN_ADDRESS, constants.MAX_UINT256, recipientAddress) | ||||
|                 .awaitTransactionSuccessAsync({ from: owner }); | ||||
|             const recipientAddressBalanceAferTransfer = await env.web3Wrapper.getBalanceInWeiAsync(recipientAddress); | ||||
|             return expect(recipientAddressBalanceAferTransfer).to.bignumber.equal(balanceOwner); | ||||
|         }); | ||||
|         it('Transfers ETH ', async () => { | ||||
|             const amountOut = new BigNumber(20); | ||||
|             await env.web3Wrapper.awaitTransactionMinedAsync( | ||||
|                 await env.web3Wrapper.sendTransactionAsync({ | ||||
|                     from: owner, | ||||
|                     to: zeroEx.address, | ||||
|                     value: amountOut, | ||||
|                 }), | ||||
|             ); | ||||
|             await zeroEx | ||||
|                 .transferTrappedTokensTo(ETH_TOKEN_ADDRESS, amountOut.minus(1), recipientAddress) | ||||
|                 .awaitTransactionSuccessAsync({ from: owner }); | ||||
|             const recipientAddressBalance = await env.web3Wrapper.getBalanceInWeiAsync(recipientAddress); | ||||
|             return expect(recipientAddressBalance).to.bignumber.be.equal(amountOut.minus(1)); | ||||
|         }); | ||||
|         it('Feature `transferTrappedTokensTo` can only be called by owner', async () => { | ||||
|             const notOwner = randomAddress(); | ||||
|             return expect( | ||||
|                 zeroEx | ||||
|                     .transferTrappedTokensTo(ETH_TOKEN_ADDRESS, constants.MAX_UINT256, recipientAddress) | ||||
|                     .awaitTransactionSuccessAsync({ from: notOwner }), | ||||
|             ).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner, owner)); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -19,6 +19,7 @@ export * from '../test/generated-wrappers/fixin_reentrancy_guard'; | ||||
| export * from '../test/generated-wrappers/fixin_token_spender'; | ||||
| export * from '../test/generated-wrappers/flash_wallet'; | ||||
| export * from '../test/generated-wrappers/full_migration'; | ||||
| export * from '../test/generated-wrappers/fund_recovery_feature'; | ||||
| export * from '../test/generated-wrappers/i_batch_fill_native_orders_feature'; | ||||
| export * from '../test/generated-wrappers/i_bootstrap_feature'; | ||||
| export * from '../test/generated-wrappers/i_bridge_adapter'; | ||||
| @@ -26,6 +27,7 @@ export * from '../test/generated-wrappers/i_erc20_bridge'; | ||||
| export * from '../test/generated-wrappers/i_erc20_transformer'; | ||||
| export * from '../test/generated-wrappers/i_feature'; | ||||
| export * from '../test/generated-wrappers/i_flash_wallet'; | ||||
| export * from '../test/generated-wrappers/i_fund_recovery_feature'; | ||||
| export * from '../test/generated-wrappers/i_liquidity_provider'; | ||||
| export * from '../test/generated-wrappers/i_liquidity_provider_feature'; | ||||
| export * from '../test/generated-wrappers/i_liquidity_provider_sandbox'; | ||||
|   | ||||
| @@ -52,6 +52,7 @@ | ||||
|         "test/generated-artifacts/FixinTokenSpender.json", | ||||
|         "test/generated-artifacts/FlashWallet.json", | ||||
|         "test/generated-artifacts/FullMigration.json", | ||||
|         "test/generated-artifacts/FundRecoveryFeature.json", | ||||
|         "test/generated-artifacts/IBatchFillNativeOrdersFeature.json", | ||||
|         "test/generated-artifacts/IBootstrapFeature.json", | ||||
|         "test/generated-artifacts/IBridgeAdapter.json", | ||||
| @@ -59,6 +60,7 @@ | ||||
|         "test/generated-artifacts/IERC20Transformer.json", | ||||
|         "test/generated-artifacts/IFeature.json", | ||||
|         "test/generated-artifacts/IFlashWallet.json", | ||||
|         "test/generated-artifacts/IFundRecoveryFeature.json", | ||||
|         "test/generated-artifacts/ILiquidityProvider.json", | ||||
|         "test/generated-artifacts/ILiquidityProviderFeature.json", | ||||
|         "test/generated-artifacts/ILiquidityProviderSandbox.json", | ||||
|   | ||||
| @@ -51,7 +51,7 @@ | ||||
|         "verdaccio": "docker run --rm -i -p 4873:4873 0xorg/verdaccio" | ||||
|     }, | ||||
|     "config": { | ||||
|         "contractsPackages": "@0x/contracts-erc20  @0x/contracts-test-utils @0x/contracts-utils @0x/contracts-zero-ex @0x/contracts-treasury", | ||||
|         "contractsPackages": "@0x/contracts-erc20 @0x/contracts-test-utils @0x/contracts-utils @0x/contracts-zero-ex @0x/contracts-treasury", | ||||
|         "nonContractPackages": "@0x/migrations @0x/contract-wrappers @0x/contract-addresses @0x/contract-artifacts @0x/contract-wrappers-test @0x/asset-swapper", | ||||
|         "ignoreTestsForPackages": "", | ||||
|         "mnemonic": "concert load couple harbor equip island argue ramp clarify fence smart topic", | ||||
|   | ||||
| @@ -1,4 +1,32 @@ | ||||
| [ | ||||
|     { | ||||
|         "timestamp": 1631646242, | ||||
|         "version": "16.27.3", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Dependencies updated" | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "timestamp": 1631639620, | ||||
|         "version": "16.27.2", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Dependencies updated" | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "version": "16.27.1", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Fix ApproximateBuys sampler to terminate if the buy amount is not met", | ||||
|                 "pr": 319 | ||||
|             } | ||||
|         ], | ||||
|         "timestamp": 1631120757 | ||||
|     }, | ||||
|     { | ||||
|         "version": "16.27.0", | ||||
|         "changes": [ | ||||
|   | ||||
| @@ -5,6 +5,18 @@ Edit the package's CHANGELOG.json file only. | ||||
|  | ||||
| CHANGELOG | ||||
|  | ||||
| ## v16.27.3 - _September 14, 2021_ | ||||
|  | ||||
|     * Dependencies updated | ||||
|  | ||||
| ## v16.27.2 - _September 14, 2021_ | ||||
|  | ||||
|     * Dependencies updated | ||||
|  | ||||
| ## v16.27.1 - _September 8, 2021_ | ||||
|  | ||||
|     * Fix ApproximateBuys sampler to terminate if the buy amount is not met (#319) | ||||
|  | ||||
| ## v16.27.0 - _September 1, 2021_ | ||||
|  | ||||
|     * Avalanche deployment (#312) | ||||
|   | ||||
| @@ -77,6 +77,7 @@ contract ApproximateBuys { | ||||
|         } | ||||
|  | ||||
|         for (uint256 i = 0; i < makerTokenAmounts.length; i++) { | ||||
|             uint256 eps = 0; | ||||
|             for (uint256 iter = 0; iter < APPROXIMATE_BUY_MAX_ITERATIONS; iter++) { | ||||
|                 // adjustedSellAmount = previousSellAmount * (target/actual) * JUMP_MULTIPLIER | ||||
|                 sellAmount = _safeGetPartialAmountCeil( | ||||
| @@ -108,7 +109,7 @@ contract ApproximateBuys { | ||||
|                 buyAmount = _buyAmount; | ||||
|                 // If we've reached our goal, exit early | ||||
|                 if (buyAmount >= makerTokenAmounts[i]) { | ||||
|                     uint256 eps = | ||||
|                     eps = | ||||
|                         (buyAmount - makerTokenAmounts[i]) * ONE_HUNDED_PERCENT_BPS / | ||||
|                         makerTokenAmounts[i]; | ||||
|                     if (eps <= APPROXIMATE_BUY_TARGET_EPSILON_BPS) { | ||||
| @@ -116,6 +117,9 @@ contract ApproximateBuys { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             if (eps == 0 || eps > APPROXIMATE_BUY_TARGET_EPSILON_BPS) { | ||||
|                 break; | ||||
|             } | ||||
|             // We do our best to close in on the requested amount, but we can either over buy or under buy and exit | ||||
|             // if we hit a max iteration limit | ||||
|             // We scale the sell amount to get the approximate target | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@0x/asset-swapper", | ||||
|     "version": "16.27.0", | ||||
|     "version": "16.27.3", | ||||
|     "engines": { | ||||
|         "node": ">=6.12" | ||||
|     }, | ||||
| @@ -62,7 +62,7 @@ | ||||
|         "@0x/base-contract": "^6.4.0", | ||||
|         "@0x/contract-addresses": "^6.7.0", | ||||
|         "@0x/contract-wrappers": "^13.17.6", | ||||
|         "@0x/contracts-erc20": "^3.3.18", | ||||
|         "@0x/contracts-erc20": "^3.3.19", | ||||
|         "@0x/contracts-zero-ex": "^0.27.0", | ||||
|         "@0x/dev-utils": "^4.2.7", | ||||
|         "@0x/json-schemas": "^6.1.3", | ||||
| @@ -98,9 +98,9 @@ | ||||
|         "@0x/contracts-exchange-libs": "^4.3.37", | ||||
|         "@0x/contracts-gen": "^2.0.38", | ||||
|         "@0x/contracts-test-utils": "^5.4.10", | ||||
|         "@0x/contracts-utils": "^4.7.18", | ||||
|         "@0x/contracts-utils": "^4.8.0", | ||||
|         "@0x/mesh-rpc-client": "^9.4.2", | ||||
|         "@0x/migrations": "^8.1.4", | ||||
|         "@0x/migrations": "^8.1.5", | ||||
|         "@0x/sol-compiler": "^4.7.3", | ||||
|         "@0x/subproviders": "^6.5.3", | ||||
|         "@0x/ts-doc-gen": "^0.0.28", | ||||
|   | ||||
| @@ -50,7 +50,7 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { | ||||
|     samplerGasLimit: 500e6, | ||||
|     ethGasStationUrl: ETH_GAS_STATION_API_URL, | ||||
|     rfqt: { | ||||
|         takerApiKeyWhitelist: [], | ||||
|         integratorsWhitelist: [], | ||||
|         makerAssetOfferings: {}, | ||||
|         txOriginBlacklist: new Set(), | ||||
|     }, | ||||
|   | ||||
| @@ -88,6 +88,7 @@ export { | ||||
|     ExchangeProxyContractOpts, | ||||
|     ExchangeProxyRefundReceiver, | ||||
|     GetExtensionContractTypeOpts, | ||||
|     Integrator, | ||||
|     LogFunction, | ||||
|     MarketBuySwapQuote, | ||||
|     MarketOperation, | ||||
|   | ||||
| @@ -75,6 +75,7 @@ export class SwapQuoter { | ||||
|     private readonly _marketOperationUtils: MarketOperationUtils; | ||||
|     private readonly _rfqtOptions?: SwapQuoterRfqOpts; | ||||
|     private readonly _quoteRequestorHttpClient: AxiosInstance; | ||||
|     private readonly _integratorIdsSet: Set<string>; | ||||
|  | ||||
|     /** | ||||
|      * Instantiates a new SwapQuoter instance | ||||
| @@ -164,6 +165,9 @@ export class SwapQuoter { | ||||
|             httpsAgent: new HttpsAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }), | ||||
|             ...(rfqt ? rfqt.axiosInstanceOpts : {}), | ||||
|         }); | ||||
|  | ||||
|         const integratorIds = this._rfqtOptions?.integratorsWhitelist.map(integrator => integrator.integratorId) || []; | ||||
|         this._integratorIdsSet = new Set(integratorIds); | ||||
|     } | ||||
|  | ||||
|     public async getBatchMarketBuySwapQuoteAsync( | ||||
| @@ -414,12 +418,11 @@ export class SwapQuoter { | ||||
|         return isOpenOrder && !willOrderExpire && isFeeTypeAllowed; | ||||
|     }; // tslint:disable-line:semicolon | ||||
|  | ||||
|     private _isApiKeyWhitelisted(apiKey: string | undefined): boolean { | ||||
|         if (!apiKey) { | ||||
|     private _isIntegratorIdWhitelisted(integratorId: string | undefined): boolean { | ||||
|         if (!integratorId) { | ||||
|             return false; | ||||
|         } | ||||
|         const whitelistedApiKeys = this._rfqtOptions ? this._rfqtOptions.takerApiKeyWhitelist : []; | ||||
|         return whitelistedApiKeys.includes(apiKey); | ||||
|         return this._integratorIdsSet.has(integratorId); | ||||
|     } | ||||
|  | ||||
|     private _isTxOriginBlacklisted(txOrigin: string | undefined): boolean { | ||||
| @@ -438,19 +441,19 @@ export class SwapQuoter { | ||||
|             return rfqt; | ||||
|         } | ||||
|         // tslint:disable-next-line: boolean-naming | ||||
|         const { apiKey, nativeExclusivelyRFQ, intentOnFilling, txOrigin } = rfqt; | ||||
|         const { integrator, nativeExclusivelyRFQ, intentOnFilling, txOrigin } = rfqt; | ||||
|         // If RFQ-T is enabled and `nativeExclusivelyRFQ` is set, then `ERC20BridgeSource.Native` should | ||||
|         // never be excluded. | ||||
|         if (nativeExclusivelyRFQ === true && !sourceFilters.isAllowed(ERC20BridgeSource.Native)) { | ||||
|             throw new Error('Native liquidity cannot be excluded if "rfqt.nativeExclusivelyRFQ" is set'); | ||||
|         } | ||||
|  | ||||
|         // If an API key was provided, but the key is not whitelisted, raise a warning and disable RFQ | ||||
|         if (!this._isApiKeyWhitelisted(apiKey)) { | ||||
|         // If an integrator ID was provided, but the ID is not whitelisted, raise a warning and disable RFQ | ||||
|         if (!this._isIntegratorIdWhitelisted(integrator.integratorId)) { | ||||
|             if (this._rfqtOptions && this._rfqtOptions.warningLogger) { | ||||
|                 this._rfqtOptions.warningLogger( | ||||
|                     { | ||||
|                         apiKey, | ||||
|                         ...integrator, | ||||
|                     }, | ||||
|                     'Attempt at using an RFQ API key that is not whitelisted. Disabling RFQ for the request lifetime.', | ||||
|                 ); | ||||
| @@ -474,7 +477,7 @@ export class SwapQuoter { | ||||
|         // Otherwise check other RFQ options | ||||
|         if ( | ||||
|             intentOnFilling && // The requestor is asking for a firm quote | ||||
|             this._isApiKeyWhitelisted(apiKey) && // A valid API key was provided | ||||
|             this._isIntegratorIdWhitelisted(integrator.integratorId) && // A valid API key was provided | ||||
|             sourceFilters.isAllowed(ERC20BridgeSource.Native) // Native liquidity is not excluded | ||||
|         ) { | ||||
|             if (!txOrigin || txOrigin === constants.NULL_ADDRESS) { | ||||
|   | ||||
| @@ -243,7 +243,7 @@ export interface RfqmRequestOptions extends RfqRequestOpts { | ||||
| export interface RfqRequestOpts { | ||||
|     takerAddress: string; | ||||
|     txOrigin: string; | ||||
|     apiKey: string; | ||||
|     integrator: Integrator; | ||||
|     intentOnFilling: boolean; | ||||
|     isIndicative?: boolean; | ||||
|     makerEndpointMaxResponseTimeMs?: number; | ||||
| @@ -293,8 +293,14 @@ export interface RfqFirmQuoteValidator { | ||||
|     getRfqtTakerFillableAmountsAsync(quotes: RfqOrder[]): Promise<BigNumber[]>; | ||||
| } | ||||
|  | ||||
| export interface Integrator { | ||||
|     integratorId: string; | ||||
|     label: string; | ||||
|     whitelistIntegratorUrls?: string[]; | ||||
| } | ||||
|  | ||||
| export interface SwapQuoterRfqOpts { | ||||
|     takerApiKeyWhitelist: string[]; | ||||
|     integratorsWhitelist: Integrator[]; | ||||
|     makerAssetOfferings: RfqMakerAssetOfferings; | ||||
|     txOriginBlacklist: Set<string>; | ||||
|     altRfqCreds?: { | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import { constants } from '../constants'; | ||||
| import { | ||||
|     AltQuoteModel, | ||||
|     AltRfqMakerAssetOfferings, | ||||
|     Integrator, | ||||
|     LogFunction, | ||||
|     MarketOperation, | ||||
|     RfqMakerAssetOfferings, | ||||
| @@ -61,6 +62,31 @@ export interface MetricsProxy { | ||||
|      * @param expirationTimeSeconds the expiration time in seconds | ||||
|      */ | ||||
|     incrementFillRatioWarningCounter(isLastLook: boolean, maker: string): void; | ||||
|  | ||||
|     /** | ||||
|      * Logs the outcome of a network (HTTP) interaction with a market maker. | ||||
|      * | ||||
|      * @param interaction.isLastLook true if the request is RFQM | ||||
|      * @param interaction.integrator the integrator that is requesting the RFQ quote | ||||
|      * @param interaction.url the URL of the market maker | ||||
|      * @param interaction.quoteType indicative or firm quote | ||||
|      * @param interaction.statusCode the statusCode returned by a market maker | ||||
|      * @param interaction.latencyMs the latency of the HTTP request (in ms) | ||||
|      * @param interaction.included if a firm quote that was returned got included in the next step of processing. | ||||
|      *                             NOTE: this does not mean that the request returned a valid fillable order. It just | ||||
|      *                             means that the network response was successful. | ||||
|      */ | ||||
|     logRfqMakerNetworkInteraction(interaction: { | ||||
|         isLastLook: boolean; | ||||
|         integrator: Integrator; | ||||
|         url: string; | ||||
|         quoteType: 'firm' | 'indicative'; | ||||
|         statusCode: number | undefined; | ||||
|         latencyMs: number; | ||||
|         included: boolean; | ||||
|         sellTokenAddress: string; | ||||
|         buyTokenAddress: string; | ||||
|     }): void; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -178,6 +204,42 @@ export class QuoteRequestor { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets both standard RFQ makers and "alternative" RFQ makers and combines them together | ||||
|      * in a single configuration map. If an integration key whitelist is present, it will be used | ||||
|      * to filter a specific makers. | ||||
|      * | ||||
|      * @param options the RfqmRequestOptions passed in | ||||
|      * @param assetOfferings the RFQM or RFQT maker offerings | ||||
|      * @returns a list of TypedMakerUrl instances | ||||
|      */ | ||||
|     public static getTypedMakerUrlsAndWhitelist( | ||||
|         options: Pick<RfqmRequestOptions, 'integrator' | 'altRfqAssetOfferings'>, | ||||
|         assetOfferings: RfqMakerAssetOfferings, | ||||
|     ): TypedMakerUrl[] { | ||||
|         const standardUrls = Object.keys(assetOfferings).map( | ||||
|             (mm: string): TypedMakerUrl => { | ||||
|                 return { pairType: RfqPairType.Standard, url: mm }; | ||||
|             }, | ||||
|         ); | ||||
|         const altUrls = options.altRfqAssetOfferings | ||||
|             ? Object.keys(options.altRfqAssetOfferings).map( | ||||
|                   (mm: string): TypedMakerUrl => { | ||||
|                       return { pairType: RfqPairType.Alt, url: mm }; | ||||
|                   }, | ||||
|               ) | ||||
|             : []; | ||||
|  | ||||
|         let typedMakerUrls = standardUrls.concat(altUrls); | ||||
|  | ||||
|         // If there is a whitelist, only allow approved maker URLs | ||||
|         if (options.integrator.whitelistIntegratorUrls !== undefined) { | ||||
|             const whitelist = new Set(options.integrator.whitelistIntegratorUrls.map(key => key.toLowerCase())); | ||||
|             typedMakerUrls = typedMakerUrls.filter(makerUrl => whitelist.has(makerUrl.url.toLowerCase())); | ||||
|         } | ||||
|         return typedMakerUrls; | ||||
|     } | ||||
|  | ||||
|     public static getDurationUntilExpirationMs(expirationTimeSeconds: BigNumber): BigNumber { | ||||
|         const expirationTimeMs = expirationTimeSeconds.times(constants.ONE_SECOND_MS); | ||||
|         const currentTimeMs = new BigNumber(Date.now()); | ||||
| @@ -401,21 +463,6 @@ export class QuoteRequestor { | ||||
|             } | ||||
|         })(); | ||||
|  | ||||
|         const standardUrls = Object.keys(assetOfferings).map( | ||||
|             (mm: string): TypedMakerUrl => { | ||||
|                 return { pairType: RfqPairType.Standard, url: mm }; | ||||
|             }, | ||||
|         ); | ||||
|         const altUrls = options.altRfqAssetOfferings | ||||
|             ? Object.keys(options.altRfqAssetOfferings).map( | ||||
|                   (mm: string): TypedMakerUrl => { | ||||
|                       return { pairType: RfqPairType.Alt, url: mm }; | ||||
|                   }, | ||||
|               ) | ||||
|             : []; | ||||
|  | ||||
|         const typedMakerUrls = standardUrls.concat(altUrls); | ||||
|  | ||||
|         const timeoutMs = | ||||
|             options.makerEndpointMaxResponseTimeMs || | ||||
|             constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs!; | ||||
| @@ -427,11 +474,25 @@ export class QuoteRequestor { | ||||
|             cancelTokenSource.cancel('timeout via cancel token'); | ||||
|         }, timeoutMs + bufferMs); | ||||
|  | ||||
|         const typedMakerUrls = QuoteRequestor.getTypedMakerUrlsAndWhitelist(options, assetOfferings); | ||||
|         const quotePromises = typedMakerUrls.map(async typedMakerUrl => { | ||||
|             // filter out requests to skip | ||||
|             const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(typedMakerUrl.url); | ||||
|             const partialLogEntry = { url: typedMakerUrl.url, quoteType, requestParams, isBlacklisted }; | ||||
|             const { isLastLook, integrator } = options; | ||||
|             const { sellTokenAddress, buyTokenAddress } = requestParams; | ||||
|             if (isBlacklisted) { | ||||
|                 this._metrics?.logRfqMakerNetworkInteraction({ | ||||
|                     isLastLook: false, | ||||
|                     url: typedMakerUrl.url, | ||||
|                     quoteType, | ||||
|                     statusCode: undefined, | ||||
|                     sellTokenAddress, | ||||
|                     buyTokenAddress, | ||||
|                     latencyMs: 0, | ||||
|                     included: false, | ||||
|                     integrator, | ||||
|                 }); | ||||
|                 this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } }); | ||||
|                 return; | ||||
|             } else if ( | ||||
| @@ -450,18 +511,32 @@ export class QuoteRequestor { | ||||
|                 try { | ||||
|                     if (typedMakerUrl.pairType === RfqPairType.Standard) { | ||||
|                         const response = await this._quoteRequestorHttpClient.get(`${typedMakerUrl.url}/${quotePath}`, { | ||||
|                             headers: { '0x-api-key': options.apiKey }, | ||||
|                             headers: { | ||||
|                                 '0x-api-key': options.integrator.integratorId, | ||||
|                                 '0x-integrator-id': options.integrator.integratorId, | ||||
|                             }, | ||||
|                             params: requestParams, | ||||
|                             timeout: timeoutMs, | ||||
|                             cancelToken: cancelTokenSource.token, | ||||
|                         }); | ||||
|                         const latencyMs = Date.now() - timeBeforeAwait; | ||||
|                         this._metrics?.logRfqMakerNetworkInteraction({ | ||||
|                             isLastLook: isLastLook || false, | ||||
|                             url: typedMakerUrl.url, | ||||
|                             quoteType, | ||||
|                             statusCode: response.status, | ||||
|                             sellTokenAddress, | ||||
|                             buyTokenAddress, | ||||
|                             latencyMs, | ||||
|                             included: true, | ||||
|                             integrator, | ||||
|                         }); | ||||
|                         this._infoLogger({ | ||||
|                             rfqtMakerInteraction: { | ||||
|                                 ...partialLogEntry, | ||||
|                                 response: { | ||||
|                                     included: true, | ||||
|                                     apiKey: options.apiKey, | ||||
|                                     apiKey: options.integrator.integratorId, | ||||
|                                     takerAddress: requestParams.takerAddress, | ||||
|                                     txOrigin: requestParams.txOrigin, | ||||
|                                     statusCode: response.status, | ||||
| @@ -479,7 +554,7 @@ export class QuoteRequestor { | ||||
|                             typedMakerUrl.url, | ||||
|                             this._altRfqCreds.altRfqApiKey, | ||||
|                             this._altRfqCreds.altRfqProfile, | ||||
|                             options.apiKey, | ||||
|                             options.integrator.integratorId, | ||||
|                             quoteType === 'firm' ? AltQuoteModel.Firm : AltQuoteModel.Indicative, | ||||
|                             makerToken, | ||||
|                             takerToken, | ||||
| @@ -492,12 +567,23 @@ export class QuoteRequestor { | ||||
|                         ); | ||||
|  | ||||
|                         const latencyMs = Date.now() - timeBeforeAwait; | ||||
|                         this._metrics?.logRfqMakerNetworkInteraction({ | ||||
|                             isLastLook: isLastLook || false, | ||||
|                             url: typedMakerUrl.url, | ||||
|                             quoteType, | ||||
|                             statusCode: quote.status, | ||||
|                             sellTokenAddress, | ||||
|                             buyTokenAddress, | ||||
|                             latencyMs, | ||||
|                             included: true, | ||||
|                             integrator, | ||||
|                         }); | ||||
|                         this._infoLogger({ | ||||
|                             rfqtMakerInteraction: { | ||||
|                                 ...partialLogEntry, | ||||
|                                 response: { | ||||
|                                     included: true, | ||||
|                                     apiKey: options.apiKey, | ||||
|                                     apiKey: options.integrator.integratorId, | ||||
|                                     takerAddress: requestParams.takerAddress, | ||||
|                                     txOrigin: requestParams.txOrigin, | ||||
|                                     statusCode: quote.status, | ||||
| @@ -511,12 +597,23 @@ export class QuoteRequestor { | ||||
|                 } catch (err) { | ||||
|                     // log error if any | ||||
|                     const latencyMs = Date.now() - timeBeforeAwait; | ||||
|                     this._metrics?.logRfqMakerNetworkInteraction({ | ||||
|                         isLastLook: isLastLook || false, | ||||
|                         url: typedMakerUrl.url, | ||||
|                         quoteType, | ||||
|                         statusCode: err.response?.status, | ||||
|                         sellTokenAddress, | ||||
|                         buyTokenAddress, | ||||
|                         latencyMs, | ||||
|                         included: false, | ||||
|                         integrator, | ||||
|                     }); | ||||
|                     this._infoLogger({ | ||||
|                         rfqtMakerInteraction: { | ||||
|                             ...partialLogEntry, | ||||
|                             response: { | ||||
|                                 included: false, | ||||
|                                 apiKey: options.apiKey, | ||||
|                                 apiKey: options.integrator.integratorId, | ||||
|                                 takerAddress: requestParams.takerAddress, | ||||
|                                 txOrigin: requestParams.txOrigin, | ||||
|                                 statusCode: err.response ? err.response.status : undefined, | ||||
| @@ -527,7 +624,7 @@ export class QuoteRequestor { | ||||
|                     rfqMakerBlacklist.logTimeoutOrLackThereof(typedMakerUrl.url, latencyMs >= timeoutMs); | ||||
|                     this._warningLogger( | ||||
|                         convertIfAxiosError(err), | ||||
|                         `Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${typedMakerUrl.url} for API key ${options.apiKey} for taker address ${options.takerAddress} and tx origin ${options.txOrigin}`, | ||||
|                         `Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${typedMakerUrl.url} for integrator ${options.integrator.integratorId} (${options.integrator.label}) for taker address ${options.takerAddress} and tx origin ${options.txOrigin}`, | ||||
|                     ); | ||||
|                     return; | ||||
|                 } | ||||
|   | ||||
| @@ -28,7 +28,11 @@ export const rfqtMocker = { | ||||
|             // Mock out RFQT responses | ||||
|             for (const mockedResponse of mockedResponses) { | ||||
|                 const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse; | ||||
|                 const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey }; | ||||
|                 const requestHeaders = { | ||||
|                     Accept: 'application/json, text/plain, */*', | ||||
|                     '0x-api-key': requestApiKey, | ||||
|                     '0x-integrator-id': requestApiKey, | ||||
|                 }; | ||||
|                 mockedAxios | ||||
|                     .onGet(`${endpoint}/${quoteType}`, { params: requestParams }, requestHeaders) | ||||
|                     .replyOnce(responseCode, responseData); | ||||
|   | ||||
| @@ -16,7 +16,7 @@ import * as _ from 'lodash'; | ||||
| import * as TypeMoq from 'typemoq'; | ||||
|  | ||||
| import { MarketOperation, QuoteRequestor, RfqRequestOpts, SignedNativeOrder } from '../src'; | ||||
| import { NativeOrderWithFillableAmounts } from '../src/types'; | ||||
| import { Integrator, NativeOrderWithFillableAmounts } from '../src/types'; | ||||
| import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; | ||||
| import { | ||||
|     BUY_SOURCE_FILTER_BY_CHAIN_ID, | ||||
| @@ -62,6 +62,10 @@ const SELL_SOURCES = SELL_SOURCE_FILTER_BY_CHAIN_ID[ChainId.Mainnet].sources; | ||||
| const TOKEN_ADJACENCY_GRAPH: TokenAdjacencyGraph = { default: [] }; | ||||
|  | ||||
| const SIGNATURE = { v: 1, r: NULL_BYTES, s: NULL_BYTES, signatureType: SignatureType.EthSign }; | ||||
| const FOO_INTEGRATOR: Integrator = { | ||||
|     integratorId: 'foo', | ||||
|     label: 'foo', | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * gets the orders required for a market sell operation by (potentially) merging native orders with | ||||
| @@ -745,7 +749,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                         feeSchedule, | ||||
|                         rfqt: { | ||||
|                             isIndicative: false, | ||||
|                             apiKey: 'foo', | ||||
|                             integrator: FOO_INTEGRATOR, | ||||
|                             takerAddress: randomAddress(), | ||||
|                             txOrigin: randomAddress(), | ||||
|                             intentOnFilling: true, | ||||
| @@ -790,7 +794,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                         ...DEFAULT_OPTS, | ||||
|                         rfqt: { | ||||
|                             isIndicative: false, | ||||
|                             apiKey: 'foo', | ||||
|                             integrator: FOO_INTEGRATOR, | ||||
|                             takerAddress: randomAddress(), | ||||
|                             intentOnFilling: true, | ||||
|                             txOrigin: randomAddress(), | ||||
| @@ -837,7 +841,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                         ...DEFAULT_OPTS, | ||||
|                         rfqt: { | ||||
|                             isIndicative: true, | ||||
|                             apiKey: 'foo', | ||||
|                             integrator: FOO_INTEGRATOR, | ||||
|                             takerAddress: randomAddress(), | ||||
|                             txOrigin: randomAddress(), | ||||
|                             intentOnFilling: true, | ||||
| @@ -896,7 +900,10 @@ describe('MarketOperationUtils tests', () => { | ||||
|                         ...DEFAULT_OPTS, | ||||
|                         rfqt: { | ||||
|                             isIndicative: false, | ||||
|                             apiKey: 'foo', | ||||
|                             integrator: { | ||||
|                                 integratorId: 'foo', | ||||
|                                 label: 'foo', | ||||
|                             }, | ||||
|                             takerAddress: randomAddress(), | ||||
|                             intentOnFilling: true, | ||||
|                             txOrigin: randomAddress(), | ||||
| @@ -954,7 +961,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                         ...DEFAULT_OPTS, | ||||
|                         rfqt: { | ||||
|                             isIndicative: false, | ||||
|                             apiKey: 'foo', | ||||
|                             integrator: FOO_INTEGRATOR, | ||||
|                             takerAddress: randomAddress(), | ||||
|                             txOrigin: randomAddress(), | ||||
|                             intentOnFilling: true, | ||||
|   | ||||
| @@ -240,7 +240,10 @@ describe('QuoteRequestor', async () => { | ||||
|                         MarketOperation.Sell, | ||||
|                         undefined, | ||||
|                         { | ||||
|                             apiKey, | ||||
|                             integrator: { | ||||
|                                 integratorId: apiKey, | ||||
|                                 label: 'foo', | ||||
|                             }, | ||||
|                             takerAddress, | ||||
|                             txOrigin: takerAddress, | ||||
|                             intentOnFilling: true, | ||||
| @@ -435,7 +438,10 @@ describe('QuoteRequestor', async () => { | ||||
|                         MarketOperation.Sell, | ||||
|                         undefined, | ||||
|                         { | ||||
|                             apiKey, | ||||
|                             integrator: { | ||||
|                                 integratorId: apiKey, | ||||
|                                 label: 'foo', | ||||
|                             }, | ||||
|                             takerAddress, | ||||
|                             txOrigin: takerAddress, | ||||
|                             intentOnFilling: true, | ||||
| @@ -551,7 +557,10 @@ describe('QuoteRequestor', async () => { | ||||
|                         MarketOperation.Sell, | ||||
|                         undefined, | ||||
|                         { | ||||
|                             apiKey, | ||||
|                             integrator: { | ||||
|                                 integratorId: apiKey, | ||||
|                                 label: 'foo', | ||||
|                             }, | ||||
|                             takerAddress, | ||||
|                             txOrigin: takerAddress, | ||||
|                             intentOnFilling: true, | ||||
| @@ -675,7 +684,10 @@ describe('QuoteRequestor', async () => { | ||||
|                         MarketOperation.Sell, | ||||
|                         undefined, | ||||
|                         { | ||||
|                             apiKey, | ||||
|                             integrator: { | ||||
|                                 integratorId: apiKey, | ||||
|                                 label: 'foo', | ||||
|                             }, | ||||
|                             takerAddress, | ||||
|                             txOrigin: takerAddress, | ||||
|                             intentOnFilling: true, | ||||
| @@ -762,7 +774,10 @@ describe('QuoteRequestor', async () => { | ||||
|                         MarketOperation.Sell, | ||||
|                         undefined, | ||||
|                         { | ||||
|                             apiKey, | ||||
|                             integrator: { | ||||
|                                 integratorId: apiKey, | ||||
|                                 label: 'foo', | ||||
|                             }, | ||||
|                             takerAddress, | ||||
|                             txOrigin: takerAddress, | ||||
|                             intentOnFilling: true, | ||||
| @@ -823,7 +838,10 @@ describe('QuoteRequestor', async () => { | ||||
|                         MarketOperation.Buy, | ||||
|                         undefined, | ||||
|                         { | ||||
|                             apiKey, | ||||
|                             integrator: { | ||||
|                                 integratorId: apiKey, | ||||
|                                 label: 'foo', | ||||
|                             }, | ||||
|                             takerAddress, | ||||
|                             txOrigin: takerAddress, | ||||
|                             intentOnFilling: true, | ||||
| @@ -834,6 +852,43 @@ describe('QuoteRequestor', async () => { | ||||
|                 quoteRequestorHttpClient, | ||||
|             ); | ||||
|         }); | ||||
|         it('should be able to handle and filter RFQ offerings', () => { | ||||
|             const tests: Array<[string[] | undefined, string[]]> = [ | ||||
|                 [['https://top.maker'], []], | ||||
|                 [undefined, ['https://foo.bar/', 'https://lorem.ipsum/']], | ||||
|                 [['https://lorem.ipsum/'], ['https://lorem.ipsum/']], | ||||
|             ]; | ||||
|             for (const test of tests) { | ||||
|                 const [apiKeyWhitelist, results] = test; | ||||
|                 const response = QuoteRequestor.getTypedMakerUrlsAndWhitelist( | ||||
|                     { | ||||
|                         integrator: { | ||||
|                             integratorId: 'foo', | ||||
|                             label: 'bar', | ||||
|                             whitelistIntegratorUrls: apiKeyWhitelist, | ||||
|                         }, | ||||
|                         altRfqAssetOfferings: {}, | ||||
|                     }, | ||||
|                     { | ||||
|                         'https://foo.bar/': [ | ||||
|                             [ | ||||
|                                 '0xA6cD4cb8c62aCDe44739E3Ed0F1d13E0e31f2d94', | ||||
|                                 '0xF45107c0200a04A8aB9C600cc52A3C89AE5D0489', | ||||
|                             ], | ||||
|                         ], | ||||
|                         'https://lorem.ipsum/': [ | ||||
|                             [ | ||||
|                                 '0xA6cD4cb8c62aCDe44739E3Ed0F1d13E0e31f2d94', | ||||
|                                 '0xF45107c0200a04A8aB9C600cc52A3C89AE5D0489', | ||||
|                             ], | ||||
|                         ], | ||||
|                     }, | ||||
|                 ); | ||||
|                 const typedUrls = response.map(typed => typed.url); | ||||
|                 expect(typedUrls).to.eql(results); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         it('should return successful alt indicative quotes', async () => { | ||||
|             const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a'; | ||||
|             const txOrigin = '0xf209925defc99488e3afff1174e48b4fa628302a'; | ||||
| @@ -1055,7 +1110,10 @@ describe('QuoteRequestor', async () => { | ||||
|                             altScenario.requestedOperation, | ||||
|                             undefined, | ||||
|                             { | ||||
|                                 apiKey, | ||||
|                                 integrator: { | ||||
|                                     integratorId: apiKey, | ||||
|                                     label: 'foo', | ||||
|                                 }, | ||||
|                                 takerAddress, | ||||
|                                 txOrigin, | ||||
|                                 intentOnFilling: true, | ||||
|   | ||||
| @@ -48,7 +48,11 @@ export const testHelpers = { | ||||
|             // Mock out Standard RFQ-T/M responses | ||||
|             for (const mockedResponse of standardMockedResponses) { | ||||
|                 const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse; | ||||
|                 const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey }; | ||||
|                 const requestHeaders = { | ||||
|                     Accept: 'application/json, text/plain, */*', | ||||
|                     '0x-api-key': requestApiKey, | ||||
|                     '0x-integrator-id': requestApiKey, | ||||
|                 }; | ||||
|                 if (mockedResponse.callback !== undefined) { | ||||
|                     mockedAxios | ||||
|                         .onGet(`${endpoint}/${quoteType}`, { params: requestParams }, requestHeaders) | ||||
|   | ||||
| @@ -1,4 +1,13 @@ | ||||
| [ | ||||
|     { | ||||
|         "timestamp": 1631120757, | ||||
|         "version": "8.1.5", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Dependencies updated" | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "timestamp": 1630459879, | ||||
|         "version": "8.1.4", | ||||
|   | ||||
| @@ -5,6 +5,10 @@ Edit the package's CHANGELOG.json file only. | ||||
|  | ||||
| CHANGELOG | ||||
|  | ||||
| ## v8.1.5 - _September 8, 2021_ | ||||
|  | ||||
|     * Dependencies updated | ||||
|  | ||||
| ## v8.1.4 - _September 1, 2021_ | ||||
|  | ||||
|     * Dependencies updated | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@0x/migrations", | ||||
|     "version": "8.1.4", | ||||
|     "version": "8.1.5", | ||||
|     "engines": { | ||||
|         "node": ">=6.12" | ||||
|     }, | ||||
| @@ -73,15 +73,15 @@ | ||||
|         "@0x/contracts-coordinator": "^3.1.38", | ||||
|         "@0x/contracts-dev-utils": "^1.3.36", | ||||
|         "@0x/contracts-erc1155": "^2.1.37", | ||||
|         "@0x/contracts-erc20": "^3.3.18", | ||||
|         "@0x/contracts-erc20": "^3.3.19", | ||||
|         "@0x/contracts-erc721": "^3.1.37", | ||||
|         "@0x/contracts-exchange": "^3.2.38", | ||||
|         "@0x/contracts-exchange-forwarder": "^4.2.38", | ||||
|         "@0x/contracts-extensions": "^6.2.32", | ||||
|         "@0x/contracts-multisig": "^4.1.38", | ||||
|         "@0x/contracts-staking": "^2.0.45", | ||||
|         "@0x/contracts-utils": "^4.7.18", | ||||
|         "@0x/contracts-zero-ex": "^0.28.3", | ||||
|         "@0x/contracts-utils": "^4.8.0", | ||||
|         "@0x/contracts-zero-ex": "^0.28.4", | ||||
|         "@0x/sol-compiler": "^4.7.3", | ||||
|         "@0x/subproviders": "^6.5.3", | ||||
|         "@0x/typescript-typings": "^5.2.0", | ||||
|   | ||||
| @@ -1,4 +1,12 @@ | ||||
| [ | ||||
|     { | ||||
|         "version": "1.9.0", | ||||
|         "changes": [ | ||||
|             { | ||||
|                 "note": "Add 'TreasuryVote' class" | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         "timestamp": 1630459879, | ||||
|         "version": "1.8.4", | ||||
|   | ||||
| @@ -67,7 +67,7 @@ | ||||
|         "@0x/contract-wrappers": "^13.17.6", | ||||
|         "@0x/json-schemas": "^6.1.3", | ||||
|         "@0x/subproviders": "^6.5.3", | ||||
|         "@0x/utils": "^6.4.3", | ||||
|         "@0x/utils": "^6.4.4", | ||||
|         "@0x/web3-wrapper": "^7.5.3", | ||||
|         "chai": "^4.0.1", | ||||
|         "ethereumjs-util": "^7.0.10", | ||||
|   | ||||
| @@ -9,3 +9,4 @@ export * from './signature_utils'; | ||||
| export * from './transformer_utils'; | ||||
| export * from './constants'; | ||||
| export * from './vip_utils'; | ||||
| export * from './treasury_votes'; | ||||
|   | ||||
							
								
								
									
										97
									
								
								packages/protocol-utils/src/treasury_votes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								packages/protocol-utils/src/treasury_votes.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils'; | ||||
| import * as ethUtil from 'ethereumjs-util'; | ||||
|  | ||||
| import { ZERO } from './constants'; | ||||
| import { EIP712_DOMAIN_PARAMETERS, getTypeHash } from './eip712_utils'; | ||||
| import { eip712SignHashWithKey, Signature } from './signature_utils'; | ||||
|  | ||||
| const VOTE_DEFAULT_VALUES = { | ||||
|     proposalId: ZERO, | ||||
|     support: false, | ||||
|     operatedPoolIds: [] as string[], | ||||
|     chainId: 1, | ||||
|     version: '1.0.0', | ||||
|     verifyingContract: NULL_ADDRESS, | ||||
| }; | ||||
|  | ||||
| export type TreasuryVoteFields = typeof VOTE_DEFAULT_VALUES; | ||||
|  | ||||
| export class TreasuryVote { | ||||
|     public static readonly CONTRACT_NAME = 'Zrx Treasury'; | ||||
|  | ||||
|     public static readonly MESSAGE_STRUCT_NAME = 'TreasuryVote'; | ||||
|     public static readonly MESSAGE_STRUCT_ABI = [ | ||||
|         { type: 'uint256', name: 'proposalId' }, | ||||
|         { type: 'bool', name: 'support' }, | ||||
|         { type: 'bytes32[]', name: 'operatedPoolIds' }, | ||||
|     ]; | ||||
|     public static readonly MESSAGE_TYPE_HASH = getTypeHash( | ||||
|         TreasuryVote.MESSAGE_STRUCT_NAME, TreasuryVote.MESSAGE_STRUCT_ABI, | ||||
|     ); | ||||
|  | ||||
|     public static readonly DOMAIN_STRUCT_NAME = 'EIP712Domain'; | ||||
|     public static readonly DOMAIN_TYPE_HASH = getTypeHash( | ||||
|         TreasuryVote.DOMAIN_STRUCT_NAME, EIP712_DOMAIN_PARAMETERS, | ||||
|     ); | ||||
|  | ||||
|     public proposalId: BigNumber; | ||||
|     public support: boolean; | ||||
|     public operatedPoolIds: string[]; | ||||
|     public chainId: number; | ||||
|     public version: string; | ||||
|     public verifyingContract: string; | ||||
|  | ||||
|     constructor(fields: Partial<TreasuryVoteFields> = {}) { | ||||
|         const _fields = { ...VOTE_DEFAULT_VALUES, ...fields }; | ||||
|         this.proposalId = _fields.proposalId; | ||||
|         this.support = _fields.support; | ||||
|         this.operatedPoolIds = _fields.operatedPoolIds; | ||||
|         this.chainId = _fields.chainId; | ||||
|         this.version = _fields.version; | ||||
|         this.verifyingContract = _fields.verifyingContract; | ||||
|     } | ||||
|  | ||||
|     public getDomainHash(): string { | ||||
|         return hexUtils.hash( | ||||
|             hexUtils.concat( | ||||
|                 hexUtils.leftPad(TreasuryVote.DOMAIN_TYPE_HASH), | ||||
|                 hexUtils.hash( | ||||
|                     hexUtils.toHex(Buffer.from(TreasuryVote.CONTRACT_NAME)), | ||||
|                 ), | ||||
|                 hexUtils.leftPad(this.chainId), | ||||
|                 hexUtils.hash( | ||||
|                     hexUtils.toHex(Buffer.from(this.version)), | ||||
|                 ), | ||||
|                 hexUtils.leftPad(this.verifyingContract), | ||||
|             ), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public getStructHash(): string { | ||||
|         return hexUtils.hash( | ||||
|             hexUtils.concat( | ||||
|                 hexUtils.leftPad(TreasuryVote.MESSAGE_TYPE_HASH), | ||||
|                 hexUtils.leftPad(this.proposalId), | ||||
|                 hexUtils.leftPad(this.support ? 1 : 0), | ||||
|                 hexUtils.hash( | ||||
|                     ethUtil.toBuffer(hexUtils.concat(...this.operatedPoolIds.map(id => hexUtils.leftPad(id)))), | ||||
|                 ), | ||||
|             ), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public getEIP712Hash(): string { | ||||
|         return hexUtils.hash( | ||||
|             hexUtils.toHex( | ||||
|                 hexUtils.concat( | ||||
|                     '0x1901', | ||||
|                     this.getDomainHash(), | ||||
|                     this.getStructHash()), | ||||
|             ), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public getSignatureWithKey(privateKey: string): Signature { | ||||
|         return eip712SignHashWithKey(this.getEIP712Hash(), privateKey); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user