diff --git a/contracts/paymasters/SubsidizerPaymaster.sol b/contracts/paymasters/SubsidizerPaymaster.sol deleted file mode 100644 index c2d1aec..0000000 --- a/contracts/paymasters/SubsidizerPaymaster.sol +++ /dev/null @@ -1,305 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.17; - -import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from '@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol'; -import {IPaymasterFlow} from '@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol'; -import {Transaction} from '@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol'; -import {BOOTLOADER_FORMAL_ADDRESS} from '@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol'; -import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; -import {PrimaryProdDataServiceConsumerBase} from '@redstone-finance/evm-connector/contracts/data-services/PrimaryProdDataServiceConsumerBase.sol'; -import {Errors} from '../libraries/Errors.sol'; -import {IERC20, SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; -import {BootloaderAuth} from '../auth/BootloaderAuth.sol'; -import {IClaveRegistry} from '../interfaces/IClaveRegistry.sol'; - -// Allowed ERC-20 tokens data struct input -struct TokenInput { - address tokenAddress; // token addresses - uint32 decimals; // decimals of the tokens - uint256 priceMarkup; // price markup percentage -} - -// Stored token data including oracle ids and price markups -struct TokenData { - uint32 decimals; // decimals of the tokens - uint192 markup; // price markup percentage -} - -/** - * @title SubsidizerPaymaster is a super of the ERC20Paymaster to pay for transaction fees in allowed ERC-20 tokens (stablecoins) by subsidizing a specific amount - * @author https://getclave.io - * @dev This contract uses Redstone oracle to get token prices, please check [Redstone docs](https://docs.redstone.finance/docs/smart-contract-devs/get-started/redstone-core). - */ -contract SubsidizerPaymaster is - IPaymaster, - PrimaryProdDataServiceConsumerBase, - Ownable, - BootloaderAuth -{ - // Using OpenZeppelin's SafeERC20 library to perform token transfers - using SafeERC20 for IERC20; - - // The nominator used for price calculation - uint256 constant PRICE_PAIR_NOMINATOR = 1e18; - // The nominator used for markup calculation - uint256 constant MARKUP_NOMINATOR = 1e6; - //The nominator used to get rid of oracle price decimals - uint256 constant ORACLE_NOMINATOR = 1e8; - // Maximum subsidized fee amount - uint256 constant MAX_GAS_TO_SUBSIDIZE = 1_250_000; - - // Clave account registry contract - address public claveRegistry; - - // Store allowed tokens addresses with their data - mapping(address => TokenData) public allowedTokens; - - // Event to be emitted when a token is used to pay for transaction - event SubsidizerPaymasterUsed(address indexed user, address token); - // Event to be emitted when a new token is allowed - event ERC20TokenAllowed(address token); - // Event to be emitted when a new token is removed - event ERC20TokenRemoved(address token); - // Event to be emitted when the balance is withdrawn - event BalanceWithdrawn(address to, uint256 amount); - - /** - * @notice Constructor function - * @param tokens TokenInput[] - Array of token addresses, markups, and their oracle ids in string - * @dev Use markup by adding percentage amount to 100, e.g. 10% markup will be 11000 - * @dev Make sure to use true decimal values for the tokens, otherwise the conversions will be incorrect - */ - constructor(TokenInput[] memory tokens, address registry) { - for (uint256 i = 0; i < tokens.length; i++) { - // Decline zero-addresses - if (tokens[i].tokenAddress == address(0)) revert Errors.INVALID_TOKEN(); - - // Decline false markup values - if (tokens[i].priceMarkup < 5000 || tokens[i].priceMarkup >= 100000) - revert Errors.INVALID_TOKEN(); - uint192 priceMarkup = uint192(tokens[i].priceMarkup * (MARKUP_NOMINATOR / 1e4)); - - allowedTokens[tokens[i].tokenAddress] = TokenData(tokens[i].decimals, priceMarkup); - } - - claveRegistry = registry; - } - - // Allow receiving ETH - receive() external payable {} - - /// @inheritdoc IPaymaster - /// @dev return the fee payer token address in the context - function validateAndPayForPaymasterTransaction( - bytes32 /**_txHash*/, - bytes32 /**_suggestedSignedHash*/, - Transaction calldata _transaction - ) external payable onlyBootloader returns (bytes4 magic, bytes memory context) { - // By default we consider the transaction as accepted. - magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC; - - // Get the user address - address userAddress = address(uint160(_transaction.from)); - - // Check if the account is a Clave account - if (!IClaveRegistry(claveRegistry).isClave(userAddress)) revert Errors.NOT_CLAVE_ACCOUNT(); - - // Revert if standart paymaster input is shorter than 4 bytes - if (_transaction.paymasterInput.length < 4) revert Errors.SHORT_PAYMASTER_INPUT(); - - // Check the paymaster input selector to detect flow - bytes4 paymasterInputSelector = bytes4(_transaction.paymasterInput[0:4]); - if (paymasterInputSelector != IPaymasterFlow.approvalBased.selector) - revert Errors.UNSUPPORTED_FLOW(); - - // Extract token address and oracle payload from the paymaster input and check if it is allowed - (address token, , bytes memory oracleCalldata) = abi.decode( - _transaction.paymasterInput[4:], - (address, uint256, bytes) - ); - if (allowedTokens[token].decimals == uint32(0)) revert Errors.UNSUPPORTED_TOKEN(); - - address thisAddress = address(this); - uint256 providedAllowance = IERC20(token).allowance(userAddress, thisAddress); - - // Required ETH to pay fees - uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas; - // Required ETH to receive from the user - uint256 userToPayETH; - if (_transaction.gasLimit > MAX_GAS_TO_SUBSIDIZE) { - userToPayETH = - (_transaction.gasLimit - MAX_GAS_TO_SUBSIDIZE) * - _transaction.maxFeePerGas; - } else { - userToPayETH = 0; - } - - // Conversion rate of ETH to token - // 0, if not receiving tokens from user - uint256 rate; - - if (userToPayETH > 0) { - rate = getPairPrice(token, oracleCalldata); - // Calculated fee amount as token - uint256 requiredToken = (userToPayETH * rate) / PRICE_PAIR_NOMINATOR; - - // Check token allowance for the fee - if (providedAllowance < requiredToken) revert Errors.LESS_ALLOWANCE_FOR_PAYMASTER(); - - // Transfer token to the fee collector - IERC20(token).safeTransferFrom(userAddress, address(this), requiredToken); - } - - // Transfer fees to the bootloader - (bool feeSuccess, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{value: requiredETH}(''); - if (!feeSuccess) revert Errors.FAILED_FEE_TRANSFER(); - - // Use fee token address as context - context = abi.encode(token, rate); - } - - /// @inheritdoc IPaymaster - function postTransaction( - bytes calldata _context, - Transaction calldata _transaction, - bytes32 /**_txHash*/, - bytes32 /**_suggestedSignedHash*/, - ExecutionResult /**_txResult*/, - uint256 _maxRefundedGas - ) external payable onlyBootloader { - (address tokenAddress, uint256 rate) = abi.decode(_context, (address, uint256)); - address fromAddress = address(uint160(_transaction.from)); - - uint256 refundAmount = calcRefundAmount( - _transaction.gasLimit, - _maxRefundedGas, - _transaction.maxFeePerGas, - rate - ); - - if (refundAmount > 0) IERC20(tokenAddress).safeTransfer(fromAddress, refundAmount); - - // Emit user address with fee payer token - emit SubsidizerPaymasterUsed(fromAddress, tokenAddress); - } - - /** - * @notice Allow a new token to be used in paymaster - * @param token TokenInput calldata - Token address and its oracle ids in string of allowed token - * @dev Only owner address can call this method - * @dev Make sure to use true decimal values for the tokens, otherwise the conversions will be incorrect - */ - function allowToken(TokenInput calldata token) external onlyOwner { - // Skip zero-addresses - if (token.tokenAddress == address(0)) { - revert Errors.INVALID_TOKEN(); - } - - // Skip false markup values - if (token.priceMarkup < 5000 || token.priceMarkup >= 100000) revert Errors.INVALID_MARKUP(); - uint192 priceMarkup = uint192(token.priceMarkup * (MARKUP_NOMINATOR / 1e4)); - - allowedTokens[token.tokenAddress] = TokenData(token.decimals, uint192(priceMarkup)); - emit ERC20TokenAllowed(token.tokenAddress); - } - - /** - * @notice Remove allowed paymaster tokens - * @param tokenAddress address - Token address to be removed - * @dev Only owner address can call this method - */ - function removeToken(address tokenAddress) external onlyOwner { - delete allowedTokens[tokenAddress]; - emit ERC20TokenRemoved(tokenAddress); - } - - /** - * @notice Withdraw paymaster funds as owner - * @param to address - Token receiver address - * @param amount uint256 - Amount to be withdrawn - * @dev Only owner address can call this method - */ - function withdraw(address to, uint256 amount) external onlyOwner { - // Send paymaster funds to the given address - (bool success, ) = payable(to).call{value: amount}(''); - if (!success) revert Errors.UNAUTHORIZED_WITHDRAW(); - - emit BalanceWithdrawn(to, amount); - } - - /** - * @notice Withdraw paymaster token funds as owner - * @param token address - Token address to withdraw - * @param to address - Token receiver address - * @param amount uint256 - Amount to be withdrawn - * @dev Only owner address can call this method - */ - function withdrawToken(address token, address to, uint256 amount) external onlyOwner { - // Send paymaster funds to the given address - IERC20(token).safeTransfer(to, amount); - } - - /** - * @notice This function calls the oracle and returns the values - * @param - bytes - Oracle manuel payload - * @return uint256 - Oracle return as token price - * @dev bytes parameter is extracted from the paymaster input by the oracle contract, so it is NOT UNUSED here - * @dev This function should be called by an external call inside this contract to pass its specific calldata - */ - function callOracle( - bytes memory //* oracleCalldata */ - ) external view returns (uint256) { - return getOracleNumericValueFromTxMsg(bytes32('ETH')); - } - - /** - * @notice This function gets the ETH/PARAM_TOKEN price from the oracle - * @param token address - Token address - * @param oracleCalldata bytes - Oracle calldata for Redstone - * @return rate uint256 - ETH/TOKEN price - * @dev TOKEN price is accepted as 1 $ - */ - function getPairPrice( - address token, - bytes memory oracleCalldata - ) private view returns (uint256 rate) { - // Used asset decimals - uint256 tokenDecimals = 10 ** (allowedTokens[token].decimals); - // Oracle value - uint256 value = this.callOracle(oracleCalldata); - - // Calculated token price - rate = - (value * allowedTokens[token].markup * tokenDecimals) / - (ORACLE_NOMINATOR * MARKUP_NOMINATOR); - } - - /** - * @notice This function calculates the gas refund amount for the user - * @param gasLimit uint256 - Gas limit of the transaction - * @param gasRefunded uint256 - Unused gas amount - * @param maxFeePerGas uint256 - Gas price from zkSync transaction - * @param rate uint256 - Conversion rate regarding to ETH/Token pair - * @return refundTokenAmount uint256 - Refund amount as token - */ - function calcRefundAmount( - uint256 gasLimit, - uint256 gasRefunded, - uint256 maxFeePerGas, - uint256 rate - ) private pure returns (uint256 refundTokenAmount) { - if (!(rate > 0)) return refundTokenAmount; - - if (gasLimit > MAX_GAS_TO_SUBSIDIZE) { - uint256 userPaidGas = gasLimit - MAX_GAS_TO_SUBSIDIZE; - uint256 gasUsed = gasLimit - gasRefunded; - - // If, gasUsed is less than we want to pay, compensate user - // Else, we can pay (userPaidGas - (gasUsed - MAX_FEE_TO_SUBSIDIZE)) at best, which can - // be simplified to _maxRefundedGas - uint256 refundGas = MAX_GAS_TO_SUBSIDIZE > gasUsed ? userPaidGas : gasRefunded; - - refundTokenAmount = (refundGas * maxFeePerGas * rate) / PRICE_PAIR_NOMINATOR; - } - } -} diff --git a/contracts/test/SubsidizerPaymasterMock.sol b/contracts/test/SubsidizerPaymasterMock.sol deleted file mode 100644 index 56dc5bf..0000000 --- a/contracts/test/SubsidizerPaymasterMock.sol +++ /dev/null @@ -1,306 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.17; - -import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from '@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol'; -import {IPaymasterFlow} from '@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol'; -import {Transaction} from '@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol'; -import {BOOTLOADER_FORMAL_ADDRESS} from '@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol'; -import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; -import {PrimaryProdDataServiceConsumerBase} from '@redstone-finance/evm-connector/contracts/data-services/PrimaryProdDataServiceConsumerBase.sol'; -import {Errors} from '../libraries/Errors.sol'; -import {IERC20, SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; -import {BootloaderAuth} from '../auth/BootloaderAuth.sol'; -import {IClaveRegistry} from '../interfaces/IClaveRegistry.sol'; - -// Allowed ERC-20 tokens data struct input -struct TokenInput { - address tokenAddress; // token addresses - uint32 decimals; // decimals of the tokens - uint256 priceMarkup; // price markup percentage -} - -// Stored token data including oracle ids and price markups -struct TokenData { - uint32 decimals; // decimals of the tokens - uint192 markup; // price markup percentage -} - -/** - * @title SubsidizerPaymaster is a super of the ERC20Paymaster to pay for transaction fees in allowed ERC-20 tokens (stablecoins) by subsidizing a specific amount - * @author https://getclave.io - * @dev This contract uses Redstone oracle to get token prices, please check [Redstone docs](https://docs.redstone.finance/docs/smart-contract-devs/get-started/redstone-core). - */ -contract SubsidizerPaymasterMock is - IPaymaster, - PrimaryProdDataServiceConsumerBase, - Ownable, - BootloaderAuth -{ - // Using OpenZeppelin's SafeERC20 library to perform token transfers - using SafeERC20 for IERC20; - - // The nominator used for price calculation - uint256 constant PRICE_PAIR_NOMINATOR = 1e18; - // The nominator used for markup calculation - uint256 constant MARKUP_NOMINATOR = 1e6; - //The nominator used to get rid of oracle price decimals - uint256 constant ORACLE_NOMINATOR = 1e8; - // Maximum subsidized fee amount - uint256 constant MAX_GAS_TO_SUBSIDIZE = 1_250_000; - - // Clave account registry contract - address public claveRegistry; - - // Store allowed tokens addresses with their data - mapping(address => TokenData) public allowedTokens; - - // Event to be emitted when a token is used to pay for transaction - event SubsidizerPaymasterUsed(address indexed user, address token); - // Event to be emitted when a new token is allowed - event ERC20TokenAllowed(address token); - // Event to be emitted when a new token is removed - event ERC20TokenRemoved(address token); - // Event to be emitted when the balance is withdrawn - event BalanceWithdrawn(address to, uint256 amount); - - /** - * @notice Constructor function - * @param tokens TokenInput[] - Array of token addresses, markups, and their oracle ids in string - * @dev Use markup by adding percentage amount to 100, e.g. 10% markup will be 11000 - * @dev Make sure to use true decimal values for the tokens, otherwise the conversions will be incorrect - */ - constructor(TokenInput[] memory tokens, address registry) { - for (uint256 i = 0; i < tokens.length; i++) { - // Decline zero-addresses - if (tokens[i].tokenAddress == address(0)) revert Errors.INVALID_TOKEN(); - - // Decline false markup values - if (tokens[i].priceMarkup < 5000 || tokens[i].priceMarkup >= 100000) - revert Errors.INVALID_TOKEN(); - uint192 priceMarkup = uint192(tokens[i].priceMarkup * (MARKUP_NOMINATOR / 1e4)); - - allowedTokens[tokens[i].tokenAddress] = TokenData(tokens[i].decimals, priceMarkup); - } - - claveRegistry = registry; - } - - // Allow receiving ETH - receive() external payable {} - - /// @inheritdoc IPaymaster - /// @dev return the fee payer token address in the context - function validateAndPayForPaymasterTransaction( - bytes32 /**_txHash*/, - bytes32 /**_suggestedSignedHash*/, - Transaction calldata _transaction - ) external payable onlyBootloader returns (bytes4 magic, bytes memory context) { - // By default we consider the transaction as accepted. - magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC; - - // Get the user address - address userAddress = address(uint160(_transaction.from)); - - // Check if the account is a Clave account - if (!IClaveRegistry(claveRegistry).isClave(userAddress)) revert Errors.NOT_CLAVE_ACCOUNT(); - - // Revert if standart paymaster input is shorter than 4 bytes - if (_transaction.paymasterInput.length < 4) revert Errors.SHORT_PAYMASTER_INPUT(); - - // Check the paymaster input selector to detect flow - bytes4 paymasterInputSelector = bytes4(_transaction.paymasterInput[0:4]); - if (paymasterInputSelector != IPaymasterFlow.approvalBased.selector) - revert Errors.UNSUPPORTED_FLOW(); - - // Extract token address and oracle payload from the paymaster input and check if it is allowed - (address token, , bytes memory oracleCalldata) = abi.decode( - _transaction.paymasterInput[4:], - (address, uint256, bytes) - ); - if (allowedTokens[token].decimals == uint32(0)) revert Errors.UNSUPPORTED_TOKEN(); - - address thisAddress = address(this); - uint256 providedAllowance = IERC20(token).allowance(userAddress, thisAddress); - - // Required ETH to pay fees - uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas; - // Required ETH to receive from the user - uint256 userToPayETH; - if (_transaction.gasLimit > MAX_GAS_TO_SUBSIDIZE) { - userToPayETH = - (_transaction.gasLimit - MAX_GAS_TO_SUBSIDIZE) * - _transaction.maxFeePerGas; - } else { - userToPayETH = 0; - } - - // Conversion rate of ETH to token - // 0, if not receiving tokens from user - uint256 rate; - - if (userToPayETH > 0) { - rate = getPairPrice(token, oracleCalldata); - // Calculated fee amount as token - uint256 requiredToken = (userToPayETH * rate) / PRICE_PAIR_NOMINATOR; - - // Check token allowance for the fee - if (providedAllowance < requiredToken) revert Errors.LESS_ALLOWANCE_FOR_PAYMASTER(); - - // Transfer token to the fee collector - IERC20(token).safeTransferFrom(userAddress, address(this), requiredToken); - } - - // Transfer fees to the bootloader - (bool feeSuccess, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{value: requiredETH}(''); - if (!feeSuccess) revert Errors.FAILED_FEE_TRANSFER(); - - // Use fee token address as context - context = abi.encode(token, rate); - } - - /// @inheritdoc IPaymaster - function postTransaction( - bytes calldata _context, - Transaction calldata _transaction, - bytes32 /**_txHash*/, - bytes32 /**_suggestedSignedHash*/, - ExecutionResult /**_txResult*/, - uint256 _maxRefundedGas - ) external payable onlyBootloader { - (address tokenAddress, uint256 rate) = abi.decode(_context, (address, uint256)); - address fromAddress = address(uint160(_transaction.from)); - - uint256 refundAmount = calcRefundAmount( - _transaction.gasLimit, - _maxRefundedGas, - _transaction.maxFeePerGas, - rate - ); - - if (refundAmount > 0) IERC20(tokenAddress).safeTransfer(fromAddress, refundAmount); - - // Emit user address with fee payer token - emit SubsidizerPaymasterUsed(fromAddress, tokenAddress); - } - - /** - * @notice Allow a new token to be used in paymaster - * @param token TokenInput calldata - Token address and its oracle ids in string of allowed token - * @dev Only owner address can call this method - * @dev Make sure to use true decimal values for the tokens, otherwise the conversions will be incorrect - */ - function allowToken(TokenInput calldata token) external onlyOwner { - // Skip zero-addresses - if (token.tokenAddress == address(0)) { - revert Errors.INVALID_TOKEN(); - } - - // Skip false markup values - if (token.priceMarkup < 5000 || token.priceMarkup >= 100000) revert Errors.INVALID_MARKUP(); - uint192 priceMarkup = uint192(token.priceMarkup * (MARKUP_NOMINATOR / 1e4)); - - allowedTokens[token.tokenAddress] = TokenData(token.decimals, uint192(priceMarkup)); - emit ERC20TokenAllowed(token.tokenAddress); - } - - /** - * @notice Remove allowed paymaster tokens - * @param tokenAddress address - Token address to be removed - * @dev Only owner address can call this method - */ - function removeToken(address tokenAddress) external onlyOwner { - delete allowedTokens[tokenAddress]; - emit ERC20TokenRemoved(tokenAddress); - } - - /** - * @notice Withdraw paymaster funds as owner - * @param to address - Token receiver address - * @param amount uint256 - Amount to be withdrawn - * @dev Only owner address can call this method - */ - function withdraw(address to, uint256 amount) external onlyOwner { - // Send paymaster funds to the given address - (bool success, ) = payable(to).call{value: amount}(''); - if (!success) revert Errors.UNAUTHORIZED_WITHDRAW(); - - emit BalanceWithdrawn(to, amount); - } - - /** - * @notice Withdraw paymaster token funds as owner - * @param token address - Token address to withdraw - * @param to address - Token receiver address - * @param amount uint256 - Amount to be withdrawn - * @dev Only owner address can call this method - */ - function withdrawToken(address token, address to, uint256 amount) external onlyOwner { - // Send paymaster funds to the given address - IERC20(token).safeTransfer(to, amount); - } - - /** - * @notice This function calls the oracle and returns the values - * @param - bytes - Oracle manuel payload - * @return uint256 - Oracle return as token price - * @dev bytes parameter is extracted from the paymaster input by the oracle contract, so it is NOT UNUSED here - * @dev This function should be called by an external call inside this contract to pass its specific calldata - */ - function callOracle( - bytes memory //* oracleCalldata */ - ) external view returns (uint256) { - // return getOracleNumericValueFromTxMsg(bytes32('ETH')); - return 1500 * ORACLE_NOMINATOR; - } - - /** - * @notice This function gets the ETH/PARAM_TOKEN price from the oracle - * @param token address - Token address - * @param oracleCalldata bytes - Oracle calldata for Redstone - * @return rate uint256 - ETH/TOKEN price - * @dev TOKEN price is accepted as 1 $ - */ - function getPairPrice( - address token, - bytes memory oracleCalldata - ) public view returns (uint256 rate) { - // Used asset decimals - uint256 tokenDecimals = 10 ** (allowedTokens[token].decimals); - // Oracle value - uint256 value = this.callOracle(oracleCalldata); - - // Calculated token price - rate = - (value * allowedTokens[token].markup * tokenDecimals) / - (ORACLE_NOMINATOR * MARKUP_NOMINATOR); - } - - /** - * @notice This function calculates the gas refund amount for the user - * @param gasLimit uint256 - Gas limit of the transaction - * @param gasRefunded uint256 - Unused gas amount - * @param maxFeePerGas uint256 - Gas price from zkSync transaction - * @param rate uint256 - Conversion rate regarding to ETH/Token pair - * @return refundTokenAmount uint256 - Refund amount as token - */ - function calcRefundAmount( - uint256 gasLimit, - uint256 gasRefunded, - uint256 maxFeePerGas, - uint256 rate - ) public pure returns (uint256 refundTokenAmount) { - if (!(rate > 0)) return refundTokenAmount; - - if (gasLimit > MAX_GAS_TO_SUBSIDIZE) { - uint256 userPaidGas = gasLimit - MAX_GAS_TO_SUBSIDIZE; - uint256 gasUsed = gasLimit - gasRefunded; - - // If, gasUsed is less than we want to pay, compensate user - // Else, we can pay (userPaidGas - (gasUsed - MAX_FEE_TO_SUBSIDIZE)) at best, which can - // be simplified to _maxRefundedGas - uint256 refundGas = MAX_GAS_TO_SUBSIDIZE > gasUsed ? userPaidGas : gasRefunded; - - refundTokenAmount = (refundGas * maxFeePerGas * rate) / PRICE_PAIR_NOMINATOR; - } - } -} diff --git a/contracts/validators/TEEValidator.sol b/contracts/validators/TEEValidator.sol index 87343e9..eee5070 100644 --- a/contracts/validators/TEEValidator.sol +++ b/contracts/validators/TEEValidator.sol @@ -10,8 +10,8 @@ import {VerifierCaller} from '../helpers/VerifierCaller.sol'; * @author https://getclave.io */ contract TEEValidator is IR1Validator, VerifierCaller { - //dummy value - address constant P256_VERIFIER = 0x4323cffC1Fda2da9928cB5A5A9dA45DC8Ee38a2f; + // RIP-7212 enabled + address constant P256_VERIFIER = 0x0000000000000000000000000000000000000100; /// @inheritdoc IR1Validator function validateSignature( diff --git a/hardhat.config.ts b/hardhat.config.ts index 33e318c..0488eaa 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -22,6 +22,7 @@ const zkSyncMainnet: NetworkUserConfig = { verifyURL: 'https://zksync2-mainnet-explorer.zksync.io/contract_verification', chainId: 324, + enableRip7212: true, }; const zkSyncSepolia: NetworkUserConfig = { @@ -30,6 +31,7 @@ const zkSyncSepolia: NetworkUserConfig = { zksync: true, verifyURL: 'https://explorer.sepolia.era.zksync.dev/contract_verification', chainId: 300, + enableRip7212: true, }; const inMemoryNode: NetworkUserConfig = { @@ -37,6 +39,7 @@ const inMemoryNode: NetworkUserConfig = { ethNetwork: '', // in-memory node doesn't support eth node; removing this line will cause an error zksync: true, chainId: 260, + enableRip7212: true, }; const dockerizedNode: NetworkUserConfig = { @@ -44,6 +47,7 @@ const dockerizedNode: NetworkUserConfig = { ethNetwork: 'http://localhost:8545', zksync: true, chainId: 270, + enableRip7212: true, }; const config: HardhatUserConfig = { @@ -67,6 +71,7 @@ const config: HardhatUserConfig = { networks: { hardhat: { zksync: true, + enableRip7212: true, }, zkSyncSepolia, zkSyncMainnet, diff --git a/package.json b/package.json index 47b0757..963bf8d 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,10 @@ "license": "MIT", "main": "index.js", "scripts": { + "compile": "npx hardhat compile", + "deploy:mvp": "npx hardhat deploy-zksync --script deploy-mvp.ts", "lint": "npx prettier --write --plugin=prettier-plugin-solidity 'contracts/**/*.sol'", "lint-check": "npx prettier --list-different --plugin=prettier-plugin-solidity 'contracts/**/*.sol'", - "test": "TEST=true npx hardhat test --network hardhat", - "compile": "npx hardhat compile", - "deploy:mvp": "npx hardhat deploy-zksync --script deploy-mvp.ts" + "test": "TEST=true npx hardhat test --network hardhat" } } diff --git a/subgraph/abis/ClaveZKStake.json b/subgraph/abis/ClaveZKStake.json new file mode 100644 index 0000000..0929591 --- /dev/null +++ b/subgraph/abis/ClaveZKStake.json @@ -0,0 +1,444 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_limitPerUser", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_totalLimit", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reward", + "type": "uint256" + } + ], + "name": "RewardPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "ZK", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "duration", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + } + ], + "name": "earned", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "finishAt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getApy", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getReward", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "lastTimeRewardApplicable", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "limitPerUser", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "notifyRewardAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "registry1", + "outputs": [ + { + "internalType": "contract IClaveRegistry", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "registry2", + "outputs": [ + { + "internalType": "contract IClaveRegistry", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerToken", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerTokenStored", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "rewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_limitPerUser", + "type": "uint256" + } + ], + "name": "setLimitPerUser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + } + ], + "name": "setRewardsDuration", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_totalLimit", + "type": "uint256" + } + ], + "name": "setTotalLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "stake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "updatedAt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "userRewardPerTokenPaid", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdrawReward", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/subgraph/abis/SwapReferralFeePayer.json b/subgraph/abis/SwapReferralFeePayer.json new file mode 100644 index 0000000..d72cb4b --- /dev/null +++ b/subgraph/abis/SwapReferralFeePayer.json @@ -0,0 +1,80 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "referred", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "Cashback", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "referrer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "ReferralFee", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "referrer", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "cashback", + "type": "uint256" + } + ], + "name": "payFee", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/subgraph/abis/SyncClaveStaking.json b/subgraph/abis/SyncClaveStaking.json new file mode 100644 index 0000000..1b876da --- /dev/null +++ b/subgraph/abis/SyncClaveStaking.json @@ -0,0 +1,725 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "_shareToken", "type": "address" }, + { + "internalType": "address", + "name": "_rewardBatchClaimer", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { "inputs": [], "name": "SafeTransferFailed", "type": "error" }, + { "inputs": [], "name": "SafeTransferFromFailed", "type": "error" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "AddRewardToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ClaimRewards", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "RemoveRewardToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "emergency", + "type": "bool" + } + ], + "name": "SetEmergency", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardRate", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardAmount", + "type": "uint256" + } + ], + "name": "SetRewardRate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "rewarder", + "type": "address" + } + ], + "name": "SetRewarder", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint24", + "name": "stakingFee", + "type": "uint24" + } + ], + "name": "SetStakingFee", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "stakingFeeRecipient", + "type": "address" + } + ], + "name": "SetStakingFeeRecipient", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Stake", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "rewardToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "leftover", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newRewardRate", + "type": "uint256" + } + ], + "name": "SupplyRewards", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "inputs": [], + "name": "_status", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" } + ], + "name": "addRewardToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint8", "name": "mode", "type": "uint8" }, + { "internalType": "bytes", "name": "claimData", "type": "bytes" } + ], + "name": "claimAndWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "uint8", "name": "mode", "type": "uint8" }, + { "internalType": "bytes", "name": "claimData", "type": "bytes" } + ], + "name": "claimRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "claveRegistry", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "emergency", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "to", "type": "address" }], + "name": "emergencyWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "rewardToken", "type": "address" } + ], + "name": "getRewardSnapshots", + "outputs": [ + { + "components": [ + { "internalType": "uint256", "name": "timestamp", "type": "uint256" }, + { "internalType": "uint256", "name": "leftover", "type": "uint256" }, + { "internalType": "uint256", "name": "supplied", "type": "uint256" }, + { + "internalType": "uint256", + "name": "newRewardRate", + "type": "uint256" + } + ], + "internalType": "struct SyncSwapStakingClave.RewardSnapshot[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "isRewardToken", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "bool", "name": "updateArray", "type": "bool" } + ], + "name": "removeRewardToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_rewardToken", "type": "address" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "removeRewardTokenFromArray", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "rescueERC20", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address payable", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "rescueETH", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "rewardBatchClaimer", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "rewardClaimers", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "rewardToken", "type": "address" } + ], + "name": "rewardData", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "rewardRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lastUpdate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardPerShare", + "type": "uint256" + } + ], + "internalType": "struct IStaking.RewardData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardDuration", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "rewardSnapshots", + "outputs": [ + { "internalType": "uint256", "name": "timestamp", "type": "uint256" }, + { "internalType": "uint256", "name": "leftover", "type": "uint256" }, + { "internalType": "uint256", "name": "supplied", "type": "uint256" }, + { "internalType": "uint256", "name": "newRewardRate", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "rewardToken", "type": "address" } + ], + "name": "rewardSnapshotsLength", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "rewardTokens", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardTokensLength", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bool", "name": "_emergency", "type": "bool" } + ], + "name": "setEmergency", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_rewardBatchClaimer", + "type": "address" + } + ], + "name": "setRewardBatchClaimer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_token", "type": "address" }, + { "internalType": "address", "name": "_rewardClaimer", "type": "address" } + ], + "name": "setRewardClaimer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_rewardDuration", + "type": "uint256" + } + ], + "name": "setRewardDuration", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_token", "type": "address" }, + { + "internalType": "uint256", + "name": "_newRewardRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_newRewardAmount", + "type": "uint256" + }, + { "internalType": "bool", "name": "shouldUpdate", "type": "bool" } + ], + "name": "setRewardRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "index", "type": "uint256" }, + { "internalType": "address", "name": "oldToken", "type": "address" }, + { "internalType": "address", "name": "newToken", "type": "address" } + ], + "name": "setRewardToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint24", "name": "_stakingFee", "type": "uint24" } + ], + "name": "setStakingFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_stakingFeeRecipient", + "type": "address" + } + ], + "name": "setStakingFeeRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "shareToken", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "address", "name": "to", "type": "address" } + ], + "name": "stake", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stakingFee", + "outputs": [{ "internalType": "uint24", "name": "", "type": "uint24" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "stakingFeeRecipient", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "supplyRewards", + "outputs": [ + { "internalType": "bool", "name": "", "type": "bool" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "totalRewardAmount", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalStaked", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "uint8", "name": "mode", "type": "uint8" }, + { "internalType": "bytes", "name": "claimData", "type": "bytes" } + ], + "name": "update", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_claveRegistry", "type": "address" } + ], + "name": "updateClaveRegistry", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "rewardToken", "type": "address" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "userRewardData", + "outputs": [ + { + "components": [ + { "internalType": "uint256", "name": "claimable", "type": "uint256" }, + { + "internalType": "uint256", + "name": "debtRewardPerShare", + "type": "uint256" + } + ], + "internalType": "struct IStaking.UserRewardData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "userStaked", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "address", "name": "to", "type": "address" } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/subgraph/abis/SyncStablePool.json b/subgraph/abis/SyncStablePool.json new file mode 100644 index 0000000..d867425 --- /dev/null +++ b/subgraph/abis/SyncStablePool.json @@ -0,0 +1,950 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "Expired", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientLiquidityMinted", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSignature", + "type": "error" + }, + { + "inputs": [], + "name": "Overflow", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Swap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "reserve0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reserve1", + "type": "uint256" + } + ], + "name": "Sync", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + }, + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, + { + "internalType": "address", + "name": "_callback", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_callbackData", + "type": "bytes" + } + ], + "name": "burn", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct IPool.TokenAmount[]", + "name": "_amounts", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + }, + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, + { + "internalType": "address", + "name": "_callback", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_callbackData", + "type": "bytes" + } + ], + "name": "burnSingle", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct IPool.TokenAmount", + "name": "_tokenAmount", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amountOut", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_sender", + "type": "address" + } + ], + "name": "getAmountIn", + "outputs": [ + { + "internalType": "uint256", + "name": "_amountIn", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amountIn", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_sender", + "type": "address" + } + ], + "name": "getAmountOut", + "outputs": [ + { + "internalType": "uint256", + "name": "_amountOut", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAssets", + "outputs": [ + { + "internalType": "address[]", + "name": "assets", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getProtocolFee", + "outputs": [ + { + "internalType": "uint24", + "name": "_protocolFee", + "type": "uint24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getReserves", + "outputs": [ + { + "internalType": "uint256", + "name": "_reserve0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_reserve1", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, + { + "internalType": "address", + "name": "_tokenIn", + "type": "address" + }, + { + "internalType": "address", + "name": "_tokenOut", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "getSwapFee", + "outputs": [ + { + "internalType": "uint24", + "name": "_swapFee", + "type": "uint24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "invariantLast", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "master", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + }, + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, + { + "internalType": "address", + "name": "_callback", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_callbackData", + "type": "bytes" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "address", + "name": "_spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "_v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "_r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "address", + "name": "_spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_deadline", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "permit2", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "poolType", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "reserve0", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "reserve1", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceID", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + }, + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, + { + "internalType": "address", + "name": "_callback", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_callbackData", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct IPool.TokenAmount", + "name": "_tokenAmount", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token0", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token0PrecisionMultiplier", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token1", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token1PrecisionMultiplier", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vault", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/subgraph/abis/SyncStaking.json b/subgraph/abis/SyncStaking.json new file mode 100644 index 0000000..43b7090 --- /dev/null +++ b/subgraph/abis/SyncStaking.json @@ -0,0 +1,27 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "reward", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ClaimRewards", + "type": "event" + } +] diff --git a/subgraph/abis/VenusReward.json b/subgraph/abis/VenusReward.json new file mode 100644 index 0000000..93cd28f --- /dev/null +++ b/subgraph/abis/VenusReward.json @@ -0,0 +1,39 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "supplier", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardTokenDelta", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardTokenTotal", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardTokenSupplyIndex", + "type": "uint256" + } + ], + "name": "DistributedSupplierRewardToken", + "type": "event" + } +] diff --git a/subgraph/abis/VenusToken.json b/subgraph/abis/VenusToken.json new file mode 100644 index 0000000..146a39c --- /dev/null +++ b/subgraph/abis/VenusToken.json @@ -0,0 +1,64 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mintAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mintTokens", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accountBalance", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "redeemAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accountBalance", + "type": "uint256" + } + ], + "name": "Redeem", + "type": "event" + } +] diff --git a/subgraph/abis/ZtakeV2.json b/subgraph/abis/ZtakeV2.json new file mode 100644 index 0000000..2c51e19 --- /dev/null +++ b/subgraph/abis/ZtakeV2.json @@ -0,0 +1,535 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_limitPerUser", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_totalLimit", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_stakeToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_registry", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reward", + "type": "uint256" + } + ], + "name": "RewardPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "duration", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + } + ], + "name": "earned", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "finishAt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_rate", + "type": "uint256" + } + ], + "name": "getApy", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getReward", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "lastTimeRewardApplicable", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "limitPerUser", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "notifyRewardAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "registry", + "outputs": [ + { + "internalType": "contract IClaveRegistry", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerToken", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerTokenStored", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardToken", + "outputs": [ + { + "internalType": "contract IERC20Metadata", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "rewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_limitPerUser", + "type": "uint256" + } + ], + "name": "setLimitPerUser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + } + ], + "name": "setRewardsDuration", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_totalLimit", + "type": "uint256" + } + ], + "name": "setTotalLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "stake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stakeToken", + "outputs": [ + { + "internalType": "contract IERC20Metadata", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_registry", + "type": "address" + } + ], + "name": "updateRegistry", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "updatedAt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "userRewardPerTokenPaid", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdrawReward", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/subgraph/abis/zeroUsdtPool.json b/subgraph/abis/zeroUsdtPool.json new file mode 100644 index 0000000..7b87c8a --- /dev/null +++ b/subgraph/abis/zeroUsdtPool.json @@ -0,0 +1,1227 @@ +[ + { + "inputs": [ + { + "internalType": "contract IPoolAddressesProvider", + "name": "provider", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "backer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "BackUnbacked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "onBehalfOf", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "enum DataTypes.InterestRateMode", + "name": "interestRateMode", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "borrowRate", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint16", + "name": "referralCode", + "type": "uint16" + } + ], + "name": "Borrow", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "initiator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "enum DataTypes.InterestRateMode", + "name": "interestRateMode", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "premium", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint16", + "name": "referralCode", + "type": "uint16" + } + ], + "name": "FlashLoan", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalDebt", + "type": "uint256" + } + ], + "name": "IsolationModeTotalDebtUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "collateralAsset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "debtAsset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "debtToCover", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "liquidatedCollateralAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "receiveAToken", + "type": "bool" + } + ], + "name": "LiquidationCall", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "onBehalfOf", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint16", + "name": "referralCode", + "type": "uint16" + } + ], + "name": "MintUnbacked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountMinted", + "type": "uint256" + } + ], + "name": "MintedToTreasury", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "RebalanceStableBorrowRate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "repayer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "useATokens", + "type": "bool" + } + ], + "name": "Repay", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "liquidityRate", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "stableBorrowRate", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "variableBorrowRate", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "liquidityIndex", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "variableBorrowIndex", + "type": "uint256" + } + ], + "name": "ReserveDataUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "ReserveUsedAsCollateralDisabled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "ReserveUsedAsCollateralEnabled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "onBehalfOf", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint16", + "name": "referralCode", + "type": "uint16" + } + ], + "name": "Supply", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum DataTypes.InterestRateMode", + "name": "interestRateMode", + "type": "uint8" + } + ], + "name": "SwapBorrowRateMode", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "categoryId", + "type": "uint8" + } + ], + "name": "UserEModeSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "inputs": [], + "name": "ADDRESSES_PROVIDER", + "outputs": [ + { + "internalType": "contract IPoolAddressesProvider", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "BRIDGE_PROTOCOL_FEE", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "FLASHLOAN_PREMIUM_TOTAL", + "outputs": [{ "internalType": "uint128", "name": "", "type": "uint128" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "FLASHLOAN_PREMIUM_TO_PROTOCOL", + "outputs": [{ "internalType": "uint128", "name": "", "type": "uint128" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_NUMBER_RESERVES", + "outputs": [{ "internalType": "uint16", "name": "", "type": "uint16" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_STABLE_RATE_BORROW_SIZE_PERCENT", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "POOL_REVISION", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "fee", "type": "uint256" } + ], + "name": "backUnbacked", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { + "internalType": "uint256", + "name": "interestRateMode", + "type": "uint256" + }, + { "internalType": "uint16", "name": "referralCode", "type": "uint16" }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" } + ], + "name": "borrow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint8", "name": "id", "type": "uint8" }, + { + "components": [ + { "internalType": "uint16", "name": "ltv", "type": "uint16" }, + { + "internalType": "uint16", + "name": "liquidationThreshold", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "liquidationBonus", + "type": "uint16" + }, + { + "internalType": "address", + "name": "priceSource", + "type": "address" + }, + { "internalType": "string", "name": "label", "type": "string" } + ], + "internalType": "struct DataTypes.EModeCategory", + "name": "category", + "type": "tuple" + } + ], + "name": "configureEModeCategory", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "uint16", "name": "referralCode", "type": "uint16" } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" } + ], + "name": "dropReserve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { + "internalType": "uint256", + "name": "balanceFromBefore", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "balanceToBefore", + "type": "uint256" + } + ], + "name": "finalizeTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiverAddress", + "type": "address" + }, + { "internalType": "address[]", "name": "assets", "type": "address[]" }, + { "internalType": "uint256[]", "name": "amounts", "type": "uint256[]" }, + { + "internalType": "uint256[]", + "name": "interestRateModes", + "type": "uint256[]" + }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "bytes", "name": "params", "type": "bytes" }, + { "internalType": "uint16", "name": "referralCode", "type": "uint16" } + ], + "name": "flashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiverAddress", + "type": "address" + }, + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "bytes", "name": "params", "type": "bytes" }, + { "internalType": "uint16", "name": "referralCode", "type": "uint16" } + ], + "name": "flashLoanSimple", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" } + ], + "name": "getConfiguration", + "outputs": [ + { + "components": [ + { "internalType": "uint256", "name": "data", "type": "uint256" } + ], + "internalType": "struct DataTypes.ReserveConfigurationMap", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint8", "name": "id", "type": "uint8" }], + "name": "getEModeCategoryData", + "outputs": [ + { + "components": [ + { "internalType": "uint16", "name": "ltv", "type": "uint16" }, + { + "internalType": "uint16", + "name": "liquidationThreshold", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "liquidationBonus", + "type": "uint16" + }, + { + "internalType": "address", + "name": "priceSource", + "type": "address" + }, + { "internalType": "string", "name": "label", "type": "string" } + ], + "internalType": "struct DataTypes.EModeCategory", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint16", "name": "id", "type": "uint16" }], + "name": "getReserveAddressById", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" } + ], + "name": "getReserveData", + "outputs": [ + { + "components": [ + { + "components": [ + { "internalType": "uint256", "name": "data", "type": "uint256" } + ], + "internalType": "struct DataTypes.ReserveConfigurationMap", + "name": "configuration", + "type": "tuple" + }, + { + "internalType": "uint128", + "name": "liquidityIndex", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "currentLiquidityRate", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "variableBorrowIndex", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "currentVariableBorrowRate", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "currentStableBorrowRate", + "type": "uint128" + }, + { + "internalType": "uint40", + "name": "lastUpdateTimestamp", + "type": "uint40" + }, + { "internalType": "uint16", "name": "id", "type": "uint16" }, + { + "internalType": "address", + "name": "aTokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "stableDebtTokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "variableDebtTokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "interestRateStrategyAddress", + "type": "address" + }, + { + "internalType": "uint128", + "name": "accruedToTreasury", + "type": "uint128" + }, + { "internalType": "uint128", "name": "unbacked", "type": "uint128" }, + { + "internalType": "uint128", + "name": "isolationModeTotalDebt", + "type": "uint128" + } + ], + "internalType": "struct DataTypes.ReserveData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" } + ], + "name": "getReserveNormalizedIncome", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" } + ], + "name": "getReserveNormalizedVariableDebt", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getReservesList", + "outputs": [ + { "internalType": "address[]", "name": "", "type": "address[]" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getUserAccountData", + "outputs": [ + { + "internalType": "uint256", + "name": "totalCollateralBase", + "type": "uint256" + }, + { "internalType": "uint256", "name": "totalDebtBase", "type": "uint256" }, + { + "internalType": "uint256", + "name": "availableBorrowsBase", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentLiquidationThreshold", + "type": "uint256" + }, + { "internalType": "uint256", "name": "ltv", "type": "uint256" }, + { "internalType": "uint256", "name": "healthFactor", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getUserConfiguration", + "outputs": [ + { + "components": [ + { "internalType": "uint256", "name": "data", "type": "uint256" } + ], + "internalType": "struct DataTypes.UserConfigurationMap", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getUserEMode", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "address", "name": "aTokenAddress", "type": "address" }, + { + "internalType": "address", + "name": "stableDebtAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "variableDebtAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "interestRateStrategyAddress", + "type": "address" + } + ], + "name": "initReserve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPoolAddressesProvider", + "name": "provider", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "collateralAsset", + "type": "address" + }, + { "internalType": "address", "name": "debtAsset", "type": "address" }, + { "internalType": "address", "name": "user", "type": "address" }, + { "internalType": "uint256", "name": "debtToCover", "type": "uint256" }, + { "internalType": "bool", "name": "receiveAToken", "type": "bool" } + ], + "name": "liquidationCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address[]", "name": "assets", "type": "address[]" } + ], + "name": "mintToTreasury", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "uint16", "name": "referralCode", "type": "uint16" } + ], + "name": "mintUnbacked", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "rebalanceStableBorrowRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { + "internalType": "uint256", + "name": "interestRateMode", + "type": "uint256" + }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" } + ], + "name": "repay", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { + "internalType": "uint256", + "name": "interestRateMode", + "type": "uint256" + } + ], + "name": "repayWithATokens", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { + "internalType": "uint256", + "name": "interestRateMode", + "type": "uint256" + }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint8", "name": "permitV", "type": "uint8" }, + { "internalType": "bytes32", "name": "permitR", "type": "bytes32" }, + { "internalType": "bytes32", "name": "permitS", "type": "bytes32" } + ], + "name": "repayWithPermit", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "rescueTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" } + ], + "name": "resetIsolationModeTotalDebt", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "data", "type": "uint256" } + ], + "internalType": "struct DataTypes.ReserveConfigurationMap", + "name": "configuration", + "type": "tuple" + } + ], + "name": "setConfiguration", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { + "internalType": "address", + "name": "rateStrategyAddress", + "type": "address" + } + ], + "name": "setReserveInterestRateStrategyAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint8", "name": "categoryId", "type": "uint8" } + ], + "name": "setUserEMode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "bool", "name": "useAsCollateral", "type": "bool" } + ], + "name": "setUserUseReserveAsCollateral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "uint16", "name": "referralCode", "type": "uint16" } + ], + "name": "supply", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "uint16", "name": "referralCode", "type": "uint16" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint8", "name": "permitV", "type": "uint8" }, + { "internalType": "bytes32", "name": "permitR", "type": "bytes32" }, + { "internalType": "bytes32", "name": "permitS", "type": "bytes32" } + ], + "name": "supplyWithPermit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { + "internalType": "uint256", + "name": "interestRateMode", + "type": "uint256" + } + ], + "name": "swapBorrowRateMode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "protocolFee", "type": "uint256" } + ], + "name": "updateBridgeProtocolFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "flashLoanPremiumTotal", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "flashLoanPremiumToProtocol", + "type": "uint128" + } + ], + "name": "updateFlashloanPremiums", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "asset", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "address", "name": "to", "type": "address" } + ], + "name": "withdraw", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/subgraph/networks.json b/subgraph/networks.json index af87059..8005f24 100644 --- a/subgraph/networks.json +++ b/subgraph/networks.json @@ -25,6 +25,9 @@ "SocialRecovery": { "address": "0x9eF467CAA8291c6DAdD08EA458D763a8258B347e", "startBlock": 24799912 + }, + "zeroUsdtPool": { + "address": "0x4d9429246EA989C9CeE203B43F6d1C7D83e3B8F8" } } } diff --git a/subgraph/schema.graphql b/subgraph/schema.graphql index 03b744f..db6f378 100644 --- a/subgraph/schema.graphql +++ b/subgraph/schema.graphql @@ -6,8 +6,18 @@ type Total @entity { transactions: Int! backedUp: Int! gasSponsored: BigInt! - invested: BigInt! - realizedGain: BigInt! +} + +type Day @entity { + "day start timestamp concat 0x0000" + id: Bytes! + createdAccounts: Int! + deployedAccounts: Int! + activeAccounts: Int! + transactions: Int! + gasSponsored: BigInt! + investFlow: [DailyEarnFlow!]! @derivedFrom(field: "day") + swappedTo: [DailySwappedTo!]! @derivedFrom(field: "day") } type Week @entity { @@ -18,9 +28,7 @@ type Week @entity { activeAccounts: Int! transactions: Int! gasSponsored: BigInt! - investIn: BigInt! - investOut: BigInt! - realizedGain: BigInt! + investFlow: [WeeklyEarnFlow!]! @derivedFrom(field: "week") swappedTo: [WeeklySwappedTo!]! @derivedFrom(field: "week") } @@ -32,12 +40,72 @@ type Month @entity { activeAccounts: Int! transactions: Int! gasSponsored: BigInt! - investIn: BigInt! - investOut: BigInt! - realizedGain: BigInt! + investFlow: [MonthlyEarnFlow!]! @derivedFrom(field: "month") swappedTo: [MonthlySwappedTo!]! @derivedFrom(field: "month") } +enum EarnProtocol { + Koi + SyncSwap + ZeroLend + Clave + Meow + Venus +} + +type EarnPosition @entity { + "account.id.concat(pool).concat(token)" + id: Bytes! + account: ClaveAccount! + pool: Bytes! + token: Bytes! + protocol: EarnProtocol! + invested: BigInt! + compoundGain: BigInt! + normalGain: BigInt! +} + +type DailyEarnFlow @entity { + "day.id.concat(erc20).concat(protocol)" + id: Bytes! + day: Day! + erc20: Bytes! + protocol: EarnProtocol! + amountIn: BigInt! + amountOut: BigInt! + claimedGain: BigInt! +} + +type WeeklyEarnFlow @entity { + "week.id.concat(erc20).concat(protocol)" + id: Bytes! + week: Week! + erc20: Bytes! + protocol: EarnProtocol! + amountIn: BigInt! + amountOut: BigInt! + claimedGain: BigInt! +} + +type MonthlyEarnFlow @entity { + "month.id.concat(erc20).concat(protocol)" + id: Bytes! + month: Month! + erc20: Bytes! + protocol: EarnProtocol! + amountIn: BigInt! + amountOut: BigInt! + claimedGain: BigInt! +} + +type DailySwappedTo @entity { + "day.id.concat(erc20)" + id: Bytes! + day: Day! + erc20: Bytes! + amount: BigInt! +} + type WeeklySwappedTo @entity { "week.id.concat(erc20)" id: Bytes! @@ -54,6 +122,13 @@ type MonthlySwappedTo @entity { amount: BigInt! } +type DayAccount @entity(immutable: true) { + "account.id.concat(day.id)" + id: Bytes! + account: ClaveAccount! + day: Day! +} + type WeekAccount @entity(immutable: true) { "account.id.concat(week.id)" id: Bytes! @@ -79,11 +154,31 @@ type ClaveAccount @entity { txCount: Int! "account implementation address" implementation: Bytes - invested: BigInt! - realizedGain: BigInt! + earnPositions: [EarnPosition!]! @derivedFrom(field: "account") transactions: [ClaveTransaction!]! @derivedFrom(field: "sender") inAppSwaps: [InAppSwap!]! @derivedFrom(field: "account") + activeDays: [DayAccount!]! @derivedFrom(field: "account") activeWeeks: [WeekAccount!]! @derivedFrom(field: "account") + activeMonths: [MonthAccount!]! @derivedFrom(field: "account") + cashbacks: [Cashback!]! @derivedFrom(field: "account") + referralFees: [ReferralFee!]! @derivedFrom(field: "account") +} + +type Cashback @entity { + "account.id.concat(erc20).concat('0xcb')" + id: Bytes! + account: ClaveAccount! + erc20: Bytes! + amount: BigInt! +} + +type ReferralFee @entity { + "account.id.refferred.id.concat(erc20)" + id: Bytes! + account: ClaveAccount! + referred: ClaveAccount! + erc20: Bytes! + amount: BigInt! } enum Paymaster { diff --git a/subgraph/src/account-factory-v2.ts b/subgraph/src/account-factory-v2.ts index a08daa5..cd795e5 100644 --- a/subgraph/src/account-factory-v2.ts +++ b/subgraph/src/account-factory-v2.ts @@ -10,10 +10,16 @@ import { Bytes } from '@graphprotocol/graph-ts'; import { ClaveAccountCreated as ClaveAccountCreatedEvent, ClaveAccountDeployed as ClaveAccountDeployedEvent, -} from '../generated/Contract/Contract'; +} from '../generated/AccountFactoryV2/AccountFactoryV2'; import { ClaveAccount } from '../generated/schema'; import { Account } from '../generated/templates'; -import { ZERO, getOrCreateMonth, getOrCreateWeek, getTotal } from './helpers'; +import { + ZERO, + getOrCreateDay, + getOrCreateMonth, + getOrCreateWeek, + getTotal, +} from './helpers'; export function handleClaveAccountCreated( event: ClaveAccountCreatedEvent, @@ -23,10 +29,12 @@ export function handleClaveAccountCreated( return; } account = new ClaveAccount(event.params.accountAddress); + const day = getOrCreateDay(event.block.timestamp); const week = getOrCreateWeek(event.block.timestamp); const month = getOrCreateMonth(event.block.timestamp); const total = getTotal(); + day.createdAccounts = day.createdAccounts + 1; week.createdAccounts = week.createdAccounts + 1; month.createdAccounts = month.createdAccounts + 1; total.createdAccounts = total.createdAccounts + 1; @@ -35,9 +43,8 @@ export function handleClaveAccountCreated( account.isRecovering = false; account.recoveryCount = 0; account.txCount = 0; - account.invested = ZERO; - account.realizedGain = ZERO; + day.save(); week.save(); month.save(); total.save(); @@ -49,9 +56,11 @@ export function handleClaveAccountDeployed( event: ClaveAccountDeployedEvent, ): void { let account = ClaveAccount.load(event.params.accountAddress); + const day = getOrCreateDay(event.block.timestamp); const week = getOrCreateWeek(event.block.timestamp); const month = getOrCreateMonth(event.block.timestamp); const total = getTotal(); + day.deployedAccounts = day.deployedAccounts + 1; week.deployedAccounts = week.deployedAccounts + 1; month.deployedAccounts = month.deployedAccounts + 1; total.deployedAccounts = total.deployedAccounts + 1; @@ -63,8 +72,6 @@ export function handleClaveAccountDeployed( account.recoveryCount = 0; account.txCount = 0; account.creationDate = ZERO; - account.invested = ZERO; - account.realizedGain = ZERO; } account.implementation = Bytes.fromHexString( @@ -72,6 +79,7 @@ export function handleClaveAccountDeployed( ); account.deployDate = event.block.timestamp; + day.save(); week.save(); month.save(); total.save(); diff --git a/subgraph/src/account-factory.ts b/subgraph/src/account-factory.ts index aa45854..5ef731f 100644 --- a/subgraph/src/account-factory.ts +++ b/subgraph/src/account-factory.ts @@ -18,7 +18,13 @@ import { NewClaveAccount as NewClaveAccountEvent } from '../generated/AccountFac import { ClaveAccount } from '../generated/schema'; import { Account } from '../generated/templates'; import { wallets } from '../wallets'; -import { ZERO, getOrCreateMonth, getOrCreateWeek, getTotal } from './helpers'; +import { + ZERO, + getOrCreateDay, + getOrCreateMonth, + getOrCreateWeek, + getTotal, +} from './helpers'; // eslint-disable-next-line @typescript-eslint/no-unused-vars export function handleOnce(_block: ethereum.Block): void { @@ -30,10 +36,12 @@ export function handleOnce(_block: ethereum.Block): void { Date.parse(createdAt).getTime(), ).div(BigInt.fromU32(1000)); const account = new ClaveAccount(Bytes.fromHexString(accountAddress)); + const day = getOrCreateDay(createdAtDate); const week = getOrCreateWeek(createdAtDate); const month = getOrCreateMonth(createdAtDate); const total = getTotal(); + day.createdAccounts = day.createdAccounts + 1; week.createdAccounts = week.createdAccounts + 1; month.createdAccounts = month.createdAccounts + 1; total.createdAccounts = total.createdAccounts + 1; @@ -42,9 +50,8 @@ export function handleOnce(_block: ethereum.Block): void { account.isRecovering = false; account.recoveryCount = 0; account.txCount = 0; - account.invested = ZERO; - account.realizedGain = ZERO; + day.save(); week.save(); month.save(); total.save(); @@ -55,9 +62,11 @@ export function handleOnce(_block: ethereum.Block): void { export function handleNewClaveAccount(event: NewClaveAccountEvent): void { let account = ClaveAccount.load(event.params.accountAddress); + const day = getOrCreateDay(event.block.timestamp); const week = getOrCreateWeek(event.block.timestamp); const month = getOrCreateMonth(event.block.timestamp); const total = getTotal(); + day.deployedAccounts = day.deployedAccounts + 1; week.deployedAccounts = week.deployedAccounts + 1; month.deployedAccounts = month.deployedAccounts + 1; total.deployedAccounts = total.deployedAccounts + 1; @@ -69,14 +78,13 @@ export function handleNewClaveAccount(event: NewClaveAccountEvent): void { account.recoveryCount = 0; account.txCount = 0; account.creationDate = ZERO; - account.invested = ZERO; - account.realizedGain = ZERO; } account.implementation = Bytes.fromHexString( '0xdd4dD37B22Fc16DBFF3daB6Ecd681798c459f275', ); account.deployDate = event.block.timestamp; + day.save(); week.save(); month.save(); total.save(); diff --git a/subgraph/src/account.ts b/subgraph/src/account.ts index e0b900a..eb29c07 100644 --- a/subgraph/src/account.ts +++ b/subgraph/src/account.ts @@ -7,12 +7,14 @@ /* eslint-disable @typescript-eslint/consistent-type-imports */ import { BigInt } from '@graphprotocol/graph-ts'; +import { ClaveAccount, ClaveTransaction } from '../generated/schema'; import { FeePaid as FeePaidEvent, Upgraded as UpgradedEvent, -} from '../generated/ClaveImplementation/ClaveImplementation'; -import { ClaveAccount, ClaveTransaction } from '../generated/schema'; +} from '../generated/templates/Account/ClaveImplementation'; import { + getOrCreateDay, + getOrCreateDayAccount, getOrCreateMonth, getOrCreateMonthAccount, getOrCreateWeek, @@ -22,11 +24,14 @@ import { export function handleFeePaid(event: FeePaidEvent): void { const transaction = new ClaveTransaction(event.transaction.hash); + const day = getOrCreateDay(event.block.timestamp); const week = getOrCreateWeek(event.block.timestamp); const month = getOrCreateMonth(event.block.timestamp); const total = getTotal(); const account = ClaveAccount.load(event.address); if (account != null) { + const dayAccount = getOrCreateDayAccount(account, day); + dayAccount.save(); const weekAccount = getOrCreateWeekAccount(account, week); weekAccount.save(); const monthAccount = getOrCreateMonthAccount(account, month); @@ -35,6 +40,7 @@ export function handleFeePaid(event: FeePaidEvent): void { account.txCount = account.txCount + 1; account.save(); } + day.transactions = day.transactions + 1; week.transactions = week.transactions + 1; month.transactions = month.transactions + 1; total.transactions = total.transactions + 1; @@ -51,6 +57,7 @@ export function handleFeePaid(event: FeePaidEvent): void { transaction.paymaster = 'None'; transaction.date = event.block.timestamp; + day.save(); week.save(); month.save(); total.save(); diff --git a/subgraph/src/clave-appa-stake.ts b/subgraph/src/clave-appa-stake.ts new file mode 100644 index 0000000..c933772 --- /dev/null +++ b/subgraph/src/clave-appa-stake.ts @@ -0,0 +1,124 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ + +/* eslint-disable @typescript-eslint/consistent-type-imports */ +import { + RewardPaid as RewardPaidEvent, + Staked as StakedEvent, + Withdrawn as WithdrawnEvent, +} from '../generated/ClaveAPPAStake/ZtakeV2'; +import { ClaveAccount } from '../generated/schema'; +import { + getOrCreateDailyEarnFlow, + getOrCreateDay, + getOrCreateEarnPosition, + getOrCreateMonth, + getOrCreateMonthlyEarnFlow, + getOrCreateWeek, + getOrCreateWeeklyEarnFlow, +} from './helpers'; + +const protocol = 'Clave'; + +export function handleStaked(event: StakedEvent): void { + const account = ClaveAccount.load(event.params.user); + if (!account) { + return; + } + + const pool = event.address; + const token = event.params.token; + const amount = event.params.amount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountIn = dailyEarnFlow.amountIn.plus(amount); + weeklyEarnFlow.amountIn = weeklyEarnFlow.amountIn.plus(amount); + monthlyEarnFlow.amountIn = monthlyEarnFlow.amountIn.plus(amount); + earnPosition.invested = earnPosition.invested.plus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} + +export function handleWithdrawn(event: WithdrawnEvent): void { + const account = ClaveAccount.load(event.transaction.from); + if (!account) { + return; + } + + const pool = event.address; + const token = event.params.token; + const amount = event.params.amount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(amount); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(amount); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(amount); + earnPosition.invested = earnPosition.invested.minus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} + +export function handleRewardPaid(event: RewardPaidEvent): void { + const account = ClaveAccount.load(event.params.user); + if (!account) { + return; + } + + const pool = event.address; + const token = event.params.token; + const amount = event.params.reward; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.claimedGain = dailyEarnFlow.claimedGain.plus(amount); + weeklyEarnFlow.claimedGain = weeklyEarnFlow.claimedGain.plus(amount); + monthlyEarnFlow.claimedGain = monthlyEarnFlow.claimedGain.plus(amount); + earnPosition.normalGain = earnPosition.normalGain.plus(amount); +} diff --git a/subgraph/src/clave-zk-stake.ts b/subgraph/src/clave-zk-stake.ts new file mode 100644 index 0000000..aeceb89 --- /dev/null +++ b/subgraph/src/clave-zk-stake.ts @@ -0,0 +1,126 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ + +/* eslint-disable @typescript-eslint/consistent-type-imports */ +import { Address } from '@graphprotocol/graph-ts'; + +import { + RewardPaid as RewardPaidEvent, + Staked as StakedEvent, + Withdrawn as WithdrawnEvent, +} from '../generated/ClaveZKStake/ClaveZKStake'; +import { ClaveAccount } from '../generated/schema'; +import { + getOrCreateDailyEarnFlow, + getOrCreateDay, + getOrCreateEarnPosition, + getOrCreateMonth, + getOrCreateMonthlyEarnFlow, + getOrCreateWeek, + getOrCreateWeeklyEarnFlow, +} from './helpers'; + +const protocol = 'Clave'; +const token = Address.fromHexString( + '0x5A7d6b2F92C77FAD6CCaBd7EE0624E64907Eaf3E', +); + +export function handleStaked(event: StakedEvent): void { + const account = ClaveAccount.load(event.params.user); + if (!account) { + return; + } + + const pool = event.address; + const amount = event.params.amount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountIn = dailyEarnFlow.amountIn.plus(amount); + weeklyEarnFlow.amountIn = weeklyEarnFlow.amountIn.plus(amount); + monthlyEarnFlow.amountIn = monthlyEarnFlow.amountIn.plus(amount); + earnPosition.invested = earnPosition.invested.plus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} + +export function handleWithdrawn(event: WithdrawnEvent): void { + const account = ClaveAccount.load(event.transaction.from); + if (!account) { + return; + } + + const pool = event.address; + const amount = event.params.amount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(amount); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(amount); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(amount); + earnPosition.invested = earnPosition.invested.minus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} + +export function handleRewardPaid(event: RewardPaidEvent): void { + const account = ClaveAccount.load(event.params.user); + if (!account) { + return; + } + + const pool = event.address; + const amount = event.params.reward; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.claimedGain = dailyEarnFlow.claimedGain.plus(amount); + weeklyEarnFlow.claimedGain = weeklyEarnFlow.claimedGain.plus(amount); + monthlyEarnFlow.claimedGain = monthlyEarnFlow.claimedGain.plus(amount); + earnPosition.normalGain = earnPosition.normalGain.plus(amount); +} diff --git a/subgraph/src/erc-20-paymaster.ts b/subgraph/src/erc-20-paymaster.ts index acaf2e7..39330ce 100644 --- a/subgraph/src/erc-20-paymaster.ts +++ b/subgraph/src/erc-20-paymaster.ts @@ -10,6 +10,8 @@ import { BigInt } from '@graphprotocol/graph-ts'; import { ERC20PaymasterUsed as ERC20PaymasterUsedEvent } from '../generated/ERC20Paymaster/ERC20Paymaster'; import { ClaveAccount, ClaveTransaction } from '../generated/schema'; import { + getOrCreateDay, + getOrCreateDayAccount, getOrCreateMonth, getOrCreateMonthAccount, getOrCreateWeek, @@ -23,13 +25,17 @@ export function handleERC20PaymasterUsed(event: ERC20PaymasterUsedEvent): void { return; } + const day = getOrCreateDay(event.block.timestamp); const week = getOrCreateWeek(event.block.timestamp); const month = getOrCreateMonth(event.block.timestamp); const total = getTotal(); + day.transactions = day.transactions + 1; week.transactions = week.transactions + 1; month.transactions = month.transactions + 1; total.transactions = total.transactions + 1; + const dayAccount = getOrCreateDayAccount(account, day); + dayAccount.save(); const weekAccount = getOrCreateWeekAccount(account, week); weekAccount.save(); const monthAccount = getOrCreateMonthAccount(account, month); @@ -52,6 +58,7 @@ export function handleERC20PaymasterUsed(event: ERC20PaymasterUsedEvent): void { account.txCount = account.txCount + 1; account.save(); + day.save(); week.save(); month.save(); total.save(); diff --git a/subgraph/src/gasless-paymaster.ts b/subgraph/src/gasless-paymaster.ts index 8d25d7a..f36a252 100644 --- a/subgraph/src/gasless-paymaster.ts +++ b/subgraph/src/gasless-paymaster.ts @@ -10,6 +10,8 @@ import { BigInt } from '@graphprotocol/graph-ts'; import { FeeSponsored as FeeSponsoredEvent } from '../generated/GaslessPaymaster/GaslessPaymaster'; import { ClaveAccount, ClaveTransaction } from '../generated/schema'; import { + getOrCreateDay, + getOrCreateDayAccount, getOrCreateMonth, getOrCreateMonthAccount, getOrCreateWeek, @@ -19,16 +21,20 @@ import { export function handleFeeSponsored(event: FeeSponsoredEvent): void { const transaction = new ClaveTransaction(event.transaction.hash); + const day = getOrCreateDay(event.block.timestamp); const week = getOrCreateWeek(event.block.timestamp); const month = getOrCreateMonth(event.block.timestamp); const total = getTotal(); const account = ClaveAccount.load(event.params.user); if (account != null) { + const dayAccount = getOrCreateDayAccount(account, day); + dayAccount.save(); const weekAccount = getOrCreateWeekAccount(account, week); weekAccount.save(); const monthAccount = getOrCreateMonthAccount(account, month); monthAccount.save(); + day.transactions = day.transactions + 1; week.transactions = week.transactions + 1; month.transactions = month.transactions + 1; total.transactions = total.transactions + 1; @@ -44,6 +50,7 @@ export function handleFeeSponsored(event: FeeSponsoredEvent): void { gasUsed = receipt.gasUsed; } const gasCost = event.transaction.gasPrice.times(gasUsed); + day.gasSponsored = day.gasSponsored.plus(gasCost); week.gasSponsored = week.gasSponsored.plus(gasCost); month.gasSponsored = month.gasSponsored.plus(gasCost); total.gasSponsored = total.gasSponsored.plus(gasCost); @@ -55,6 +62,7 @@ export function handleFeeSponsored(event: FeeSponsoredEvent): void { account.save(); } + day.save(); week.save(); month.save(); total.save(); diff --git a/subgraph/src/helpers.ts b/subgraph/src/helpers.ts index 5f70bdc..66adad4 100644 --- a/subgraph/src/helpers.ts +++ b/subgraph/src/helpers.ts @@ -13,20 +13,71 @@ import { Bytes } from '@graphprotocol/graph-ts'; import { BigInt } from '@graphprotocol/graph-ts'; import { + Cashback, ClaveAccount, + DailyEarnFlow, + Day, + DayAccount, + EarnPosition, Month, MonthAccount, + MonthlyEarnFlow, + ReferralFee, Total, Week, WeekAccount, + WeeklyEarnFlow, } from '../generated/schema'; export const ZERO = BigInt.fromI32(0); export const ONE = BigInt.fromI32(1); -const START_TIMESTAMP = BigInt.fromU32(1706045075); +const START_TIMESTAMP = BigInt.fromU32(1705881660); +const MONTH_START_TIMESTAMP = BigInt.fromU32(1704067260); +const DAY = BigInt.fromU32(86_400); const WEEK = BigInt.fromU32(604_800); const MONTH = BigInt.fromU32(2_592_000); +export function getOrCreateDay(timestamp: BigInt): Day { + let dayNumber = timestamp.minus(START_TIMESTAMP).div(DAY); + let dayId = Bytes.fromByteArray( + Bytes.fromBigInt(START_TIMESTAMP.plus(dayNumber.times(DAY))), + ).concat(Bytes.fromHexString('0x0000')); + let day = Day.load(dayId); + + if (day !== null) { + return day; + } + + day = new Day(dayId); + day.createdAccounts = 0; + day.deployedAccounts = 0; + day.activeAccounts = 0; + day.transactions = 0; + day.gasSponsored = ZERO; + + return day; +} + +export function getOrCreateDayAccount( + account: ClaveAccount, + day: Day, +): DayAccount { + let dayAccountId = account.id.concat(day.id); + let dayAccount = DayAccount.load(dayAccountId); + + if (dayAccount != null) { + return dayAccount; + } + + day.activeAccounts = day.activeAccounts + 1; + + dayAccount = new DayAccount(dayAccountId); + dayAccount.account = account.id; + dayAccount.day = day.id; + + return dayAccount; +} + export function getOrCreateWeek(timestamp: BigInt): Week { let weekNumber = timestamp.minus(START_TIMESTAMP).div(WEEK); let weekId = Bytes.fromByteArray( @@ -43,9 +94,6 @@ export function getOrCreateWeek(timestamp: BigInt): Week { week.deployedAccounts = 0; week.activeAccounts = 0; week.transactions = 0; - week.investIn = ZERO; - week.investOut = ZERO; - week.realizedGain = ZERO; week.gasSponsored = ZERO; return week; @@ -72,9 +120,9 @@ export function getOrCreateWeekAccount( } export function getOrCreateMonth(timestamp: BigInt): Month { - let monthNumber = timestamp.minus(START_TIMESTAMP).div(MONTH); + let monthNumber = timestamp.minus(MONTH_START_TIMESTAMP).div(MONTH); let monthId = Bytes.fromByteArray( - Bytes.fromBigInt(START_TIMESTAMP.plus(monthNumber.times(MONTH))), + Bytes.fromBigInt(MONTH_START_TIMESTAMP.plus(monthNumber.times(MONTH))), ).concat(Bytes.fromHexString('0x00')); let month = Month.load(monthId); @@ -87,9 +135,6 @@ export function getOrCreateMonth(timestamp: BigInt): Month { month.deployedAccounts = 0; month.activeAccounts = 0; month.transactions = 0; - month.investIn = ZERO; - month.investOut = ZERO; - month.realizedGain = ZERO; month.gasSponsored = ZERO; return month; @@ -128,9 +173,148 @@ export function getTotal(): Total { total.deployedAccounts = 0; total.transactions = 0; total.backedUp = 0; - total.invested = ZERO; - total.realizedGain = ZERO; total.gasSponsored = ZERO; return total; } + +export function getOrCreateEarnPosition( + account: ClaveAccount, + pool: Bytes, + token: Bytes, + protocol: string, +): EarnPosition { + let earnPositionId = account.id.concat(pool).concat(token); + let earnPosition = EarnPosition.load(earnPositionId); + + if (earnPosition !== null) { + return earnPosition; + } + + earnPosition = new EarnPosition(earnPositionId); + earnPosition.account = account.id; + earnPosition.pool = pool; + earnPosition.token = token; + earnPosition.protocol = protocol; + earnPosition.invested = ZERO; + earnPosition.compoundGain = ZERO; + earnPosition.normalGain = ZERO; + + return earnPosition; +} + +export function getOrCreateDailyEarnFlow( + day: Day, + token: Bytes, + protocol: string, +): DailyEarnFlow { + let dailyEarnFlowId = day.id.concat(token).concat(Bytes.fromUTF8(protocol)); + let dailyEarnFlow = DailyEarnFlow.load(dailyEarnFlowId); + + if (dailyEarnFlow !== null) { + return dailyEarnFlow; + } + + dailyEarnFlow = new DailyEarnFlow(dailyEarnFlowId); + dailyEarnFlow.day = day.id; + dailyEarnFlow.erc20 = token; + dailyEarnFlow.protocol = protocol; + dailyEarnFlow.amountIn = ZERO; + dailyEarnFlow.amountOut = ZERO; + dailyEarnFlow.claimedGain = ZERO; + + return dailyEarnFlow; +} + +export function getOrCreateWeeklyEarnFlow( + week: Week, + token: Bytes, + protocol: string, +): WeeklyEarnFlow { + let weeklyEarnFlowId = week.id + .concat(token) + .concat(Bytes.fromUTF8(protocol)); + let weeklyEarnFlow = WeeklyEarnFlow.load(weeklyEarnFlowId); + + if (weeklyEarnFlow !== null) { + return weeklyEarnFlow; + } + + weeklyEarnFlow = new WeeklyEarnFlow(weeklyEarnFlowId); + weeklyEarnFlow.week = week.id; + weeklyEarnFlow.erc20 = token; + weeklyEarnFlow.protocol = protocol; + weeklyEarnFlow.amountIn = ZERO; + weeklyEarnFlow.amountOut = ZERO; + weeklyEarnFlow.claimedGain = ZERO; + + return weeklyEarnFlow; +} + +export function getOrCreateMonthlyEarnFlow( + month: Month, + token: Bytes, + protocol: string, +): MonthlyEarnFlow { + let monthlyEarnFlowId = month.id + .concat(token) + .concat(Bytes.fromUTF8(protocol)); + let monthlyEarnFlow = MonthlyEarnFlow.load(monthlyEarnFlowId); + + if (monthlyEarnFlow !== null) { + return monthlyEarnFlow; + } + + monthlyEarnFlow = new MonthlyEarnFlow(monthlyEarnFlowId); + monthlyEarnFlow.month = month.id; + monthlyEarnFlow.erc20 = token; + monthlyEarnFlow.protocol = protocol; + monthlyEarnFlow.amountIn = ZERO; + monthlyEarnFlow.amountOut = ZERO; + monthlyEarnFlow.claimedGain = ZERO; + + return monthlyEarnFlow; +} + +export function getOrCreateCashback( + account: ClaveAccount, + token: Bytes, +): Cashback { + let cashbackId = account.id + .concat(token) + .concat(Bytes.fromHexString('0xcb')); + let cashback = Cashback.load(cashbackId); + + if (cashback !== null) { + return cashback; + } + + cashback = new Cashback(cashbackId); + cashback.account = account.id; + cashback.erc20 = token; + cashback.amount = ZERO; + + return cashback; +} + +export function getOrCreateReferralFee( + referrer: ClaveAccount, + referred: ClaveAccount, + token: Bytes, +): ReferralFee { + let referralFeeId = referrer.id.concat(referred.id).concat(token); + + let referralFee = ReferralFee.load(referralFeeId); + + if (referralFee !== null) { + return referralFee; + } + + referralFee = new ReferralFee(referralFeeId); + referralFee.account = referrer.id; + referralFee.referred = referred.id; + referralFee.erc20 = token; + referralFee.amount = ZERO; + + return referralFee; +} diff --git a/subgraph/src/koi-pair.ts b/subgraph/src/koi-pair.ts index 96da83c..3bf4a67 100644 --- a/subgraph/src/koi-pair.ts +++ b/subgraph/src/koi-pair.ts @@ -5,13 +5,28 @@ */ /* eslint-disable @typescript-eslint/consistent-type-imports */ +import { Address } from '@graphprotocol/graph-ts'; + import { Burn as BurnEvent, Claim as ClaimEvent, Mint as MintEvent, } from '../generated/KoiUsdceUsdt/KoiPair'; import { ClaveAccount } from '../generated/schema'; -import { getOrCreateMonth, getOrCreateWeek, getTotal } from './helpers'; +import { + getOrCreateDailyEarnFlow, + getOrCreateDay, + getOrCreateEarnPosition, + getOrCreateMonth, + getOrCreateMonthlyEarnFlow, + getOrCreateWeek, + getOrCreateWeeklyEarnFlow, +} from './helpers'; + +const protocol = 'Koi'; +const token = Address.fromHexString( + '0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4', +); export function handleMint(event: MintEvent): void { const account = ClaveAccount.load(event.transaction.from); @@ -19,21 +34,32 @@ export function handleMint(event: MintEvent): void { return; } + const pool = event.address; const amount = event.params.amount1.plus(event.params.amount0); + const day = getOrCreateDay(event.block.timestamp); const week = getOrCreateWeek(event.block.timestamp); const month = getOrCreateMonth(event.block.timestamp); - const total = getTotal(); - - week.investIn = week.investIn.plus(amount); - month.investIn = month.investIn.plus(amount); - total.invested = total.invested.plus(amount); - account.invested = account.invested.plus(amount); - week.save(); - month.save(); - total.save(); - account.save(); + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountIn = dailyEarnFlow.amountIn.plus(amount); + weeklyEarnFlow.amountIn = weeklyEarnFlow.amountIn.plus(amount); + monthlyEarnFlow.amountIn = monthlyEarnFlow.amountIn.plus(amount); + earnPosition.invested = earnPosition.invested.plus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); } export function handleBurn(event: BurnEvent): void { @@ -42,21 +68,32 @@ export function handleBurn(event: BurnEvent): void { return; } + const pool = event.address; const amount = event.params.amount1.plus(event.params.amount0); + const day = getOrCreateDay(event.block.timestamp); const week = getOrCreateWeek(event.block.timestamp); const month = getOrCreateMonth(event.block.timestamp); - const total = getTotal(); - week.investOut = week.investOut.plus(amount); - month.investOut = month.investOut.plus(amount); - total.invested = total.invested.minus(amount); - account.invested = account.invested.minus(amount); - - week.save(); - month.save(); - total.save(); - account.save(); + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(amount); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(amount); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(amount); + earnPosition.invested = earnPosition.invested.minus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); } export function handleClaim(event: ClaimEvent): void { @@ -65,19 +102,25 @@ export function handleClaim(event: ClaimEvent): void { return; } + const pool = event.address; const amount = event.params.amount1.plus(event.params.amount0); + const day = getOrCreateDay(event.block.timestamp); const week = getOrCreateWeek(event.block.timestamp); const month = getOrCreateMonth(event.block.timestamp); - const total = getTotal(); - - week.realizedGain = week.realizedGain.plus(amount); - month.realizedGain = month.realizedGain.plus(amount); - total.realizedGain = total.realizedGain.plus(amount); - account.realizedGain = account.realizedGain.plus(amount); - week.save(); - month.save(); - total.save(); - account.save(); + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.claimedGain = dailyEarnFlow.claimedGain.plus(amount); + weeklyEarnFlow.claimedGain = weeklyEarnFlow.claimedGain.plus(amount); + monthlyEarnFlow.claimedGain = monthlyEarnFlow.claimedGain.plus(amount); + earnPosition.normalGain = earnPosition.normalGain.plus(amount); } diff --git a/subgraph/src/meow-staking.ts b/subgraph/src/meow-staking.ts new file mode 100644 index 0000000..76ae80a --- /dev/null +++ b/subgraph/src/meow-staking.ts @@ -0,0 +1,129 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ + +/* eslint-disable @typescript-eslint/consistent-type-imports */ +import { Address } from '@graphprotocol/graph-ts'; + +import { + RewardPaid as RewardPaidEvent, + Staked as StakedEvent, + Withdrawn as WithdrawnEvent, +} from '../generated/ClaveZKStake/ClaveZKStake'; +import { ClaveAccount } from '../generated/schema'; +import { + getOrCreateDailyEarnFlow, + getOrCreateDay, + getOrCreateEarnPosition, + getOrCreateMonth, + getOrCreateMonthlyEarnFlow, + getOrCreateWeek, + getOrCreateWeeklyEarnFlow, +} from './helpers'; + +const protocol = 'Meow'; +const token = Address.fromHexString( + '0x79db8c67d0c33203da4Efb58F7D325E1e0d4d692', +); +const reward = Address.fromHexString( + '0x5A7d6b2F92C77FAD6CCaBd7EE0624E64907Eaf3E', +); + +export function handleStaked(event: StakedEvent): void { + const account = ClaveAccount.load(event.params.user); + if (!account) { + return; + } + + const pool = event.address; + const amount = event.params.amount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountIn = dailyEarnFlow.amountIn.plus(amount); + weeklyEarnFlow.amountIn = weeklyEarnFlow.amountIn.plus(amount); + monthlyEarnFlow.amountIn = monthlyEarnFlow.amountIn.plus(amount); + earnPosition.invested = earnPosition.invested.plus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} + +export function handleWithdrawn(event: WithdrawnEvent): void { + const account = ClaveAccount.load(event.transaction.from); + if (!account) { + return; + } + + const pool = event.address; + const amount = event.params.amount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(amount); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(amount); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(amount); + earnPosition.invested = earnPosition.invested.minus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} + +export function handleRewardPaid(event: RewardPaidEvent): void { + const account = ClaveAccount.load(event.params.user); + if (!account) { + return; + } + + const pool = event.address; + const amount = event.params.reward; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, reward, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, reward, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, reward, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + reward, + protocol, + ); + + dailyEarnFlow.claimedGain = dailyEarnFlow.claimedGain.plus(amount); + weeklyEarnFlow.claimedGain = weeklyEarnFlow.claimedGain.plus(amount); + monthlyEarnFlow.claimedGain = monthlyEarnFlow.claimedGain.plus(amount); + earnPosition.normalGain = earnPosition.normalGain.plus(amount); +} diff --git a/subgraph/src/odos-router.ts b/subgraph/src/odos-router.ts index 031fc51..ecc7e3b 100644 --- a/subgraph/src/odos-router.ts +++ b/subgraph/src/odos-router.ts @@ -8,11 +8,17 @@ import { Swap as SwapEvent } from '../generated/OdosRouter/OdosRouter'; import { ClaveAccount, + DailySwappedTo, InAppSwap, MonthlySwappedTo, WeeklySwappedTo, } from '../generated/schema'; -import { ZERO, getOrCreateMonth, getOrCreateWeek } from './helpers'; +import { + ZERO, + getOrCreateDay, + getOrCreateMonth, + getOrCreateWeek, +} from './helpers'; export function handleSwap(event: SwapEvent): void { const account = ClaveAccount.load(event.params.sender); @@ -20,10 +26,22 @@ export function handleSwap(event: SwapEvent): void { return; } + const day = getOrCreateDay(event.block.timestamp); const week = getOrCreateWeek(event.block.timestamp); const month = getOrCreateMonth(event.block.timestamp); const tokenOutAddress = event.params.outputToken; + const dailySwappedToId = day.id.concat(tokenOutAddress); + let dailySwappedTo = DailySwappedTo.load(dailySwappedToId); + if (!dailySwappedTo) { + dailySwappedTo = new DailySwappedTo(dailySwappedToId); + dailySwappedTo.day = day.id; + dailySwappedTo.erc20 = tokenOutAddress; + dailySwappedTo.amount = ZERO; + } + + dailySwappedTo.amount = dailySwappedTo.amount.plus(event.params.amountOut); + const weeklySwappedToId = week.id.concat(tokenOutAddress); let weeklySwappedTo = WeeklySwappedTo.load(weeklySwappedToId); if (!weeklySwappedTo) { @@ -61,6 +79,7 @@ export function handleSwap(event: SwapEvent): void { inAppSwap.tokenOut = event.params.outputToken; inAppSwap.date = event.block.timestamp; + dailySwappedTo.save(); weeklySwappedTo.save(); monthlySwappedTo.save(); inAppSwap.save(); diff --git a/subgraph/src/referral.ts b/subgraph/src/referral.ts new file mode 100644 index 0000000..7a4d504 --- /dev/null +++ b/subgraph/src/referral.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/consistent-type-imports */ + +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { + Cashback as CashbackEvent, + ReferralFee as ReferralFeeEvent, +} from '../generated/SwapReferralFeePayer/SwapReferralFeePayer'; +import { ClaveAccount } from '../generated/schema'; +import { getOrCreateCashback, getOrCreateReferralFee } from './helpers'; + +export function handleCashback(event: CashbackEvent): void { + const account = ClaveAccount.load(event.params.referred); + if (!account) { + return; + } + + const cashback = getOrCreateCashback(account, event.params.token); + cashback.amount = cashback.amount.plus(event.params.fee); + cashback.save(); +} + +export function handleReferralFee(event: ReferralFeeEvent): void { + const referrer = ClaveAccount.load(event.params.referrer); + if (!referrer) { + return; + } + + const referred = ClaveAccount.load(event.transaction.from); + if (!referred) { + return; + } + + const referralFee = getOrCreateReferralFee( + referrer, + referred, + event.params.token, + ); + referralFee.amount = referralFee.amount.plus(event.params.fee); + referralFee.save(); +} diff --git a/subgraph/src/sync-clave-staking.ts b/subgraph/src/sync-clave-staking.ts new file mode 100644 index 0000000..dd98b15 --- /dev/null +++ b/subgraph/src/sync-clave-staking.ts @@ -0,0 +1,54 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ + +/* eslint-disable @typescript-eslint/consistent-type-imports */ +import { ClaimRewards as ClaimRewardsEvent } from '../generated/SyncClaveStaking/SyncClaveStaking'; +import { ClaveAccount } from '../generated/schema'; +import { + getOrCreateDailyEarnFlow, + getOrCreateDay, + getOrCreateEarnPosition, + getOrCreateMonth, + getOrCreateMonthlyEarnFlow, + getOrCreateWeek, + getOrCreateWeeklyEarnFlow, +} from './helpers'; + +const protocol = 'SyncSwap'; + +export function handleClaimRewards(event: ClaimRewardsEvent): void { + const account = ClaveAccount.load(event.params.account); + if (!account) { + return; + } + + const pool = event.address; + const token = event.params.token; + const amount = event.params.amount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.claimedGain = dailyEarnFlow.claimedGain.plus(amount); + weeklyEarnFlow.claimedGain = weeklyEarnFlow.claimedGain.plus(amount); + monthlyEarnFlow.claimedGain = monthlyEarnFlow.claimedGain.plus(amount); + earnPosition.normalGain = earnPosition.normalGain.plus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} diff --git a/subgraph/src/sync-stable-pair.ts b/subgraph/src/sync-stable-pair.ts new file mode 100644 index 0000000..192b6a0 --- /dev/null +++ b/subgraph/src/sync-stable-pair.ts @@ -0,0 +1,186 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ + +/* eslint-disable @typescript-eslint/consistent-type-imports */ +import { Address } from '@graphprotocol/graph-ts'; + +import { + Burn as BurnEvent, + Mint as MintEvent, +} from '../generated/SyncEthWstethPool/SyncStablePool'; +import { ClaveAccount } from '../generated/schema'; +import { + ZERO, + getOrCreateDailyEarnFlow, + getOrCreateDay, + getOrCreateEarnPosition, + getOrCreateMonth, + getOrCreateMonthlyEarnFlow, + getOrCreateWeek, + getOrCreateWeeklyEarnFlow, +} from './helpers'; + +const protocol = 'SyncSwap'; +const token = Address.fromHexString( + '0x000000000000000000000000000000000000800A', +); +const tokenb = Address.fromHexString( + '0x703b52F2b28fEbcB60E1372858AF5b18849FE867', +); + +export function handleMint(event: MintEvent): void { + const account = ClaveAccount.load(event.transaction.from); + if (!account) { + return; + } + + const pool = event.address; + const amount0 = event.params.amount0; + const amount1 = event.params.amount1; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountIn = dailyEarnFlow.amountIn.plus(amount0); + weeklyEarnFlow.amountIn = weeklyEarnFlow.amountIn.plus(amount0); + monthlyEarnFlow.amountIn = monthlyEarnFlow.amountIn.plus(amount0); + earnPosition.invested = earnPosition.invested.plus(amount0); + + const dailyEarnFlow2 = getOrCreateDailyEarnFlow(day, tokenb, protocol); + const weeklyEarnFlow2 = getOrCreateWeeklyEarnFlow(week, tokenb, protocol); + const monthlyEarnFlow2 = getOrCreateMonthlyEarnFlow( + month, + tokenb, + protocol, + ); + const earnPosition2 = getOrCreateEarnPosition( + account, + pool, + tokenb, + protocol, + ); + + dailyEarnFlow2.amountIn = dailyEarnFlow2.amountIn.plus(amount1); + weeklyEarnFlow2.amountIn = weeklyEarnFlow2.amountIn.plus(amount1); + monthlyEarnFlow2.amountIn = monthlyEarnFlow2.amountIn.plus(amount1); + earnPosition2.invested = earnPosition2.invested.plus(amount1); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); + + dailyEarnFlow2.save(); + weeklyEarnFlow2.save(); + monthlyEarnFlow2.save(); + earnPosition2.save(); +} + +export function handleBurn(event: BurnEvent): void { + const account = ClaveAccount.load(event.transaction.from); + if (!account) { + return; + } + + const pool = event.address; + const amount0 = event.params.amount0; + const amount1 = event.params.amount1; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + const invested = earnPosition.invested; + + if (amount0.gt(invested)) { + const compoundGain = amount0.minus(invested); + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(invested); + dailyEarnFlow.claimedGain = + dailyEarnFlow.claimedGain.plus(compoundGain); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(invested); + weeklyEarnFlow.claimedGain = + weeklyEarnFlow.claimedGain.plus(compoundGain); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(invested); + monthlyEarnFlow.claimedGain = + monthlyEarnFlow.claimedGain.plus(compoundGain); + earnPosition.invested = ZERO; + earnPosition.compoundGain = + earnPosition.compoundGain.plus(compoundGain); + } else { + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(amount0); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(amount0); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(amount0); + earnPosition.invested = earnPosition.invested.minus(amount0); + } + + const dailyEarnFlow2 = getOrCreateDailyEarnFlow(day, tokenb, protocol); + const weeklyEarnFlow2 = getOrCreateWeeklyEarnFlow(week, tokenb, protocol); + const monthlyEarnFlow2 = getOrCreateMonthlyEarnFlow( + month, + tokenb, + protocol, + ); + const earnPosition2 = getOrCreateEarnPosition( + account, + pool, + tokenb, + protocol, + ); + + const invested2 = earnPosition2.invested; + + if (amount1.gt(invested2)) { + const compoundGain2 = amount1.minus(invested2); + dailyEarnFlow2.amountOut = dailyEarnFlow2.amountOut.plus(invested2); + dailyEarnFlow2.claimedGain = + dailyEarnFlow2.claimedGain.plus(compoundGain2); + weeklyEarnFlow2.amountOut = weeklyEarnFlow2.amountOut.plus(invested2); + weeklyEarnFlow2.claimedGain = + weeklyEarnFlow2.claimedGain.plus(compoundGain2); + monthlyEarnFlow2.amountOut = monthlyEarnFlow2.amountOut.plus(invested2); + monthlyEarnFlow2.claimedGain = + monthlyEarnFlow2.claimedGain.plus(compoundGain2); + earnPosition2.invested = ZERO; + earnPosition2.compoundGain = + earnPosition2.compoundGain.plus(compoundGain2); + } else { + dailyEarnFlow2.amountOut = dailyEarnFlow2.amountOut.plus(amount1); + weeklyEarnFlow2.amountOut = weeklyEarnFlow2.amountOut.plus(amount1); + monthlyEarnFlow2.amountOut = monthlyEarnFlow2.amountOut.plus(amount1); + earnPosition2.invested = earnPosition2.invested.minus(amount1); + } + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); + + dailyEarnFlow2.save(); + weeklyEarnFlow2.save(); + monthlyEarnFlow2.save(); + earnPosition2.save(); +} diff --git a/subgraph/src/sync-staking.ts b/subgraph/src/sync-staking.ts new file mode 100644 index 0000000..09abbab --- /dev/null +++ b/subgraph/src/sync-staking.ts @@ -0,0 +1,54 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ + +/* eslint-disable @typescript-eslint/consistent-type-imports */ +import { ClaimRewards as ClaimRewardsEvent } from '../generated/SyncStaking/SyncStaking'; +import { ClaveAccount } from '../generated/schema'; +import { + getOrCreateDailyEarnFlow, + getOrCreateDay, + getOrCreateEarnPosition, + getOrCreateMonth, + getOrCreateMonthlyEarnFlow, + getOrCreateWeek, + getOrCreateWeeklyEarnFlow, +} from './helpers'; + +const protocol = 'SyncSwap'; + +export function handleClaimRewards(event: ClaimRewardsEvent): void { + const account = ClaveAccount.load(event.params.from); + if (!account) { + return; + } + + const pool = event.address; + const token = event.params.reward; + const amount = event.params.amount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.claimedGain = dailyEarnFlow.claimedGain.plus(amount); + weeklyEarnFlow.claimedGain = weeklyEarnFlow.claimedGain.plus(amount); + monthlyEarnFlow.claimedGain = monthlyEarnFlow.claimedGain.plus(amount); + earnPosition.normalGain = earnPosition.normalGain.plus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} diff --git a/subgraph/src/venus-pool-usdce.ts b/subgraph/src/venus-pool-usdce.ts new file mode 100644 index 0000000..034f5e8 --- /dev/null +++ b/subgraph/src/venus-pool-usdce.ts @@ -0,0 +1,116 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ + +/* eslint-disable @typescript-eslint/consistent-type-imports */ +import { Address } from '@graphprotocol/graph-ts'; + +import { + Mint as MintEvent, + Redeem as RedeemEvent, +} from '../generated/VenusUsdce/VenusToken'; +import { ClaveAccount } from '../generated/schema'; +import { + ZERO, + getOrCreateDailyEarnFlow, + getOrCreateDay, + getOrCreateEarnPosition, + getOrCreateMonth, + getOrCreateMonthlyEarnFlow, + getOrCreateWeek, + getOrCreateWeeklyEarnFlow, +} from './helpers'; + +const protocol = 'Venus'; + +const token = Address.fromHexString( + '0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4', +); + +export function handleMint(event: MintEvent): void { + const account = ClaveAccount.load(event.params.minter); + if (!account) { + return; + } + + const pool = event.address; + const amount = event.params.mintAmount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountIn = dailyEarnFlow.amountIn.plus(amount); + weeklyEarnFlow.amountIn = weeklyEarnFlow.amountIn.plus(amount); + monthlyEarnFlow.amountIn = monthlyEarnFlow.amountIn.plus(amount); + earnPosition.invested = earnPosition.invested.plus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} + +export function handleRedeem(event: RedeemEvent): void { + const account = ClaveAccount.load(event.params.redeemer); + if (!account) { + return; + } + + const pool = event.address; + const amount = event.params.redeemAmount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + const invested = earnPosition.invested; + + if (amount.gt(invested)) { + const compoundGain = amount.minus(invested); + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(invested); + dailyEarnFlow.claimedGain = + dailyEarnFlow.claimedGain.plus(compoundGain); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(invested); + weeklyEarnFlow.claimedGain = + weeklyEarnFlow.claimedGain.plus(compoundGain); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(invested); + monthlyEarnFlow.claimedGain = + monthlyEarnFlow.claimedGain.plus(compoundGain); + earnPosition.invested = ZERO; + earnPosition.compoundGain = + earnPosition.compoundGain.plus(compoundGain); + } else { + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(amount); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(amount); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(amount); + earnPosition.invested = invested.minus(amount); + } + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} diff --git a/subgraph/src/venus-pool-usdt.ts b/subgraph/src/venus-pool-usdt.ts new file mode 100644 index 0000000..de242f9 --- /dev/null +++ b/subgraph/src/venus-pool-usdt.ts @@ -0,0 +1,116 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ + +/* eslint-disable @typescript-eslint/consistent-type-imports */ +import { Address } from '@graphprotocol/graph-ts'; + +import { + Mint as MintEvent, + Redeem as RedeemEvent, +} from '../generated/VenusUsdt/VenusToken'; +import { ClaveAccount } from '../generated/schema'; +import { + ZERO, + getOrCreateDailyEarnFlow, + getOrCreateDay, + getOrCreateEarnPosition, + getOrCreateMonth, + getOrCreateMonthlyEarnFlow, + getOrCreateWeek, + getOrCreateWeeklyEarnFlow, +} from './helpers'; + +const protocol = 'Venus'; + +const token = Address.fromHexString( + '0x493257fD37EDB34451f62EDf8D2a0C418852bA4C', +); + +export function handleMint(event: MintEvent): void { + const account = ClaveAccount.load(event.params.minter); + if (!account) { + return; + } + + const pool = event.address; + const amount = event.params.mintAmount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountIn = dailyEarnFlow.amountIn.plus(amount); + weeklyEarnFlow.amountIn = weeklyEarnFlow.amountIn.plus(amount); + monthlyEarnFlow.amountIn = monthlyEarnFlow.amountIn.plus(amount); + earnPosition.invested = earnPosition.invested.plus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} + +export function handleRedeem(event: RedeemEvent): void { + const account = ClaveAccount.load(event.params.redeemer); + if (!account) { + return; + } + + const pool = event.address; + const amount = event.params.redeemAmount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + const invested = earnPosition.invested; + + if (amount.gt(invested)) { + const compoundGain = amount.minus(invested); + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(invested); + dailyEarnFlow.claimedGain = + dailyEarnFlow.claimedGain.plus(compoundGain); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(invested); + weeklyEarnFlow.claimedGain = + weeklyEarnFlow.claimedGain.plus(compoundGain); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(invested); + monthlyEarnFlow.claimedGain = + monthlyEarnFlow.claimedGain.plus(compoundGain); + earnPosition.invested = ZERO; + earnPosition.compoundGain = + earnPosition.compoundGain.plus(compoundGain); + } else { + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(amount); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(amount); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(amount); + earnPosition.invested = invested.minus(amount); + } + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} diff --git a/subgraph/src/venus-reward.ts b/subgraph/src/venus-reward.ts new file mode 100644 index 0000000..615ddb1 --- /dev/null +++ b/subgraph/src/venus-reward.ts @@ -0,0 +1,60 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +/* eslint-disable @typescript-eslint/consistent-type-imports */ +import { Address } from '@graphprotocol/graph-ts'; + +import { DistributedSupplierRewardToken as DistributedSupplierRewardTokenEvent } from '../generated/VenusReward/VenusReward'; +import { ClaveAccount } from '../generated/schema'; +import { + getOrCreateDailyEarnFlow, + getOrCreateDay, + getOrCreateEarnPosition, + getOrCreateMonth, + getOrCreateMonthlyEarnFlow, + getOrCreateWeek, + getOrCreateWeeklyEarnFlow, +} from './helpers'; + +const protocol = 'Venus'; +const token = Address.fromHexString( + '0xD78ABD81a3D57712a3af080dc4185b698Fe9ac5A', +); + +export function handleDistributedSupplierRewardToken( + event: DistributedSupplierRewardTokenEvent, +): void { + const account = ClaveAccount.load(event.params.supplier); + if (!account) { + return; + } + + const pool = event.address; + const amount = event.params.rewardTokenTotal; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.claimedGain = dailyEarnFlow.claimedGain.plus(amount); + weeklyEarnFlow.claimedGain = weeklyEarnFlow.claimedGain.plus(amount); + monthlyEarnFlow.claimedGain = monthlyEarnFlow.claimedGain.plus(amount); + earnPosition.normalGain = earnPosition.normalGain.plus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} diff --git a/subgraph/src/zero-usdt-pool.ts b/subgraph/src/zero-usdt-pool.ts new file mode 100644 index 0000000..a077237 --- /dev/null +++ b/subgraph/src/zero-usdt-pool.ts @@ -0,0 +1,112 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ + +/* eslint-disable @typescript-eslint/consistent-type-imports */ +import { ClaveAccount } from '../generated/schema'; +import { + Supply as SupplyEvent, + Withdraw as WithdrawEvent, +} from '../generated/zeroUsdtPool/zeroUsdtPool'; +import { + ZERO, + getOrCreateDailyEarnFlow, + getOrCreateDay, + getOrCreateEarnPosition, + getOrCreateMonth, + getOrCreateMonthlyEarnFlow, + getOrCreateWeek, + getOrCreateWeeklyEarnFlow, +} from './helpers'; + +const protocol = 'ZeroLend'; + +export function handleSupply(event: SupplyEvent): void { + const account = ClaveAccount.load(event.params.onBehalfOf); + if (!account) { + return; + } + + const pool = event.address; + const token = event.params.reserve; + const amount = event.params.amount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + dailyEarnFlow.amountIn = dailyEarnFlow.amountIn.plus(amount); + weeklyEarnFlow.amountIn = weeklyEarnFlow.amountIn.plus(amount); + monthlyEarnFlow.amountIn = monthlyEarnFlow.amountIn.plus(amount); + earnPosition.invested = earnPosition.invested.plus(amount); + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} + +export function handleWithdraw(event: WithdrawEvent): void { + const account = ClaveAccount.load(event.params.to); + if (!account) { + return; + } + + const pool = event.address; + const token = event.params.reserve; + const amount = event.params.amount; + + const day = getOrCreateDay(event.block.timestamp); + const week = getOrCreateWeek(event.block.timestamp); + const month = getOrCreateMonth(event.block.timestamp); + + const dailyEarnFlow = getOrCreateDailyEarnFlow(day, token, protocol); + const weeklyEarnFlow = getOrCreateWeeklyEarnFlow(week, token, protocol); + const monthlyEarnFlow = getOrCreateMonthlyEarnFlow(month, token, protocol); + const earnPosition = getOrCreateEarnPosition( + account, + pool, + token, + protocol, + ); + + const invested = earnPosition.invested; + + if (amount.gt(invested)) { + const compoundGain = amount.minus(invested); + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(invested); + dailyEarnFlow.claimedGain = + dailyEarnFlow.claimedGain.plus(compoundGain); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(invested); + weeklyEarnFlow.claimedGain = + weeklyEarnFlow.claimedGain.plus(compoundGain); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(invested); + monthlyEarnFlow.claimedGain = + monthlyEarnFlow.claimedGain.plus(compoundGain); + earnPosition.invested = ZERO; + earnPosition.compoundGain = + earnPosition.compoundGain.plus(compoundGain); + } else { + dailyEarnFlow.amountOut = dailyEarnFlow.amountOut.plus(amount); + weeklyEarnFlow.amountOut = weeklyEarnFlow.amountOut.plus(amount); + monthlyEarnFlow.amountOut = monthlyEarnFlow.amountOut.plus(amount); + earnPosition.invested = invested.minus(amount); + } + + dailyEarnFlow.save(); + weeklyEarnFlow.save(); + monthlyEarnFlow.save(); + earnPosition.save(); +} diff --git a/subgraph/subgraph.yaml b/subgraph/subgraph.yaml index ebbb0af..8c902d5 100644 --- a/subgraph/subgraph.yaml +++ b/subgraph/subgraph.yaml @@ -1,45 +1,25 @@ specVersion: 1.0.0 indexerHints: - prune: auto + prune: 86400 schema: file: ./schema.graphql dataSources: - # - kind: ethereum - # name: KoiFactory - # network: zksync-era - # source: - # address: '0x40be1cBa6C5B47cDF9da7f963B6F761F4C60627D' - # abi: KoiFactory - # startBlock: 9672 - # mapping: - # kind: ethereum/events - # apiVersion: 0.0.7 - # language: wasm/assemblyscript - # entities: - # - KoiPair - # abis: - # - name: KoiFactory - # file: ./abis/KoiFactory.json - # eventHandlers: - # - event: PairCreated(address indexed token0, address indexed token1, bool stable, address pair, uint, uint fee) - # handler: handlePairCreated - # file: ./src/koi-factory.ts - kind: ethereum name: KoiUsdceUsdt network: zksync-era source: address: '0x9d2811b85c1d736427722817b69e4d1e98016bb0' abi: KoiPair - startBlock: 24799912 + startBlock: 30099184 mapping: kind: ethereum/events apiVersion: 0.0.7 language: wasm/assemblyscript entities: - - Total - - Week - - Month - - ClaveAccount + - DailyEarnFlow + - WeeklyEarnFlow + - MonthlyEarnFlow + - EarnPosition abis: - name: KoiPair file: ./abis/KoiPair.json @@ -51,6 +31,29 @@ dataSources: - event: Claim(indexed address,indexed address,uint256,uint256) handler: handleClaim file: ./src/koi-pair.ts + - kind: ethereum + name: SwapReferralFeePayer + network: zksync-era + source: + address: '0x007966D09BF27c206c75B7048319dbf3a0852Df8' + abi: SwapReferralFeePayer + startBlock: 42221017 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - Cashback + - Referral + abis: + - name: SwapReferralFeePayer + file: ./abis/SwapReferralFeePayer.json + eventHandlers: + - event: ReferralFee(indexed address,indexed address,uint256) + handler: handleReferralFee + - event: Cashback(indexed address,indexed address,uint256) + handler: handleCashback + file: ./src/referral.ts - kind: ethereum name: OdosRouter network: zksync-era @@ -64,6 +67,7 @@ dataSources: language: wasm/assemblyscript entities: - InAppSwap + - DailySwappedTo - WeeklySwappedTo - MonthlySwappedTo abis: @@ -86,6 +90,7 @@ dataSources: language: wasm/assemblyscript entities: - ClaveAccount + - Day - Week - Month - Total @@ -113,6 +118,7 @@ dataSources: language: wasm/assemblyscript entities: - ClaveAccount + - Day - Week - Month - Total @@ -138,6 +144,8 @@ dataSources: language: wasm/assemblyscript entities: - ClaveTransaction + - Day + - DayAccount - Week - WeekAccount - Month @@ -165,6 +173,8 @@ dataSources: language: wasm/assemblyscript entities: - ClaveTransaction + - Day + - DayAccount - Week - WeekAccount - Month @@ -192,6 +202,8 @@ dataSources: language: wasm/assemblyscript entities: - ClaveTransaction + - Day + - DayAccount - Week - WeekAccount - Month @@ -219,6 +231,8 @@ dataSources: language: wasm/assemblyscript entities: - ClaveTransaction + - Day + - DayAccount - Week - WeekAccount - Month @@ -262,6 +276,256 @@ dataSources: - event: RecoveryStopped(indexed address) handler: handleRecoveryStopped file: ./src/social-recovery.ts + - kind: ethereum + name: SyncEthWstethPool + network: zksync-era + source: + address: '0x12e7A9423d9128287E63017eE6d1f20e1C237f15' + abi: SyncStablePool + startBlock: 30099184 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - DailyEarnFlow + - WeeklyEarnFlow + - MonthlyEarnFlow + - EarnPosition + abis: + - name: SyncStablePool + file: ./abis/SyncStablePool.json + eventHandlers: + - event: Burn(indexed address,uint256,uint256,uint256,indexed address) + handler: handleBurn + - event: Mint(indexed address,uint256,uint256,uint256,indexed address) + handler: handleMint + file: ./src/sync-stable-pair.ts + - kind: ethereum + name: SyncStaking + network: zksync-era + source: + address: '0x2B9a7d5cD64E5c1446b32e034e75A5C93B0C8bB5' + abi: SyncStaking + startBlock: 30099184 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - DailyEarnFlow + - WeeklyEarnFlow + - MonthlyEarnFlow + - EarnPosition + abis: + - name: SyncStaking + file: ./abis/SyncStaking.json + eventHandlers: + - event: ClaimRewards(indexed address,indexed address,uint256) + handler: handleClaimRewards + file: ./src/sync-staking.ts + - kind: ethereum + name: SyncClaveStaking + network: zksync-era + source: + address: '0x6fEbba4a360F43B71560519bCD90B3F45c8F441E' + abi: SyncClaveStaking + startBlock: 39511808 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - DailyEarnFlow + - WeeklyEarnFlow + - MonthlyEarnFlow + - EarnPosition + abis: + - name: SyncClaveStaking + file: ./abis/SyncClaveStaking.json + eventHandlers: + - event: ClaimRewards(indexed address,indexed address,uint256) + handler: handleClaimRewards + file: ./src/sync-staking.ts + - kind: ethereum + name: zeroUsdtPool + network: zksync-era + source: + address: '0x4d9429246EA989C9CeE203B43F6d1C7D83e3B8F8' + abi: zeroUsdtPool + startBlock: 30099184 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - DailyEarnFlow + - WeeklyEarnFlow + - MonthlyEarnFlow + - EarnPosition + abis: + - name: zeroUsdtPool + file: ./abis/zeroUsdtPool.json + eventHandlers: + - event: Supply(indexed address,address,indexed address,uint256,indexed uint16) + handler: handleSupply + - event: Withdraw(indexed address,indexed address,indexed address,uint256) + handler: handleWithdraw + file: ./src/zero-usdt-pool.ts + - kind: ethereum + name: ClaveZKStake + network: zksync-era + source: + address: '0x9248F1Ee8cBD029F3D22A92EB270333a39846fB2' + abi: ClaveZKStake + startBlock: 36908411 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - DailyEarnFlow + - WeeklyEarnFlow + - MonthlyEarnFlow + - EarnPosition + abis: + - name: ClaveZKStake + file: ./abis/ClaveZKStake.json + eventHandlers: + - event: Staked(indexed address,uint256) + handler: handleStaked + - event: Withdrawn(indexed address,uint256) + handler: handleWithdrawn + - event: RewardPaid(indexed address,uint256) + handler: handleRewardPaid + file: ./src/clave-zk-stake.ts + - kind: ethereum + name: ClaveAPPAStake + network: zksync-era + source: + address: '0x20999BD9fA71175e4A430CDB7950a66916E6F4d1' + abi: ZtakeV2 + startBlock: 41763539 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - DailyEarnFlow + - WeeklyEarnFlow + - MonthlyEarnFlow + - EarnPosition + abis: + - name: ZtakeV2 + file: ./abis/ZtakeV2.json + eventHandlers: + - event: Staked(indexed address,indexed address,uint256) + handler: handleStaked + - event: Withdrawn(indexed address,indexed address,uint256) + handler: handleWithdrawn + - event: RewardPaid(indexed address,indexed address,uint256) + handler: handleRewardPaid + file: ./src/clave-appa-stake.ts + - kind: ethereum + name: MeowStake + network: zksync-era + source: + address: '0x0C71c7B6FD654EE0D3137a2Eb790CAd8Ba702540' + abi: ClaveZKStake + startBlock: 36908411 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - DailyEarnFlow + - WeeklyEarnFlow + - MonthlyEarnFlow + - EarnPosition + abis: + - name: ClaveZKStake + file: ./abis/ClaveZKStake.json + eventHandlers: + - event: Staked(indexed address,uint256) + handler: handleStaked + - event: Withdrawn(indexed address,uint256) + handler: handleWithdrawn + - event: RewardPaid(indexed address,uint256) + handler: handleRewardPaid + file: ./src/meow-staking.ts + - kind: ethereum + name: VenusUsdt + network: zksync-era + source: + address: '0x69cDA960E3b20DFD480866fFfd377Ebe40bd0A46' + abi: VenusToken + startBlock: 43552193 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - DailyEarnFlow + - WeeklyEarnFlow + - MonthlyEarnFlow + - EarnPosition + abis: + - name: VenusToken + file: ./abis/VenusToken.json + eventHandlers: + - event: Mint(indexed address,uint256,uint256,uint256) + handler: handleMint + - event: Redeem(indexed address,uint256,uint256,uint256) + handler: handleRedeem + file: ./src/venus-pool-usdt.ts + - kind: ethereum + name: VenusUsdce + network: zksync-era + source: + address: '0x1af23bd57c62a99c59ad48236553d0dd11e49d2d' + abi: VenusToken + startBlock: 43552199 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - DailyEarnFlow + - WeeklyEarnFlow + - MonthlyEarnFlow + - EarnPosition + abis: + - name: VenusToken + file: ./abis/VenusToken.json + eventHandlers: + - event: Mint(indexed address,uint256,uint256,uint256) + handler: handleMint + - event: Redeem(indexed address,uint256,uint256,uint256) + handler: handleRedeem + file: ./src/venus-pool-usdce.ts + - kind: ethereum + name: VenusReward + network: zksync-era + source: + address: '0x7C7846A74AB38A8d554Bc5f7652eCf8Efb58c894' + abi: VenusReward + startBlock: 44575350 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - DailyEarnFlow + - WeeklyEarnFlow + - MonthlyEarnFlow + - EarnPosition + abis: + - name: VenusReward + file: ./abis/VenusReward.json + eventHandlers: + - event: DistributedSupplierRewardToken(indexed address,indexed address,uint256,uint256,uint256) + handler: handleDistributedSupplierRewardToken + file: ./src/venus-reward.ts templates: - kind: ethereum name: Account @@ -275,6 +539,8 @@ templates: entities: - Owner - ClaveTransaction + - Day + - DayAccount - Week - WeekAccount - Month diff --git a/subgraph/tests/new-account-utils.ts b/subgraph/tests/new-account-utils.ts index fd85124..14168ef 100644 --- a/subgraph/tests/new-account-utils.ts +++ b/subgraph/tests/new-account-utils.ts @@ -9,7 +9,6 @@ import { Address, ethereum } from '@graphprotocol/graph-ts'; import { newMockEvent } from 'matchstick-as'; import { NewClaveAccount } from '../generated/AccountFactory/AccountFactory'; -import { Transfer } from '../generated/erc20/ERC20'; export function createNewClaveAccountEvent( accountAddress: Address, @@ -26,22 +25,3 @@ export function createNewClaveAccountEvent( return newClaveAccountEvent; } - -export function createNewTransferEvent( - from: Address, - to: Address, - value: ethereum.Value, -): Transfer { - const newTransferEvent = changetype(newMockEvent()); - newTransferEvent.parameters = []; - - newTransferEvent.parameters.push( - new ethereum.EventParam('from', ethereum.Value.fromAddress(from)), - ); - newTransferEvent.parameters.push( - new ethereum.EventParam('to', ethereum.Value.fromAddress(to)), - ); - newTransferEvent.parameters.push(new ethereum.EventParam('value', value)); - - return newTransferEvent; -} diff --git a/test/accounts/managers/hookmanager.test.ts b/test/accounts/managers/hookmanager.test.ts new file mode 100644 index 0000000..2162814 --- /dev/null +++ b/test/accounts/managers/hookmanager.test.ts @@ -0,0 +1,442 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { assert, expect } from 'chai'; +import type { ec } from 'elliptic'; +import { AbiCoder, randomBytes, solidityPackedKeccak256 } from 'ethers'; +import * as hre from 'hardhat'; +import { Contract } from 'zksync-ethers'; +import { Provider, Wallet, utils } from 'zksync-ethers'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../../deploy/utils'; +import { ClaveDeployer } from '../../utils/deployer'; +import { fixture } from '../../utils/fixture'; +import { addHook, removeHook } from '../../utils/managers/hookmanager'; +import { HOOKS, VALIDATORS } from '../../utils/names'; +import { ethTransfer, prepareTeeTx } from '../../utils/transactions'; + +describe('Clave Contracts - Manager tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let teeValidator: Contract; + let account: Contract; + let keyPair: ec.KeyPair; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [, , , , teeValidator, account, keyPair] = await fixture( + deployer, + VALIDATORS.TEE, + ); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + }); + + describe('Hook Manager', () => { + it('should check existing hooks', async () => { + expect(await account.listHooks(HOOKS.VALIDATION)).to.deep.eq([]); + expect(await account.listHooks(HOOKS.EXECUTION)).to.deep.eq([]); + }); + + describe('Validation hooks', async () => { + let validationHook: Contract; + + before(async () => { + validationHook = await deployer.deployCustomContract( + 'MockValidationHook', + [], + ); + }); + + it('should add a validation hook', async () => { + expect(await account.isHook(await validationHook.getAddress())) + .to.be.false; + + await addHook( + provider, + account, + teeValidator, + validationHook, + HOOKS.VALIDATION, + keyPair, + ); + + expect(await account.isHook(await validationHook.getAddress())) + .to.be.true; + + const expectedHooks = [await validationHook.getAddress()]; + expect(await account.listHooks(HOOKS.VALIDATION)).to.deep.eq( + expectedHooks, + ); + }); + + it('should set hook data correctly', async () => { + const key = randomBytes(32); + const data = '0xc1ae'; + + await validationHook.setHookData( + await account.getAddress(), + key, + data, + ); + + expect( + await account.getHookData( + await validationHook.getAddress(), + key, + ), + ).to.eq(data); + }); + + it('should run validation hooks succcesfully', async () => { + const transfer = ethTransfer(await richWallet.getAddress(), 1); + + const hookData = [ + AbiCoder.defaultAbiCoder().encode(['bool'], [false]), + ]; + + const tx = await prepareTeeTx( + provider, + account, + transfer, + await teeValidator.getAddress(), + keyPair, + hookData, + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + }); + + it('should remove a validation hook', async () => { + expect(await account.isHook(await validationHook.getAddress())) + .to.be.true; + + const hookData = [ + AbiCoder.defaultAbiCoder().encode(['bool'], [false]), + ]; + + await removeHook( + provider, + account, + teeValidator, + validationHook, + HOOKS.VALIDATION, + keyPair, + hookData, + ); + expect(await account.isHook(await validationHook.getAddress())) + .to.be.false; + + expect(await account.listHooks(HOOKS.VALIDATION)).to.deep.eq( + [], + ); + }); + }); + + describe('Execution hooks', async () => { + let executionHook: Contract; + + before(async () => { + executionHook = await deployer.deployCustomContract( + 'MockExecutionHook', + [], + ); + }); + + it('should add a execution hook', async () => { + expect(await account.isHook(await executionHook.getAddress())) + .to.be.false; + + await addHook( + provider, + account, + teeValidator, + executionHook, + HOOKS.EXECUTION, + keyPair, + ); + + expect(await account.isHook(await executionHook.getAddress())) + .to.be.true; + + const expectedHooks = [await executionHook.getAddress()]; + expect(await account.listHooks(HOOKS.EXECUTION)).to.deep.eq( + expectedHooks, + ); + }); + + it('should remove a execution hook', async () => { + expect(await account.isHook(await executionHook.getAddress())) + .to.be.true; + + await removeHook( + provider, + account, + teeValidator, + executionHook, + HOOKS.EXECUTION, + keyPair, + ); + expect(await account.isHook(await executionHook.getAddress())) + .to.be.false; + + expect(await account.listHooks(HOOKS.EXECUTION)).to.deep.eq([]); + }); + }); + + describe('Common execution and validation hook tests', async () => { + let validationHook: Contract; + let executionHook: Contract; + + before(async () => { + validationHook = await deployer.deployCustomContract( + 'MockValidationHook', + [], + ); + executionHook = await deployer.deployCustomContract( + 'MockExecutionHook', + [], + ); + }); + + it('should revert adding a hook with unauthorized msg.sender', async () => { + expect(await account.isHook(await validationHook.getAddress())) + .to.be.false; + expect(await account.isHook(await executionHook.getAddress())) + .to.be.false; + + await expect( + account.addHook( + await validationHook.getAddress(), + HOOKS.VALIDATION, + ), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + + await expect( + account.addHook( + await executionHook.getAddress(), + HOOKS.EXECUTION, + ), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + }); + + it('should revert adding a hook with NO interface', async () => { + const noInterfaceHook = Wallet.createRandom(); + expect(await account.isHook(await validationHook.getAddress())) + .to.be.false; + + try { + await addHook( + provider, + account, + teeValidator, + new Contract(await noInterfaceHook.getAddress(), []), + HOOKS.VALIDATION, + keyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + + try { + await addHook( + provider, + account, + teeValidator, + new Contract(await noInterfaceHook.getAddress(), []), + HOOKS.EXECUTION, + keyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + }); + + it('should revert adding a hook with invalid hookAndData length', async () => { + const hookAndData = (await validationHook.getAddress()).slice( + 0, + 10, + ); + const addHookTx = await account.addHook.populateTransaction( + hookAndData, + HOOKS.VALIDATION, + ); + + const tx = await prepareTeeTx( + provider, + account, + addHookTx, + await teeValidator.getAddress(), + keyPair, + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + try { + await txReceipt.wait(); + assert(false, 'Should revert'); + } catch (err) {} + }); + + describe('Added hooks failure tests', async () => { + before(async () => { + await addHook( + provider, + account, + teeValidator, + validationHook, + HOOKS.VALIDATION, + keyPair, + ); + + const key = randomBytes(32); + const data = '0xc1ae'; + await validationHook.setHookData( + await account.getAddress(), + key, + data, + ); + + const hookData = [ + AbiCoder.defaultAbiCoder().encode(['bool'], [false]), + ]; + await addHook( + provider, + account, + teeValidator, + executionHook, + HOOKS.EXECUTION, + keyPair, + hookData, + ); + }); + + beforeEach(async () => { + expect( + await account.isHook(await validationHook.getAddress()), + ).to.be.true; + expect( + await account.isHook(await executionHook.getAddress()), + ).to.be.true; + }); + + it('should revert removing hooks with unauthorized msg.sender', async () => { + await expect( + account.removeHook( + await validationHook.getAddress(), + HOOKS.VALIDATION, + ), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + await expect( + account.removeHook( + await executionHook.getAddress(), + HOOKS.EXECUTION, + ), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + }); + + it('should revert when validation hooks failed', async () => { + const transfer = ethTransfer( + await richWallet.getAddress(), + 5, + ); + const hookData = [ + AbiCoder.defaultAbiCoder().encode(['bool'], [true]), + ]; + const tx = await prepareTeeTx( + provider, + account, + transfer, + await teeValidator.getAddress(), + keyPair, + hookData, + ); + + try { + await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + assert(false, 'Should revert'); + } catch (e) {} + }); + + it('should revert when execution hooks failed', async () => { + const transfer = ethTransfer( + await richWallet.getAddress(), + 5, + ); + const hookData = [ + AbiCoder.defaultAbiCoder().encode(['bool'], [false]), + ]; + + const tx = await prepareTeeTx( + provider, + account, + transfer, + await teeValidator.getAddress(), + keyPair, + hookData, + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + + try { + await txReceipt.wait(); + assert(false, 'Should revert'); + } catch (err) {} + }); + + it('should revert setting hook data with unauthorized msg.sender, not from hooks', async function () { + const key = randomBytes(32); + const data = '0xc1ae'; + + await expect( + account.setHookData(key, data), + ).to.be.revertedWithCustomError(account, 'NOT_FROM_HOOK'); + }); + + it('should revert setting hook data with invalid key', async function () { + const key = solidityPackedKeccak256( + ['string'], + ['HookManager.context'], + ); + const data = '0xc1ae'; + + await expect( + validationHook.setHookData( + await account.getAddress(), + key, + data, + ), + ).to.be.revertedWithCustomError(account, 'INVALID_KEY'); + }); + }); + }); + }); +}); diff --git a/test/accounts/managers/modulemanager.test.ts b/test/accounts/managers/modulemanager.test.ts new file mode 100644 index 0000000..ec0da62 --- /dev/null +++ b/test/accounts/managers/modulemanager.test.ts @@ -0,0 +1,229 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { assert, expect } from 'chai'; +import type { ec } from 'elliptic'; +import { AbiCoder, concat, parseEther } from 'ethers'; +import * as hre from 'hardhat'; +import { Contract, Provider, Wallet, utils } from 'zksync-ethers'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../../deploy/utils'; +import { ClaveDeployer } from '../../utils/deployer'; +import { fixture } from '../../utils/fixture'; +import { addModule, removeModule } from '../../utils/managers/modulemanager'; +import { VALIDATORS } from '../../utils/names'; +import { prepareTeeTx } from '../../utils/transactions'; + +describe('Clave Contracts - Manager tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let teeValidator: Contract; + let account: Contract; + let keyPair: ec.KeyPair; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [, , , , teeValidator, account, keyPair] = await fixture( + deployer, + VALIDATORS.TEE, + ); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + }); + + describe('Module Manager', () => { + let mockModule: Contract; + + describe('Main module functionalities', () => { + it('should check existing modules', async () => { + expect(await account.listModules()).to.deep.eq([]); + }); + + it('should add a new module', async () => { + mockModule = await deployer.deployCustomContract( + 'MockModule', + [], + ); + expect(await account.isModule(await mockModule.getAddress())).to + .be.false; + + const initData = AbiCoder.defaultAbiCoder().encode( + ['uint256'], + [parseEther('42')], + ); + await addModule( + provider, + account, + teeValidator, + mockModule, + initData, + keyPair, + ); + expect(await account.isModule(await mockModule.getAddress())).to + .be.true; + + const expectedModules = [await mockModule.getAddress()]; + expect(await account.listModules()).to.deep.eq(expectedModules); + }); + + it('should execute from a module', async () => { + const amount = parseEther('42'); + const delta = parseEther('0.01'); + + const accountBalanceBefore = await provider.getBalance( + await account.getAddress(), + ); + const receiverBalanceBefore = await provider.getBalance( + await richWallet.getAddress(), + ); + + await mockModule.testExecuteFromModule( + await account.getAddress(), + await richWallet.getAddress(), + ); + + const accountBalanceAfter = await provider.getBalance( + await account.getAddress(), + ); + const receiverBalanceAfter = await provider.getBalance( + await richWallet.getAddress(), + ); + + expect(accountBalanceAfter).to.be.closeTo( + accountBalanceBefore - amount, + delta, + ); + expect(receiverBalanceAfter).to.be.closeTo( + receiverBalanceBefore + amount, + delta, + ); + }); + + it('should remove a module', async () => { + expect(await account.isModule(await mockModule.getAddress())).to + .be.true; + + await removeModule( + provider, + account, + teeValidator, + mockModule, + keyPair, + ); + + expect(await account.isModule(await mockModule.getAddress())).to + .be.false; + const expectedModules: Array = []; + expect(await account.listModules()).to.deep.eq(expectedModules); + }); + }); + + describe('Alternative module cases', () => { + let newMockModule: Contract; + + before(async () => { + newMockModule = await deployer.deployCustomContract( + 'MockModule', + [], + ); + }); + + it('should revert adding module with unauthorized msg.sender', async () => { + const initData = AbiCoder.defaultAbiCoder().encode( + ['uint256'], + [parseEther('42')], + ); + const moduleAndData = concat([ + await newMockModule.getAddress(), + initData, + ]); + + await expect( + account.addModule(moduleAndData), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + }); + + it('should revert removing module with unauthorized msg.sender', async () => { + const initData = AbiCoder.defaultAbiCoder().encode( + ['uint256'], + [parseEther('42')], + ); + await addModule( + provider, + account, + teeValidator, + newMockModule, + initData, + keyPair, + ); + expect(await account.isModule(await newMockModule.getAddress())) + .to.be.true; + + await expect( + account.removeModule(await newMockModule.getAddress()), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + }); + + it('should revert adding module invalid moduleAndData length', async () => { + const moduleAndData = (await mockModule.getAddress()).slice( + 0, + 10, + ); + const addModuleTx = await account.addModule.populateTransaction( + moduleAndData, + ); + const tx = await prepareTeeTx( + provider, + account, + addModuleTx, + await teeValidator.getAddress(), + keyPair, + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + try { + await txReceipt.wait(); + assert(false, 'Should revert'); + } catch (err) {} + }); + + it('should revert adding module with no interface', async () => { + const noInterfaceModule = Wallet.createRandom(); + const initData = AbiCoder.defaultAbiCoder().encode( + ['uint256'], + [parseEther('42')], + ); + + try { + await addModule( + provider, + account, + teeValidator, + new Contract(await noInterfaceModule.getAddress(), []), + initData, + keyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + }); + }); + }); +}); diff --git a/test/accounts/managers/ownermanager.test.ts b/test/accounts/managers/ownermanager.test.ts new file mode 100644 index 0000000..2e070c1 --- /dev/null +++ b/test/accounts/managers/ownermanager.test.ts @@ -0,0 +1,396 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { assert, expect } from 'chai'; +import type { ec } from 'elliptic'; +import type { BytesLike } from 'ethers'; +import { ZeroAddress, parseEther } from 'ethers'; +import * as hre from 'hardhat'; +import type { Contract } from 'zksync-ethers'; +import { Provider, Wallet, utils } from 'zksync-ethers'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../../deploy/utils'; +import { ClaveDeployer } from '../../utils/deployer'; +import { fixture } from '../../utils/fixture'; +import { + addK1Key, + addR1Key, + removeK1Key, + removeR1Key, + resetOwners, +} from '../../utils/managers/ownermanager'; +import { VALIDATORS } from '../../utils/names'; +import { encodePublicKey, genKey } from '../../utils/p256'; +import { ethTransfer, prepareTeeTx } from '../../utils/transactions'; + +describe('Clave Contracts - Manager tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let teeValidator: Contract; + let account: Contract; + let keyPair: ec.KeyPair; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [, , , , teeValidator, account, keyPair] = await fixture( + deployer, + VALIDATORS.TEE, + ); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + }); + + describe('Owner Manager', () => { + it('should check existing key', async () => { + const newPublicKey = encodePublicKey(keyPair); + expect(await account.r1IsOwner(newPublicKey)).to.be.true; + }); + + describe('Full tests with a new r1 key, adding-removing-validating', () => { + let newKeyPair: ec.KeyPair; + let newPublicKey: string; + + it('should create a new r1 key and add as a new owner', async () => { + newKeyPair = genKey(); + newPublicKey = encodePublicKey(newKeyPair); + + expect(await account.r1IsOwner(newPublicKey)).to.be.false; + + await addR1Key( + provider, + account, + teeValidator, + newPublicKey, + keyPair, + ); + + expect(await account.r1IsOwner(newPublicKey)).to.be.true; + + const expectedOwners = [newPublicKey, encodePublicKey(keyPair)]; + expect(await account.r1ListOwners()).to.deep.eq(expectedOwners); + }); + + it('should send a tx with the new key', async () => { + const amount = parseEther('1'); + const richAddress = await richWallet.getAddress(); + const richBalanceBefore = await provider.getBalance( + richAddress, + ); + + const txData = ethTransfer(richAddress, amount); + const tx = await prepareTeeTx( + provider, + account, + txData, + await teeValidator.getAddress(), + newKeyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const richBalanceAfter = await provider.getBalance(richAddress); + expect(richBalanceAfter).to.be.equal( + richBalanceBefore + amount, + ); + }); + + it('should remove an r1 key', async () => { + expect(await account.r1IsOwner(newPublicKey)).to.be.true; + + await removeR1Key( + provider, + account, + teeValidator, + newPublicKey, + keyPair, + ); + + expect(await account.r1IsOwner(newPublicKey)).to.be.false; + + const expectedOwners = [encodePublicKey(keyPair)]; + + expect(await account.r1ListOwners()).to.deep.eq(expectedOwners); + }); + + it('should not send any tx with the removed key', async () => { + const amount = parseEther('1'); + const richAddress = await richWallet.getAddress(); + const richBalanceBefore = await provider.getBalance( + richAddress, + ); + + expect(richBalanceBefore).to.be.greaterThan(amount); + + const txData = ethTransfer(richAddress, amount); + const tx = await prepareTeeTx( + provider, + account, + txData, + await teeValidator.getAddress(), + newKeyPair, + ); + await expect( + provider.broadcastTransaction(utils.serializeEip712(tx)), + ).to.be.reverted; + }); + }); + + describe('Non-full tests with a new k1 key, adding-removing, not validating', () => { + let newK1Address: string; + it('should add a new k1 key', async () => { + newK1Address = await Wallet.createRandom().getAddress(); + + expect(await account.k1IsOwner(newK1Address)).to.be.false; + + await addK1Key( + provider, + account, + teeValidator, + newK1Address, + keyPair, + ); + + expect(await account.k1IsOwner(newK1Address)).to.be.true; + + const expectedOwners = [newK1Address]; + expect(await account.k1ListOwners()).to.deep.eq(expectedOwners); + }); + + it('should remove the new k1 key', async () => { + expect(await account.k1IsOwner(newK1Address)).to.be.true; + + await removeK1Key( + provider, + account, + teeValidator, + newK1Address, + keyPair, + ); + + expect(await account.k1IsOwner(newK1Address)).to.be.false; + const expectedOwners: Array = []; + expect(await account.k1ListOwners()).to.deep.eq(expectedOwners); + }); + }); + + describe('Additinal tests for r1 and k1 keys', () => { + let replacedKeyPair: ec.KeyPair; + + it('Should reset owners', async () => { + const newKeyPair = genKey(); + const newPublicKey = encodePublicKey(newKeyPair); + await addR1Key( + provider, + account, + teeValidator, + newPublicKey, + keyPair, + ); + + const newK1Address = await Wallet.createRandom().getAddress(); + await addK1Key( + provider, + account, + teeValidator, + newK1Address, + keyPair, + ); + + const expectedR1Owners = [ + newPublicKey, + encodePublicKey(keyPair), + ]; + expect(await account.r1ListOwners()).to.deep.eq( + expectedR1Owners, + ); + + const expectedK1Owners = [newK1Address]; + expect(await account.k1ListOwners()).to.deep.eq( + expectedK1Owners, + ); + + await resetOwners( + provider, + account, + teeValidator, + newPublicKey, + keyPair, + ); + replacedKeyPair = newKeyPair; + + const expectedNewR1Owners = [encodePublicKey(replacedKeyPair)]; + const expectedNewK1Owners: Array = []; + + expect(await account.r1ListOwners()).to.deep.eq( + expectedNewR1Owners, + ); + expect(await account.k1ListOwners()).to.deep.eq( + expectedNewK1Owners, + ); + }); + + it('Should revert the r1 owner with invalid length', async () => { + let invalidLength = Math.ceil(Math.random() * 200) * 2; + invalidLength = invalidLength === 128 ? 130 : invalidLength; + + const invalidPubkey = '0x' + 'C'.repeat(invalidLength); + + try { + await addR1Key( + provider, + account, + teeValidator, + invalidPubkey, + replacedKeyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + }); + + it('Should revert adding new r1 or k1 owner with unauthorized msg.sender', async function () { + const newKeyPair = genKey(); + const newPublicKey = encodePublicKey(newKeyPair); + + await expect( + account.r1AddOwner(newPublicKey), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + + const randomWallet = Wallet.createRandom().connect(provider); + + await expect( + account.k1AddOwner(await randomWallet.getAddress()), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + }); + + it('Should revert removing r1 or k1 owners and resetting owners with unauthorized msg.sender, then should reset owners to the initial', async function () { + const newKeyPair = genKey(); + const newPublicKey = encodePublicKey(newKeyPair); + + await addR1Key( + provider, + account, + teeValidator, + newPublicKey, + replacedKeyPair, + ); + + expect(await account.r1IsOwner(newPublicKey)).to.be.true; + + await expect( + account.r1RemoveOwner(newPublicKey), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + + const newAddress = await Wallet.createRandom().getAddress(); + + await addK1Key( + provider, + account, + teeValidator, + newAddress, + replacedKeyPair, + ); + + expect(await account.k1IsOwner(newAddress)).to.be.true; + + const randomWallet = Wallet.createRandom().connect(provider); + + await expect( + account.k1RemoveOwner(await randomWallet.getAddress()), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + + await resetOwners( + provider, + account, + teeValidator, + encodePublicKey(replacedKeyPair), + replacedKeyPair, + ); + + const expectedNewR1Owners = [encodePublicKey(replacedKeyPair)]; + const expectedNewK1Owners: Array = []; + + expect(await account.r1ListOwners()).to.deep.eq( + expectedNewR1Owners, + ); + expect(await account.k1ListOwners()).to.deep.eq( + expectedNewK1Owners, + ); + + await expect( + account.resetOwners(newPublicKey), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + }); + + it('Should revert adding zero address as k1 owner', async function () { + try { + await addK1Key( + provider, + account, + teeValidator, + ZeroAddress, + replacedKeyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + }); + + it('Should revert removing the last r1 owner', async function () { + try { + await removeR1Key( + provider, + account, + teeValidator, + encodePublicKey(replacedKeyPair), + replacedKeyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + }); + + it('Should revert resetting owners while new r1 owner has invalid length', async function () { + let invalidLength = Math.ceil(Math.random() * 200) * 2; + invalidLength = invalidLength === 128 ? 130 : invalidLength; + + const invalidPubkey = '0x' + 'C'.repeat(invalidLength); + + try { + await removeR1Key( + provider, + account, + teeValidator, + invalidPubkey, + replacedKeyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + }); + }); + }); +}); diff --git a/test/accounts/managers/upgrademanager.test.ts b/test/accounts/managers/upgrademanager.test.ts new file mode 100644 index 0000000..4827c1d --- /dev/null +++ b/test/accounts/managers/upgrademanager.test.ts @@ -0,0 +1,94 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { assert, expect } from 'chai'; +import type { ec } from 'elliptic'; +import * as hre from 'hardhat'; +import type { Contract, Wallet } from 'zksync-ethers'; +import { Provider } from 'zksync-ethers'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../../deploy/utils'; +import { ClaveDeployer } from '../../utils/deployer'; +import { fixture } from '../../utils/fixture'; +import { upgradeTx } from '../../utils/managers/upgrademanager'; +import { VALIDATORS } from '../../utils/names'; + +describe('Clave Contracts - Manager tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let teeValidator: Contract; + let account: Contract; + let keyPair: ec.KeyPair; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [, , , , teeValidator, account, keyPair] = await fixture( + deployer, + VALIDATORS.TEE, + ); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + }); + + describe('Upgrade Manager', () => { + let mockImplementation: Contract; + + before(async () => { + mockImplementation = await deployer.deployCustomContract( + 'MockImplementation', + [], + ); + }); + + it('should revert to a new implementation with unauthorized msg.sender', async () => { + await expect( + account.upgradeTo(await mockImplementation.getAddress()), + ).to.be.revertedWithCustomError(account, 'NOT_FROM_SELF'); + }); + + it('should upgrade to a new implementation', async () => { + expect(await account.implementation()).not.to.be.eq( + await mockImplementation.getAddress(), + ); + + await upgradeTx( + provider, + account, + teeValidator, + mockImplementation, + keyPair, + ); + + expect(await account.implementation()).to.eq( + await mockImplementation.getAddress(), + ); + }); + + it('should revert upgrading to the same implementation', async () => { + expect(await account.implementation()).to.be.eq( + await mockImplementation.getAddress(), + ); + + try { + await upgradeTx( + provider, + account, + teeValidator, + mockImplementation, + keyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + }); + }); +}); diff --git a/test/accounts/managers/validatormanager.test.ts b/test/accounts/managers/validatormanager.test.ts new file mode 100644 index 0000000..60be8d7 --- /dev/null +++ b/test/accounts/managers/validatormanager.test.ts @@ -0,0 +1,350 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { assert, expect } from 'chai'; +import type { ec } from 'elliptic'; +import { parseEther } from 'ethers'; +import * as hre from 'hardhat'; +import { Contract, Provider, Wallet, utils } from 'zksync-ethers'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../../deploy/utils'; +import { ClaveDeployer } from '../../utils/deployer'; +import { fixture } from '../../utils/fixture'; +import { + addK1Validator, + addR1Validator, + removeK1Validator, + removeR1Validator, +} from '../../utils/managers/validatormanager'; +import { VALIDATORS } from '../../utils/names'; +import { ethTransfer, prepareMockTx } from '../../utils/transactions'; + +describe('Clave Contracts - Manager tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let teeValidator: Contract; + let account: Contract; + let keyPair: ec.KeyPair; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [, , , , teeValidator, account, keyPair] = await fixture( + deployer, + VALIDATORS.TEE, + ); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + }); + + describe('Validator Manager', () => { + it('should check existing validator', async () => { + const validatorAddress = await teeValidator.getAddress(); + + expect(await account.r1IsValidator(validatorAddress)).to.be.true; + }); + + describe('Full tests with r1 validator type, adding-removing-validating', () => { + let newR1Validator: Contract; + + it('should add a new r1 validator', async () => { + newR1Validator = await deployer.validator(VALIDATORS.MOCK); + const validatorAddress = await newR1Validator.getAddress(); + + expect(await account.r1IsValidator(validatorAddress)).to.be + .false; + + await addR1Validator( + provider, + account, + teeValidator, + newR1Validator, + keyPair, + ); + + expect(await account.r1IsValidator(validatorAddress)).to.be + .true; + + const expectedValidators = [ + validatorAddress, + await teeValidator.getAddress(), + ]; + + expect(await account.r1ListValidators()).to.deep.eq( + expectedValidators, + ); + }); + + it('should send a tx with the new r1 validator', async () => { + const amount = parseEther('1'); + const richAddress = await richWallet.getAddress(); + const richBalanceBefore = await provider.getBalance( + richAddress, + ); + + const txData = ethTransfer(richAddress, amount); + const tx = await prepareMockTx( + provider, + account, + txData, + await newR1Validator.getAddress(), + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const richBalanceAfter = await provider.getBalance(richAddress); + expect(richBalanceAfter).to.be.equal( + richBalanceBefore + amount, + ); + }); + + it('should remove the new r1 validator', async () => { + const validatorAddress = await newR1Validator.getAddress(); + expect(await account.r1IsValidator(validatorAddress)).to.be + .true; + + await removeR1Validator( + provider, + account, + teeValidator, + newR1Validator, + keyPair, + ); + + expect(await account.r1IsValidator(validatorAddress)).to.be + .false; + + const expectedValidators = [await teeValidator.getAddress()]; + + expect(await account.r1ListValidators()).to.deep.eq( + expectedValidators, + ); + }); + }); + + describe('Non-full tests with k1 validator type, adding-removing, not-validating', () => { + let newK1Validator: Contract; + + it('should add a new k1 validator', async () => { + newK1Validator = await deployer.validator(VALIDATORS.EOA); + const validatorAddress = await newK1Validator.getAddress(); + + expect(await account.k1IsValidator(newK1Validator)).to.be.false; + + await addK1Validator( + provider, + account, + teeValidator, + newK1Validator, + keyPair, + ); + + expect(await account.k1IsValidator(newK1Validator)).to.be.true; + + const expectedValidators = [validatorAddress]; + + expect(await account.k1ListValidators()).to.deep.eq( + expectedValidators, + ); + }); + + it('should remove a k1 validator', async () => { + const validatorAddress = await newK1Validator.getAddress(); + expect(await account.k1IsValidator(validatorAddress)).to.be + .true; + + await removeK1Validator( + provider, + account, + teeValidator, + newK1Validator, + keyPair, + ); + + expect(await account.r1IsValidator(validatorAddress)).to.be + .false; + + expect(await account.k1ListValidators()).to.deep.eq([]); + }); + }); + + describe('Additional tests for r1 and k1 validators', () => { + it('should revert adding new r1 and k1 validator with unauthorized msg.sender', async () => { + const newR1Validator = await deployer.validator( + VALIDATORS.MOCK, + ); + const r1ValidatorAddress = await newR1Validator.getAddress(); + + const newK1Validator = await deployer.validator(VALIDATORS.EOA); + const k1ValidatorAddress = await newK1Validator.getAddress(); + + await expect( + account.r1AddValidator(r1ValidatorAddress), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + + await expect( + account.k1AddValidator(k1ValidatorAddress), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + }); + + it('should revert adding new r1 and k1 validator with unauthorized msg.sender', async () => { + const newR1Validator = await deployer.validator( + VALIDATORS.MOCK, + ); + const r1ValidatorAddress = await newR1Validator.getAddress(); + + await addR1Validator( + provider, + account, + teeValidator, + newR1Validator, + keyPair, + ); + expect(await account.r1IsValidator(r1ValidatorAddress)).to.be + .true; + + await expect( + account.r1RemoveValidator( + await newR1Validator.getAddress(), + ), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + + const newK1Validator = await deployer.validator(VALIDATORS.EOA); + const k1ValidatorAddress = await newK1Validator.getAddress(); + + await addK1Validator( + provider, + account, + teeValidator, + newK1Validator, + keyPair, + ); + expect(await account.k1IsValidator(k1ValidatorAddress)).to.be + .true; + + await expect( + account.k1RemoveValidator( + await newK1Validator.getAddress(), + ), + ).to.be.revertedWithCustomError( + account, + 'NOT_FROM_SELF_OR_MODULE', + ); + + await removeR1Validator( + provider, + account, + teeValidator, + newR1Validator, + keyPair, + ); + await removeK1Validator( + provider, + account, + teeValidator, + newK1Validator, + keyPair, + ); + }); + + it('should revert adding new r1 and k1 validator with WRONG interface', async () => { + const wrongR1Validator = await deployer.validator( + VALIDATORS.EOA, + ); + + const wrongK1Validator = await deployer.validator( + VALIDATORS.MOCK, + ); + + try { + await addR1Validator( + provider, + account, + teeValidator, + wrongR1Validator, + keyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + + try { + await addK1Validator( + provider, + account, + teeValidator, + wrongK1Validator, + keyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + }); + + it('should revert adding new r1 and k1 validator with NO interface', async () => { + const noInterfaceValidator = Wallet.createRandom(); + const validatorAddress = + await noInterfaceValidator.getAddress(); + + try { + await addR1Validator( + provider, + account, + teeValidator, + new Contract(validatorAddress, []), + keyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + + try { + await addK1Validator( + provider, + account, + teeValidator, + new Contract(validatorAddress, []), + keyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + }); + + it('should revert removing the last r1 validator', async () => { + const expectedValidators = [await teeValidator.getAddress()]; + + expect(await account.r1ListValidators()).to.deep.eq( + expectedValidators, + ); + + try { + await removeR1Validator( + provider, + account, + teeValidator, + teeValidator, + keyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + }); + }); + }); +}); diff --git a/test/accounts/modules/cloudrecovery.test.ts b/test/accounts/modules/cloudrecovery.test.ts new file mode 100644 index 0000000..f883711 --- /dev/null +++ b/test/accounts/modules/cloudrecovery.test.ts @@ -0,0 +1,261 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { assert, expect } from 'chai'; +import type { ec } from 'elliptic'; +import { AbiCoder } from 'ethers'; +import * as hre from 'hardhat'; +import type { Contract } from 'zksync-ethers'; +import { Provider, Wallet } from 'zksync-ethers'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../../deploy/utils'; +import { ClaveDeployer } from '../../utils/deployer'; +import { fixture } from '../../utils/fixture'; +import { addModule } from '../../utils/managers/modulemanager'; +import { VALIDATORS } from '../../utils/names'; +import { encodePublicKey, genKey } from '../../utils/p256'; +import { + executeRecovery, + startCloudRecovery, + stopRecovery, + updateCloudGuardian, +} from '../../utils/recovery/recovery'; + +describe('Clave Contracts - Manager tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let teeValidator: Contract; + let account: Contract; + let keyPair: ec.KeyPair; + + let cloudRecoveryModule: Contract; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [, , , , teeValidator, account, keyPair] = await fixture( + deployer, + VALIDATORS.TEE, + ); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + + cloudRecoveryModule = await deployer.deployCustomContract( + 'CloudRecoveryModule', + ['TEST', '0', 0], + ); + }); + + describe('Module Tests - Cloud Recovery Module', () => { + let cloudGuardian: Wallet; + let newKeyPair: ec.KeyPair; + + describe('Adding & Initializing module', () => { + before(async () => { + cloudGuardian = new Wallet( + Wallet.createRandom().privateKey, + provider, + ); + + newKeyPair = genKey(); + }); + + it('should check existing modules', async () => { + expect(await account.listModules()).to.deep.eq([]); + }); + + it('should add a new module', async () => { + expect( + await account.isModule( + await cloudRecoveryModule.getAddress(), + ), + ).to.be.false; + + const initData = AbiCoder.defaultAbiCoder().encode( + ['address'], + [await cloudGuardian.getAddress()], + ); + await addModule( + provider, + account, + teeValidator, + cloudRecoveryModule, + initData, + keyPair, + ); + expect( + await account.isModule( + await cloudRecoveryModule.getAddress(), + ), + ).to.be.true; + + const expectedModules = [ + await cloudRecoveryModule.getAddress(), + ]; + expect(await account.listModules()).to.deep.eq(expectedModules); + }); + + it('should init the module successfully', async () => { + const status = await cloudRecoveryModule.isInited( + await account.getAddress(), + ); + expect(status).to.eq(true); + }); + + it('should assign the guardian correctly', async () => { + const guardian = await cloudRecoveryModule.getGuardian( + await account.getAddress(), + ); + expect(guardian).to.eq(await cloudGuardian.getAddress()); + }); + }); + + describe('Recovery process', () => { + it('should start recovery process', async () => { + expect( + await cloudRecoveryModule.isRecovering( + await account.getAddress(), + ), + ).to.be.false; + + expect(await account.r1ListOwners()).to.deep.eq([ + encodePublicKey(keyPair), + ]); + + await startCloudRecovery( + cloudGuardian, + account, + cloudRecoveryModule, + encodePublicKey(newKeyPair), + ); + + expect( + await cloudRecoveryModule.isRecovering( + await account.getAddress(), + ), + ).to.be.true; + }); + + it('should execute recovery process', async () => { + expect( + await cloudRecoveryModule.isRecovering( + await account.getAddress(), + ), + ).to.be.true; + + expect(await account.r1ListOwners()).to.deep.eq([ + encodePublicKey(keyPair), + ]); + + await executeRecovery(account, cloudRecoveryModule); + + expect( + await cloudRecoveryModule.isRecovering( + await account.getAddress(), + ), + ).to.be.false; + + expect(await account.r1ListOwners()).to.deep.eq([ + encodePublicKey(newKeyPair), + ]); + }); + }); + + describe('Alternative recovery process scenarios', () => { + let newNewKeyPair: ec.KeyPair; + + before(async () => { + newNewKeyPair = genKey(); + }); + + it('shoud revert changing the guardian if recovery is in progress', async () => { + expect( + await cloudRecoveryModule.isRecovering( + await account.getAddress(), + ), + ).to.be.false; + + await startCloudRecovery( + cloudGuardian, + account, + cloudRecoveryModule, + encodePublicKey(newNewKeyPair), + ); + + expect( + await cloudRecoveryModule.isRecovering( + await account.getAddress(), + ), + ).to.be.true; + + expect( + await cloudRecoveryModule.isRecovering( + await account.getAddress(), + ), + ).to.be.true; + const guardian = await cloudRecoveryModule.getGuardian( + await account.getAddress(), + ); + expect(guardian).to.eq(await cloudGuardian.getAddress()); + + const newGuardianAddress = new Wallet( + Wallet.createRandom().privateKey, + provider, + ); + + try { + await updateCloudGuardian( + provider, + account, + cloudRecoveryModule, + teeValidator, + await newGuardianAddress.getAddress(), + keyPair, + ); + assert(false, 'Should revert'); + } catch (err) {} + + expect( + await cloudRecoveryModule.isRecovering( + await account.getAddress(), + ), + ).to.be.true; + const guardianLater = await cloudRecoveryModule.getGuardian( + await account.getAddress(), + ); + expect(guardian).to.eq(guardianLater); + }); + + it('should stop the recovery process', async () => { + expect( + await cloudRecoveryModule.isRecovering( + await account.getAddress(), + ), + ).to.be.true; + + await stopRecovery( + provider, + account, + cloudRecoveryModule, + teeValidator, + newKeyPair, + ); + + expect( + await cloudRecoveryModule.isRecovering( + await account.getAddress(), + ), + ).to.be.false; + }); + }); + }); +}); diff --git a/test/accounts/modules/socialrecovery.test.ts b/test/accounts/modules/socialrecovery.test.ts new file mode 100644 index 0000000..994768a --- /dev/null +++ b/test/accounts/modules/socialrecovery.test.ts @@ -0,0 +1,280 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { expect } from 'chai'; +import type { ec } from 'elliptic'; +import { AbiCoder, parseEther } from 'ethers'; +import * as hre from 'hardhat'; +import type { Contract } from 'zksync-ethers'; +import { Provider, Wallet, utils } from 'zksync-ethers'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../../deploy/utils'; +import { ClaveDeployer } from '../../utils/deployer'; +import { fixture } from '../../utils/fixture'; +import { addModule } from '../../utils/managers/modulemanager'; +import { VALIDATORS } from '../../utils/names'; +import { encodePublicKey, genKey } from '../../utils/p256'; +import { + executeRecovery, + startSocialRecovery, + stopRecovery, + updateSocialRecoveryConfig, +} from '../../utils/recovery/recovery'; +import { ethTransfer, prepareTeeTx } from '../../utils/transactions'; + +describe('Clave Contracts - Manager tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let teeValidator: Contract; + let account: Contract; + let keyPair: ec.KeyPair; + + let socialRecoveryModule: Contract; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [, , , , teeValidator, account, keyPair] = await fixture( + deployer, + VALIDATORS.TEE, + ); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + + socialRecoveryModule = await deployer.deployCustomContract( + 'SocialRecoveryModule', + ['TEST', '0', 0, 0], + ); + }); + + describe('Module Tests - Social Recovery Module', () => { + let socialGuardian: Wallet; + let secondGuardian: Wallet; + let newKeyPair: ec.KeyPair; + + describe('Adding & Initializing module', () => { + before(async () => { + socialGuardian = new Wallet( + Wallet.createRandom().privateKey, + provider, + ); + secondGuardian = new Wallet( + Wallet.createRandom().privateKey, + provider, + ); + + newKeyPair = genKey(); + }); + + it('should check existing modules', async () => { + expect(await account.listModules()).to.deep.eq([]); + }); + + it('should add a new module', async () => { + expect( + await account.isModule( + await socialRecoveryModule.getAddress(), + ), + ).to.be.false; + + const initData = AbiCoder.defaultAbiCoder().encode( + ['tuple(uint128, uint128, address[])'], + [[1, 1, [await socialGuardian.getAddress()]]], + ); + await addModule( + provider, + account, + teeValidator, + socialRecoveryModule, + initData, + keyPair, + ); + expect( + await account.isModule( + await socialRecoveryModule.getAddress(), + ), + ).to.be.true; + + const expectedModules = [ + await socialRecoveryModule.getAddress(), + ]; + expect(await account.listModules()).to.deep.eq(expectedModules); + }); + + it('should init the module successfully', async () => { + const status = await socialRecoveryModule.isInited( + await account.getAddress(), + ); + expect(status).to.eq(true); + }); + + it('should assign the guardian correctly', async () => { + const guardians = await socialRecoveryModule.getGuardians( + await account.getAddress(), + ); + expect(guardians).to.deep.eq([ + await socialGuardian.getAddress(), + ]); + }); + + it('should change the guardian correctly', async () => { + await updateSocialRecoveryConfig( + provider, + account, + socialRecoveryModule, + teeValidator, + [1, 1, [await secondGuardian.getAddress()]], + keyPair, + ); + + const guardians = await socialRecoveryModule.getGuardians( + await account.getAddress(), + ); + expect(guardians).to.deep.eq([ + await secondGuardian.getAddress(), + ]); + }); + + it('should add multiple guardians correctly', async () => { + await updateSocialRecoveryConfig( + provider, + account, + socialRecoveryModule, + teeValidator, + [ + 1, + 1, + [ + await socialGuardian.getAddress(), + await secondGuardian.getAddress(), + ], + ], + keyPair, + ); + + const guardians = await socialRecoveryModule.getGuardians( + await account.getAddress(), + ); + expect(guardians).to.deep.eq([ + await socialGuardian.getAddress(), + await secondGuardian.getAddress(), + ]); + }); + }); + + describe('Recovering account', () => { + it('should start the recovery process by guardian', async () => { + const accountAddress = await account.getAddress(); + + const isRecoveringBefore = + await socialRecoveryModule.isRecovering(accountAddress); + expect(isRecoveringBefore).to.be.false; + + expect(await account.r1ListOwners()).to.deep.eq([ + encodePublicKey(keyPair), + ]); + + await startSocialRecovery( + socialGuardian, + account, + socialRecoveryModule, + newKeyPair, + ); + + const isRecoveringAfter = + await socialRecoveryModule.isRecovering(accountAddress); + expect(isRecoveringAfter).to.be.true; + }); + + it('should decline the social recovery', async () => { + const accountAddress = await account.getAddress(); + + const isRecoveringBefore = + await socialRecoveryModule.isRecovering(accountAddress); + expect(isRecoveringBefore).to.be.true; + + await stopRecovery( + provider, + account, + socialRecoveryModule, + teeValidator, + keyPair, + ); + + const isRecoveringAfter = + await socialRecoveryModule.isRecovering(accountAddress); + expect(isRecoveringAfter).to.be.false; + }); + + it('should execute the social recovery', async () => { + const accountAddress = await account.getAddress(); + + const isRecoveringBefore = + await socialRecoveryModule.isRecovering(accountAddress); + expect(isRecoveringBefore).to.be.false; + + await startSocialRecovery( + socialGuardian, + account, + socialRecoveryModule, + newKeyPair, + ); + + const isRecoveringAfter = + await socialRecoveryModule.isRecovering(accountAddress); + expect(isRecoveringAfter).to.be.true; + + expect(await account.r1ListOwners()).to.deep.eq([ + encodePublicKey(keyPair), + ]); + + await executeRecovery(account, socialRecoveryModule); + + const isRecoveringAfterExecution = + await socialRecoveryModule.isRecovering(accountAddress); + expect(isRecoveringAfterExecution).to.be.false; + + expect(await account.r1ListOwners()).to.deep.eq([ + encodePublicKey(newKeyPair), + ]); + }); + + it('should send tx with new keys after recovery', async () => { + const amount = parseEther('1'); + const richAddress = await richWallet.getAddress(); + + const richBalanceBefore = await provider.getBalance( + richAddress, + ); + + const txData = ethTransfer(richAddress, amount); + const tx = await prepareTeeTx( + provider, + account, + txData, + await teeValidator.getAddress(), + newKeyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const richBalanceAfter = await provider.getBalance(richAddress); + + expect(richBalanceAfter).to.be.equal( + richBalanceBefore + amount, + ); + }); + }); + }); +}); diff --git a/test/accounts/transactions.test.ts b/test/accounts/transactions.test.ts new file mode 100644 index 0000000..1892fc8 --- /dev/null +++ b/test/accounts/transactions.test.ts @@ -0,0 +1,214 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { expect } from 'chai'; +import { parseEther } from 'ethers'; +import * as hre from 'hardhat'; +import type { Contract, Wallet } from 'zksync-ethers'; +import { Provider, utils } from 'zksync-ethers'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../deploy/utils'; +import type { CallStruct } from '../../typechain-types/contracts/batch/BatchCaller'; +import { ClaveDeployer } from '../utils/deployer'; +import { fixture } from '../utils/fixture'; +import { + ethTransfer, + prepareMockBatchTx, + prepareMockTx, +} from '../utils/transactions'; + +describe('Clave Contracts - Account tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let batchCaller: Contract; + let mockValidator: Contract; + let account: Contract; + + let erc20: Contract; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [batchCaller, , , , mockValidator, account] = await fixture(deployer); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + + erc20 = await deployer.deployCustomContract('MockStable', []); + await erc20.mint(accountAddress, parseEther('100000')); + }); + + describe('Transactions', () => { + let accountAddress: string; + let richAddress: string; + let accountBalanceBefore: bigint; + let richBalanceBefore: bigint; + + before(async () => { + const addresses = await Promise.all([ + account.getAddress(), + richWallet.getAddress(), + ]); + [accountAddress, richAddress] = addresses; + }); + + beforeEach(async () => { + const balances = await Promise.all([ + provider.getBalance(accountAddress), + provider.getBalance(richAddress), + ]); + [accountBalanceBefore, richBalanceBefore] = balances; + }); + + it('should send ETH', async () => { + const amount = parseEther('1'); + const delta = parseEther('0.01'); + + const txData = ethTransfer(richAddress, amount); + const tx = await prepareMockTx( + provider, + account, + txData, + await mockValidator.getAddress(), + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const [accountBalanceAfter, richBalanceAfter] = await Promise.all([ + provider.getBalance(accountAddress), + provider.getBalance(richAddress), + ]); + + expect(accountBalanceAfter).to.be.closeTo( + accountBalanceBefore - amount, + delta, + ); + expect(richBalanceAfter).to.be.equal(richBalanceBefore + amount); + }); + + it('should send ERC20 token / contract interaction and pay gas', async () => { + const amount = parseEther('100'); + + const [accountERC20BalanceBefore, richERC20BalanceBefore] = + await Promise.all([ + erc20.balanceOf(accountAddress), + erc20.balanceOf(richAddress), + ]); + + const txData = { + to: await erc20.getAddress(), + value: 0, + data: erc20.interface.encodeFunctionData('transfer', [ + richAddress, + amount, + ]), + }; + const tx = await prepareMockTx( + provider, + account, + txData, + await mockValidator.getAddress(), + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const [accountERC20BalanceAfter, richERC20BalanceAfter] = + await Promise.all([ + erc20.balanceOf(accountAddress), + erc20.balanceOf(richAddress), + ]); + + expect(accountERC20BalanceAfter).to.be.equal( + accountERC20BalanceBefore - amount, + ); + expect(richERC20BalanceAfter).to.be.equal( + richERC20BalanceBefore + amount, + ); + + // Gas payment check + const accountBalanceAfter = await provider.getBalance( + accountAddress, + ); + expect(accountBalanceBefore).to.be.greaterThan(accountBalanceAfter); + }); + + it('should send batch tx / delegate call', async () => { + const amount = parseEther('100'); + const delta = parseEther('0.01'); + + const [accountERC20BalanceBefore, richERC20BalanceBefore] = + await Promise.all([ + erc20.balanceOf(accountAddress), + erc20.balanceOf(richAddress), + ]); + + const calls: Array = [ + { + target: richAddress, + allowFailure: false, + value: amount, + callData: '0x', + }, + { + target: await erc20.getAddress(), + allowFailure: false, + value: 0, + callData: erc20.interface.encodeFunctionData('transfer', [ + richAddress, + amount, + ]), + }, + ]; + + const batchTx = await prepareMockBatchTx( + provider, + account, + await batchCaller.getAddress(), + calls, + await mockValidator.getAddress(), + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(batchTx), + ); + await txReceipt.wait(); + + const [accountBalanceAfter, richBalanceAfter] = await Promise.all([ + provider.getBalance(accountAddress), + provider.getBalance(richAddress), + ]); + + expect(accountBalanceAfter).to.be.closeTo( + accountBalanceBefore - amount, + delta, + ); + expect(richBalanceAfter).to.be.equal(richBalanceBefore + amount); + + const [accountERC20BalanceAfter, richERC20BalanceAfter] = + await Promise.all([ + erc20.balanceOf(accountAddress), + erc20.balanceOf(richAddress), + ]); + + expect(accountERC20BalanceAfter).to.be.equal( + accountERC20BalanceBefore - amount, + ); + expect(richERC20BalanceAfter).to.be.equal( + richERC20BalanceBefore + amount, + ); + }); + }); +}); diff --git a/test/accounts/validators/eoavalidator.test.ts b/test/accounts/validators/eoavalidator.test.ts new file mode 100644 index 0000000..4dcb7d4 --- /dev/null +++ b/test/accounts/validators/eoavalidator.test.ts @@ -0,0 +1,182 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { assert, expect } from 'chai'; +import type { ec } from 'elliptic'; +import type { HDNodeWallet } from 'ethers'; +import { parseEther } from 'ethers'; +import * as hre from 'hardhat'; +import { Provider, Wallet, utils } from 'zksync-ethers'; +import type { Contract } from 'zksync-ethers'; +import type { TransactionLike } from 'zksync-ethers/build/types'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../../deploy/utils'; +import { ClaveDeployer } from '../../utils/deployer'; +import { fixture } from '../../utils/fixture'; +import { addK1Key } from '../../utils/managers/ownermanager'; +import { addK1Validator } from '../../utils/managers/validatormanager'; +import { VALIDATORS } from '../../utils/names'; +import { + ethTransfer, + prepareEOATx, + prepareMockTx, +} from '../../utils/transactions'; + +describe('Clave Contracts - Validator tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let teeValidator: Contract; + let account: Contract; + let keyPair: ec.KeyPair; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [, , , , teeValidator, account, keyPair] = await fixture( + deployer, + VALIDATORS.TEE, + ); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + }); + + describe('EOAValidator', () => { + let newK1Validator: Contract; + let newK1Owner: HDNodeWallet; + + before(async () => { + newK1Validator = await deployer.validator(VALIDATORS.EOA); + newK1Owner = Wallet.createRandom(); + + await addK1Validator( + provider, + account, + teeValidator, + newK1Validator, + keyPair, + ); + + await addK1Key( + provider, + account, + teeValidator, + await newK1Owner.getAddress(), + keyPair, + ); + }); + + it('should check existing validator', async () => { + const teeValidatorAddress = await teeValidator.getAddress(); + const k1ValidatorAddress = await newK1Validator.getAddress(); + const k1OwnerAddress = await newK1Owner.getAddress(); + + expect(await account.r1IsValidator(teeValidatorAddress)).to.be.true; + + expect(await account.k1IsValidator(k1ValidatorAddress)).to.be.true; + + expect(await account.k1IsOwner(k1OwnerAddress)).to.be.true; + }); + + describe('Signature checks', () => { + let richAddress: string; + let richBalanceBefore: bigint; + let amount: bigint; + + let txData: TransactionLike; + + beforeEach(async () => { + richBalanceBefore = await provider.getBalance(richAddress); + }); + + before(async () => { + richAddress = await richWallet.getAddress(); + + amount = parseEther('1'); + + txData = ethTransfer(richAddress, amount); + }); + + describe('Valid tx and signature', () => { + it('should send a tx', async () => { + const tx = await prepareEOATx( + provider, + account, + txData, + await newK1Validator.getAddress(), + newK1Owner, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const richBalanceAfter = await provider.getBalance( + richAddress, + ); + expect(richBalanceAfter).to.eq(richBalanceBefore + amount); + }); + }); + + describe('Invalid tx and signature', () => { + it('should revert sending the tx', async () => { + const mockTxData = txData; + mockTxData.to = await Wallet.createRandom().getAddress(); // corrupted tx data + + const tx = await prepareEOATx( + provider, + account, + txData, + await newK1Validator.getAddress(), + newK1Owner, + ); + + try { + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + assert(false); + } catch (err) {} + + const richBalanceAfter = await provider.getBalance( + richAddress, + ); + expect(richBalanceAfter).to.eq(richBalanceBefore); + }); + }); + + describe('Tx and wrong signature', () => { + it('should revert sending the tx', async () => { + const tx = await prepareMockTx( + provider, + account, + txData, + await newK1Validator.getAddress(), + ); + + try { + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + assert(false); + } catch (err) {} + + const richBalanceAfter = await provider.getBalance( + richAddress, + ); + expect(richBalanceAfter).to.eq(richBalanceBefore); + }); + }); + }); + }); +}); diff --git a/test/accounts/validators/passkeyvalidator.test.ts b/test/accounts/validators/passkeyvalidator.test.ts new file mode 100644 index 0000000..7aeb0fa --- /dev/null +++ b/test/accounts/validators/passkeyvalidator.test.ts @@ -0,0 +1,149 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { assert, expect } from 'chai'; +import type { ec } from 'elliptic'; +import { parseEther } from 'ethers'; +import * as hre from 'hardhat'; +import { Provider, Wallet, utils } from 'zksync-ethers'; +import type { Contract } from 'zksync-ethers'; +import type { TransactionLike } from 'zksync-ethers/build/types'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../../deploy/utils'; +import { ClaveDeployer } from '../../utils/deployer'; +import { fixture } from '../../utils/fixture'; +import { VALIDATORS } from '../../utils/names'; +import { + ethTransfer, + prepareMockTx, + preparePasskeyTx, +} from '../../utils/transactions'; + +describe('Clave Contracts - Validator tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let passkeyValidator: Contract; + let account: Contract; + let keyPair: ec.KeyPair; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [, , , , passkeyValidator, account, keyPair] = await fixture( + deployer, + VALIDATORS.PASSKEY, + ); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + }); + + describe('PasskeyValidator', () => { + it('should check existing validator', async () => { + const validatorAddress = await passkeyValidator.getAddress(); + + expect(await account.r1IsValidator(validatorAddress)).to.be.true; + }); + + describe('Signature checks', () => { + let richAddress: string; + let richBalanceBefore: bigint; + let amount: bigint; + + let txData: TransactionLike; + + beforeEach(async () => { + richBalanceBefore = await provider.getBalance(richAddress); + }); + + before(async () => { + richAddress = await richWallet.getAddress(); + + amount = parseEther('1'); + + txData = ethTransfer(richAddress, amount); + }); + + describe('Valid tx and signature', () => { + it('should send a tx', async () => { + const tx = await preparePasskeyTx( + provider, + account, + txData, + await passkeyValidator.getAddress(), + keyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const richBalanceAfter = await provider.getBalance( + richAddress, + ); + expect(richBalanceAfter).to.eq(richBalanceBefore + amount); + }); + }); + + describe('Invalid tx and signature', () => { + it('should revert sending the tx', async () => { + const mockTxData = txData; + mockTxData.to = await Wallet.createRandom().getAddress(); // corrupted tx data + + const tx = await preparePasskeyTx( + provider, + account, + txData, + await passkeyValidator.getAddress(), + keyPair, + ); + + try { + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + assert(false); + } catch (err) {} + + const richBalanceAfter = await provider.getBalance( + richAddress, + ); + expect(richBalanceAfter).to.eq(richBalanceBefore); + }); + }); + + describe('Tx and wrong signature', () => { + it('should revert sending the tx', async () => { + const tx = await prepareMockTx( + provider, + account, + txData, + await passkeyValidator.getAddress(), + ); + + try { + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + assert(false); + } catch (err) {} + + const richBalanceAfter = await provider.getBalance( + richAddress, + ); + expect(richBalanceAfter).to.eq(richBalanceBefore); + }); + }); + }); + }); +}); diff --git a/test/accounts/validators/teevalidator.test.ts b/test/accounts/validators/teevalidator.test.ts new file mode 100644 index 0000000..f526148 --- /dev/null +++ b/test/accounts/validators/teevalidator.test.ts @@ -0,0 +1,149 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { assert, expect } from 'chai'; +import type { ec } from 'elliptic'; +import { parseEther } from 'ethers'; +import * as hre from 'hardhat'; +import { Provider, Wallet, utils } from 'zksync-ethers'; +import type { Contract } from 'zksync-ethers'; +import type { TransactionLike } from 'zksync-ethers/build/types'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../../deploy/utils'; +import { ClaveDeployer } from '../../utils/deployer'; +import { fixture } from '../../utils/fixture'; +import { VALIDATORS } from '../../utils/names'; +import { + ethTransfer, + prepareMockTx, + prepareTeeTx, +} from '../../utils/transactions'; + +describe('Clave Contracts - Validator tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let teeValidator: Contract; + let account: Contract; + let keyPair: ec.KeyPair; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [, , , , teeValidator, account, keyPair] = await fixture( + deployer, + VALIDATORS.TEE, + ); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + }); + + describe('TEEValidator', () => { + it('should check existing validator', async () => { + const validatorAddress = await teeValidator.getAddress(); + + expect(await account.r1IsValidator(validatorAddress)).to.be.true; + }); + + describe('Signature checks', () => { + let richAddress: string; + let richBalanceBefore: bigint; + let amount: bigint; + + let txData: TransactionLike; + + beforeEach(async () => { + richBalanceBefore = await provider.getBalance(richAddress); + }); + + before(async () => { + richAddress = await richWallet.getAddress(); + + amount = parseEther('1'); + + txData = ethTransfer(richAddress, amount); + }); + + describe('Valid tx and signature', () => { + it('should send a tx', async () => { + const tx = await prepareTeeTx( + provider, + account, + txData, + await teeValidator.getAddress(), + keyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const richBalanceAfter = await provider.getBalance( + richAddress, + ); + expect(richBalanceAfter).to.eq(richBalanceBefore + amount); + }); + }); + + describe('Invalid tx and signature', () => { + it('should revert sending the tx', async () => { + const mockTxData = txData; + mockTxData.to = await Wallet.createRandom().getAddress(); // corrupted tx data + + const tx = await prepareTeeTx( + provider, + account, + txData, + await teeValidator.getAddress(), + keyPair, + ); + + try { + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + assert(false); + } catch (err) {} + + const richBalanceAfter = await provider.getBalance( + richAddress, + ); + expect(richBalanceAfter).to.eq(richBalanceBefore); + }); + }); + + describe('Tx and wrong signature', () => { + it('should revert sending the tx', async () => { + const tx = await prepareMockTx( + provider, + account, + txData, + await teeValidator.getAddress(), + ); + + try { + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + assert(false); + } catch (err) {} + + const richBalanceAfter = await provider.getBalance( + richAddress, + ); + expect(richBalanceAfter).to.eq(richBalanceBefore); + }); + }); + }); + }); +}); diff --git a/test/clave.test.ts b/test/clave.test.ts deleted file mode 100644 index 03500db..0000000 --- a/test/clave.test.ts +++ /dev/null @@ -1,2498 +0,0 @@ -/** - * Copyright Clave - All Rights Reserved - * Unauthorized copying of this file, via any medium is strictly prohibited - * Proprietary and confidential - */ -import { assert, expect } from 'chai'; -import type { ec } from 'elliptic'; -import { - AbiCoder, - WeiPerEther, - ZeroAddress, - concat, - ethers, - parseEther, - randomBytes, - solidityPackedKeccak256, -} from 'ethers'; -import * as hre from 'hardhat'; -import { Contract, Provider, Wallet, utils } from 'zksync-ethers'; - -import { LOCAL_RICH_WALLETS, deployContract, getWallet } from '../deploy/utils'; -import type { CallStruct } from '../typechain-types/contracts/batch/BatchCaller'; -import { encodePublicKey, genKey } from './utils/p256'; -import { getGaslessPaymasterInput } from './utils/paymaster'; -import { ethTransfer, prepareBatchTx, prepareTeeTx } from './utils/transaction'; - -let provider: Provider; -let richWallet: Wallet; -let keyPair: ec.KeyPair; - -let batchCaller: Contract; -let mockValidator: Contract; -let implementation: Contract; -let factory: Contract; -let account: Contract; -let registry: Contract; - -beforeEach(async () => { - provider = new Provider(hre.network.config.url, undefined, { - cacheTimeout: -1, - }); - richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); - keyPair = genKey(); - const publicKey = encodePublicKey(keyPair); - batchCaller = await deployContract(hre, 'BatchCaller', undefined, { - wallet: richWallet, - silent: true, - }); - mockValidator = await deployContract(hre, 'MockValidator', undefined, { - wallet: richWallet, - silent: true, - }); - implementation = await deployContract( - hre, - 'ClaveImplementation', - [await batchCaller.getAddress()], - { - wallet: richWallet, - silent: true, - }, - ); - registry = await deployContract(hre, 'ClaveRegistry', undefined, { - wallet: richWallet, - silent: true, - }); - - //TODO: WHY DOES THIS HELP - await deployContract( - hre, - 'ClaveProxy', - [await implementation.getAddress()], - { wallet: richWallet, silent: true }, - ); - - const accountArtifact = await hre.zksyncEthers.loadArtifact('ClaveProxy'); - const bytecodeHash = utils.hashBytecode(accountArtifact.bytecode); - factory = await deployContract( - hre, - 'AccountFactory', - [ - await implementation.getAddress(), - await registry.getAddress(), - bytecodeHash, - richWallet.address, - ], - { - wallet: richWallet, - silent: true, - }, - ); - await registry.setFactory(await factory.getAddress()); - - const salt = ethers.randomBytes(32); - const call: CallStruct = { - target: ZeroAddress, - allowFailure: false, - value: 0, - callData: '0x', - }; - - const abiCoder = ethers.AbiCoder.defaultAbiCoder(); - const initializer = - '0x77ba2e75' + - abiCoder - .encode( - [ - 'bytes', - 'address', - 'bytes[]', - 'tuple(address target,bool allowFailure,uint256 value,bytes calldata)', - ], - [ - publicKey, - await mockValidator.getAddress(), - [], - [call.target, call.allowFailure, call.value, call.callData], - ], - ) - .slice(2); - - const tx = await factory.deployAccount(salt, initializer); - await tx.wait(); - - const accountAddress = await factory.getAddressForSalt(salt); - account = new Contract( - accountAddress, - implementation.interface, - richWallet, - ); - // 100 ETH transfered to Account - await ( - await richWallet.sendTransaction({ - to: await account.getAddress(), - value: parseEther('100'), - }) - ).wait(); -}); - -describe('Account no module no hook TEE validator', function () { - describe('Should', function () { - it('Have correct state after deployment', async function () { - expect(await provider.getBalance(await account.getAddress())).to.eq( - parseEther('100'), - ); - - const expectedR1Validators = [await mockValidator.getAddress()]; - const expectedK1Validators: Array = []; - const expectedR1Owners = [encodePublicKey(keyPair)]; - const expectedK1Owners: Array = []; - const expectedModules: Array = []; - const expectedHooks: Array = []; - const expectedImplementation = await implementation.getAddress(); - - expect(await account.r1ListValidators()).to.deep.eq( - expectedR1Validators, - ); - expect(await account.k1ListValidators()).to.deep.eq( - expectedK1Validators, - ); - expect(await account.r1ListOwners()).to.deep.eq(expectedR1Owners); - expect(await account.k1ListOwners()).to.deep.eq(expectedK1Owners); - expect(await account.listModules()).to.deep.eq(expectedModules); - expect(await account.listHooks(false)).to.deep.eq(expectedHooks); - expect(await account.listHooks(true)).to.deep.eq(expectedHooks); - expect(await account.implementation()).to.eq( - expectedImplementation, - ); - }); - - it('Registers contract to the registry', async function () { - expect(await registry.isClave(await account.getAddress())).to.be - .true; - }); - - it('Not show other contracts in registry', async function () { - expect(await registry.isClave(await factory.getAddress())).not.to.be - .true; - }); - - it('Transfer ETH correctly', async function () { - const amount = parseEther('10'); - const delta = parseEther('0.01'); - - const accountBalanceBefore = await provider.getBalance( - await account.getAddress(), - ); - const receiverBalanceBefore = await provider.getBalance( - await richWallet.getAddress(), - ); - - const transfer = ethTransfer(await richWallet.getAddress(), amount); - const tx = await prepareTeeTx( - provider, - account, - transfer, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - await txReceipt.wait(); - - const accountBalanceAfter = await provider.getBalance( - await account.getAddress(), - ); - const receiverBalanceAfter = await provider.getBalance( - await richWallet.getAddress(), - ); - - expect(accountBalanceAfter).to.be.closeTo( - accountBalanceBefore - amount, - delta, - ); - expect(receiverBalanceAfter).to.eq(receiverBalanceBefore + amount); - }); - - it('Make batch transaction correctly', async function () { - const accountBalanceBefore = await provider.getBalance( - await account.getAddress(), - ); - - const receiver1 = await Wallet.createRandom().getAddress(); - const receiver2 = await Wallet.createRandom().getAddress(); - - const calls: Array = [ - { - target: receiver1, - allowFailure: false, - value: parseEther('0.1'), - callData: '0x', - }, - { - target: receiver2, - allowFailure: false, - value: parseEther('0.2'), - callData: '0x', - }, - ]; - - const batchTx = await prepareBatchTx( - provider, - account, - await batchCaller.getAddress(), - calls, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(batchTx), - ); - await txReceipt.wait(); - - const accountBalanceAfter = await provider.getBalance( - await account.getAddress(), - ); - - expect(accountBalanceAfter).to.be.closeTo( - accountBalanceBefore - parseEther('0.3'), - parseEther('0.01'), - ); - expect(await provider.getBalance(receiver1)).to.eq( - parseEther('0.1'), - ); - expect(await provider.getBalance(receiver2)).to.eq( - parseEther('0.2'), - ); - }); - }); - - describe('Owner manager', function () { - describe('Should not revert when', function () { - it('Adds a new r1 owner correctly', async function () { - const newKeyPair = genKey(); - const newPublicKey = encodePublicKey(newKeyPair); - - expect(await account.r1IsOwner(newPublicKey)).to.be.false; - - const addOwnerTx = await account.r1AddOwner.populateTransaction( - newPublicKey, - ); - - const tx = await prepareTeeTx( - provider, - account, - addOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.r1IsOwner(newPublicKey)).to.be.true; - - const expectedOwners = [newPublicKey, encodePublicKey(keyPair)]; - - expect(await account.r1ListOwners()).to.deep.eq(expectedOwners); - }); - - it('Removes an r1 owner correctly', async function () { - const newKeyPair = genKey(); - const newPublicKey = encodePublicKey(newKeyPair); - - const addOwnerTx = await account.r1AddOwner.populateTransaction( - newPublicKey, - ); - - const tx = await prepareTeeTx( - provider, - account, - addOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.r1IsOwner(newPublicKey)).to.be.true; - - const removeOwnerTx = - await account.r1RemoveOwner.populateTransaction( - newPublicKey, - ); - - const tx2 = await prepareTeeTx( - provider, - account, - removeOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt2 = await provider.broadcastTransaction( - utils.serializeEip712(tx2), - ); - await txReceipt2.wait(); - - expect(await account.r1IsOwner(newPublicKey)).to.be.false; - - const expectedOwners = [encodePublicKey(keyPair)]; - - expect(await account.r1ListOwners()).to.deep.eq(expectedOwners); - }); - - it('Adds a new k1 owner correctly', async function () { - const newAddress = await Wallet.createRandom().getAddress(); - - expect(await account.k1IsOwner(newAddress)).to.be.false; - - const addOwnerTx = await account.k1AddOwner.populateTransaction( - newAddress, - ); - - const tx = await prepareTeeTx( - provider, - account, - addOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.k1IsOwner(newAddress)).to.be.true; - - const expectedOwners = [newAddress]; - - expect(await account.k1ListOwners()).to.deep.eq(expectedOwners); - }); - - it('Removes a k1 owner correctly', async function () { - const newAddress = await Wallet.createRandom().getAddress(); - - const addOwnerTx = await account.k1AddOwner.populateTransaction( - newAddress, - ); - - const tx = await prepareTeeTx( - provider, - account, - addOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.k1IsOwner(newAddress)).to.be.true; - - const removeOwnerTx = - await account.k1RemoveOwner.populateTransaction(newAddress); - - const tx2 = await prepareTeeTx( - provider, - account, - removeOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt2 = await provider.broadcastTransaction( - utils.serializeEip712(tx2), - ); - await txReceipt2.wait(); - - expect(await account.k1IsOwner(newAddress)).to.be.false; - - const expectedOwners: Array = []; - - expect(await account.k1ListOwners()).to.deep.eq(expectedOwners); - }); - - it('Resets owners correctly', async function () { - const newAddress = await Wallet.createRandom().getAddress(); - - const addOwnerTx = await account.k1AddOwner.populateTransaction( - newAddress, - ); - - const tx = await prepareTeeTx( - provider, - account, - addOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.k1IsOwner(newAddress)).to.be.true; - - const newKeyPair = genKey(); - const newPublicKey = encodePublicKey(newKeyPair); - - const resetOwnersTx = - await account.resetOwners.populateTransaction(newPublicKey); - - const tx2 = await prepareTeeTx( - provider, - account, - resetOwnersTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt2 = await provider.broadcastTransaction( - utils.serializeEip712(tx2), - ); - await txReceipt2.wait(); - - const expectedR1Owners = [newPublicKey]; - const expectedK1Owners: Array = []; - - expect(await account.r1ListOwners()).to.deep.eq( - expectedR1Owners, - ); - expect(await account.k1ListOwners()).to.deep.eq( - expectedK1Owners, - ); - }); - }); - - describe('Should revert when', function () { - it('Adds r1 owner with invalid length', async function () { - let invalidLength = Math.ceil(Math.random() * 200) * 2; - invalidLength = invalidLength === 128 ? 130 : invalidLength; - - const invalidPubkey = '0x' + 'C'.repeat(invalidLength); - - const addOwnerTx = await account.r1AddOwner.populateTransaction( - invalidPubkey, - ); - - const tx = await prepareTeeTx( - provider, - account, - addOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'INVALID_PUBKEY_LENGTH', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Adds r1 owner with unauthorized msg.sender', async function () { - const newKeyPair = genKey(); - const newPublicKey = encodePublicKey(newKeyPair); - - await expect( - account.r1AddOwner(newPublicKey), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - - it('Adds await zero getAddress() as k1 owner', async function () { - const addOwnerTx = await account.k1AddOwner.populateTransaction( - ZeroAddress, - ); - - const tx = await prepareTeeTx( - provider, - account, - addOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'INVALID_ADDRESS', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Adds k1 owner with unauthorized msg.sender', async function () { - const randomWallet = Wallet.createRandom().connect(provider); - - await expect( - account.k1AddOwner(await randomWallet.getAddress()), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - - it('Removes r1 owner with unauthorized msg.sender', async function () { - const newKeyPair = genKey(); - const newPublicKey = encodePublicKey(newKeyPair); - - const addOwnerTx = await account.r1AddOwner.populateTransaction( - newPublicKey, - ); - - const tx = await prepareTeeTx( - provider, - account, - addOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.r1IsOwner(newPublicKey)).to.be.true; - - await expect( - account.r1RemoveOwner(newPublicKey), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - - it('Removes last r1 owner', async function () { - const removeOwnerTx = - await account.r1RemoveOwner.populateTransaction( - encodePublicKey(keyPair), - ); - - const tx = await prepareTeeTx( - provider, - account, - removeOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'EMPTY_R1_OWNERS', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Removes k1 owner with unauthorized msg.sender', async function () { - const newAddress = await Wallet.createRandom().getAddress(); - - const addOwnerTx = await account.k1AddOwner.populateTransaction( - newAddress, - ); - - const tx = await prepareTeeTx( - provider, - account, - addOwnerTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.k1IsOwner(newAddress)).to.be.true; - - const randomWallet = Wallet.createRandom().connect(provider); - - await expect( - account.k1RemoveOwner(await randomWallet.getAddress()), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - - it('Clear owners with unauthorized msg.sender', async function () { - const newKeyPair = genKey(); - const newPublicKey = encodePublicKey(newKeyPair); - - await expect( - account.resetOwners(newPublicKey), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - - it('Reset owners new r1 owner invalid length', async function () { - let invalidLength = Math.ceil(Math.random() * 200) * 2; - invalidLength = invalidLength === 128 ? 130 : invalidLength; - - const invalidPubkey = '0x' + 'C'.repeat(invalidLength); - - const resetOwnersTx = - await account.resetOwners.populateTransaction( - invalidPubkey, - ); - - const tx = await prepareTeeTx( - provider, - account, - resetOwnersTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'INVALID_PUBKEY_LENGTH', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - }); - }); - - describe('Validator manager', function () { - describe('Should not revert when', function () { - it('Adds a new r1 validator correctly', async function () { - const newR1Validator = await deployContract( - hre, - 'MockValidator', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - expect( - await account.r1IsValidator( - await newR1Validator.getAddress(), - ), - ).to.be.false; - - const addValidatorTx = - await account.r1AddValidator.populateTransaction( - await newR1Validator.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - addValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect( - await account.r1IsValidator( - await newR1Validator.getAddress(), - ), - ).to.be.true; - - const expectedValidators = [ - await newR1Validator.getAddress(), - await mockValidator.getAddress(), - ]; - - expect(await account.r1ListValidators()).to.deep.eq( - expectedValidators, - ); - }); - - it('Adds a new k1 validator correctly', async function () { - const k1Validator = await deployContract( - hre, - 'EOAValidator', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - expect( - await account.k1IsValidator(await k1Validator.getAddress()), - ).to.be.false; - - const addValidatorTx = - await account.k1AddValidator.populateTransaction( - await k1Validator.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - addValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect( - await account.k1IsValidator(await k1Validator.getAddress()), - ).to.be.true; - - const expectedValidators = [await k1Validator.getAddress()]; - - expect(await account.k1ListValidators()).to.deep.eq( - expectedValidators, - ); - }); - - it('Removes an r1 validator correctly', async function () { - const newR1Validator = await deployContract( - hre, - 'MockValidator', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const addValidatorTx = - await account.r1AddValidator.populateTransaction( - await newR1Validator.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - addValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect( - await account.r1IsValidator( - await newR1Validator.getAddress(), - ), - ).to.be.true; - - const removeValidatorTx = - await account.r1RemoveValidator.populateTransaction( - await newR1Validator.getAddress(), - ); - - const tx2 = await prepareTeeTx( - provider, - account, - removeValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt2 = await provider.broadcastTransaction( - utils.serializeEip712(tx2), - ); - await txReceipt2.wait(); - - expect( - await account.r1IsValidator( - await newR1Validator.getAddress(), - ), - ).to.be.false; - - const expectedValidators = [await mockValidator.getAddress()]; - - expect(await account.r1ListValidators()).to.deep.eq( - expectedValidators, - ); - }); - - it('Removes a k1 validator correctly', async function () { - const k1Validator = await deployContract( - hre, - 'EOAValidator', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const addValidatorTx = - await account.k1AddValidator.populateTransaction( - await k1Validator.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - addValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect( - await account.k1IsValidator(await k1Validator.getAddress()), - ).to.be.true; - - const removeValidatorTx = - await account.k1RemoveValidator.populateTransaction( - await k1Validator.getAddress(), - ); - - const tx2 = await prepareTeeTx( - provider, - account, - removeValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt2 = await provider.broadcastTransaction( - utils.serializeEip712(tx2), - ); - await txReceipt2.wait(); - - expect( - await account.k1IsValidator(await k1Validator.getAddress()), - ).to.be.false; - - const expectedValidators: Array = []; - - expect(await account.k1ListValidators()).to.deep.eq( - expectedValidators, - ); - }); - }); - - describe('Should revert when', function () { - it('Adds r1 validator with unauthorized msg.sender', async function () { - const newR1Validator = await deployContract( - hre, - 'MockValidator', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - await expect( - account.r1AddValidator(await newR1Validator.getAddress()), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - - it('Adds r1 validator with WRONG interface', async function () { - const wrongInterfaceValidator = await deployContract( - hre, - 'EOAValidator', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const addValidatorTx = - await account.r1AddValidator.populateTransaction( - await wrongInterfaceValidator.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - addValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'VALIDATOR_ERC165_FAIL', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Adds r1 validator with NO interface', async function () { - const noInterfaceValidator = Wallet.createRandom(); - - const addValidatorTx = - await account.r1AddValidator.populateTransaction( - await noInterfaceValidator.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - addValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'VALIDATOR_ERC165_FAIL', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Removes r1 validator with unauthorized msg.sender', async function () { - const newR1Validator = await deployContract( - hre, - 'MockValidator', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const addValidatorTx = - await account.r1AddValidator.populateTransaction( - await newR1Validator.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - addValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect( - await account.r1IsValidator( - await newR1Validator.getAddress(), - ), - ).to.be.true; - - await expect( - account.r1RemoveValidator( - await newR1Validator.getAddress(), - ), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - - it('Removes last r1 validator', async function () { - const removeValidatorTx = - await account.r1RemoveValidator.populateTransaction( - await mockValidator.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - removeValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'EMPTY_R1_VALIDATORS', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Adds k1 validator with unauthorized msg.sender', async function () { - const k1Validator = await deployContract( - hre, - 'EOAValidator', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - await expect( - account.k1AddValidator(await k1Validator.getAddress()), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - - it('Adds k1 validator with WRONG interface', async function () { - const wrongInterfaceValidator = await deployContract( - hre, - 'MockValidator', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const addValidatorTx = - await account.k1AddValidator.populateTransaction( - await wrongInterfaceValidator.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - addValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'VALIDATOR_ERC165_FAIL', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Adds k1 validator with NO interface', async function () { - const noInterfaceValidator = Wallet.createRandom(); - - const addValidatorTx = - await account.k1AddValidator.populateTransaction( - await noInterfaceValidator.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - addValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'VALIDATOR_ERC165_FAIL', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Removes k1 validator with unauthorized msg.sender', async function () { - const k1Validator = await deployContract( - hre, - 'EOAValidator', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const addValidatorTx = - await account.k1AddValidator.populateTransaction( - await k1Validator.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - addValidatorTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect( - await account.k1IsValidator(await k1Validator.getAddress()), - ).to.be.true; - - await expect( - account.k1RemoveValidator(await k1Validator.getAddress()), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - }); - }); - - describe('Module manager', function () { - describe('Should not revert when', function () { - it('Adds a new module correctly', async function () { - const mockModule = await deployContract( - hre, - 'MockModule', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - expect(await account.isModule(await mockModule.getAddress())).to - .be.false; - - const initData = AbiCoder.defaultAbiCoder().encode( - ['uint256'], - [parseEther('42')], - ); - const moduleAndData = concat([ - await mockModule.getAddress(), - initData, - ]); - - const addModuleTx = await account.addModule.populateTransaction( - moduleAndData, - ); - - const tx = await prepareTeeTx( - provider, - account, - addModuleTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.isModule(await mockModule.getAddress())).to - .be.true; - - const expectedModules = [await mockModule.getAddress()]; - - expect(await account.listModules()).to.deep.eq(expectedModules); - }); - - it('Removes a module correctly', async function () { - const mockModule = await deployContract( - hre, - 'MockModule', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const initData = AbiCoder.defaultAbiCoder().encode( - ['uint256'], - [parseEther('42')], - ); - const moduleAndData = concat([ - await mockModule.getAddress(), - initData, - ]); - - const addModuleTx = await account.addModule.populateTransaction( - moduleAndData, - ); - - const tx = await prepareTeeTx( - provider, - account, - addModuleTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.isModule(await mockModule.getAddress())).to - .be.true; - - const removeModuleTx = - await account.removeModule.populateTransaction( - await mockModule.getAddress(), - ); - - const tx2 = await prepareTeeTx( - provider, - account, - removeModuleTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt2 = await provider.broadcastTransaction( - utils.serializeEip712(tx2), - ); - await txReceipt2.wait(); - - expect(await account.isModule(await mockModule.getAddress())).to - .be.false; - - const expectedModules: Array = []; - - expect(await account.listModules()).to.deep.eq(expectedModules); - }); - - it('Executes from module correctly', async function () { - const amount = parseEther('42'); - const delta = parseEther('0.01'); - - const mockModule = await deployContract( - hre, - 'MockModule', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const initData = AbiCoder.defaultAbiCoder().encode( - ['uint256'], - [parseEther('42')], - ); - const moduleAndData = concat([ - await mockModule.getAddress(), - initData, - ]); - - const addModuleTx = await account.addModule.populateTransaction( - moduleAndData, - ); - - const tx = await prepareTeeTx( - provider, - account, - addModuleTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - const accountBalanceBefore = await provider.getBalance( - await account.getAddress(), - ); - const receiverBalanceBefore = await provider.getBalance( - await richWallet.getAddress(), - ); - - await mockModule.testExecuteFromModule( - await account.getAddress(), - await richWallet.getAddress(), - ); - - const accountBalanceAfter = await provider.getBalance( - await account.getAddress(), - ); - const receiverBalanceAfter = await provider.getBalance( - await richWallet.getAddress(), - ); - - expect(accountBalanceAfter).to.be.closeTo( - accountBalanceBefore - amount, - delta, - ); - - expect(receiverBalanceAfter).to.be.closeTo( - receiverBalanceBefore + amount, - delta, - ); - }); - }); - describe('Should revert when', function () { - it('Adds module with unauthorized msg.sender', async function () { - const mockModule = await deployContract( - hre, - 'MockModule', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const initData = AbiCoder.defaultAbiCoder().encode( - ['uint256'], - [parseEther('42')], - ); - const moduleAndData = concat([ - await mockModule.getAddress(), - initData, - ]); - - await expect( - account.addModule(moduleAndData), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - - it('Adds module with invalid moduleAndData length', async function () { - const mockModule = await deployContract( - hre, - 'MockModule', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const moduleAndData = (await mockModule.getAddress()).slice( - 0, - 10, - ); - - const addModuleTx = await account.addModule.populateTransaction( - moduleAndData, - ); - - const tx = await prepareTeeTx( - provider, - account, - addModuleTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'EMPTY_MODULE_ADDRESS', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Adds module with no interface', async function () { - const noInterfaceModule = Wallet.createRandom(); - - const initData = AbiCoder.defaultAbiCoder().encode( - ['uint256'], - [parseEther('42')], - ); - const moduleAndData = concat([ - await noInterfaceModule.getAddress(), - initData, - ]); - - const addModuleTx = await account.addModule.populateTransaction( - moduleAndData, - ); - - const tx = await prepareTeeTx( - provider, - account, - addModuleTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'MODULE_ERC165_FAIL', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Removes module with unauthorized msg.sender', async function () { - const mockModule = await deployContract( - hre, - 'MockModule', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const initData = AbiCoder.defaultAbiCoder().encode( - ['uint256'], - [parseEther('42')], - ); - const moduleAndData = concat([ - await mockModule.getAddress(), - initData, - ]); - - const addModuleTx = await account.addModule.populateTransaction( - moduleAndData, - ); - - const tx = await prepareTeeTx( - provider, - account, - addModuleTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - await expect( - account.removeModule(await mockModule.getAddress()), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - }); - }); - - describe('Upgrade manager', function () { - describe('Should not revert when', function () { - it('Upgrades to a new implementation correctly', async function () { - const mockImplementation = await deployContract( - hre, - 'MockImplementation', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const upgradeTx = await account.upgradeTo.populateTransaction( - await mockImplementation.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - upgradeTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - await txReceipt.wait(); - - expect(await account.implementation()).to.eq( - await mockImplementation.getAddress(), - ); - }); - }); - describe('Should revert when', function () { - it('Upgrades to a new implementation with unauthorized msg.sender', async function () { - const mockImplementation = await deployContract( - hre, - 'MockImplementation', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - await expect( - account.upgradeTo(await mockImplementation.getAddress()), - ).to.be.revertedWithCustomError(account, 'NOT_FROM_SELF'); - }); - - it('Upgrades to same implementation', async function () { - const upgradeTx = await account.upgradeTo.populateTransaction( - await implementation.getAddress(), - ); - - const tx = await prepareTeeTx( - provider, - account, - upgradeTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'SAME_IMPLEMENTATION', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - }); - }); - - describe('Hook manager', function () { - describe('Should not revert when', function () { - it('Adds a new validation hook correctly', async function () { - const mockHook = await deployContract( - hre, - 'MockValidationHook', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - expect(await account.isHook(await mockHook.getAddress())).to.be - .false; - - const addHookTx = await account.addHook.populateTransaction( - await mockHook.getAddress(), - true, - ); - - const tx = await prepareTeeTx( - provider, - account, - addHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.isHook(await mockHook.getAddress())).to.be - .true; - - const expectedHooks = [await mockHook.getAddress()]; - expect(await account.listHooks(true)).to.deep.eq(expectedHooks); - }); - - it('Adds a new execution hook correctly', async function () { - const mockHook = await deployContract( - hre, - 'MockExecutionHook', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - expect(await account.isHook(await mockHook.getAddress())).to.be - .false; - - const addHookTx = await account.addHook.populateTransaction( - await mockHook.getAddress(), - false, - ); - - const tx = await prepareTeeTx( - provider, - account, - addHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.isHook(await mockHook.getAddress())).to.be - .true; - - const expectedHooks = [await mockHook.getAddress()]; - expect(await account.listHooks(false)).to.deep.eq( - expectedHooks, - ); - }); - - it('Removes a hook correctly', async function () { - const mockHook = await deployContract( - hre, - 'MockExecutionHook', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - expect(await account.isHook(await mockHook.getAddress())).to.be - .false; - - const addHookTx = await account.addHook.populateTransaction( - await mockHook.getAddress(), - false, - ); - - const tx = await prepareTeeTx( - provider, - account, - addHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.isHook(await mockHook.getAddress())).to.be - .true; - - const removeHookTx = - await account.removeHook.populateTransaction( - await mockHook.getAddress(), - false, - ); - - const tx2 = await prepareTeeTx( - provider, - account, - removeHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt2 = await provider.broadcastTransaction( - utils.serializeEip712(tx2), - ); - txReceipt2.wait(); - - expect(await account.isHook(await mockHook.getAddress())).to.be - .false; - - const expectedHooks: Array = []; - expect(await account.listHooks(true)).to.deep.eq(expectedHooks); - }); - - it('Sets hook data correctly', async function () { - const mockHook = await deployContract( - hre, - 'MockValidationHook', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - expect(await account.isHook(await mockHook.getAddress())).to.be - .false; - - const addHookTx = await account.addHook.populateTransaction( - await mockHook.getAddress(), - true, - ); - - const tx = await prepareTeeTx( - provider, - account, - addHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - expect(await account.isHook(await mockHook.getAddress())).to.be - .true; - - const key = randomBytes(32); - const data = '0xc1ae'; - - await mockHook.setHookData( - await account.getAddress(), - key, - data, - ); - - expect( - await account.getHookData(await mockHook.getAddress(), key), - ).to.eq(data); - }); - - it('Runs validation hooks successfully', async function () { - const mockHook = await deployContract( - hre, - 'MockValidationHook', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - expect(await account.isHook(await mockHook.getAddress())).to.be - .false; - - const addHookTx = await account.addHook.populateTransaction( - await mockHook.getAddress(), - true, - ); - - const tx = await prepareTeeTx( - provider, - account, - addHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - const transfer = ethTransfer(await richWallet.getAddress(), 1); - - const hookData = [ - AbiCoder.defaultAbiCoder().encode(['bool'], [false]), - ]; - - const tx2 = await prepareTeeTx( - provider, - account, - transfer, - await mockValidator.getAddress(), - keyPair, - hookData, - ); - - const txReceipt2 = await provider.broadcastTransaction( - utils.serializeEip712(tx2), - ); - await txReceipt2.wait(); - }); - }); - - describe('Should revert when', function () { - it('Adds hook with unauthorized msg.sender', async function () { - const mockHook = await deployContract( - hre, - 'MockValidationHook', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - await expect( - account.addHook(await mockHook.getAddress(), true), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - - it('Adds hook with invalid hookAndData length', async function () { - const mockHook = await deployContract( - hre, - 'MockValidationHook', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const hookAndData = (await mockHook.getAddress()).slice(0, 10); - - const addHookTx = await account.addHook.populateTransaction( - hookAndData, - true, - ); - - const tx = await prepareTeeTx( - provider, - account, - addHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'EMPTY_HOOK_ADDRESS', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Adds hook with NO interface', async function () { - const noInterfaceHook = Wallet.createRandom(); - - const addHookTx = await account.addHook.populateTransaction( - await noInterfaceHook.getAddress(), - true, - ); - - const tx = await prepareTeeTx( - provider, - account, - addHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - // await expect(txReceipt.wait()).to.be.revertedWithCustomError( - // account, - // 'HOOK_ERC165_FAIL', - // ); - - try { - await txReceipt.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Removes hook with unauthorized msg.sender', async function () { - const mockHook = await deployContract( - hre, - 'MockValidationHook', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const addHookTx = await account.addHook.populateTransaction( - await mockHook.getAddress(), - true, - ); - - const tx = await prepareTeeTx( - provider, - account, - addHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - await expect( - account.removeHook(await mockHook.getAddress(), true), - ).to.be.revertedWithCustomError( - account, - 'NOT_FROM_SELF_OR_MODULE', - ); - }); - - it('Sets hook data with unauthorized msg.sender', async function () { - const key = randomBytes(32); - const data = '0xc1ae'; - - await expect( - account.setHookData(key, data), - ).to.be.revertedWithCustomError(account, 'NOT_FROM_HOOK'); - }); - - it('Sets hook data with invalid key', async function () { - const mockHook = await deployContract( - hre, - 'MockValidationHook', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const addHookTx = await account.addHook.populateTransaction( - await mockHook.getAddress(), - true, - ); - - const tx = await prepareTeeTx( - provider, - account, - addHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - const key = solidityPackedKeccak256( - ['string'], - ['HookManager.context'], - ); - const data = '0xc1ae'; - - await expect( - mockHook.setHookData(await account.getAddress(), key, data), - ).to.be.revertedWithCustomError(account, 'INVALID_KEY'); - }); - - it('Run validation hooks fails', async function () { - const mockHook = await deployContract( - hre, - 'MockValidationHook', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const addHookTx = await account.addHook.populateTransaction( - await mockHook.getAddress(), - true, - ); - - const tx = await prepareTeeTx( - provider, - account, - addHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - const transfer = ethTransfer(await richWallet.getAddress(), 5); - - const hookData = [ - AbiCoder.defaultAbiCoder().encode(['bool'], [true]), - ]; - - const tx2 = await prepareTeeTx( - provider, - account, - transfer, - await mockValidator.getAddress(), - keyPair, - hookData, - ); - - try { - await provider.broadcastTransaction( - utils.serializeEip712(tx2), - ); - assert(false, 'Should revert'); - } catch (e) {} - }); - - it('Run execution hooks fails', async function () { - const mockHook = await deployContract( - hre, - 'MockExecutionHook', - undefined, - { - wallet: richWallet, - silent: true, - }, - ); - - const addHookTx = await account.addHook.populateTransaction( - await mockHook.getAddress(), - false, - ); - - const tx = await prepareTeeTx( - provider, - account, - addHookTx, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - await txReceipt.wait(); - - const transfer = ethTransfer(await richWallet.getAddress(), 5); - - const tx2 = await prepareTeeTx( - provider, - account, - transfer, - await mockValidator.getAddress(), - keyPair, - ); - - const txReceipt2 = await provider.broadcastTransaction( - utils.serializeEip712(tx2), - ); - - try { - await txReceipt2.wait(); - assert(false, 'Should revert'); - } catch (e) {} - }); - }); - }); - - describe('Paymaster', function () { - let mockToken: Contract; - let gaslessPaymaster: Contract; - let erc20Paymaster: Contract; - let subsidizerPaymaster: Contract; - - beforeEach(async function () { - mockToken = await deployContract(hre, 'MockStable', undefined, { - wallet: richWallet, - silent: true, - }); - - gaslessPaymaster = await deployContract( - hre, - 'GaslessPaymaster', - [await registry.getAddress(), 2], - { - wallet: richWallet, - silent: true, - }, - ); - - erc20Paymaster = await deployContract( - hre, - 'ERC20PaymasterMock', - [ - [ - { - tokenAddress: await mockToken.getAddress(), - decimals: 18, - priceMarkup: 20000, - }, - ], - ], - { - wallet: richWallet, - silent: true, - }, - ); - - subsidizerPaymaster = await deployContract( - hre, - 'SubsidizerPaymasterMock', - [ - [ - { - tokenAddress: await mockToken.getAddress(), - decimals: 18, - priceMarkup: 20000, - }, - ], - await registry.getAddress(), - ], - { - wallet: richWallet, - silent: true, - }, - ); - - await mockToken.mint(await account.getAddress(), parseEther('100')); - - await ( - await richWallet.sendTransaction({ - to: await gaslessPaymaster.getAddress(), - value: parseEther('50'), - }) - ).wait(); - - await ( - await richWallet.sendTransaction({ - to: await erc20Paymaster.getAddress(), - value: parseEther('50'), - }) - ).wait(); - - await ( - await richWallet.sendTransaction({ - to: await subsidizerPaymaster.getAddress(), - value: parseEther('50'), - }) - ).wait(); - }); - - it('Should fund the account with mock token', async function () { - expect( - await mockToken.balanceOf(await account.getAddress()), - ).to.be.eq(parseEther('100')); - }); - - it('Should fund the paymasters', async function () { - expect( - await provider.getBalance(await gaslessPaymaster.getAddress()), - ).to.eq(parseEther('50')); - expect( - await provider.getBalance(await erc20Paymaster.getAddress()), - ).to.eq(parseEther('50')); - expect( - await provider.getBalance( - await subsidizerPaymaster.getAddress(), - ), - ).to.eq(parseEther('50')); - }); - - it('Should prepare an oracle payload', async function () { - //TODO - }); - - it('Should pay gas with token', async function () { - //TODO - }); - - it('Should send tx without paying gas', async function () { - const amount = parseEther('10'); - - const accountBalanceBefore = await provider.getBalance( - await account.getAddress(), - ); - const receiverBalanceBefore = await provider.getBalance( - await richWallet.getAddress(), - ); - const paymasterBalanceBefore = await provider.getBalance( - await gaslessPaymaster.getAddress(), - ); - - const transfer = ethTransfer(await richWallet.getAddress(), amount); - const tx = await prepareTeeTx( - provider, - account, - transfer, - await mockValidator.getAddress(), - keyPair, - [], - getGaslessPaymasterInput(await gaslessPaymaster.getAddress()), - ); - - const txReceipt = await provider.broadcastTransaction( - utils.serializeEip712(tx), - ); - - await txReceipt.wait(); - - const accountBalanceAfter = await provider.getBalance( - await account.getAddress(), - ); - const receiverBalanceAfter = await provider.getBalance( - await richWallet.getAddress(), - ); - const paymasterBalanceAfter = await provider.getBalance( - await gaslessPaymaster.getAddress(), - ); - - expect(accountBalanceAfter + amount).to.be.equal( - accountBalanceBefore, - ); - expect(receiverBalanceBefore + amount).to.be.equal( - receiverBalanceAfter, - ); - expect(paymasterBalanceAfter).is.lessThan(paymasterBalanceBefore); - }); - - it('Should pay subsidized gas with token', async function () { - //TODO - }); - - describe('Subsidizer refunds calculations', function () { - const MAX_GAS_TO_SUBSIDIZE = 1_250_000n; - - const calcRefund = ( - gaslimit: bigint, - gasused: bigint, - max: bigint, - ): bigint => { - const userpaid = gaslimit > max ? gaslimit - max : 0; - const gasrefunded = gaslimit - gasused; - - if (userpaid === 0) { - return 0n; - } - - if (max > gasused) { - return userpaid; - } else { - return gasrefunded; - } - }; - - const calcExpected = ( - refunded: bigint, - maxFeePerGas: bigint, - rate: bigint, - ): bigint => { - return (refunded * maxFeePerGas * rate) / WeiPerEther; - }; - - type Config = { - gasLimit: bigint; - gasUsed: bigint; - maxFeePerGas: bigint; - rate: bigint; - maxGasToSubsidize: bigint; - }; - - const checkRefund = async ( - pmConfig: Config, - ): Promise<[bigint, bigint]> => { - const expected = calcExpected( - calcRefund( - pmConfig.gasLimit, - pmConfig.gasUsed, - pmConfig.maxGasToSubsidize, - ), - pmConfig.maxFeePerGas, - pmConfig.rate, - ); - - const real = await subsidizerPaymaster.calcRefundAmount( - pmConfig.gasLimit, - pmConfig.gasLimit - pmConfig.gasUsed, - pmConfig.maxFeePerGas, - pmConfig.rate, - ); - - return [expected, real]; - }; - - it('Should calculate if gasUsed is bigger than maxGasToSubsidize', async function () { - const pmRate = await subsidizerPaymaster.getPairPrice( - await mockToken.getAddress(), - '0x', - ); - - const pmConfig: Config = { - gasLimit: 2_000_000n, - gasUsed: 1_500_000n, - maxFeePerGas: 100n, - rate: BigInt(pmRate.toString()), - maxGasToSubsidize: 1_000_000n, - }; - - const values = await checkRefund(pmConfig); - expect(values[0]).to.be.equal(values[1]); - }); - - it('Should calculate if gasUsed is equal to gasLimit and bigger than maxGasToSubsidize', async function () { - const pmRate = await subsidizerPaymaster.getPairPrice( - await mockToken.getAddress(), - '0x', - ); - - const pmConfig = { - gasLimit: 2_000_000n, - gasUsed: 2_000_000n, - maxFeePerGas: 100n, - rate: BigInt(pmRate.toString()), - maxGasToSubsidize: MAX_GAS_TO_SUBSIDIZE, - }; - - const values = await checkRefund(pmConfig); - expect(values[0]).to.be.equal(values[1]); - }); - - it('Should calculate if gasUsed is less than gasLimit and both less than maxGasToSubsidize', async function () { - const pmRate = await subsidizerPaymaster.getPairPrice( - await mockToken.getAddress(), - '0x', - ); - - const pmConfig = { - gasLimit: 1_000_000n, - gasUsed: 500_000n, - maxFeePerGas: 100n, - rate: BigInt(pmRate.toString()), - maxGasToSubsidize: MAX_GAS_TO_SUBSIDIZE, - }; - - const values = await checkRefund(pmConfig); - expect(values[0]).to.be.equal(values[1]); - }); - - it('Should calculate if gasUsed is equal to gasLimit and both less than maxGasToSubsidize', async function () { - const pmRate = await subsidizerPaymaster.getPairPrice( - await mockToken.getAddress(), - '0x', - ); - - const pmConfig = { - gasLimit: 500_000n, - gasUsed: 500_000n, - maxFeePerGas: 100n, - rate: BigInt(pmRate.toString()), - maxGasToSubsidize: MAX_GAS_TO_SUBSIDIZE, - }; - - const values = await checkRefund(pmConfig); - expect(values[0]).to.be.equal(values[1]); - }); - - it('Should calculate if gasUsed is less then maxGasToSubsidize ', async function () { - const pmRate = await subsidizerPaymaster.getPairPrice( - await mockToken.getAddress(), - '0x', - ); - - const pmConfig = { - gasLimit: 2_000_000n, - gasUsed: 500_000n, - maxFeePerGas: 100n, - rate: BigInt(pmRate.toString()), - maxGasToSubsidize: MAX_GAS_TO_SUBSIDIZE, - }; - - const values = await checkRefund(pmConfig); - expect(values[0]).to.be.equal(values[1]); - }); - }); - }); -}); diff --git a/test/deployments/deployer.test.ts b/test/deployments/deployer.test.ts new file mode 100644 index 0000000..684b1a7 --- /dev/null +++ b/test/deployments/deployer.test.ts @@ -0,0 +1,106 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { expect } from 'chai'; +import type { ec } from 'elliptic'; +import type { BytesLike } from 'ethers'; +import { parseEther } from 'ethers'; +import * as hre from 'hardhat'; +import type { Contract, Wallet } from 'zksync-ethers'; +import { Provider } from 'zksync-ethers'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../deploy/utils'; +import { ClaveDeployer } from '../utils/deployer'; +import { fixture } from '../utils/fixture'; +import { encodePublicKey } from '../utils/p256'; + +describe('Clave Contracts - Deployer class tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let batchCaller: Contract; + let registry: Contract; + let implementation: Contract; + let factory: Contract; + let mockValidator: Contract; + let account: Contract; + let keyPair: ec.KeyPair; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [ + batchCaller, + registry, + implementation, + factory, + mockValidator, + account, + keyPair, + ] = await fixture(deployer); + + await deployer.fund(100, await account.getAddress()); + }); + + describe('Contracts', () => { + it('should deploy the contracts', async () => { + expect(await batchCaller.getAddress()).not.to.be.undefined; + expect(await registry.getAddress()).not.to.be.undefined; + expect(await implementation.getAddress()).not.to.be.undefined; + expect(await factory.getAddress()).not.to.be.undefined; + expect(await mockValidator.getAddress()).not.to.be.undefined; + expect(await account.getAddress()).not.to.be.undefined; + }); + }); + + describe('States', () => { + it('should fund the account', async () => { + const balance = await provider.getBalance( + await account.getAddress(), + ); + expect(balance).to.eq(parseEther('100')); + }); + + it('account keeps correct states', async () => { + const validatorAddress = await mockValidator.getAddress(); + const implementationAddress = await implementation.getAddress(); + + const expectedR1Validators = [validatorAddress]; + const expectedK1Validators: Array = []; + const expectedR1Owners = [encodePublicKey(keyPair)]; + const expectedK1Owners: Array = []; + const expectedModules: Array = []; + const expectedHooks: Array = []; + const expectedImplementation = implementationAddress; + + expect(await account.r1ListValidators()).to.deep.eq( + expectedR1Validators, + ); + expect(await account.k1ListValidators()).to.deep.eq( + expectedK1Validators, + ); + expect(await account.r1ListOwners()).to.deep.eq(expectedR1Owners); + expect(await account.k1ListOwners()).to.deep.eq(expectedK1Owners); + expect(await account.listModules()).to.deep.eq(expectedModules); + expect(await account.listHooks(false)).to.deep.eq(expectedHooks); + expect(await account.listHooks(true)).to.deep.eq(expectedHooks); + expect(await account.implementation()).to.eq( + expectedImplementation, + ); + }); + + it('registry is deployed and states are expected', async function () { + const accountAddress = await account.getAddress(); + const factoryAddress = await factory.getAddress(); + + expect(await registry.isClave(accountAddress)).to.be.true; + expect(await registry.isClave(factoryAddress)).not.to.be.true; + }); + }); +}); diff --git a/test/deployments/rip7212.test.ts b/test/deployments/rip7212.test.ts new file mode 100644 index 0000000..5f70c9b --- /dev/null +++ b/test/deployments/rip7212.test.ts @@ -0,0 +1,34 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { expect } from 'chai'; +import * as hre from 'hardhat'; +import { Provider } from 'zksync-ethers'; + +describe('RIP-7212 tests', () => { + let provider: Provider; + + before(async () => { + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + }); + + it('should enable rip-7212', async () => { + const precompileAddress = '0x0000000000000000000000000000000000000100'; + const data = + '0x4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e'; + + const result = await provider.call({ + to: precompileAddress, + data: data, + value: 0, + }); + + expect(result).to.be.eq( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + }); +}); diff --git a/test/paymasters/erc20paymaster.test.ts b/test/paymasters/erc20paymaster.test.ts new file mode 100644 index 0000000..32025b6 --- /dev/null +++ b/test/paymasters/erc20paymaster.test.ts @@ -0,0 +1,284 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { expect } from 'chai'; +import { parseEther } from 'ethers'; +import * as hre from 'hardhat'; +import type { Contract, Wallet } from 'zksync-ethers'; +import { Provider, utils } from 'zksync-ethers'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../deploy/utils'; +import type { CallStruct } from '../../typechain-types/contracts/batch/BatchCaller'; +import { ClaveDeployer } from '../utils/deployer'; +import { fixture } from '../utils/fixture'; +import { PAYMASTERS } from '../utils/names'; +import { getERC20PaymasterInput, getOraclePayload } from '../utils/paymasters'; +import { + ethTransfer, + prepareMockBatchTx, + prepareMockTx, +} from '../utils/transactions'; + +describe('Clave Contracts - Paymaster tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let batchCaller: Contract; + let mockValidator: Contract; + let account: Contract; + + let erc20Paymaster: Contract; + let erc20: Contract; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [batchCaller, , , , mockValidator, account] = await fixture(deployer); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + + erc20 = await deployer.deployCustomContract('MockStable', []); + await erc20.mint(accountAddress, parseEther('100000')); + + erc20Paymaster = await deployer.paymaster(PAYMASTERS.ERC20_MOCK, { + erc20: [ + { + tokenAddress: await erc20.getAddress(), + decimals: 18, + priceMarkup: 20000, + }, + ], + }); + + await deployer.fund(50, await erc20Paymaster.getAddress()); + }); + + it('Should fund the paymaster and account', async () => { + expect( + await provider.getBalance(await erc20Paymaster.getAddress()), + ).to.eq(parseEther('50')); + + expect(await provider.getBalance(await account.getAddress())).to.eq( + parseEther('10000'), + ); + + expect(await erc20.balanceOf(await account.getAddress())).to.be.eq( + parseEther('100000'), + ); + }); + + describe('ERC20 Paymaster (Mocked Oracle)', () => { + let accountAddress: string; + let richAddress: string; + let paymasterAddress: string; + + let accountBalanceBefore: bigint; + let richBalanceBefore: bigint; + let paymasterBalanceBefore: bigint; + let accountERC20BalanceBefore: bigint; + let paymasterERC20BalanceBefore: bigint; + + before(async () => { + const addresses = await Promise.all([ + account.getAddress(), + richWallet.getAddress(), + erc20Paymaster.getAddress(), + ]); + [accountAddress, richAddress, paymasterAddress] = addresses; + }); + + beforeEach(async () => { + const balances = await Promise.all([ + provider.getBalance(accountAddress), + provider.getBalance(richAddress), + provider.getBalance(paymasterAddress), + erc20.balanceOf(accountAddress), + erc20.balanceOf(paymasterAddress), + ]); + [ + accountBalanceBefore, + richBalanceBefore, + paymasterBalanceBefore, + accountERC20BalanceBefore, + paymasterERC20BalanceBefore, + ] = balances; + }); + + it('should send ETH and pay gas with erc20 token', async () => { + const amount = parseEther('1'); + + const txData = ethTransfer(richAddress, amount); + const tx = await prepareMockTx( + provider, + account, + txData, + await mockValidator.getAddress(), + getERC20PaymasterInput( + paymasterAddress, + await erc20.getAddress(), + parseEther('50'), + await getOraclePayload(erc20Paymaster), + ), + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const [accountBalanceAfter, richBalanceAfter] = await Promise.all([ + provider.getBalance(accountAddress), + provider.getBalance(richAddress), + ]); + + expect(accountBalanceAfter).to.be.eq(accountBalanceBefore - amount); + expect(richBalanceAfter).to.be.equal(richBalanceBefore + amount); + + // Gas payment check + const gaslessPaymasterBalanceAfter = await provider.getBalance( + await erc20Paymaster.getAddress(), + ); + + expect(gaslessPaymasterBalanceAfter).is.lessThan( + paymasterBalanceBefore, + ); + + const [accountERC20BalanceAfter, paymasterERC20BalanceAfter] = + await Promise.all([ + erc20.balanceOf(accountAddress), + erc20.balanceOf(paymasterAddress), + ]); + + expect( + accountERC20BalanceBefore + paymasterERC20BalanceBefore, + ).to.be.equal( + accountERC20BalanceAfter + paymasterERC20BalanceAfter, + ); + }); + + it('should send ERC20 token / contract interaction and pay gas with erc20 token', async () => { + const amount = parseEther('100'); + + const richERC20BalanceBefore = await erc20.balanceOf(richAddress); + + const txData = { + to: await erc20.getAddress(), + value: 0, + data: erc20.interface.encodeFunctionData('transfer', [ + richAddress, + amount, + ]), + }; + const tx = await prepareMockTx( + provider, + account, + txData, + await mockValidator.getAddress(), + getERC20PaymasterInput( + paymasterAddress, + await erc20.getAddress(), + parseEther('50'), + await getOraclePayload(erc20Paymaster), + ), + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const richERC20BalanceAfter = await erc20.balanceOf(richAddress); + + expect(richERC20BalanceAfter).to.be.equal( + richERC20BalanceBefore + amount, + ); + + // Gas payment check + const accountBalanceAfter = await provider.getBalance( + accountAddress, + ); + expect(accountBalanceBefore).to.be.eq(accountBalanceAfter); + + const gaslessPaymasterBalanceAfter = await provider.getBalance( + await erc20Paymaster.getAddress(), + ); + expect(gaslessPaymasterBalanceAfter).is.lessThan( + paymasterBalanceBefore, + ); + }); + + it('should send batch tx / delegate call and pay gas with erc20 token', async () => { + const amount = parseEther('100'); + + const richERC20BalanceBefore = await erc20.balanceOf(richAddress); + + const calls: Array = [ + { + target: richAddress, + allowFailure: false, + value: amount, + callData: '0x', + }, + { + target: await erc20.getAddress(), + allowFailure: false, + value: 0, + callData: erc20.interface.encodeFunctionData('transfer', [ + richAddress, + amount, + ]), + }, + ]; + + const batchTx = await prepareMockBatchTx( + provider, + account, + await batchCaller.getAddress(), + calls, + await mockValidator.getAddress(), + [], + getERC20PaymasterInput( + paymasterAddress, + await erc20.getAddress(), + parseEther('50'), + await getOraclePayload(erc20Paymaster), + ), + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(batchTx), + ); + await txReceipt.wait(); + + const [accountBalanceAfter, richBalanceAfter] = await Promise.all([ + provider.getBalance(accountAddress), + provider.getBalance(richAddress), + ]); + + expect(accountBalanceAfter).to.be.eq(accountBalanceBefore - amount); + expect(richBalanceAfter).to.be.equal(richBalanceBefore + amount); + + const richERC20BalanceAfter = await erc20.balanceOf(richAddress); + + expect(richERC20BalanceAfter).to.be.equal( + richERC20BalanceBefore + amount, + ); + + // Gas payment check + const gaslessPaymasterBalanceAfter = await provider.getBalance( + await erc20Paymaster.getAddress(), + ); + + expect(gaslessPaymasterBalanceAfter).is.lessThan( + paymasterBalanceBefore, + ); + }); + }); +}); diff --git a/test/paymasters/gaslesspaymaster.test.ts b/test/paymasters/gaslesspaymaster.test.ts new file mode 100644 index 0000000..0f68df1 --- /dev/null +++ b/test/paymasters/gaslesspaymaster.test.ts @@ -0,0 +1,412 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { assert, expect } from 'chai'; +import { parseEther } from 'ethers'; +import * as hre from 'hardhat'; +import type { Contract, Wallet } from 'zksync-ethers'; +import { Provider, utils } from 'zksync-ethers'; + +import { LOCAL_RICH_WALLETS, getWallet } from '../../deploy/utils'; +import type { CallStruct } from '../../typechain-types/contracts/batch/BatchCaller'; +import { ClaveDeployer } from '../utils/deployer'; +import { fixture } from '../utils/fixture'; +import { PAYMASTERS } from '../utils/names'; +import { getGaslessPaymasterInput } from '../utils/paymasters'; +import { + ethTransfer, + prepareMockBatchTx, + prepareMockTx, +} from '../utils/transactions'; + +describe('Clave Contracts - Paymaster tests', () => { + let deployer: ClaveDeployer; + let provider: Provider; + let richWallet: Wallet; + let batchCaller: Contract; + let registry: Contract; + let mockValidator: Contract; + let account: Contract; + + let gaslessPaymaster: Contract; + let erc20: Contract; + + before(async () => { + richWallet = getWallet(hre, LOCAL_RICH_WALLETS[0].privateKey); + deployer = new ClaveDeployer(hre, richWallet); + provider = new Provider(hre.network.config.url, undefined, { + cacheTimeout: -1, + }); + + [batchCaller, registry, , , mockValidator, account] = await fixture( + deployer, + ); + + const accountAddress = await account.getAddress(); + + await deployer.fund(10000, accountAddress); + + erc20 = await deployer.deployCustomContract('MockStable', []); + await erc20.mint(accountAddress, parseEther('100000')); + + gaslessPaymaster = await deployer.paymaster(PAYMASTERS.GASLESS, { + gasless: [await registry.getAddress(), 3], + }); + + await deployer.fund(50, await gaslessPaymaster.getAddress()); + }); + + it('Should fund the paymaster and account', async () => { + expect( + await provider.getBalance(await gaslessPaymaster.getAddress()), + ).to.eq(parseEther('50')); + + expect(await provider.getBalance(await account.getAddress())).to.eq( + parseEther('10000'), + ); + + expect(await erc20.balanceOf(await account.getAddress())).to.be.eq( + parseEther('100000'), + ); + }); + + describe('Gasless Paymaster', () => { + let accountAddress: string; + let richAddress: string; + let paymasterAddress: string; + + let accountBalanceBefore: bigint; + let richBalanceBefore: bigint; + let paymasterBalanceBefore: bigint; + + let paymasterUserLimit: bigint; + + before(async () => { + const addresses = await Promise.all([ + account.getAddress(), + richWallet.getAddress(), + gaslessPaymaster.getAddress(), + ]); + [accountAddress, richAddress, paymasterAddress] = addresses; + }); + + beforeEach(async () => { + const balances = await Promise.all([ + provider.getBalance(accountAddress), + provider.getBalance(richAddress), + provider.getBalance(paymasterAddress), + ]); + [accountBalanceBefore, richBalanceBefore, paymasterBalanceBefore] = + balances; + + paymasterUserLimit = await gaslessPaymaster.getRemainingUserLimit( + accountAddress, + ); + }); + + it('should send ETH and do not pay gas', async () => { + const amount = parseEther('1'); + + const txData = ethTransfer(richAddress, amount); + const tx = await prepareMockTx( + provider, + account, + txData, + await mockValidator.getAddress(), + getGaslessPaymasterInput(paymasterAddress), + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const [accountBalanceAfter, richBalanceAfter] = await Promise.all([ + provider.getBalance(accountAddress), + provider.getBalance(richAddress), + ]); + + expect(accountBalanceAfter).to.be.eq(accountBalanceBefore - amount); + expect(richBalanceAfter).to.be.equal(richBalanceBefore + amount); + + // Gas payment check + const gaslessPaymasterBalanceAfter = await provider.getBalance( + await gaslessPaymaster.getAddress(), + ); + + expect(gaslessPaymasterBalanceAfter).is.lessThan( + paymasterBalanceBefore, + ); + + const newPaymasterUserLimit: bigint = + await gaslessPaymaster.getRemainingUserLimit(accountAddress); + expect(paymasterUserLimit).to.be.eq( + newPaymasterUserLimit + BigInt(1), + ); + }); + + it('should send ERC20 token / contract interaction and do not pay gas', async () => { + const amount = parseEther('100'); + + const [accountERC20BalanceBefore, richERC20BalanceBefore] = + await Promise.all([ + erc20.balanceOf(accountAddress), + erc20.balanceOf(richAddress), + ]); + + const txData = { + to: await erc20.getAddress(), + value: 0, + data: erc20.interface.encodeFunctionData('transfer', [ + richAddress, + amount, + ]), + }; + const tx = await prepareMockTx( + provider, + account, + txData, + await mockValidator.getAddress(), + getGaslessPaymasterInput(paymasterAddress), + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const [accountERC20BalanceAfter, richERC20BalanceAfter] = + await Promise.all([ + erc20.balanceOf(accountAddress), + erc20.balanceOf(richAddress), + ]); + + expect(accountERC20BalanceAfter).to.be.equal( + accountERC20BalanceBefore - amount, + ); + expect(richERC20BalanceAfter).to.be.equal( + richERC20BalanceBefore + amount, + ); + + // Gas payment check + const accountBalanceAfter = await provider.getBalance( + accountAddress, + ); + expect(accountBalanceBefore).to.be.eq(accountBalanceAfter); + + const gaslessPaymasterBalanceAfter = await provider.getBalance( + await gaslessPaymaster.getAddress(), + ); + + expect(gaslessPaymasterBalanceAfter).is.lessThan( + paymasterBalanceBefore, + ); + + const newPaymasterUserLimit: bigint = + await gaslessPaymaster.getRemainingUserLimit(accountAddress); + expect(paymasterUserLimit).to.be.eq( + newPaymasterUserLimit + BigInt(1), + ); + }); + + it('should send batch tx / delegate call and do not pay gas', async () => { + const amount = parseEther('100'); + + const [accountERC20BalanceBefore, richERC20BalanceBefore] = + await Promise.all([ + erc20.balanceOf(accountAddress), + erc20.balanceOf(richAddress), + ]); + + const calls: Array = [ + { + target: richAddress, + allowFailure: false, + value: amount, + callData: '0x', + }, + { + target: await erc20.getAddress(), + allowFailure: false, + value: 0, + callData: erc20.interface.encodeFunctionData('transfer', [ + richAddress, + amount, + ]), + }, + ]; + + const batchTx = await prepareMockBatchTx( + provider, + account, + await batchCaller.getAddress(), + calls, + await mockValidator.getAddress(), + [], + getGaslessPaymasterInput(paymasterAddress), + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(batchTx), + ); + await txReceipt.wait(); + + const [accountBalanceAfter, richBalanceAfter] = await Promise.all([ + provider.getBalance(accountAddress), + provider.getBalance(richAddress), + ]); + + expect(accountBalanceAfter).to.be.eq(accountBalanceBefore - amount); + expect(richBalanceAfter).to.be.equal(richBalanceBefore + amount); + + const [accountERC20BalanceAfter, richERC20BalanceAfter] = + await Promise.all([ + erc20.balanceOf(accountAddress), + erc20.balanceOf(richAddress), + ]); + + expect(accountERC20BalanceAfter).to.be.equal( + accountERC20BalanceBefore - amount, + ); + expect(richERC20BalanceAfter).to.be.equal( + richERC20BalanceBefore + amount, + ); + + // Gas payment check + const gaslessPaymasterBalanceAfter = await provider.getBalance( + await gaslessPaymaster.getAddress(), + ); + + expect(gaslessPaymasterBalanceAfter).is.lessThan( + paymasterBalanceBefore, + ); + + const newPaymasterUserLimit: bigint = + await gaslessPaymaster.getRemainingUserLimit(accountAddress); + expect(paymasterUserLimit).to.be.eq( + newPaymasterUserLimit + BigInt(1), + ); + }); + + it('should revert if userLimit is reached', async () => { + expect(paymasterUserLimit).to.be.eq(BigInt(0)); + + const amount = parseEther('1'); + + const txData = ethTransfer(richAddress, amount); + const tx = await prepareMockTx( + provider, + account, + txData, + await mockValidator.getAddress(), + getGaslessPaymasterInput(paymasterAddress), + ); + + try { + await provider.broadcastTransaction(utils.serializeEip712(tx)); + assert(false, 'Should revert'); + } catch (error) {} + + const accountBalanceAfter = await provider.getBalance( + accountAddress, + ); + expect(accountBalanceAfter).to.be.eq(accountBalanceBefore); + }); + + it('should be able to increase user limit', async () => { + const newUserLimit = 4; + const tx = await gaslessPaymaster.updateUserLimit(newUserLimit); + await tx.wait(); + + const updatedUserLimit = await gaslessPaymaster.userLimit(); + expect(updatedUserLimit).to.be.eq(newUserLimit); + }); + + it('should send ETH and do not pay gas after increasing the user limit', async () => { + const amount = parseEther('1'); + + const txData = ethTransfer(richAddress, amount); + const tx = await prepareMockTx( + provider, + account, + txData, + await mockValidator.getAddress(), + getGaslessPaymasterInput(paymasterAddress), + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const [accountBalanceAfter, richBalanceAfter] = await Promise.all([ + provider.getBalance(accountAddress), + provider.getBalance(richAddress), + ]); + + expect(accountBalanceAfter).to.be.eq(accountBalanceBefore - amount); + expect(richBalanceAfter).to.be.equal(richBalanceBefore + amount); + + // Gas payment check + const gaslessPaymasterBalanceAfter = await provider.getBalance( + await gaslessPaymaster.getAddress(), + ); + + expect(gaslessPaymasterBalanceAfter).is.lessThan( + paymasterBalanceBefore, + ); + + const newPaymasterUserLimit: bigint = + await gaslessPaymaster.getRemainingUserLimit(accountAddress); + expect(paymasterUserLimit).to.be.eq( + newPaymasterUserLimit + BigInt(1), + ); + }); + + it('should be able to add limitless addresses', async () => { + const tx = await gaslessPaymaster.addLimitlessAddresses([ + accountAddress, + ]); + await tx.wait(); + + expect(await gaslessPaymaster.limitlessAddresses(accountAddress)).to + .be.true; + }); + + it('should send ETH and do not pay gas after being limitless address', async () => { + expect(paymasterUserLimit).to.be.eq(BigInt(0)); + + const amount = parseEther('1'); + + const txData = ethTransfer(richAddress, amount); + const tx = await prepareMockTx( + provider, + account, + txData, + await mockValidator.getAddress(), + getGaslessPaymasterInput(paymasterAddress), + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); + + const [accountBalanceAfter, richBalanceAfter] = await Promise.all([ + provider.getBalance(accountAddress), + provider.getBalance(richAddress), + ]); + + expect(accountBalanceAfter).to.be.eq(accountBalanceBefore - amount); + expect(richBalanceAfter).to.be.equal(richBalanceBefore + amount); + + // Gas payment check + const gaslessPaymasterBalanceAfter = await provider.getBalance( + await gaslessPaymaster.getAddress(), + ); + + expect(gaslessPaymasterBalanceAfter).is.lessThan( + paymasterBalanceBefore, + ); + }); + }); +}); diff --git a/test/utils/buffer.ts b/test/utils/buffer.ts new file mode 100644 index 0000000..1473a94 --- /dev/null +++ b/test/utils/buffer.ts @@ -0,0 +1,40 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +export function bufferFromBase64url(base64url: string): Buffer { + return Buffer.from(toBase64(base64url), 'base64'); +} + +export function bufferFromString(string: string): Buffer { + return Buffer.from(string, 'utf8'); +} + +function toBase64(base64url: string | Buffer): string { + base64url = base64url.toString(); + return padString(base64url).replace(/\-/g, '+').replace(/_/g, '/'); +} + +function padString(input: string): string { + const segmentLength = 4; + const stringLength = input.length; + const diff = stringLength % segmentLength; + + if (!diff) { + return input; + } + + let position = stringLength; + let padLength = segmentLength - diff; + const paddedStringLength = stringLength + padLength; + const buffer = Buffer.alloc(paddedStringLength); + + buffer.write(input); + + while (padLength--) { + buffer.write('=', position++); + } + + return buffer.toString(); +} diff --git a/test/utils/deployer.ts b/test/utils/deployer.ts new file mode 100644 index 0000000..bc787c1 --- /dev/null +++ b/test/utils/deployer.ts @@ -0,0 +1,250 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import type { ec } from 'elliptic'; +import { AbiCoder, ZeroAddress, parseEther, randomBytes } from 'ethers'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { Wallet } from 'zksync-ethers'; +import { Contract, utils } from 'zksync-ethers'; + +import { deployContract, getWallet } from '../../deploy/utils'; +import type { CallStruct } from '../../typechain-types/contracts/batch/BatchCaller'; +import { CONTRACT_NAMES, PAYMASTERS, type VALIDATORS } from './names'; +import { encodePublicKey } from './p256'; + +// This class helps deploy Clave contracts for the tests +export class ClaveDeployer { + private hre: HardhatRuntimeEnvironment; + private deployerWallet: Wallet; + + constructor( + hre: HardhatRuntimeEnvironment, + deployerWallet: string | Wallet, + ) { + this.hre = hre; + + typeof deployerWallet === 'string' + ? (this.deployerWallet = getWallet(this.hre, deployerWallet)) + : (this.deployerWallet = deployerWallet); + } + + public async batchCaller(): Promise { + return await deployContract( + this.hre, + CONTRACT_NAMES.BATCH_CALLER, + undefined, + { + wallet: this.deployerWallet, + silent: true, + }, + ); + } + + public async registry(): Promise { + return await deployContract( + this.hre, + CONTRACT_NAMES.REGISTRY, + undefined, + { + wallet: this.deployerWallet, + silent: true, + }, + ); + } + + public async implementation(batchCaller: Contract): Promise { + return await deployContract( + this.hre, + CONTRACT_NAMES.IMPLEMENTATION, + [await batchCaller.getAddress()], + { + wallet: this.deployerWallet, + silent: true, + }, + ); + } + + public async factory( + implementation: Contract, + registry: Contract, + ): Promise { + // TODO: WHY DOES THIS HELP + await deployContract( + this.hre, + CONTRACT_NAMES.PROXY, + [await implementation.getAddress()], + { wallet: this.deployerWallet, silent: true }, + ); + + // Deploy factory contract + const accountArtifact = await this.hre.zksyncEthers.loadArtifact( + CONTRACT_NAMES.PROXY, + ); + const bytecodeHash = utils.hashBytecode(accountArtifact.bytecode); + const factory = await deployContract( + this.hre, + CONTRACT_NAMES.FACTORY, + [ + await implementation.getAddress(), + await registry.getAddress(), + bytecodeHash, + this.deployerWallet.address, + ], + { + wallet: this.deployerWallet, + silent: true, + }, + ); + + // Assign the factory address to the registry + const factorySetTx = await registry.setFactory( + await factory.getAddress(), + ); + await factorySetTx.wait(); + + return factory; + } + + public async setupFactory(): Promise<{ + batchCaller: Contract; + registry: Contract; + implementation: Contract; + factory: Contract; + }> { + const batchCaller = await this.batchCaller(); + const registry = await this.registry(); + const implementation = await this.implementation(batchCaller); + const factory = await this.factory(implementation, registry); + + return { batchCaller, registry, implementation, factory }; + } + + public async validator(name: VALIDATORS): Promise { + return await deployContract(this.hre, name, undefined, { + wallet: this.deployerWallet, + silent: true, + }); + } + + public async account( + keyPair: ec.KeyPair, + factory: Contract, + validator: Contract, + ): Promise { + const publicKey = encodePublicKey(keyPair); + + const salt = randomBytes(32); + const call: CallStruct = { + target: ZeroAddress, + allowFailure: false, + value: 0, + callData: '0x', + }; + + const abiCoder = AbiCoder.defaultAbiCoder(); + const initializer = + '0x77ba2e75' + + abiCoder + .encode( + [ + 'bytes', + 'address', + 'bytes[]', + 'tuple(address target,bool allowFailure,uint256 value,bytes calldata)', + ], + [ + publicKey, + await validator.getAddress(), + [], + [ + call.target, + call.allowFailure, + call.value, + call.callData, + ], + ], + ) + .slice(2); + + const deployPromise = await Promise.all([ + // Deploy account + (async (): Promise => { + const deployTx = await factory.deployAccount(salt, initializer); + await deployTx.wait(); + })(), + // Calculate new account address + (async (): Promise => { + return await factory.getAddressForSalt(salt); + })(), + ]); + + const accountAddress = deployPromise[1]; + const implementationInterface = ( + await this.hre.zksyncEthers.loadArtifact( + CONTRACT_NAMES.IMPLEMENTATION, + ) + ).abi; + + const account = new Contract( + accountAddress, + implementationInterface, + this.deployerWallet, + ); + + return account; + } + + public async paymaster( + name: PAYMASTERS, + config: { + gasless?: [registryAddress: string, limit: number]; + erc20?: Array<{ + tokenAddress: string; + decimals: number; + priceMarkup: number; + }>; + }, + ): Promise { + if ( + (name === PAYMASTERS.GASLESS && !config.gasless) || + (name === PAYMASTERS.ERC20 && !config.erc20) || + (name === PAYMASTERS.ERC20_MOCK && !config.erc20) + ) { + throw new Error('Config mismatch.'); + } + + return await deployContract( + this.hre, + name, + name == PAYMASTERS.GASLESS ? config.gasless : [config.erc20], + { + wallet: this.deployerWallet, + silent: true, + }, + ); + } + + public async fund( + ethAmount: number, + accountAddress: string, + ): Promise { + await ( + await this.deployerWallet.sendTransaction({ + to: accountAddress, + value: parseEther(ethAmount.toString()), + }) + ).wait(); + } + + public async deployCustomContract( + name: string, + constructorArgs: Array, + ): Promise { + return await deployContract(this.hre, name, constructorArgs, { + wallet: this.deployerWallet, + silent: true, + }); + } +} diff --git a/test/utils/fixture.ts b/test/utils/fixture.ts new file mode 100644 index 0000000..9dc7414 --- /dev/null +++ b/test/utils/fixture.ts @@ -0,0 +1,45 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import type { ec } from 'elliptic'; +import type { Contract } from 'zksync-ethers'; + +import type { ClaveDeployer } from './deployer'; +import { VALIDATORS } from './names'; +import { genKey } from './p256'; + +export type fixtureTypes = [ + batchCaller: Contract, + registry: Contract, + implementation: Contract, + factory: Contract, + validator: Contract, + account: Contract, + keyPair: ec.KeyPair, +]; + +export const fixture = async ( + deployer: ClaveDeployer, + validatorOption: VALIDATORS = VALIDATORS.MOCK, +): Promise => { + const keyPair = genKey(); + + const batchCaller = await deployer.batchCaller(); + const registry = await deployer.registry(); + const implementation = await deployer.implementation(batchCaller); + const factory = await deployer.factory(implementation, registry); + const validator = await deployer.validator(validatorOption); + const account = await deployer.account(keyPair, factory, validator); + + return [ + batchCaller, + registry, + implementation, + factory, + validator, + account, + keyPair, + ]; +}; diff --git a/test/utils/managers/hookmanager.ts b/test/utils/managers/hookmanager.ts new file mode 100644 index 0000000..18faa6a --- /dev/null +++ b/test/utils/managers/hookmanager.ts @@ -0,0 +1,66 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import type { ec } from 'elliptic'; +import type { BytesLike } from 'ethers'; +import type { Contract, Provider } from 'zksync-ethers'; +import { utils } from 'zksync-ethers'; + +import type { HOOKS } from '../names'; +import { prepareTeeTx } from '../transactions'; + +export async function addHook( + provider: Provider, + account: Contract, + validator: Contract, + hook: Contract, + isValidation: HOOKS, + keyPair: ec.KeyPair, + hookData: Array = [], +): Promise { + const addHookTx = await account.addHook.populateTransaction( + await hook.getAddress(), + isValidation == 1 ? true : false, + ); + const tx = await prepareTeeTx( + provider, + account, + addHookTx, + await validator.getAddress(), + keyPair, + hookData, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function removeHook( + provider: Provider, + account: Contract, + validator: Contract, + hook: Contract, + isValidation: HOOKS, + keyPair: ec.KeyPair, + hookData: Array = [], +): Promise { + const removeHookTx = await account.removeHook.populateTransaction( + await hook.getAddress(), + isValidation == 1 ? true : false, + ); + const tx = await prepareTeeTx( + provider, + account, + removeHookTx, + await validator.getAddress(), + keyPair, + hookData, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} diff --git a/test/utils/managers/modulemanager.ts b/test/utils/managers/modulemanager.ts new file mode 100644 index 0000000..17e355f --- /dev/null +++ b/test/utils/managers/modulemanager.ts @@ -0,0 +1,62 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import type { ec } from 'elliptic'; +import { concat } from 'ethers'; +import type { Contract, Provider } from 'zksync-ethers'; +import { utils } from 'zksync-ethers'; + +import { prepareTeeTx } from '../transactions'; + +export async function addModule( + provider: Provider, + account: Contract, + validator: Contract, + module: Contract, + initData: string, + keyPair: ec.KeyPair, +): Promise { + const moduleAndData = concat([await module.getAddress(), initData]); + + const addModuleTx = await account.addModule.populateTransaction( + moduleAndData, + ); + const tx = await prepareTeeTx( + provider, + account, + addModuleTx, + await validator.getAddress(), + keyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function removeModule( + provider: Provider, + account: Contract, + validator: Contract, + module: Contract, + keyPair: ec.KeyPair, +): Promise { + const removeModuleTx = await account.removeModule.populateTransaction( + await module.getAddress(), + ); + + const tx = await prepareTeeTx( + provider, + account, + removeModuleTx, + await validator.getAddress(), + keyPair, + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} diff --git a/test/utils/managers/ownermanager.ts b/test/utils/managers/ownermanager.ts new file mode 100644 index 0000000..bac1872 --- /dev/null +++ b/test/utils/managers/ownermanager.ts @@ -0,0 +1,128 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import type { ec } from 'elliptic'; +import type { Contract, Provider } from 'zksync-ethers'; +import { utils } from 'zksync-ethers'; + +import { prepareTeeTx } from '../transactions'; + +export async function addR1Key( + provider: Provider, + account: Contract, + validator: Contract, + newPublicKey: string, + keyPair: ec.KeyPair, +): Promise { + const addOwnerTx = await account.r1AddOwner.populateTransaction( + newPublicKey, + ); + const tx = await prepareTeeTx( + provider, + account, + addOwnerTx, + await validator.getAddress(), + keyPair, + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function addK1Key( + provider: Provider, + account: Contract, + validator: Contract, + newK1Address: string, + keyPair: ec.KeyPair, +): Promise { + const addOwnerTx = await account.k1AddOwner.populateTransaction( + newK1Address, + ); + const tx = await prepareTeeTx( + provider, + account, + addOwnerTx, + await validator.getAddress(), + keyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function removeR1Key( + provider: Provider, + account: Contract, + validator: Contract, + removingPublicKey: string, + keyPair: ec.KeyPair, +): Promise { + const removeOwnerTxData = await account.r1RemoveOwner.populateTransaction( + removingPublicKey, + ); + const tx = await prepareTeeTx( + provider, + account, + removeOwnerTxData, + await validator.getAddress(), + keyPair, + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function removeK1Key( + provider: Provider, + account: Contract, + validator: Contract, + removingAddress: string, + keyPair: ec.KeyPair, +): Promise { + const removeOwnerTx = await account.k1RemoveOwner.populateTransaction( + removingAddress, + ); + const tx = await prepareTeeTx( + provider, + account, + removeOwnerTx, + await validator.getAddress(), + keyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function resetOwners( + provider: Provider, + account: Contract, + validator: Contract, + newPublicKey: string, + keyPair: ec.KeyPair, +): Promise { + const resetOwnersTx = await account.resetOwners.populateTransaction( + newPublicKey, + ); + const tx = await prepareTeeTx( + provider, + account, + resetOwnersTx, + await validator.getAddress(), + keyPair, + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} diff --git a/test/utils/managers/upgrademanager.ts b/test/utils/managers/upgrademanager.ts new file mode 100644 index 0000000..2e07178 --- /dev/null +++ b/test/utils/managers/upgrademanager.ts @@ -0,0 +1,33 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import type { ec } from 'elliptic'; +import type { Contract, Provider } from 'zksync-ethers'; +import { utils } from 'zksync-ethers'; + +import { prepareTeeTx } from '../transactions'; + +export async function upgradeTx( + provider: Provider, + account: Contract, + validator: Contract, + newImplementation: Contract, + keyPair: ec.KeyPair, +): Promise { + const upgradeTx = await account.upgradeTo.populateTransaction( + await newImplementation.getAddress(), + ); + const tx = await prepareTeeTx( + provider, + account, + upgradeTx, + await validator.getAddress(), + keyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} diff --git a/test/utils/managers/validatormanager.ts b/test/utils/managers/validatormanager.ts new file mode 100644 index 0000000..f6a6ecd --- /dev/null +++ b/test/utils/managers/validatormanager.ts @@ -0,0 +1,104 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import type { ec } from 'elliptic'; +import type { Contract, Provider } from 'zksync-ethers'; +import { utils } from 'zksync-ethers'; + +import { prepareTeeTx } from '../transactions'; + +export async function addR1Validator( + provider: Provider, + account: Contract, + validator: Contract, + newR1Validator: Contract, + keyPair: ec.KeyPair, +): Promise { + const addValidatorTx = await account.r1AddValidator.populateTransaction( + await newR1Validator.getAddress(), + ); + const tx = await prepareTeeTx( + provider, + account, + addValidatorTx, + await validator.getAddress(), + keyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function removeR1Validator( + provider: Provider, + account: Contract, + validator: Contract, + removingR1Validator: Contract, + keyPair: ec.KeyPair, +): Promise { + const removeValidatorTx = + await account.r1RemoveValidator.populateTransaction( + removingR1Validator, + ); + const tx = await prepareTeeTx( + provider, + account, + removeValidatorTx, + await validator.getAddress(), + keyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function addK1Validator( + provider: Provider, + account: Contract, + validator: Contract, + newK1Validator: Contract, + keyPair: ec.KeyPair, +): Promise { + const addValidatorTx = await account.k1AddValidator.populateTransaction( + await newK1Validator.getAddress(), + ); + const tx = await prepareTeeTx( + provider, + account, + addValidatorTx, + await validator.getAddress(), + keyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function removeK1Validator( + provider: Provider, + account: Contract, + validator: Contract, + removingK1Validator: Contract, + keyPair: ec.KeyPair, +): Promise { + const removeValidatorTx = + await account.k1RemoveValidator.populateTransaction( + removingK1Validator, + ); + const tx = await prepareTeeTx( + provider, + account, + removeValidatorTx, + await validator.getAddress(), + keyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} diff --git a/test/utils/names.ts b/test/utils/names.ts new file mode 100644 index 0000000..c7f3192 --- /dev/null +++ b/test/utils/names.ts @@ -0,0 +1,32 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ + +export const CONTRACT_NAMES = { + BATCH_CALLER: 'BatchCaller', + REGISTRY: 'ClaveRegistry', + IMPLEMENTATION: 'ClaveImplementation', + PROXY: 'ClaveProxy', + FACTORY: 'AccountFactory', + MOCK_VALIDATOR: 'MockValidator', +}; + +export enum VALIDATORS { + MOCK = 'MockValidator', + TEE = 'TEEValidator', + EOA = 'EOAValidator', + PASSKEY = 'PasskeyValidator', +} + +export enum HOOKS { + VALIDATION = 1, + EXECUTION = 0, +} + +export enum PAYMASTERS { + GASLESS = 'GaslessPaymaster', + ERC20 = 'ERC20Paymaster', + ERC20_MOCK = 'ERC20PaymasterMock', +} diff --git a/test/utils/passkey.ts b/test/utils/passkey.ts new file mode 100644 index 0000000..b068819 --- /dev/null +++ b/test/utils/passkey.ts @@ -0,0 +1,31 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import { sha256 } from 'ethers'; + +import { bufferFromBase64url, bufferFromString } from './buffer'; + +// hash of 'https://getclave.io' + (BE, BS, UP, UV) flags set + unincremented sign counter +const authData = bufferFromBase64url( + 'F1-vhQTCzdfAF3iosO_Uh07LOu_X67cHmpQfe-iJfUEdAAAAAA', +); +const clientDataPrefix = bufferFromString( + '{"type":"webauthn.get","challenge":"', +); +const clientDataSuffix = bufferFromString('","origin":"https://getclave.io"}'); + +export function getSignedData(challenge: string): string { + const challengeBuffer = Buffer.from(challenge.slice(2), 'hex'); + const challengeBase64 = challengeBuffer.toString('base64url'); + const clientData = Buffer.concat([ + clientDataPrefix, + bufferFromString(challengeBase64), + clientDataSuffix, + ]); + + const clientDataHash = Buffer.from(sha256(clientData).slice(2), 'hex'); + + return sha256(Buffer.concat([authData, clientDataHash])); +} diff --git a/test/utils/paymaster.ts b/test/utils/paymasters.ts similarity index 78% rename from test/utils/paymaster.ts rename to test/utils/paymasters.ts index 9912045..1dc3649 100644 --- a/test/utils/paymaster.ts +++ b/test/utils/paymasters.ts @@ -4,7 +4,8 @@ * Proprietary and confidential */ import type { ethers } from 'ethers'; -import { type types, utils } from 'zksync-ethers'; +import { utils } from 'zksync-ethers'; +import type { Contract, types } from 'zksync-ethers'; export function getGaslessPaymasterInput( paymasterAddress: types.Address, @@ -28,3 +29,10 @@ export function getERC20PaymasterInput( innerInput: oraclePayload, }); } + +export async function getOraclePayload( + paymasterContract: Contract, +): Promise { + paymasterContract; + return '0x'; +} diff --git a/test/utils/recovery/recovery.ts b/test/utils/recovery/recovery.ts new file mode 100644 index 0000000..92eb42d --- /dev/null +++ b/test/utils/recovery/recovery.ts @@ -0,0 +1,173 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import type { ec } from 'elliptic'; +import type { Contract, Provider, Wallet } from 'zksync-ethers'; +import { utils } from 'zksync-ethers'; + +import { encodePublicKey } from '../p256'; +import { prepareTeeTx } from '../transactions'; + +type StartRecoveryParams = { + recoveringAddress: string; + newOwner: string; + nonce: number; +}; + +type RecoveryData = { + recoveringAddress: string; + newOwner: string; + nonce: number; +}; + +async function signRecoveryEIP712Hash( + params: StartRecoveryParams | RecoveryData, + recoveryContract: Contract, + wallet: Wallet, +): Promise { + const eip712Hash = await recoveryContract.getEip712Hash(params); + const signature = wallet.signingKey.sign(eip712Hash).serialized; + return signature; +} + +export async function startCloudRecovery( + cloudGuardian: Wallet, + account: Contract, + module: Contract, + newOwner: string, +): Promise { + const recoveringAddress = await account.getAddress(); + const recoveryNonce = await module.recoveryNonces(recoveringAddress); + + const params: StartRecoveryParams = { + recoveringAddress: recoveringAddress, + newOwner, + nonce: recoveryNonce, + }; + + const signature = await signRecoveryEIP712Hash( + params, + module, + cloudGuardian, + ); + + const startRecoveryTx = await module.startRecovery(params, signature); + await startRecoveryTx.wait(); +} + +export async function startSocialRecovery( + socialGuardian: Wallet, + account: Contract, + module: Contract, + newKeyPair: ec.KeyPair, +): Promise { + const recoveringAddress = await account.getAddress(); + const recoveryNonce = await module.recoveryNonces(recoveringAddress); + + const recoveryData = { + recoveringAddress: await account.getAddress(), + newOwner: encodePublicKey(newKeyPair), + nonce: recoveryNonce, + }; + + const signature = await signRecoveryEIP712Hash( + recoveryData, + module, + socialGuardian, + ); + + const guardianData = { + guardian: await socialGuardian.getAddress(), + signature: signature, + }; + + const startRecoveryTx = await module.startRecovery(recoveryData, [ + guardianData, + ]); + await startRecoveryTx.wait(); +} + +export async function stopRecovery( + provider: Provider, + account: Contract, + module: Contract, + validator: Contract, + keyPair: ec.KeyPair, +): Promise { + const stopRecoveryTx = await module.stopRecovery.populateTransaction(); + + const tx = await prepareTeeTx( + provider, + account, + stopRecoveryTx, + await validator.getAddress(), + keyPair, + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function updateCloudGuardian( + provider: Provider, + account: Contract, + module: Contract, + validator: Contract, + newAddress: string, + keyPair: ec.KeyPair, +): Promise { + const updateGuardianTx = await module.updateGuardian.populateTransaction( + newAddress, + ); + const tx = await prepareTeeTx( + provider, + account, + updateGuardianTx, + await validator.getAddress(), + keyPair, + ); + + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function updateSocialRecoveryConfig( + provider: Provider, + account: Contract, + module: Contract, + validator: Contract, + config: [number, number, Array], + keyPair: ec.KeyPair, +): Promise { + const updateConfigTx = await module.updateConfig.populateTransaction({ + threshold: config[0], + timelock: config[1], + guardians: config[2], + }); + + const tx = await prepareTeeTx( + provider, + account, + updateConfigTx, + await validator.getAddress(), + keyPair, + ); + const txReceipt = await provider.broadcastTransaction( + utils.serializeEip712(tx), + ); + await txReceipt.wait(); +} + +export async function executeRecovery( + account: Contract, + module: Contract, +): Promise { + const tx = await module.executeRecovery(await account.getAddress()); + await tx.wait(); +} diff --git a/test/utils/snapshot.ts b/test/utils/snapshot.ts deleted file mode 100644 index 9fa3221..0000000 --- a/test/utils/snapshot.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright Clave - All Rights Reserved - * Unauthorized copying of this file, via any medium is strictly prohibited - * Proprietary and confidential - */ -import { appendFileSync } from 'fs'; -import { env } from 'process'; -import { read } from 'read-last-lines'; - -// Adds headers to snapshot file to specify what are the gas costs for -export async function snapshotHeader(header: string): Promise { - // Check if we are in snapshot mode - if (env.NODE_ENV == 'snapshot') { - // This prevents race conditions - while ( - !(await read('./.gas-snapshot', 1)).trim().endsWith('(opcodes)') - ) { - await sleep(50); - } - appendFileSync('./.gas-snapshot', `\n"${header}"\n`); - } -} - -export async function snapshotFirstHeader(header: string): Promise { - // Check if we are in snapshot mode - if (env.NODE_ENV === 'snapshot') { - appendFileSync('./.gas-snapshot', `\n"${header}"\n`); - } -} - -async function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} diff --git a/test/utils/transaction.ts b/test/utils/transaction.ts deleted file mode 100644 index 36a9e8d..0000000 --- a/test/utils/transaction.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Copyright Clave - All Rights Reserved - * Unauthorized copying of this file, via any medium is strictly prohibited - * Proprietary and confidential - */ -import type { ec } from 'elliptic'; -import { type BigNumberish, ethers, parseEther } from 'ethers'; -import type { Contract, Provider, types } from 'zksync-ethers'; -import { EIP712Signer, utils } from 'zksync-ethers'; - -import type { ClaveProxy } from '../../typechain-types'; -import type { CallStruct } from '../../typechain-types/contracts/batch/BatchCaller'; -import { sign } from './p256'; - -export const ethTransfer = ( - to: string, - value: BigNumberish, -): types.TransactionLike => { - return { - to, - value, - data: '0x', - }; -}; - -export async function prepareMockTx( - provider: Provider, - account: ClaveProxy, - tx: types.TransactionLike, - validatorAddress: string, - paymasterParams?: types.PaymasterParams, -): Promise { - const abiCoder = ethers.AbiCoder.defaultAbiCoder(); - const signature = abiCoder.encode( - ['bytes', 'address', 'bytes[]'], - ['0x' + 'C1AE'.repeat(32), validatorAddress, []], - ); - - tx = { - ...tx, - from: await account.getAddress(), - nonce: await provider.getTransactionCount(await account.getAddress()), - gasLimit: 10_000_000, - gasPrice: await provider.getGasPrice(), - chainId: (await provider.getNetwork()).chainId, - type: 113, - customData: { - gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, - customSignature: signature, - paymasterParams: paymasterParams, - } as types.Eip712Meta, - }; - - return tx; -} - -export async function prepareTeeTx( - provider: Provider, - account: Contract, - tx: types.TransactionLike, - validatorAddress: string, - keyPair: ec.KeyPair, - hookData: Array = [], - paymasterParams?: types.PaymasterParams, -): Promise { - if (tx.value == undefined) { - tx.value = parseEther('0'); - } - - tx = { - ...tx, - from: await account.getAddress(), - nonce: await provider.getTransactionCount(await account.getAddress()), - gasLimit: 30_000_000, - gasPrice: await provider.getGasPrice(), - chainId: (await provider.getNetwork()).chainId, - type: 113, - customData: { - gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, - paymasterParams: paymasterParams, - } as types.Eip712Meta, - }; - - const signedTxHash = EIP712Signer.getSignedDigest(tx); - - const abiCoder = ethers.AbiCoder.defaultAbiCoder(); - let signature = sign(signedTxHash.toString(), keyPair); - - signature = abiCoder.encode( - ['bytes', 'address', 'bytes[]'], - [signature, validatorAddress, hookData], - ); - - tx.customData = { - ...tx.customData, - customSignature: signature, - }; - - return tx; -} - -export async function prepareBatchTx( - provider: Provider, - account: Contract, - BatchCallerAddress: string, - calls: Array, - validatorAddress: string, - keyPair: ec.KeyPair, - hookData: Array = [], - paymasterParams?: types.PaymasterParams, -): Promise { - const abiCoder = ethers.AbiCoder.defaultAbiCoder(); - const data = - '0x8f0273a9' + - abiCoder - .encode( - [ - 'tuple(address target, bool allowFailure, uint256 value, bytes callData)[]', - ], - [calls], - ) - .slice(2); - - const tx = { - to: BatchCallerAddress, - from: await account.getAddress(), - nonce: await provider.getTransactionCount(await account.getAddress()), - gasLimit: 30_000_000, - gasPrice: await provider.getGasPrice(), - data, - value: parseEther('0'), - chainId: (await provider.getNetwork()).chainId, - type: 113, - customData: { - gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, - paymasterParams, - } as types.Eip712Meta, - }; - - const signedTxHash = EIP712Signer.getSignedDigest(tx); - - let signature = sign(signedTxHash.toString(), keyPair); - - signature = abiCoder.encode( - ['bytes', 'address', 'bytes[]'], - [signature, validatorAddress, hookData], - ); - - tx.customData = { - ...tx.customData, - customSignature: signature, - }; - - return tx; -} diff --git a/test/utils/transactions.ts b/test/utils/transactions.ts new file mode 100644 index 0000000..eefd5c7 --- /dev/null +++ b/test/utils/transactions.ts @@ -0,0 +1,327 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import type { ec } from 'elliptic'; +import type { BigNumberish, HDNodeWallet } from 'ethers'; +import { ethers, parseEther, sha256 } from 'ethers'; +import type { Contract, Provider, types } from 'zksync-ethers'; +import { EIP712Signer, utils } from 'zksync-ethers'; + +import type { CallStruct } from '../../typechain-types/contracts/batch/BatchCaller'; +import { sign } from './p256'; +import { getSignedData } from './passkey'; + +export const ethTransfer = ( + to: string, + value: BigNumberish, +): types.TransactionLike => { + return { + to, + value, + data: '0x', + }; +}; + +export async function prepareMockTx( + provider: Provider, + account: Contract, + tx: types.TransactionLike, + validatorAddress: string, + paymasterParams?: types.PaymasterParams, +): Promise { + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + const signature = abiCoder.encode( + ['bytes', 'address', 'bytes[]'], + ['0x' + 'C1AE'.repeat(32), validatorAddress, []], + ); + + tx = { + ...tx, + from: await account.getAddress(), + nonce: await provider.getTransactionCount(await account.getAddress()), + gasLimit: 30_000_000, + gasPrice: await provider.getGasPrice(), + chainId: (await provider.getNetwork()).chainId, + type: 113, + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + customSignature: signature, + paymasterParams: paymasterParams, + } as types.Eip712Meta, + }; + + return tx; +} + +export async function prepareMockBatchTx( + provider: Provider, + account: Contract, + BatchCallerAddress: string, + calls: Array, + validatorAddress: string, + hookData: Array = [], + paymasterParams?: types.PaymasterParams, +): Promise { + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + + const data = + '0x8f0273a9' + + abiCoder + .encode( + [ + 'tuple(address target, bool allowFailure, uint256 value, bytes callData)[]', + ], + [calls], + ) + .slice(2); + + let totalValue: BigNumberish = '0'; + for (const call of calls) { + totalValue += call.value; + } + + const tx = { + to: BatchCallerAddress, + from: await account.getAddress(), + nonce: await provider.getTransactionCount(await account.getAddress()), + gasLimit: 30_000_000, + gasPrice: await provider.getGasPrice(), + data, + value: totalValue, + chainId: (await provider.getNetwork()).chainId, + type: 113, + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams, + } as types.Eip712Meta, + }; + + const signature = abiCoder.encode( + ['bytes', 'address', 'bytes[]'], + ['0x' + 'C1AE'.repeat(32), validatorAddress, hookData], + ); + + tx.customData = { + ...tx.customData, + customSignature: signature, + }; + + return tx; +} + +export async function prepareTeeTx( + provider: Provider, + account: Contract, + tx: types.TransactionLike, + validatorAddress: string, + keyPair: ec.KeyPair, + hookData: Array = [], + paymasterParams?: types.PaymasterParams, +): Promise { + if (tx.value == undefined) { + tx.value = parseEther('0'); + } + + tx = { + ...tx, + from: await account.getAddress(), + nonce: await provider.getTransactionCount(await account.getAddress()), + gasLimit: 30_000_000, + gasPrice: await provider.getGasPrice(), + chainId: (await provider.getNetwork()).chainId, + type: 113, + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams: paymasterParams, + } as types.Eip712Meta, + }; + + const signedTxHash = EIP712Signer.getSignedDigest(tx); + + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + let signature = sign(sha256(signedTxHash.toString()), keyPair); + + signature = abiCoder.encode( + ['bytes', 'address', 'bytes[]'], + [signature, validatorAddress, hookData], + ); + + tx.customData = { + ...tx.customData, + customSignature: signature, + }; + + return tx; +} + +export async function prepareBatchTx( + provider: Provider, + account: Contract, + BatchCallerAddress: string, + calls: Array, + validatorAddress: string, + keyPair: ec.KeyPair, + hookData: Array = [], + paymasterParams?: types.PaymasterParams, +): Promise { + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + const data = + '0x8f0273a9' + + abiCoder + .encode( + [ + 'tuple(address target, bool allowFailure, uint256 value, bytes callData)[]', + ], + [calls], + ) + .slice(2); + + let totalValue: BigNumberish = '0'; + for (const call of calls) { + totalValue += call.value; + } + + const tx = { + to: BatchCallerAddress, + from: await account.getAddress(), + nonce: await provider.getTransactionCount(await account.getAddress()), + gasLimit: 30_000_000, + gasPrice: await provider.getGasPrice(), + data, + value: totalValue, + chainId: (await provider.getNetwork()).chainId, + type: 113, + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams, + } as types.Eip712Meta, + }; + + const signedTxHash = EIP712Signer.getSignedDigest(tx); + + let signature = sign(sha256(signedTxHash.toString()), keyPair); + + signature = abiCoder.encode( + ['bytes', 'address', 'bytes[]'], + [signature, validatorAddress, hookData], + ); + + tx.customData = { + ...tx.customData, + customSignature: signature, + }; + + return tx; +} + +export async function prepareEOATx( + provider: Provider, + account: Contract, + tx: types.TransactionLike, + validatorAddress: string, + wallet: HDNodeWallet, + hookData: Array = [], + paymasterParams?: types.PaymasterParams, +): Promise { + if (tx.value == undefined) { + tx.value = parseEther('0'); + } + + tx = { + ...tx, + from: await account.getAddress(), + nonce: await provider.getTransactionCount(await account.getAddress()), + gasLimit: 30_000_000, + gasPrice: await provider.getGasPrice(), + chainId: (await provider.getNetwork()).chainId, + type: 113, + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams: paymasterParams, + } as types.Eip712Meta, + }; + + const signedTxHash = EIP712Signer.getSignedDigest(tx); + + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + let signature = wallet.signingKey.sign(signedTxHash).serialized; + + signature = abiCoder.encode( + ['bytes', 'address', 'bytes[]'], + [signature, validatorAddress, hookData], + ); + + tx.customData = { + ...tx.customData, + customSignature: signature, + }; + + return tx; +} + +export async function preparePasskeyTx( + provider: Provider, + account: Contract, + tx: types.TransactionLike, + validatorAddress: string, + keyPair: ec.KeyPair, + hookData: Array = [], + paymasterParams?: types.PaymasterParams, +): Promise { + if (tx.value == undefined) { + tx.value = parseEther('0'); + } + + tx = { + ...tx, + from: await account.getAddress(), + nonce: await provider.getTransactionCount(await account.getAddress()), + gasLimit: 30_000_000, + gasPrice: await provider.getGasPrice(), + chainId: (await provider.getNetwork()).chainId, + type: 113, + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams: paymasterParams, + } as types.Eip712Meta, + }; + + const signedTxHash = EIP712Signer.getSignedDigest(tx); + + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + let signature = sign(getSignedData(signedTxHash.toString()), keyPair); + // Perform malleability check and invert 's' if it's too large + const rs = signature.slice(2); + const r = '0x' + rs.slice(0, 64); + let s = BigInt('0x' + rs.slice(64, 128)); + + // Maximum allowed value for 's' in secp256r1 + const lowSmax = BigInt( + '0x7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8', + ); + + if (s > lowSmax) { + // If 's' is too large, invert it + s = + BigInt( + '0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', + ) - s; + // Reconstruct the signature with inverted 's' + signature = r + s.toString(16).padStart(64, '0'); + } + signature = '0x01' + signature.slice(2); + + signature = abiCoder.encode( + ['bytes', 'address', 'bytes[]'], + [signature, validatorAddress, hookData], + ); + + tx.customData = { + ...tx.customData, + customSignature: signature, + }; + + return tx; +}