diff --git a/contracts/interfaces/opty/IERC20PermitLegacy-0.8.x.sol b/contracts/interfaces/opty/IERC20PermitLegacy-0.8.x.sol new file mode 100644 index 00000000..1a3d8acb --- /dev/null +++ b/contracts/interfaces/opty/IERC20PermitLegacy-0.8.x.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title Legacy ERC20Permit interface + * @author OptyFi + */ +interface IERC20PermitLegacy { + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/contracts/mocks/contracts/TestHub.sol b/contracts/mocks/contracts/TestHub.sol new file mode 100644 index 00000000..450e1e3d --- /dev/null +++ b/contracts/mocks/contracts/TestHub.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "../../protocol/meta-tx/GSN/libraries/GsnTypes.sol"; +import "../../protocol/meta-tx/GSN/interfaces/IPaymaster.sol"; + +import "../../protocol/meta-tx/GSN/RelayHub.sol"; + +import "../utils/AllEvents.sol"; + +/** + * This mock relay hub contract is only used to test the paymaster's 'pre-' and 'postRelayedCall' in isolation. + */ +contract TestHub is RelayHub, AllEvents { + constructor( + IStakeManager _stakeManager, + address _penalizer, + address _batchGateway, + address _relayRegistrar, + RelayHubConfig memory _config + ) + RelayHub(_stakeManager, _penalizer, _batchGateway, _relayRegistrar, _config) + // solhint-disable-next-line no-empty-blocks + { + + } + + function callPreRC( + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData, + uint256 maxPossibleGas + ) external returns (bytes memory context, bool revertOnRecipientRevert) { + IPaymaster paymaster = IPaymaster(relayRequest.relayData.paymaster); + IPaymaster.GasAndDataLimits memory limits = paymaster.getGasAndDataLimits(); + return + paymaster.preRelayedCall{ gas: limits.preRelayedCallGasLimit }( + relayRequest, + signature, + approvalData, + maxPossibleGas + ); + } + + function callPostRC( + IPaymaster paymaster, + bytes calldata context, + uint256 gasUseWithoutPost, + GsnTypes.RelayData calldata relayData + ) external { + IPaymaster.GasAndDataLimits memory limits = paymaster.getGasAndDataLimits(); + paymaster.postRelayedCall{ gas: limits.postRelayedCallGasLimit }(context, true, gasUseWithoutPost, relayData); + } +} diff --git a/contracts/mocks/contracts/TestToken.sol b/contracts/mocks/contracts/TestToken.sol new file mode 100644 index 00000000..bd65d751 --- /dev/null +++ b/contracts/mocks/contracts/TestToken.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestToken is ERC20 { + /* solhint-disable no-empty-blocks */ + constructor(string memory name, string memory symbol) public ERC20(name, symbol) {} + + function mint(address account, uint256 amount) public { + _mint(account, amount); + } +} diff --git a/contracts/mocks/utils/AllEvents.sol b/contracts/mocks/utils/AllEvents.sol new file mode 100644 index 00000000..33e0f97f --- /dev/null +++ b/contracts/mocks/utils/AllEvents.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.8.7; + +/** + * In order to help the Truffle tests to decode events in the transactions' results, + * the events must be declared in a top-level contract. + * Implement this empty interface in order to add event signatures to any contract. + * + */ +interface AllEvents { + event Received(address indexed sender, uint256 eth); + event Withdrawal(address indexed src, uint256 wad); + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + event TokensCharged( + uint256 gasUseWithoutPost, + uint256 gasJustPost, + uint256 tokenActualCharge, + uint256 ethActualCharge + ); + event UniswapReverted(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin); + + event Swap( + address indexed sender, + address indexed recipient, + int256 amount0, + int256 amount1, + uint160 sqrtPriceX96, + uint128 liquidity, + int24 tick + ); +} diff --git a/contracts/mocks/utils/RelayRegistrar.sol b/contracts/mocks/utils/RelayRegistrar.sol new file mode 100644 index 00000000..b55beab6 --- /dev/null +++ b/contracts/mocks/utils/RelayRegistrar.sol @@ -0,0 +1,143 @@ +// solhint-disable not-rely-on-time +//SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.6; +/* solhint-disable no-inline-assembly */ + +// #if ENABLE_CONSOLE_LOG +import "hardhat/console.sol"; +// #endif + +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; + +import "../../protocol/meta-tx/GSN/libraries/MinLibBytes.sol"; +import "../../protocol/meta-tx/GSN/interfaces/IRelayHub.sol"; +import "../../protocol/meta-tx/GSN/interfaces/IRelayRegistrar.sol"; + +/** + * @title The RelayRegistrar Implementation + * @notice Keeps a list of registered relayers. + * + * @notice Provides view functions to read the list of registered relayers and filters out invalid ones. + * + * @notice Protects the list from spamming entries: only staked relayers are added. + */ +contract RelayRegistrar is IRelayRegistrar, Ownable, ERC165 { + using MinLibBytes for bytes; + + uint256 private constant MAX_RELAYS_RETURNED_COUNT = 1000; + + /// @notice Mapping from `RelayHub` address to a mapping from a Relay Manager address to its registration details. + mapping(address => mapping(address => RelayInfo)) internal values; + + /// @notice Mapping from `RelayHub` address to an array of Relay Managers that are registered on that `RelayHub`. + mapping(address => address[]) internal indexedValues; + + uint256 private immutable creationBlock; + + uint256 private relayRegistrationMaxAge; + + constructor(uint256 _relayRegistrationMaxAge) { + setRelayRegistrationMaxAge(_relayRegistrationMaxAge); + creationBlock = block.number; + } + + /// @inheritdoc IRelayRegistrar + function getCreationBlock() external view override returns (uint256) { + return creationBlock; + } + + /// @inheritdoc IRelayRegistrar + function getRelayRegistrationMaxAge() external view override returns (uint256) { + return relayRegistrationMaxAge; + } + + /// @inheritdoc IRelayRegistrar + function setRelayRegistrationMaxAge(uint256 _relayRegistrationMaxAge) public override onlyOwner { + relayRegistrationMaxAge = _relayRegistrationMaxAge; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(IRelayRegistrar).interfaceId || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IRelayRegistrar + function registerRelayServer(address relayHub, bytes32[3] calldata url) external override { + address relayManager = msg.sender; + IRelayHub(relayHub).onRelayServerRegistered(relayManager); + emit RelayServerRegistered(relayManager, relayHub, url); + storeRelayServerRegistration(relayManager, relayHub, url); + } + + function addItem(address relayHub, address relayManager) internal returns (RelayInfo storage) { + RelayInfo storage storageInfo = values[relayHub][relayManager]; + if (storageInfo.lastSeenBlockNumber == 0) { + indexedValues[relayHub].push(relayManager); + } + return storageInfo; + } + + function storeRelayServerRegistration( + address relayManager, + address relayHub, + bytes32[3] calldata url + ) internal { + RelayInfo storage storageInfo = addItem(relayHub, relayManager); + if (storageInfo.firstSeenBlockNumber == 0) { + storageInfo.firstSeenBlockNumber = uint32(block.number); + storageInfo.firstSeenTimestamp = uint40(block.timestamp); + } + storageInfo.lastSeenBlockNumber = uint32(block.number); + storageInfo.lastSeenTimestamp = uint40(block.timestamp); + storageInfo.relayManager = relayManager; + storageInfo.urlParts = url; + } + + /// @inheritdoc IRelayRegistrar + function getRelayInfo(address relayHub, address relayManager) public view override returns (RelayInfo memory) { + RelayInfo memory info = values[relayHub][relayManager]; + require(info.lastSeenBlockNumber != 0, "relayManager not found"); + return info; + } + + /// @inheritdoc IRelayRegistrar + function readRelayInfos(address relayHub) public view override returns (RelayInfo[] memory info) { + uint256 blockTimestamp = block.timestamp; + uint256 oldestBlockTimestamp = + blockTimestamp >= relayRegistrationMaxAge ? blockTimestamp - relayRegistrationMaxAge : 0; + return readRelayInfosInRange(relayHub, 0, oldestBlockTimestamp, MAX_RELAYS_RETURNED_COUNT); + } + + /// @inheritdoc IRelayRegistrar + function readRelayInfosInRange( + address relayHub, + uint256 oldestBlockNumber, + uint256 oldestBlockTimestamp, + uint256 maxCount + ) public view override returns (RelayInfo[] memory info) { + address[] storage items = indexedValues[relayHub]; + uint256 filled = 0; + info = new RelayInfo[](items.length < maxCount ? items.length : maxCount); + for (uint256 i = 0; i < items.length; i++) { + address relayManager = items[i]; + RelayInfo memory relayInfo = getRelayInfo(relayHub, relayManager); + if ( + relayInfo.lastSeenBlockNumber < oldestBlockNumber || relayInfo.lastSeenTimestamp < oldestBlockTimestamp + ) { + continue; + } + // solhint-disable-next-line no-empty-blocks + try IRelayHub(relayHub).verifyRelayManagerStaked(relayManager) {} catch ( + bytes memory /*lowLevelData*/ + ) { + continue; + } + info[filled++] = relayInfo; + if (filled >= maxCount) break; + } + assembly { + mstore(info, filled) + } + } +} diff --git a/contracts/mocks/utils/StakeManager.sol b/contracts/mocks/utils/StakeManager.sol new file mode 100644 index 00000000..eb8df95b --- /dev/null +++ b/contracts/mocks/utils/StakeManager.sol @@ -0,0 +1,275 @@ +// solhint-disable not-rely-on-time +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; +import "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; + +import "../../protocol/meta-tx/GSN/interfaces/IStakeManager.sol"; + +/** + * @title The StakeManager implementation + * @notice An IStakeManager instance that accepts stakes in any ERC-20 token. + * + * @notice Single StakeInfo of a single RelayManager can only have one token address assigned to it. + * + * @notice It cannot be changed after the first time 'stakeForRelayManager' is called as it is equivalent to withdrawal. + */ +contract StakeManager is IStakeManager, Ownable, ERC165 { + using SafeERC20 for IERC20; + + string public override versionSM = "3.0.0-beta.2+opengsn.stakemanager.istakemanager"; + uint256 internal immutable maxUnstakeDelay; + + AbandonedRelayServerConfig internal abandonedRelayServerConfig; + + address internal burnAddress; + uint256 internal immutable creationBlock; + + /// maps relay managers to their stakes + mapping(address => StakeInfo) public stakes; + + /// @inheritdoc IStakeManager + function getStakeInfo(address relayManager) + external + view + override + returns (StakeInfo memory stakeInfo, bool isSenderAuthorizedHub) + { + bool isHubAuthorized = authorizedHubs[relayManager][msg.sender].removalTime == type(uint256).max; + return (stakes[relayManager], isHubAuthorized); + } + + /// @inheritdoc IStakeManager + function setBurnAddress(address _burnAddress) public override onlyOwner { + burnAddress = _burnAddress; + emit BurnAddressSet(burnAddress); + } + + /// @inheritdoc IStakeManager + function getBurnAddress() external view override returns (address) { + return burnAddress; + } + + /// @inheritdoc IStakeManager + function setDevAddress(address _devAddress) public override onlyOwner { + abandonedRelayServerConfig.devAddress = _devAddress; + emit DevAddressSet(abandonedRelayServerConfig.devAddress); + } + + /// @inheritdoc IStakeManager + function getAbandonedRelayServerConfig() external view override returns (AbandonedRelayServerConfig memory) { + return abandonedRelayServerConfig; + } + + /// @inheritdoc IStakeManager + function getMaxUnstakeDelay() external view override returns (uint256) { + return maxUnstakeDelay; + } + + /// maps relay managers to a map of addressed of their authorized hubs to the information on that hub + mapping(address => mapping(address => RelayHubInfo)) public authorizedHubs; + + constructor( + uint256 _maxUnstakeDelay, + uint256 _abandonmentDelay, + uint256 _escheatmentDelay, + address _burnAddress, + address _devAddress + ) { + require(_burnAddress != address(0), "transfers to address(0) may fail"); + setBurnAddress(_burnAddress); + setDevAddress(_devAddress); + creationBlock = block.number; + maxUnstakeDelay = _maxUnstakeDelay; + abandonedRelayServerConfig.abandonmentDelay = _abandonmentDelay; + abandonedRelayServerConfig.escheatmentDelay = _escheatmentDelay; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(IStakeManager).interfaceId || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IStakeManager + function getCreationBlock() external view override returns (uint256) { + return creationBlock; + } + + /// @inheritdoc IStakeManager + function setRelayManagerOwner(address owner) external override { + require(owner != address(0), "invalid owner"); + require(stakes[msg.sender].owner == address(0), "already owned"); + stakes[msg.sender].owner = owner; + emit OwnerSet(msg.sender, owner); + } + + /// @inheritdoc IStakeManager + function stakeForRelayManager( + IERC20 token, + address relayManager, + uint256 unstakeDelay, + uint256 amount + ) external override relayOwnerOnly(relayManager) { + require(unstakeDelay >= stakes[relayManager].unstakeDelay, "unstakeDelay cannot be decreased"); + require(unstakeDelay <= maxUnstakeDelay, "unstakeDelay too big"); + require(token != IERC20(address(0)), "must specify stake token address"); + require( + stakes[relayManager].token == IERC20(address(0)) || stakes[relayManager].token == token, + "stake token address is incorrect" + ); + token.safeTransferFrom(msg.sender, address(this), amount); + stakes[relayManager].token = token; + stakes[relayManager].stake += amount; + stakes[relayManager].unstakeDelay = unstakeDelay; + emit StakeAdded( + relayManager, + stakes[relayManager].owner, + stakes[relayManager].token, + stakes[relayManager].stake, + stakes[relayManager].unstakeDelay + ); + } + + /// @inheritdoc IStakeManager + function unlockStake(address relayManager) external override relayOwnerOnly(relayManager) { + StakeInfo storage info = stakes[relayManager]; + require(info.withdrawTime == 0, "already pending"); + info.withdrawTime = block.timestamp + info.unstakeDelay; + emit StakeUnlocked(relayManager, msg.sender, info.withdrawTime); + } + + /// @inheritdoc IStakeManager + function withdrawStake(address relayManager) external override relayOwnerOnly(relayManager) { + StakeInfo storage info = stakes[relayManager]; + require(info.withdrawTime > 0, "Withdrawal is not scheduled"); + require(info.withdrawTime <= block.timestamp, "Withdrawal is not due"); + uint256 amount = info.stake; + info.stake = 0; + info.withdrawTime = 0; + info.token.safeTransfer(msg.sender, amount); + emit StakeWithdrawn(relayManager, msg.sender, info.token, amount); + } + + /// @notice Prevents any address other than a registered Relay Owner from calling this method. + modifier relayOwnerOnly(address relayManager) { + StakeInfo storage info = stakes[relayManager]; + require(info.owner == msg.sender, "not owner"); + _; + } + + /// @notice Prevents any address other than a registered Relay Manager from calling this method. + modifier managerOnly() { + StakeInfo storage info = stakes[msg.sender]; + require(info.owner != address(0), "not manager"); + _; + } + + /// @inheritdoc IStakeManager + function authorizeHubByOwner(address relayManager, address relayHub) + external + override + relayOwnerOnly(relayManager) + { + _authorizeHub(relayManager, relayHub); + } + + /// @inheritdoc IStakeManager + function authorizeHubByManager(address relayHub) external override managerOnly { + _authorizeHub(msg.sender, relayHub); + } + + function _authorizeHub(address relayManager, address relayHub) internal { + authorizedHubs[relayManager][relayHub].removalTime = type(uint256).max; + emit HubAuthorized(relayManager, relayHub); + } + + /// @inheritdoc IStakeManager + function unauthorizeHubByOwner(address relayManager, address relayHub) + external + override + relayOwnerOnly(relayManager) + { + _unauthorizeHub(relayManager, relayHub); + } + + /// @inheritdoc IStakeManager + function unauthorizeHubByManager(address relayHub) external override managerOnly { + _unauthorizeHub(msg.sender, relayHub); + } + + function _unauthorizeHub(address relayManager, address relayHub) internal { + RelayHubInfo storage hubInfo = authorizedHubs[relayManager][relayHub]; + require(hubInfo.removalTime == type(uint256).max, "hub not authorized"); + hubInfo.removalTime = block.timestamp + stakes[relayManager].unstakeDelay; + emit HubUnauthorized(relayManager, relayHub, hubInfo.removalTime); + } + + /// @inheritdoc IStakeManager + function penalizeRelayManager( + address relayManager, + address beneficiary, + uint256 amount + ) external override { + uint256 removalTime = authorizedHubs[relayManager][msg.sender].removalTime; + require(removalTime != 0, "hub not authorized"); + require(removalTime > block.timestamp, "hub authorization expired"); + + // Half of the stake will be burned (sent to address 0) + require(stakes[relayManager].stake >= amount, "penalty exceeds stake"); + stakes[relayManager].stake = stakes[relayManager].stake - amount; + + uint256 toBurn = amount / 2; + uint256 reward = amount - toBurn; + + // Stake ERC-20 token is burned and transferred + stakes[relayManager].token.safeTransfer(burnAddress, toBurn); + stakes[relayManager].token.safeTransfer(beneficiary, reward); + emit StakePenalized(relayManager, beneficiary, stakes[relayManager].token, reward); + } + + /// @inheritdoc IStakeManager + function isRelayEscheatable(address relayManager) public view override returns (bool) { + IStakeManager.StakeInfo memory stakeInfo = stakes[relayManager]; + return + stakeInfo.abandonedTime != 0 && + stakeInfo.abandonedTime + abandonedRelayServerConfig.escheatmentDelay < block.timestamp; + } + + /// @inheritdoc IStakeManager + function markRelayAbandoned(address relayManager) external override onlyOwner { + StakeInfo storage info = stakes[relayManager]; + require(info.stake > 0, "relay manager not staked"); + require(info.abandonedTime == 0, "relay manager already abandoned"); + require( + info.keepaliveTime + abandonedRelayServerConfig.abandonmentDelay < block.timestamp, + "relay manager was alive recently" + ); + info.abandonedTime = block.timestamp; + emit RelayServerAbandoned(relayManager, info.abandonedTime); + } + + /// @inheritdoc IStakeManager + function escheatAbandonedRelayStake(address relayManager) external override onlyOwner { + StakeInfo storage info = stakes[relayManager]; + require(isRelayEscheatable(relayManager), "relay server not escheatable yet"); + uint256 amount = info.stake; + info.stake = 0; + info.withdrawTime = 0; + info.token.safeTransfer(abandonedRelayServerConfig.devAddress, amount); + emit AbandonedRelayManagerStakeEscheated(relayManager, msg.sender, info.token, amount); + } + + /// @inheritdoc IStakeManager + function updateRelayKeepaliveTime(address relayManager) external override { + StakeInfo storage info = stakes[relayManager]; + bool isHubAuthorized = authorizedHubs[relayManager][msg.sender].removalTime == type(uint256).max; + bool isRelayOwner = info.owner == msg.sender; + require(isHubAuthorized || isRelayOwner, "must be called by owner or hub"); + info.abandonedTime = 0; + info.keepaliveTime = block.timestamp; + emit RelayServerKeepalive(relayManager, info.keepaliveTime); + } +} diff --git a/contracts/mocks/utils/TokenGasCalculator.sol b/contracts/mocks/utils/TokenGasCalculator.sol new file mode 100644 index 00000000..fe654792 --- /dev/null +++ b/contracts/mocks/utils/TokenGasCalculator.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; + +import "../../protocol/meta-tx/GSN/RelayHub.sol"; +import "../../protocol/meta-tx/GSN/BasePaymaster.sol"; +import "./AllEvents.sol"; + +/** + * Calculate the postRelayedCall gas usage for a TokenPaymaster. + * + */ +contract TokenGasCalculator is RelayHub, AllEvents { + //(The Paymaster calls back calculateCharge, depositFor in the relayHub, + //so the calculator has to implement them just like a real RelayHub + // solhint-disable-next-line no-empty-blocks + constructor( + IStakeManager _stakeManager, + address _penalizer, + address _batchGateway, + address _relayRegistrar, + RelayHubConfig memory _config + ) + RelayHub(_stakeManager, _penalizer, _batchGateway, _relayRegistrar, _config) + // solhint-disable-next-line no-empty-blocks + { + + } + + /** + * calculate actual cost of postRelayedCall. + * usage: + * - create this calculator. + * - create an instance of your TokenPaymaster, with your token's Uniswap instance. + * - move some tokens (1000 "wei") to the calculator (msg.sender is given approval to pull them back at the end) + * - set the calculator as owner of this calculator. + * - call this method. + * - use the returned values to set your real TokenPaymaster.setPostGasUsage() + * the above can be ran on a "forked" network, so that it will have the real token, uniswap instances, + * but still leave no side-effect on the network. + */ + function calculatePostGas( + BasePaymaster paymaster, + bytes memory ctx1, + bytes memory paymasterData + ) public returns (uint256 gasUsedByPost) { + GsnTypes.RelayData memory relayData = + GsnTypes.RelayData(1, 1, 0, address(0), address(0), address(0), paymasterData, 0); + + //with precharge + uint256 gas0 = gasleft(); + paymaster.postRelayedCall(ctx1, true, 1000000000000000, relayData); + uint256 gas1 = gasleft(); + gasUsedByPost = gas0 - gas1; + emit GasUsed(gasUsedByPost); + } + + event GasUsed(uint256 gasUsedByPost); +} diff --git a/contracts/protocol/meta-tx/Forwarder.sol b/contracts/protocol/meta-tx/Forwarder.sol new file mode 100644 index 00000000..5268f39a --- /dev/null +++ b/contracts/protocol/meta-tx/Forwarder.sol @@ -0,0 +1,182 @@ +// solhint-disable not-rely-on-time +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; + +import "./interfaces/IForwarder.sol"; + +/** + * @title The Forwarder Implementation + * @notice This implementation of the `IForwarder` interface uses ERC-712 signatures and stored nonces for verification. + */ +contract Forwarder is IForwarder, ERC165 { + using ECDSA for bytes32; + + address private constant DRY_RUN_ADDRESS = 0x0000000000000000000000000000000000000000; + + string public constant GENERIC_PARAMS = + "address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime"; + + string public constant EIP712_DOMAIN_TYPE = + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; + + mapping(bytes32 => bool) public typeHashes; + mapping(bytes32 => bool) public domains; + + // Nonces of senders, used to prevent replay attacks + mapping(address => uint256) private nonces; + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} + + /// @inheritdoc IForwarder + function getNonce(address from) public view override returns (uint256) { + return nonces[from]; + } + + constructor() { + string memory requestType = string(abi.encodePacked("ForwardRequest(", GENERIC_PARAMS, ")")); + registerRequestTypeInternal(requestType); + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(IForwarder).interfaceId || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IForwarder + function verify( + ForwardRequest calldata req, + bytes32 domainSeparator, + bytes32 requestTypeHash, + bytes calldata suffixData, + bytes calldata sig + ) external view override { + _verifyNonce(req); + _verifySig(req, domainSeparator, requestTypeHash, suffixData, sig); + } + + /// @inheritdoc IForwarder + function execute( + ForwardRequest calldata req, + bytes32 domainSeparator, + bytes32 requestTypeHash, + bytes calldata suffixData, + bytes calldata sig + ) external payable override returns (bool success, bytes memory ret) { + _verifySig(req, domainSeparator, requestTypeHash, suffixData, sig); + _verifyAndUpdateNonce(req); + + require(req.validUntilTime == 0 || req.validUntilTime > block.timestamp, "FWD: request expired"); + + uint256 gasForTransfer = 0; + if (req.value != 0) { + gasForTransfer = 40000; //buffer in case we need to move eth after the transaction. + } + bytes memory callData = abi.encodePacked(req.data, req.from); + require((gasleft() * 63) / 64 >= req.gas + gasForTransfer, "FWD: insufficient gas"); + // solhint-disable-next-line avoid-low-level-calls + (success, ret) = req.to.call{ gas: req.gas, value: req.value }(callData); + + if (req.value != 0 && address(this).balance > 0) { + // can't fail: req.from signed (off-chain) the request, so it must be an EOA... + payable(req.from).transfer(address(this).balance); + } + + return (success, ret); + } + + function _verifyNonce(ForwardRequest calldata req) internal view { + require(nonces[req.from] == req.nonce, "FWD: nonce mismatch"); + } + + function _verifyAndUpdateNonce(ForwardRequest calldata req) internal { + require(nonces[req.from]++ == req.nonce, "FWD: nonce mismatch"); + } + + /// @inheritdoc IForwarder + function registerRequestType(string calldata typeName, string calldata typeSuffix) external override { + for (uint256 i = 0; i < bytes(typeName).length; i++) { + bytes1 c = bytes(typeName)[i]; + require(c != "(" && c != ")", "FWD: invalid typename"); + } + + string memory requestType = string(abi.encodePacked(typeName, "(", GENERIC_PARAMS, ",", typeSuffix)); + registerRequestTypeInternal(requestType); + } + + /// @inheritdoc IForwarder + function registerDomainSeparator(string calldata name, string calldata version) external override { + uint256 chainId; + /* solhint-disable-next-line no-inline-assembly */ + assembly { + chainId := chainid() + } + + bytes memory domainValue = + abi.encode( + keccak256(bytes(EIP712_DOMAIN_TYPE)), + keccak256(bytes(name)), + keccak256(bytes(version)), + chainId, + address(this) + ); + + bytes32 domainHash = keccak256(domainValue); + + domains[domainHash] = true; + emit DomainRegistered(domainHash, domainValue); + } + + function registerRequestTypeInternal(string memory requestType) internal { + bytes32 requestTypehash = keccak256(bytes(requestType)); + typeHashes[requestTypehash] = true; + emit RequestTypeRegistered(requestTypehash, requestType); + } + + function _verifySig( + ForwardRequest calldata req, + bytes32 domainSeparator, + bytes32 requestTypeHash, + bytes calldata suffixData, + bytes calldata sig + ) internal view virtual { + require(domains[domainSeparator], "FWD: unregistered domain sep."); + require(typeHashes[requestTypeHash], "FWD: unregistered typehash"); + + bytes32 digest = + keccak256( + abi.encodePacked("\x19\x01", domainSeparator, keccak256(_getEncoded(req, requestTypeHash, suffixData))) + ); + // solhint-disable-next-line avoid-tx-origin + require(tx.origin == DRY_RUN_ADDRESS || digest.recover(sig) == req.from, "FWD: signature mismatch"); + } + + /** + * @notice Creates a byte array that is a valid ABI encoding of a request of a `RequestType` type. See `execute()`. + */ + function _getEncoded( + ForwardRequest calldata req, + bytes32 requestTypeHash, + bytes calldata suffixData + ) public pure returns (bytes memory) { + // we use encodePacked since we append suffixData as-is, not as dynamic param. + // still, we must make sure all first params are encoded as abi.encode() + // would encode them - as 256-bit-wide params. + return + abi.encodePacked( + requestTypeHash, + uint256(uint160(req.from)), + uint256(uint160(req.to)), + req.value, + req.gas, + req.nonce, + keccak256(req.data), + req.validUntilTime, + suffixData + ); + } +} diff --git a/contracts/protocol/meta-tx/GSN/BasePaymaster.sol b/contracts/protocol/meta-tx/GSN/BasePaymaster.sol new file mode 100644 index 00000000..491f49cf --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/BasePaymaster.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165Checker.sol"; + +import "./libraries/GsnTypes.sol"; +import "./interfaces/IPaymaster.sol"; +import "./interfaces/IRelayHub.sol"; +import "./libraries/GsnEip712Library.sol"; +import "../interfaces/IForwarder.sol"; + +/** + * @notice An abstract base class to be inherited by a concrete Paymaster. + * A subclass must implement: + * - preRelayedCall + * - postRelayedCall + */ +abstract contract BasePaymaster is IPaymaster, Ownable, ERC165 { + using ERC165Checker for address; + + IRelayHub internal relayHub; + address private _trustedForwarder; + + /// @inheritdoc IPaymaster + function getRelayHub() public view override returns (address) { + return address(relayHub); + } + + //overhead of forwarder verify+signature, plus hub overhead. + uint256 public constant FORWARDER_HUB_OVERHEAD = 500000; + + //These parameters are documented in IPaymaster.GasAndDataLimits + uint256 public constant PRE_RELAYED_CALL_GAS_LIMIT = 1000000; + uint256 public constant POST_RELAYED_CALL_GAS_LIMIT = 1100000; + uint256 public constant PAYMASTER_ACCEPTANCE_BUDGET = PRE_RELAYED_CALL_GAS_LIMIT + FORWARDER_HUB_OVERHEAD; + uint256 public constant CALLDATA_SIZE_LIMIT = 10500; + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return + interfaceId == type(IPaymaster).interfaceId || + interfaceId == type(Ownable).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @inheritdoc IPaymaster + function getGasAndDataLimits() public view virtual override returns (IPaymaster.GasAndDataLimits memory limits) { + return + IPaymaster.GasAndDataLimits( + PAYMASTER_ACCEPTANCE_BUDGET, + PRE_RELAYED_CALL_GAS_LIMIT, + POST_RELAYED_CALL_GAS_LIMIT, + CALLDATA_SIZE_LIMIT + ); + } + + /** + * @notice this method must be called from preRelayedCall to validate that the forwarder + * is approved by the paymaster as well as by the recipient contract. + */ + function _verifyForwarder(GsnTypes.RelayRequest calldata relayRequest) internal view virtual { + require(getTrustedForwarder() == relayRequest.relayData.forwarder, "Forwarder is not trusted"); + GsnEip712Library.verifyForwarderTrusted(relayRequest); + } + + function _verifyRelayHubOnly() internal view virtual { + require(msg.sender == getRelayHub(), "can only be called by RelayHub"); + } + + function _verifyValue(GsnTypes.RelayRequest calldata relayRequest) internal view virtual { + require(relayRequest.request.value == 0, "value transfer not supported"); + } + + function _verifyPaymasterData(GsnTypes.RelayRequest calldata relayRequest) internal view virtual { + require(relayRequest.relayData.paymasterData.length == 0, "should have no paymasterData"); + } + + function _verifyApprovalData(bytes calldata approvalData) internal view virtual { + require(approvalData.length == 0, "should have no approvalData"); + } + + /** + * @notice The owner of the Paymaster can change the instance of the RelayHub this Paymaster works with. + * :warning: **Warning** :warning: The deposit on the previous RelayHub must be withdrawn first. + */ + function setRelayHub(IRelayHub hub) public onlyOwner { + require(address(hub).supportsInterface(type(IRelayHub).interfaceId), "target is not a valid IRelayHub"); + relayHub = hub; + } + + /** + * @notice The owner of the Paymaster can change the instance of the Forwarder this Paymaster works with. + * @notice the Recipients must trust this Forwarder as well in order for the configuration to remain functional. + */ + function setTrustedForwarder(address forwarder) public virtual onlyOwner { + require(forwarder.supportsInterface(type(IForwarder).interfaceId), "target is not a valid IForwarder"); + _trustedForwarder = forwarder; + } + + function getTrustedForwarder() public view virtual override returns (address) { + return _trustedForwarder; + } + + /** + * @notice Any native Ether transferred into the paymaster is transferred as a deposit to the RelayHub. + * This way, we don't need to understand the RelayHub API in order to replenish the paymaster. + */ + receive() external payable virtual { + require(address(relayHub) != address(0), "relay hub address not set"); + relayHub.depositFor{ value: msg.value }(address(this)); + } + + /** + * @notice Withdraw deposit from the RelayHub. + * @param amount The amount to be subtracted from the sender. + * @param target The target to which the amount will be transferred. + */ + function withdrawRelayHubDepositTo(uint256 amount, address payable target) public onlyOwner { + relayHub.withdraw(target, amount); + } + + /// @inheritdoc IPaymaster + function preRelayedCall( + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData, + uint256 maxPossibleGas + ) external override returns (bytes memory, bool) { + _verifyRelayHubOnly(); + _verifyForwarder(relayRequest); + _verifyValue(relayRequest); + _verifyPaymasterData(relayRequest); + return _preRelayedCall(relayRequest, signature, approvalData, maxPossibleGas); + } + + /** + * @notice internal logic the paymasters need to provide to select which transactions they are willing to pay for + * @notice see the documentation for `IPaymaster::preRelayedCall` for details + */ + function _preRelayedCall( + GsnTypes.RelayRequest calldata, + bytes calldata, + bytes calldata, + uint256 + ) internal virtual returns (bytes memory, bool); + + /// @inheritdoc IPaymaster + function postRelayedCall( + bytes calldata context, + bool success, + uint256 gasUseWithoutPost, + GsnTypes.RelayData calldata relayData + ) external override { + _verifyRelayHubOnly(); + _postRelayedCall(context, success, gasUseWithoutPost, relayData); + } + + /** + * @notice internal logic the paymasters need to provide if they need to take some action after the transaction + * @notice see the documentation for `IPaymaster::postRelayedCall` for details + */ + function _postRelayedCall( + bytes calldata, + bool, + uint256, + GsnTypes.RelayData calldata + ) internal virtual; +} diff --git a/contracts/protocol/meta-tx/GSN/Penalizer.sol b/contracts/protocol/meta-tx/GSN/Penalizer.sol new file mode 100644 index 00000000..025bac98 --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/Penalizer.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; + +import "./libraries/RLPReader.sol"; +import "./libraries/GsnUtils.sol"; +import "./interfaces/IRelayHub.sol"; +import "./interfaces/IPenalizer.sol"; + +/** + * @title The Penalizer Implementation + * + * @notice This Penalizer supports parsing Legacy, Type 1 and Type 2 raw RLP Encoded transactions. + */ +contract Penalizer is IPenalizer, ERC165 { + using ECDSA for bytes32; + + /// @inheritdoc IPenalizer + string public override versionPenalizer = "3.0.0-beta.2+opengsn.penalizer.ipenalizer"; + + uint256 internal immutable penalizeBlockDelay; + uint256 internal immutable penalizeBlockExpiration; + + constructor(uint256 _penalizeBlockDelay, uint256 _penalizeBlockExpiration) { + penalizeBlockDelay = _penalizeBlockDelay; + penalizeBlockExpiration = _penalizeBlockExpiration; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(IPenalizer).interfaceId || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IPenalizer + function getPenalizeBlockDelay() external view override returns (uint256) { + return penalizeBlockDelay; + } + + /// @inheritdoc IPenalizer + function getPenalizeBlockExpiration() external view override returns (uint256) { + return penalizeBlockExpiration; + } + + function isLegacyTransaction(bytes calldata rawTransaction) internal pure returns (bool) { + uint8 transactionTypeByte = uint8(rawTransaction[0]); + return (transactionTypeByte >= 0xc0 && transactionTypeByte <= 0xfe); + } + + function isTransactionType1(bytes calldata rawTransaction) internal pure returns (bool) { + return (uint8(rawTransaction[0]) == 1); + } + + function isTransactionType2(bytes calldata rawTransaction) internal pure returns (bool) { + return (uint8(rawTransaction[0]) == 2); + } + + /// @return `true` if raw transaction is of types Legacy, 1 or 2. `false` otherwise. + function isTransactionTypeValid(bytes calldata rawTransaction) public pure returns (bool) { + return + isLegacyTransaction(rawTransaction) || + isTransactionType1(rawTransaction) || + isTransactionType2(rawTransaction); + } + + /// @return transaction The details that the `Penalizer` needs to decide if the transaction is penalizable. + function decodeTransaction(bytes calldata rawTransaction) public pure returns (Transaction memory transaction) { + if (isTransactionType1(rawTransaction)) { + (transaction.nonce, transaction.gasLimit, transaction.to, transaction.value, transaction.data) = RLPReader + .decodeTransactionType1(rawTransaction); + } else if (isTransactionType2(rawTransaction)) { + (transaction.nonce, transaction.gasLimit, transaction.to, transaction.value, transaction.data) = RLPReader + .decodeTransactionType2(rawTransaction); + } else { + (transaction.nonce, transaction.gasLimit, transaction.to, transaction.value, transaction.data) = RLPReader + .decodeLegacyTransaction(rawTransaction); + } + return transaction; + } + + mapping(bytes32 => uint256) public commits; + + /// @inheritdoc IPenalizer + function commit(bytes32 commitHash) external override { + uint256 readyBlockNumber = block.number + penalizeBlockDelay; + commits[commitHash] = readyBlockNumber; + emit CommitAdded(msg.sender, commitHash, readyBlockNumber); + } + + /// Modifier that verifies there was a `commit` operation before this call that has not expired yet. + modifier commitRevealOnly() { + bytes32 commitHash = keccak256(abi.encodePacked(keccak256(msg.data), msg.sender)); + uint256 readyBlockNumber = commits[commitHash]; + delete commits[commitHash]; + // msg.sender can only be fake during off-chain view call, allowing Penalizer process to check transactions + if (msg.sender != address(type(uint160).max)) { + require(readyBlockNumber != 0, "no commit"); + require(readyBlockNumber < block.number, "reveal penalize too soon"); + require(readyBlockNumber + penalizeBlockExpiration > block.number, "reveal penalize too late"); + } + _; + } + + /// @inheritdoc IPenalizer + function penalizeRepeatedNonce( + bytes calldata unsignedTx1, + bytes calldata signature1, + bytes calldata unsignedTx2, + bytes calldata signature2, + IRelayHub hub, + uint256 randomValue + ) public override commitRevealOnly { + (randomValue); + _penalizeRepeatedNonce(unsignedTx1, signature1, unsignedTx2, signature2, hub); + } + + function _penalizeRepeatedNonce( + bytes calldata unsignedTx1, + bytes calldata signature1, + bytes calldata unsignedTx2, + bytes calldata signature2, + IRelayHub hub + ) private { + address addr1 = keccak256(unsignedTx1).recover(signature1); + address addr2 = keccak256(unsignedTx2).recover(signature2); + + require(addr1 == addr2, "Different signer"); + require(addr1 != address(0), "ecrecover failed"); + + Transaction memory decodedTx1 = decodeTransaction(unsignedTx1); + Transaction memory decodedTx2 = decodeTransaction(unsignedTx2); + + // checking that the same nonce is used in both transaction, with both signed by the same address + // and the actual data is different + // note: we compare the hash of the tx to save gas over iterating both byte arrays + require(decodedTx1.nonce == decodedTx2.nonce, "Different nonce"); + + bytes memory dataToCheck1 = + abi.encodePacked(decodedTx1.data, decodedTx1.gasLimit, decodedTx1.to, decodedTx1.value); + + bytes memory dataToCheck2 = + abi.encodePacked(decodedTx2.data, decodedTx2.gasLimit, decodedTx2.to, decodedTx2.value); + + require(keccak256(dataToCheck1) != keccak256(dataToCheck2), "tx is equal"); + + penalize(addr1, hub); + } + + /// @inheritdoc IPenalizer + function penalizeIllegalTransaction( + bytes calldata unsignedTx, + bytes calldata signature, + IRelayHub hub, + uint256 randomValue + ) public override commitRevealOnly { + (randomValue); + _penalizeIllegalTransaction(unsignedTx, signature, hub); + } + + function _penalizeIllegalTransaction( + bytes calldata unsignedTx, + bytes calldata signature, + IRelayHub hub + ) private { + if (isTransactionTypeValid(unsignedTx)) { + Transaction memory decodedTx = decodeTransaction(unsignedTx); + if (decodedTx.to == address(hub)) { + bytes4 selector = GsnUtils.getMethodSig(decodedTx.data); + bool isWrongMethodCall = selector != IRelayHub.relayCall.selector; + require(isWrongMethodCall, "Legal relay transaction"); + } + } + address relay = keccak256(unsignedTx).recover(signature); + require(relay != address(0), "ecrecover failed"); + penalize(relay, hub); + } + + function penalize(address relayWorker, IRelayHub hub) private { + hub.penalize(relayWorker, payable(msg.sender)); + } +} diff --git a/contracts/protocol/meta-tx/GSN/RelayHub.sol b/contracts/protocol/meta-tx/GSN/RelayHub.sol new file mode 100644 index 00000000..ab3698ec --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/RelayHub.sol @@ -0,0 +1,670 @@ +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable not-rely-on-time */ +/* solhint-disable avoid-tx-origin */ +/* solhint-disable bracket-align */ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +// #if ENABLE_CONSOLE_LOG +import "hardhat/console.sol"; +// #endif + +import "./libraries/MinLibBytes.sol"; +import "@openzeppelin/contracts-0.8.x/utils/Address.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165Checker.sol"; +import "@openzeppelin/contracts-0.8.x/utils/math/Math.sol"; +import "@openzeppelin/contracts-0.8.x/access/Ownable.sol"; +import "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; + +import "./libraries/GsnUtils.sol"; +import "./libraries/GsnEip712Library.sol"; +import "./libraries/RelayHubValidator.sol"; +import "./libraries/GsnTypes.sol"; +import "./interfaces/IRelayHub.sol"; +import "./interfaces/IPaymaster.sol"; +import "../interfaces/IForwarder.sol"; +import "./interfaces/IStakeManager.sol"; +import "./interfaces/IRelayRegistrar.sol"; +import "./interfaces/IStakeManager.sol"; + +/** + * @title The RelayHub Implementation + * @notice This contract implements the `IRelayHub` interface for the EVM-compatible networks. + */ +contract RelayHub is IRelayHub, Ownable, ERC165 { + using ERC165Checker for address; + using Address for address; + + address private constant DRY_RUN_ADDRESS = 0x0000000000000000000000000000000000000000; + + /// @inheritdoc IRelayHub + function versionHub() public pure virtual override returns (string memory) { + return "3.0.0-beta.2+opengsn.hub.irelayhub"; + } + + IStakeManager internal immutable stakeManager; + address internal immutable penalizer; + address internal immutable batchGateway; + address internal immutable relayRegistrar; + + RelayHubConfig internal config; + + /// @inheritdoc IRelayHub + function getConfiguration() public view override returns (RelayHubConfig memory) { + return config; + } + + /// @inheritdoc IRelayHub + function setConfiguration(RelayHubConfig memory _config) public override onlyOwner { + require(_config.devFee < 100, "dev fee too high"); + config = _config; + emit RelayHubConfigured(config); + } + + // maps ERC-20 token address to a minimum stake for it + mapping(IERC20 => uint256) internal minimumStakePerToken; + + /// @inheritdoc IRelayHub + function setMinimumStakes(IERC20[] memory token, uint256[] memory minimumStake) public override onlyOwner { + require(token.length == minimumStake.length, "setMinimumStakes: wrong length"); + for (uint256 i = 0; i < token.length; i++) { + minimumStakePerToken[token[i]] = minimumStake[i]; + emit StakingTokenDataChanged(address(token[i]), minimumStake[i]); + } + } + + // maps relay worker's address to its manager's address + mapping(address => address) internal workerToManager; + + // maps relay managers to the number of their workers + mapping(address => uint256) internal workerCount; + + mapping(address => uint256) internal balances; + + uint256 internal immutable creationBlock; + uint256 internal deprecationTime = type(uint256).max; + + constructor( + IStakeManager _stakeManager, + address _penalizer, + address _batchGateway, + address _relayRegistrar, + RelayHubConfig memory _config + ) { + creationBlock = block.number; + stakeManager = _stakeManager; + penalizer = _penalizer; + batchGateway = _batchGateway; + relayRegistrar = _relayRegistrar; + setConfiguration(_config); + } + + /// @inheritdoc IRelayHub + function getCreationBlock() external view virtual override returns (uint256) { + return creationBlock; + } + + /// @inheritdoc IRelayHub + function getDeprecationTime() external view override returns (uint256) { + return deprecationTime; + } + + /// @inheritdoc IRelayHub + function getStakeManager() external view override returns (IStakeManager) { + return stakeManager; + } + + /// @inheritdoc IRelayHub + function getPenalizer() external view override returns (address) { + return penalizer; + } + + /// @inheritdoc IRelayHub + function getBatchGateway() external view override returns (address) { + return batchGateway; + } + + /// @inheritdoc IRelayHub + function getRelayRegistrar() external view override returns (address) { + return relayRegistrar; + } + + /// @inheritdoc IRelayHub + function getMinimumStakePerToken(IERC20 token) external view override returns (uint256) { + return minimumStakePerToken[token]; + } + + /// @inheritdoc IRelayHub + function getWorkerManager(address worker) external view override returns (address) { + return workerToManager[worker]; + } + + /// @inheritdoc IRelayHub + function getWorkerCount(address manager) external view override returns (uint256) { + return workerCount[manager]; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return + interfaceId == type(IRelayHub).interfaceId || + interfaceId == type(Ownable).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @inheritdoc IRelayHub + function onRelayServerRegistered(address relayManager) external override { + require(msg.sender == relayRegistrar, "caller is not relay registrar"); + verifyRelayManagerStaked(relayManager); + require(workerCount[relayManager] > 0, "no relay workers"); + stakeManager.updateRelayKeepaliveTime(relayManager); + } + + /// @inheritdoc IRelayHub + function addRelayWorkers(address[] calldata newRelayWorkers) external override { + address relayManager = msg.sender; + uint256 newWorkerCount = workerCount[relayManager] + newRelayWorkers.length; + workerCount[relayManager] = newWorkerCount; + require(newWorkerCount <= config.maxWorkerCount, "too many workers"); + + verifyRelayManagerStaked(relayManager); + + for (uint256 i = 0; i < newRelayWorkers.length; i++) { + require(workerToManager[newRelayWorkers[i]] == address(0), "this worker has a manager"); + workerToManager[newRelayWorkers[i]] = relayManager; + } + + emit RelayWorkersAdded(relayManager, newRelayWorkers, newWorkerCount); + } + + /// @inheritdoc IRelayHub + function depositFor(address target) public payable virtual override { + require(target.supportsInterface(type(IPaymaster).interfaceId), "target is not a valid IPaymaster"); + uint256 amount = msg.value; + + balances[target] = balances[target] + amount; + + emit Deposited(target, msg.sender, amount); + } + + /// @inheritdoc IRelayHub + function balanceOf(address target) external view override returns (uint256) { + return balances[target]; + } + + /// @inheritdoc IRelayHub + function withdraw(address payable dest, uint256 amount) public override { + uint256[] memory amounts = new uint256[](1); + address payable[] memory destinations = new address payable[](1); + amounts[0] = amount; + destinations[0] = dest; + withdrawMultiple(destinations, amounts); + } + + /// @inheritdoc IRelayHub + function withdrawMultiple(address payable[] memory dest, uint256[] memory amount) public override { + address payable account = payable(msg.sender); + for (uint256 i = 0; i < amount.length; i++) { + // #if ENABLE_CONSOLE_LOG + console.log("withdrawMultiple %s %s %s", balances[account], dest[i], amount[i]); + // #endif + uint256 balance = balances[account]; + require(balance >= amount[i], "insufficient funds"); + balances[account] = balance - amount[i]; + (bool success, ) = dest[i].call{ value: amount[i] }(""); + require(success, "Transfer failed."); + emit Withdrawn(account, dest[i], amount[i]); + } + } + + function verifyGasAndDataLimits( + uint256 maxAcceptanceBudget, + GsnTypes.RelayRequest calldata relayRequest, + uint256 initialGasLeft + ) private view returns (IPaymaster.GasAndDataLimits memory gasAndDataLimits, uint256 maxPossibleGas) { + gasAndDataLimits = IPaymaster(relayRequest.relayData.paymaster).getGasAndDataLimits{ gas: 50000 }(); + require(msg.data.length <= gasAndDataLimits.calldataSizeLimit, "msg.data exceeded limit"); + + require(maxAcceptanceBudget >= gasAndDataLimits.acceptanceBudget, "acceptance budget too high"); + require( + gasAndDataLimits.acceptanceBudget >= gasAndDataLimits.preRelayedCallGasLimit, + "acceptance budget too low" + ); + + maxPossibleGas = relayRequest.relayData.transactionCalldataGasUsed + initialGasLeft; + + uint256 maxPossibleCharge = calculateCharge(maxPossibleGas, relayRequest.relayData); + + // We don't yet know how much gas will be used by the recipient, so we make sure there are enough funds to pay + // for the maximum possible charge. + require(maxPossibleCharge <= balances[relayRequest.relayData.paymaster], "Paymaster balance too low"); + } + + struct RelayCallData { + bool success; + bytes4 functionSelector; + uint256 initialGasLeft; + bytes recipientContext; + bytes relayedCallReturnValue; + IPaymaster.GasAndDataLimits gasAndDataLimits; + RelayCallStatus status; + uint256 innerGasUsed; + uint256 maxPossibleGas; + uint256 innerGasLimit; + uint256 gasBeforeInner; + uint256 gasUsed; + uint256 devCharge; + bytes retData; + address relayManager; + bytes32 relayRequestId; + uint256 tmpInitialGas; + bytes relayCallStatus; + } + + /// @inheritdoc IRelayHub + function relayCall( + string calldata domainSeparatorName, + uint256 maxAcceptanceBudget, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData + ) + external + override + returns ( + bool paymasterAccepted, + uint256 charge, + IRelayHub.RelayCallStatus status, + bytes memory returnValue + ) + { + RelayCallData memory vars; + vars.initialGasLeft = aggregateGasleft(); + vars.relayRequestId = GsnUtils.getRelayRequestID(relayRequest, signature); + + // #if ENABLE_CONSOLE_LOG + console.log("relayCall relayRequestId"); + console.logBytes32(vars.relayRequestId); + console.log("relayCall relayRequest.request.from", relayRequest.request.from); + console.log("relayCall relayRequest.request.to", relayRequest.request.to); + console.log("relayCall relayRequest.request.value", relayRequest.request.value); + console.log("relayCall relayRequest.request.gas", relayRequest.request.gas); + console.log("relayCall relayRequest.request.nonce", relayRequest.request.nonce); + console.log("relayCall relayRequest.request.validUntilTime", relayRequest.request.validUntilTime); + + console.log("relayCall relayRequest.relayData.maxFeePerGas", relayRequest.relayData.maxFeePerGas); + console.log( + "relayCall relayRequest.relayData.maxPriorityFeePerGas", + relayRequest.relayData.maxPriorityFeePerGas + ); + console.log( + "relayCall relayRequest.relayData.transactionCalldataGasUsed", + relayRequest.relayData.transactionCalldataGasUsed + ); + console.log("relayCall relayRequest.relayData.relayWorker", relayRequest.relayData.relayWorker); + console.log("relayCall relayRequest.relayData.paymaster", relayRequest.relayData.paymaster); + console.log("relayCall relayRequest.relayData.forwarder", relayRequest.relayData.forwarder); + console.log("relayCall relayRequest.relayData.clientId", relayRequest.relayData.clientId); + + console.log("relayCall domainSeparatorName"); + console.logString(domainSeparatorName); + console.log("relayCall signature"); + console.logBytes(signature); + console.log("relayCall approvalData"); + console.logBytes(approvalData); + console.log("relayCall relayRequest.request.data"); + console.logBytes(relayRequest.request.data); + console.log("relayCall relayRequest.relayData.paymasterData"); + console.logBytes(relayRequest.relayData.paymasterData); + console.log("relayCall maxAcceptanceBudget", maxAcceptanceBudget); + // #endif + + require(!isDeprecated(), "hub deprecated"); + vars.functionSelector = relayRequest.request.data.length >= 4 + ? MinLibBytes.readBytes4(relayRequest.request.data, 0) + : bytes4(0); + + if (msg.sender != batchGateway && tx.origin != DRY_RUN_ADDRESS) { + require(signature.length != 0, "missing signature or bad gateway"); + require(msg.sender == tx.origin, "relay worker must be EOA"); + require(msg.sender == relayRequest.relayData.relayWorker, "Not a right worker"); + } + + if (tx.origin != DRY_RUN_ADDRESS) { + vars.relayManager = workerToManager[relayRequest.relayData.relayWorker]; + require(vars.relayManager != address(0), "Unknown relay worker"); + verifyRelayManagerStaked(vars.relayManager); + } + + (vars.gasAndDataLimits, vars.maxPossibleGas) = verifyGasAndDataLimits( + maxAcceptanceBudget, + relayRequest, + vars.initialGasLeft + ); + + RelayHubValidator.verifyTransactionPacking(domainSeparatorName, relayRequest, signature, approvalData); + + { + //How much gas to pass down to innerRelayCall. must be lower than the default 63/64 + // actually, min(gasleft*63/64, gasleft-GAS_RESERVE) might be enough. + vars.innerGasLimit = (gasleft() * 63) / 64 - config.gasReserve; + vars.gasBeforeInner = aggregateGasleft(); + + /* + Preparing to calculate "gasUseWithoutPost": + MPG = calldataGasUsage + vars.initialGasLeft :: max possible gas, an approximate gas limit for the current transaction + GU1 = MPG - gasleft(called right before innerRelayCall) :: gas actually used by current transaction until that point + GU2 = innerGasLimit - gasleft(called inside the innerRelayCall just before preRelayedCall) :: gas actually used by innerRelayCall before calling postRelayCall + GWP1 = GU1 + GU2 :: gas actually used by the entire transaction before calling postRelayCall + TGO = config.gasOverhead + config.postOverhead :: extra that will be added to the charge to cover hidden costs + GWP = GWP1 + TGO :: transaction "gas used without postRelayCall" + */ + vars.tmpInitialGas = + relayRequest.relayData.transactionCalldataGasUsed + + vars.initialGasLeft + + vars.innerGasLimit + + config.gasOverhead + + config.postOverhead; + // Calls to the recipient are performed atomically inside an inner transaction which may revert in case of + // errors in the recipient. In either case (revert or regular execution) the return data encodes the + // RelayCallStatus value. + (vars.success, vars.relayCallStatus) = address(this).call{ gas: vars.innerGasLimit }( + abi.encodeWithSelector( + RelayHub.innerRelayCall.selector, + domainSeparatorName, + relayRequest, + signature, + approvalData, + vars.gasAndDataLimits, + vars.tmpInitialGas - aggregateGasleft(), /* totalInitialGas */ + vars.maxPossibleGas + ) + ); + vars.innerGasUsed = vars.gasBeforeInner - aggregateGasleft(); + (vars.status, vars.relayedCallReturnValue) = abi.decode(vars.relayCallStatus, (RelayCallStatus, bytes)); + if (vars.relayedCallReturnValue.length > 0) { + emit TransactionResult(vars.status, vars.relayedCallReturnValue); + } + } + { + if (!vars.success) { + //Failure cases where the PM doesn't pay + if ( + vars.status == RelayCallStatus.RejectedByPreRelayed || //can only be thrown if rejectOnRecipientRevert==true + ((vars.innerGasUsed <= + vars.gasAndDataLimits.acceptanceBudget + relayRequest.relayData.transactionCalldataGasUsed) && + (vars.status == RelayCallStatus.RejectedByForwarder || + vars.status == RelayCallStatus.RejectedByRecipientRevert)) + ) { + emit TransactionRejectedByPaymaster( + vars.relayManager, + relayRequest.relayData.paymaster, + vars.relayRequestId, + relayRequest.request.from, + relayRequest.request.to, + msg.sender, + vars.functionSelector, + vars.innerGasUsed, + vars.relayedCallReturnValue + ); + return (false, 0, vars.status, vars.relayedCallReturnValue); + } + } + + // We now perform the actual charge calculation, based on the measured gas used + vars.gasUsed = + relayRequest.relayData.transactionCalldataGasUsed + + (vars.initialGasLeft - aggregateGasleft()) + + config.gasOverhead; + charge = calculateCharge(vars.gasUsed, relayRequest.relayData); + vars.devCharge = calculateDevCharge(charge); + + balances[relayRequest.relayData.paymaster] = balances[relayRequest.relayData.paymaster] - charge; + balances[vars.relayManager] = balances[vars.relayManager] + (charge - vars.devCharge); + if (vars.devCharge > 0) { + // save some gas in case of zero dev charge + balances[config.devAddress] = balances[config.devAddress] + vars.devCharge; + } + + { + address from = relayRequest.request.from; + address to = relayRequest.request.to; + address paymaster = relayRequest.relayData.paymaster; + emit TransactionRelayed( + vars.relayManager, + msg.sender, + vars.relayRequestId, + from, + to, + paymaster, + vars.functionSelector, + vars.status, + charge + ); + } + + // avoid variable size memory copying after gas calculation completed on-chain + if (tx.origin == DRY_RUN_ADDRESS) { + return (true, charge, vars.status, vars.relayedCallReturnValue); + } + return (true, charge, vars.status, ""); + } + } + + struct InnerRelayCallData { + uint256 initialGasLeft; + uint256 gasUsedToCallInner; + uint256 balanceBefore; + bytes32 preReturnValue; + bool relayedCallSuccess; + bytes relayedCallReturnValue; + bytes recipientContext; + bytes data; + bool rejectOnRecipientRevert; + } + + /** + * @notice This method can only by called by this `RelayHub`. + * It wraps the execution of the `RelayRequest` in a revertable frame context. + */ + function innerRelayCall( + string calldata domainSeparatorName, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData, + IPaymaster.GasAndDataLimits calldata gasAndDataLimits, + uint256 totalInitialGas, + uint256 maxPossibleGas + ) external returns (RelayCallStatus, bytes memory) { + InnerRelayCallData memory vars; + vars.initialGasLeft = aggregateGasleft(); + vars.gasUsedToCallInner = totalInitialGas - gasleft(); + // A new gas measurement is performed inside innerRelayCall, since + // due to EIP150 available gas amounts cannot be directly compared across external calls + + // This external function can only be called by RelayHub itself, creating an internal transaction. Calls to the + // recipient (preRelayedCall, the relayedCall, and postRelayedCall) are called from inside this transaction. + require(msg.sender == address(this), "Must be called by RelayHub"); + + // If either pre or post reverts, the whole internal transaction will be reverted, reverting all side effects on + // the recipient. The recipient will still be charged for the used gas by the relay. + + // The paymaster is no allowed to withdraw balance from RelayHub during a relayed transaction. We check pre and + // post state to ensure this doesn't happen. + vars.balanceBefore = balances[relayRequest.relayData.paymaster]; + + // First preRelayedCall is executed. + // Note: we open a new block to avoid growing the stack too much. + vars.data = abi.encodeWithSelector( + IPaymaster.preRelayedCall.selector, + relayRequest, + signature, + approvalData, + maxPossibleGas + ); + { + bool success; + bytes memory retData; + (success, retData) = relayRequest.relayData.paymaster.call{ gas: gasAndDataLimits.preRelayedCallGasLimit }( + vars.data + ); + if (!success) { + GsnEip712Library.truncateInPlace(retData); + revertWithStatus(RelayCallStatus.RejectedByPreRelayed, retData); + } + (vars.recipientContext, vars.rejectOnRecipientRevert) = abi.decode(retData, (bytes, bool)); + } + + // The actual relayed call is now executed. The sender's address is appended at the end of the transaction data + + { + bool forwarderSuccess; + (forwarderSuccess, vars.relayedCallSuccess, vars.relayedCallReturnValue) = GsnEip712Library.execute( + domainSeparatorName, + relayRequest, + signature + ); + if (!forwarderSuccess) { + revertWithStatus(RelayCallStatus.RejectedByForwarder, vars.relayedCallReturnValue); + } + + if (vars.rejectOnRecipientRevert && !vars.relayedCallSuccess) { + // we trusted the recipient, but it reverted... + revertWithStatus(RelayCallStatus.RejectedByRecipientRevert, vars.relayedCallReturnValue); + } + } + // Finally, postRelayedCall is executed, with the relayedCall execution's status and a charge estimate + // We now determine how much the recipient will be charged, to pass this value to postRelayedCall for accurate + // accounting. + vars.data = abi.encodeWithSelector( + IPaymaster.postRelayedCall.selector, + vars.recipientContext, + vars.relayedCallSuccess, + vars.gasUsedToCallInner + (vars.initialGasLeft - aggregateGasleft()), /*gasUseWithoutPost*/ + relayRequest.relayData + ); + + { + (bool successPost, bytes memory ret) = + relayRequest.relayData.paymaster.call{ gas: gasAndDataLimits.postRelayedCallGasLimit }(vars.data); + + if (!successPost) { + revertWithStatus(RelayCallStatus.PostRelayedFailed, ret); + } + } + + if (balances[relayRequest.relayData.paymaster] < vars.balanceBefore) { + revertWithStatus(RelayCallStatus.PaymasterBalanceChanged, ""); + } + + return ( + vars.relayedCallSuccess ? RelayCallStatus.OK : RelayCallStatus.RelayedCallFailed, + vars.relayedCallReturnValue + ); + } + + /** + * @dev Reverts the transaction with return data set to the ABI encoding of the status argument (and revert reason data) + */ + function revertWithStatus(RelayCallStatus status, bytes memory ret) private pure { + bytes memory data = abi.encode(status, ret); + GsnEip712Library.truncateInPlace(data); + + assembly { + let dataSize := mload(data) + let dataPtr := add(data, 32) + + revert(dataPtr, dataSize) + } + } + + /// @inheritdoc IRelayHub + function calculateDevCharge(uint256 charge) public view virtual override returns (uint256) { + if (config.devFee == 0) { + // save some gas in case of zero dev charge + return 0; + } + unchecked { return (charge * config.devFee) / 100; } + } + + /// @inheritdoc IRelayHub + function calculateCharge(uint256 gasUsed, GsnTypes.RelayData calldata relayData) + public + view + virtual + override + returns (uint256) + { + uint256 basefee; + if (relayData.maxFeePerGas == relayData.maxPriorityFeePerGas) { + basefee = 0; + } else { + basefee = block.basefee; + } + uint256 chargeableGasPrice = + Math.min(relayData.maxFeePerGas, Math.min(tx.gasprice, basefee + relayData.maxPriorityFeePerGas)); + return config.baseRelayFee + (gasUsed * chargeableGasPrice * (config.pctRelayFee + 100)) / 100; + } + + /// @inheritdoc IRelayHub + function verifyRelayManagerStaked(address relayManager) public view override { + (IStakeManager.StakeInfo memory info, bool isHubAuthorized) = stakeManager.getStakeInfo(relayManager); + uint256 minimumStake = minimumStakePerToken[info.token]; + require(info.token != IERC20(address(0)), "relay manager not staked"); + require(info.stake >= minimumStake, "stake amount is too small"); + require(minimumStake != 0, "staking this token is forbidden"); + require(info.unstakeDelay >= config.minimumUnstakeDelay, "unstake delay is too small"); + require(info.withdrawTime == 0, "stake has been withdrawn"); + require(isHubAuthorized, "this hub is not authorized by SM"); + } + + /// @inheritdoc IRelayHub + function deprecateHub(uint256 _deprecationTime) public override onlyOwner { + require(!isDeprecated(), "Already deprecated"); + deprecationTime = _deprecationTime; + emit HubDeprecated(deprecationTime); + } + + /// @inheritdoc IRelayHub + function isDeprecated() public view override returns (bool) { + return block.timestamp >= deprecationTime; + } + + /// @notice Prevents any address other than the `Penalizer` from calling this method. + modifier penalizerOnly() { + require(msg.sender == penalizer, "Not penalizer"); + _; + } + + /// @inheritdoc IRelayHub + function penalize(address relayWorker, address payable beneficiary) external override penalizerOnly { + address relayManager = workerToManager[relayWorker]; + // The worker must be controlled by a manager with a locked stake + require(relayManager != address(0), "Unknown relay worker"); + (IStakeManager.StakeInfo memory stakeInfo, ) = stakeManager.getStakeInfo(relayManager); + require(stakeInfo.stake > 0, "relay manager not staked"); + stakeManager.penalizeRelayManager(relayManager, beneficiary, stakeInfo.stake); + } + + /// @inheritdoc IRelayHub + function isRelayEscheatable(address relayManager) public view override returns (bool) { + return stakeManager.isRelayEscheatable(relayManager); + } + + /// @inheritdoc IRelayHub + function escheatAbandonedRelayBalance(address relayManager) external override onlyOwner { + require(stakeManager.isRelayEscheatable(relayManager), "relay server not escheatable yet"); + uint256 balance = balances[relayManager]; + balances[relayManager] = 0; + balances[config.devAddress] = balances[config.devAddress] + balance; + emit AbandonedRelayManagerBalanceEscheated(relayManager, balance); + } + + /// @inheritdoc IRelayHub + function aggregateGasleft() public view virtual override returns (uint256) { + return gasleft(); + } +} diff --git a/contracts/protocol/meta-tx/GSN/interfaces/IGsnRelayHub.sol b/contracts/protocol/meta-tx/GSN/interfaces/IGsnRelayHub.sol new file mode 100644 index 00000000..9686f227 --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/interfaces/IGsnRelayHub.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier:MIT + +pragma solidity ^0.8.0; + +import "../libraries/GsnTypes.sol"; + +interface IGsnRelayHub { + function balanceOf(address target) external view returns (uint256); + + function calculateCharge(uint256 gasUsed, GsnTypes.RelayData calldata relayData) external view returns (uint256); + + function depositFor(address target) external payable; + + function relayCall( + uint256 maxAcceptanceBudget, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData, + uint256 externalGasLimit + ) external returns (bool paymasterAccepted, bytes memory returnValue); + + function withdraw(uint256 amount, address payable dest) external; +} diff --git a/contracts/protocol/meta-tx/GSN/interfaces/IPaymaster.sol b/contracts/protocol/meta-tx/GSN/interfaces/IPaymaster.sol new file mode 100644 index 00000000..9758365a --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/interfaces/IPaymaster.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/interfaces/IERC165.sol"; + +import "../libraries/GsnTypes.sol"; + +/** + * @title The Paymaster Interface + * @notice Contracts implementing this interface exist to make decision about paying the transaction fee to the relay. + * + * @notice There are two callbacks here that are executed by the RelayHub: `preRelayedCall` and `postRelayedCall`. + * + * @notice It is recommended that your implementation inherits from the abstract BasePaymaster contract. + */ +interface IPaymaster is IERC165 { + /** + * @notice The limits this Paymaster wants to be imposed by the RelayHub on user input. See `getGasAndDataLimits`. + */ + struct GasAndDataLimits { + uint256 acceptanceBudget; + uint256 preRelayedCallGasLimit; + uint256 postRelayedCallGasLimit; + uint256 calldataSizeLimit; + } + + /** + * @notice Return the Gas Limits for Paymaster's functions and maximum msg.data length values for this Paymaster. + * This function allows different paymasters to have different properties without changes to the RelayHub. + * @return limits An instance of the `GasAndDataLimits` struct + * + * ##### `acceptanceBudget` + * If the transactions consumes more than `acceptanceBudget` this Paymaster will be charged for gas no matter what. + * Transaction that gets rejected after consuming more than `acceptanceBudget` gas is on this Paymaster's expense. + * + * Should be set to an amount gas this Paymaster expects to spend deciding whether to accept or reject a request. + * This includes gas consumed by calculations in the `preRelayedCall`, `Forwarder` and the recipient contract. + * + * :warning: **Warning** :warning: As long this value is above `preRelayedCallGasLimit` + * (see defaults in `BasePaymaster`), the Paymaster is guaranteed it will never pay for rejected transactions. + * If this value is below `preRelayedCallGasLimit`, it might might make Paymaster open to a "griefing" attack. + * + * The relayers should prefer lower `acceptanceBudget`, as it improves their chances of being compensated. + * From a Relay's point of view, this is the highest gas value a bad Paymaster may cost the relay, + * since the paymaster will pay anything above that value regardless of whether the transaction succeeds or reverts. + * Specifying value too high might make the call rejected by relayers (see `maxAcceptanceBudget` in server config). + * + * ##### `preRelayedCallGasLimit` + * The max gas usage of preRelayedCall. Any revert of the `preRelayedCall` is a request rejection by the paymaster. + * As long as `acceptanceBudget` is above `preRelayedCallGasLimit`, any such revert is not payed by the paymaster. + * + * ##### `postRelayedCallGasLimit` + * The max gas usage of postRelayedCall. The Paymaster is not charged for the maximum, only for actually used gas. + * Note that an OOG will revert the inner transaction, but the paymaster will be charged for it anyway. + */ + function getGasAndDataLimits() external view returns (GasAndDataLimits memory limits); + + /** + * @notice :warning: **Warning** :warning: using incorrect Forwarder may cause the Paymaster to agreeing to pay for invalid transactions. + * @return trustedForwarder The address of the `Forwarder` that is trusted by this Paymaster to execute the requests. + */ + function getTrustedForwarder() external view returns (address trustedForwarder); + + /** + * @return relayHub The address of the `RelayHub` that is trusted by this Paymaster to execute the requests. + */ + function getRelayHub() external view returns (address relayHub); + + /** + * @notice Called by the Relay in view mode and later by the `RelayHub` on-chain to validate that + * the Paymaster agrees to pay for this call. + * + * The request is considered to be rejected by the Paymaster in one of the following conditions: + * - `preRelayedCall()` method reverts + * - the `Forwarder` reverts because of nonce or signature error + * - the `Paymaster` returned `rejectOnRecipientRevert: true` and the recipient contract reverted + * (and all that did not consume more than `acceptanceBudget` gas). + * + * In any of the above cases, all Paymaster calls and the recipient call are reverted. + * In any other case the Paymaster will pay for the gas cost of the transaction. + * Note that even if `postRelayedCall` is reverted the Paymaster will be charged. + * + + * @param relayRequest - the full relay request structure + * @param signature - user's EIP712-compatible signature of the `relayRequest`. + * Note that in most cases the paymaster shouldn't try use it at all. It is always checked + * by the forwarder immediately after preRelayedCall returns. + * @param approvalData - extra dapp-specific data (e.g. signature from trusted party) + * @param maxPossibleGas - based on values returned from `getGasAndDataLimits` + * the RelayHub will calculate the maximum possible amount of gas the user may be charged for. + * In order to convert this value to wei, the Paymaster has to call "relayHub.calculateCharge()" + * + * @return context + * A byte array to be passed to postRelayedCall. + * Can contain any data needed by this Paymaster in any form or be empty if no extra data is needed. + * @return rejectOnRecipientRevert + * The flag that allows a Paymaster to "delegate" the rejection to the recipient code. + * It also means the Paymaster trust the recipient to reject fast: both preRelayedCall, + * forwarder check and recipient checks must fit into the GasLimits.acceptanceBudget, + * otherwise the TX is paid by the Paymaster. + * `true` if the Paymaster wants to reject the TX if the recipient reverts. + * `false` if the Paymaster wants rejects by the recipient to be completed on chain and paid by the Paymaster. + */ + function preRelayedCall( + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData, + uint256 maxPossibleGas + ) external returns (bytes memory context, bool rejectOnRecipientRevert); + + /** + * @notice This method is called after the actual relayed function call. + * It may be used to record the transaction (e.g. charge the caller by some contract logic) for this call. + * + * Revert in this functions causes a revert of the client's relayed call (and preRelayedCall(), but the Paymaster + * is still committed to pay the relay for the entire transaction. + * + * @param context The call context, as returned by the preRelayedCall + * @param success `true` if the relayed call succeeded, false if it reverted + * @param gasUseWithoutPost The actual amount of gas used by the entire transaction, EXCEPT + * the gas used by the postRelayedCall itself. + * @param relayData The relay params of the request. can be used by relayHub.calculateCharge() + * + */ + function postRelayedCall( + bytes calldata context, + bool success, + uint256 gasUseWithoutPost, + GsnTypes.RelayData calldata relayData + ) external; + + /** + * @return version The SemVer string of this Paymaster's version. + */ + function versionPaymaster() external view returns (string memory); +} diff --git a/contracts/protocol/meta-tx/GSN/interfaces/IPenalizer.sol b/contracts/protocol/meta-tx/GSN/interfaces/IPenalizer.sol new file mode 100644 index 00000000..204715e2 --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/interfaces/IPenalizer.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; + +import "./IRelayHub.sol"; + +/** + * @title The Penalizer Interface + * @notice In some cases the behavior of a Relay Server may be found to be illegal. + * It is the responsibility of a `Penalizer` contract to judge whether there was a penalizable event. + * + * @notice In case there was, the `Penalizer` will direct the `RelayHub` to slash the stake of the faulty Relay Server. + */ +interface IPenalizer is IERC165 { + /// @notice Emitted once the reporter submits the first step in the commit-reveal process. + event CommitAdded(address indexed sender, bytes32 indexed commitHash, uint256 readyBlockNumber); + + struct Transaction { + uint256 nonce; + uint256 gasLimit; + address to; + uint256 value; + bytes data; + } + + /** + * @notice Called by the reporter as the first step in the commit-reveal process. + * Any sender can call it to make sure no-one can front-run it to claim this penalization. + * @param commitHash The hash of the report of a penalizable behaviour the reporter wants to reveal. + * Calculated as `commit(keccak(encodedPenalizeFunction))`. + */ + function commit(bytes32 commitHash) external; + + /** + * @notice Called by the reporter as the second step in the commit-reveal process. + * If a Relay Worker attacked the system by signing multiple transactions with same nonce so only one is accepted, + * anyone can grab both transactions from the blockchain and submit them here. + * Check whether `unsignedTx1` != `unsignedTx2`, that both are signed by the same address, + * and that `unsignedTx1.nonce` == `unsignedTx2.nonce`. + * If all conditions are met, relay is considered an "offending relay". + * The offending relay will be unregistered immediately, its stake will be forfeited and given + * to the address who reported it (the `msg.sender`), thus incentivizing anyone to report offending relays. + */ + function penalizeRepeatedNonce( + bytes calldata unsignedTx1, + bytes calldata signature1, + bytes calldata unsignedTx2, + bytes calldata signature2, + IRelayHub hub, + uint256 randomValue + ) external; + + /** + * @notice Called by the reporter as the second step in the commit-reveal process. + * The Relay Workers are not allowed to make calls other than to the `relayCall` method. + */ + function penalizeIllegalTransaction( + bytes calldata unsignedTx, + bytes calldata signature, + IRelayHub hub, + uint256 randomValue + ) external; + + /// @return a SemVer-compliant version of the `Penalizer` contract. + function versionPenalizer() external view returns (string memory); + + /// @return The minimum delay between commit and reveal steps. + function getPenalizeBlockDelay() external view returns (uint256); + + /// @return The maximum delay between commit and reveal steps. + function getPenalizeBlockExpiration() external view returns (uint256); +} diff --git a/contracts/protocol/meta-tx/GSN/interfaces/IRelayHub.sol b/contracts/protocol/meta-tx/GSN/interfaces/IRelayHub.sol new file mode 100644 index 00000000..5f72bf58 --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/interfaces/IRelayHub.sol @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/interfaces/IERC165.sol"; + +import "../libraries/GsnTypes.sol"; +import "./IStakeManager.sol"; + +/** + * @title The RelayHub interface + * @notice The implementation of this interface provides all the information the GSN client needs to + * create a valid `RelayRequest` and also serves as an entry point for such requests. + * + * @notice The RelayHub also handles all the related financial records and hold the balances of participants. + * The Paymasters keep their Ether deposited in the `RelayHub` in order to pay for the `RelayRequest`s that thay choose + * to pay for, and Relay Servers keep their earned Ether in the `RelayHub` until they choose to `withdraw()` + * + * @notice The RelayHub on each supported network only needs a single instance and there is usually no need for dApp + * developers or Relay Server operators to redeploy, reimplement, modify or override the `RelayHub`. + */ +interface IRelayHub is IERC165 { + /** + * @notice A struct that contains all the parameters of the `RelayHub` that can be modified after the deployment. + */ + struct RelayHubConfig { + // maximum number of worker accounts allowed per manager + uint256 maxWorkerCount; + // Gas set aside for all relayCall() instructions to prevent unexpected out-of-gas exceptions + uint256 gasReserve; + // Gas overhead to calculate gasUseWithoutPost + uint256 postOverhead; + // Gas cost of all relayCall() instructions after actual 'calculateCharge()' + // Assume that relay has non-zero balance (costs 15'000 more otherwise). + uint256 gasOverhead; + // Minimum unstake delay seconds of a relay manager's stake on the StakeManager + uint256 minimumUnstakeDelay; + // Developers address + address devAddress; + // 0 < fee < 100, as percentage of total charge from paymaster to relayer + uint8 devFee; + // baseRelayFee The base fee the Relay Server charges for a single transaction in Ether, in wei. + uint80 baseRelayFee; + // pctRelayFee The percent of the total charge to add as a Relay Server fee to the total charge. + uint16 pctRelayFee; + } + + /// @notice Emitted when a configuration of the `RelayHub` is changed + event RelayHubConfigured(RelayHubConfig config); + + /// @notice Emitted when relays are added by a relayManager + event RelayWorkersAdded(address indexed relayManager, address[] newRelayWorkers, uint256 workersCount); + + /// @notice Emitted when an account withdraws funds from the `RelayHub`. + event Withdrawn(address indexed account, address indexed dest, uint256 amount); + + /// @notice Emitted when `depositFor` is called, including the amount and account that was funded. + event Deposited(address indexed paymaster, address indexed from, uint256 amount); + + /// @notice Emitted for each token configured for staking in setMinimumStakes + event StakingTokenDataChanged(address token, uint256 minimumStake); + + /** + * @notice Emitted when an attempt to relay a call fails and the `Paymaster` does not accept the transaction. + * The actual relayed call was not executed, and the recipient not charged. + * @param reason contains a revert reason returned from preRelayedCall or forwarder. + */ + event TransactionRejectedByPaymaster( + address indexed relayManager, + address indexed paymaster, + bytes32 indexed relayRequestID, + address from, + address to, + address relayWorker, + bytes4 selector, + uint256 innerGasUsed, + bytes reason + ); + + /** + * @notice Emitted when a transaction is relayed. Note that the actual internal function call might be reverted. + * The reason for a revert will be indicated in the `status` field of a corresponding `RelayCallStatus` value. + * @notice `charge` is the Ether value deducted from the `Paymaster` balance. + * The amount added to the `relayManager` balance will be lower if there is an activated `devFee` in the `config`. + */ + event TransactionRelayed( + address indexed relayManager, + address indexed relayWorker, + bytes32 indexed relayRequestID, + address from, + address to, + address paymaster, + bytes4 selector, + RelayCallStatus status, + uint256 charge + ); + + /// @notice This event is emitted in case the internal function returns a value or reverts with a revert string. + event TransactionResult(RelayCallStatus status, bytes returnValue); + + /// @notice This event is emitted in case this `RelayHub` is deprecated and will stop serving transactions soon. + event HubDeprecated(uint256 deprecationTime); + + /** + * @notice This event is emitted in case a `relayManager` has been deemed "abandoned" for being + * unresponsive for a prolonged period of time. + * @notice This event means the entire balance of the relay has been transferred to the `devAddress`. + */ + event AbandonedRelayManagerBalanceEscheated(address indexed relayManager, uint256 balance); + + /** + * Error codes that describe all possible failure reasons reported in the `TransactionRelayed` event `status` field. + * @param OK The transaction was successfully relayed and execution successful - never included in the event. + * @param RelayedCallFailed The transaction was relayed, but the relayed call failed. + * @param RejectedByPreRelayed The transaction was not relayed due to preRelatedCall reverting. + * @param RejectedByForwarder The transaction was not relayed due to forwarder check (signature,nonce). + * @param PostRelayedFailed The transaction was relayed and reverted due to postRelatedCall reverting. + * @param PaymasterBalanceChanged The transaction was relayed and reverted due to the paymaster balance change. + */ + enum RelayCallStatus { + OK, + RelayedCallFailed, + RejectedByPreRelayed, + RejectedByForwarder, + RejectedByRecipientRevert, + PostRelayedFailed, + PaymasterBalanceChanged + } + + /** + * @notice Add new worker addresses controlled by the sender who must be a staked Relay Manager address. + * Emits a `RelayWorkersAdded` event. + * This function can be called multiple times, emitting new events. + */ + function addRelayWorkers(address[] calldata newRelayWorkers) external; + + /** + * @notice The `RelayRegistrar` callback to notify the `RelayHub` that this `relayManager` has updated registration. + */ + function onRelayServerRegistered(address relayManager) external; + + // Balance management + + /** + * @notice Deposits ether for a `Paymaster`, so that it can and pay for relayed transactions. + * :warning: **Warning** :warning: Unused balance can only be withdrawn by the holder itself, by calling `withdraw`. + * Emits a `Deposited` event. + */ + function depositFor(address target) external payable; + + /** + * @notice Withdraws from an account's balance, sending it back to the caller. + * Relay Managers call this to retrieve their revenue, and `Paymasters` can also use it to reduce their funding. + * Emits a `Withdrawn` event. + */ + function withdraw(address payable dest, uint256 amount) external; + + /** + * @notice Withdraws from an account's balance, sending funds to multiple provided addresses. + * Relay Managers call this to retrieve their revenue, and `Paymasters` can also use it to reduce their funding. + * Emits a `Withdrawn` event for each destination. + */ + function withdrawMultiple(address payable[] memory dest, uint256[] memory amount) external; + + // Relaying + + /** + * @notice Relays a transaction. For this to succeed, multiple conditions must be met: + * - `Paymaster`'s `preRelayCall` method must succeed and not revert. + * - the `msg.sender` must be a registered Relay Worker that the user signed to use. + * - the transaction's gas fees must be equal or larger than the ones that were signed by the sender. + * - the transaction must have enough gas to run all internal transactions if they use all gas available to them. + * - the `Paymaster` must have enough balance to pay the Relay Worker if all gas is spent. + * + * @notice If all conditions are met, the call will be relayed and the `Paymaster` charged. + * + * @param domainSeparatorName The name of the Domain Separator used to verify the EIP-712 signature + * @param maxAcceptanceBudget The maximum valid value for `paymaster.getGasLimits().acceptanceBudget` to return. + * @param relayRequest All details of the requested relayed call. + * @param signature The client's EIP-712 signature over the `relayRequest` struct. + * @param approvalData The dapp-specific data forwarded to the `Paymaster`'s `preRelayedCall` method. + * This value is **not** verified by the `RelayHub` in any way. + * As an example, it can be used to pass some kind of a third-party signature to the `Paymaster` for verification. + * + * Emits a `TransactionRelayed` event regardless of whether the transaction succeeded or failed. + */ + function relayCall( + string calldata domainSeparatorName, + uint256 maxAcceptanceBudget, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData + ) + external + returns ( + bool paymasterAccepted, + uint256 charge, + IRelayHub.RelayCallStatus status, + bytes memory returnValue + ); + + /** + * @notice In case the Relay Worker has been found to be in violation of some rules by the `Penalizer` contract, + * the `Penalizer` will call this method to execute a penalization. + * The `RelayHub` will look up the Relay Manager of the given Relay Worker and will forward the call to + * the `StakeManager` contract. The `RelayHub` does not perform the actual penalization either. + * @param relayWorker The address of the Relay Worker that committed a penalizable offense. + * @param beneficiary The address that called the `Penalizer` and will receive a reward for it. + */ + function penalize(address relayWorker, address payable beneficiary) external; + + /** + * @notice Sets or changes the configuration of this `RelayHub`. + * @param _config The new configuration. + */ + function setConfiguration(RelayHubConfig memory _config) external; + + /** + * @notice Sets or changes the minimum amount of a given `token` that needs to be staked so that the Relay Manager + * is considered to be 'staked' by this `RelayHub`. Zero value means this token is not allowed for staking. + * @param token An array of addresses of ERC-20 compatible tokens. + * @param minimumStake An array of minimal amounts necessary for a corresponding token, in wei. + */ + function setMinimumStakes(IERC20[] memory token, uint256[] memory minimumStake) external; + + /** + * @notice Deprecate hub by reverting all incoming `relayCall()` calls starting from a given timestamp + * @param _deprecationTime The timestamp in seconds after which the `RelayHub` stops serving transactions. + */ + function deprecateHub(uint256 _deprecationTime) external; + + /** + * @notice + * @param relayManager + */ + function escheatAbandonedRelayBalance(address relayManager) external; + + /** + * @notice The fee is expressed as a base fee in wei plus percentage of the actual charge. + * For example, a value '40' stands for a 40% fee, so the recipient will be charged for 1.4 times the spent amount. + * @param gasUsed An amount of gas used by the transaction. + * @param relayData The details of a transaction signed by the sender. + * @return The calculated charge, in wei. + */ + function calculateCharge(uint256 gasUsed, GsnTypes.RelayData calldata relayData) external view returns (uint256); + + /** + * @notice The fee is expressed as a percentage of the actual charge. + * For example, a value '40' stands for a 40% fee, so the Relay Manager will only get 60% of the `charge`. + * @param charge The amount of Ether in wei the Paymaster will be charged for this transaction. + * @return The calculated devFee, in wei. + */ + function calculateDevCharge(uint256 charge) external view returns (uint256); + + /* getters */ + + /// @return config The configuration of the `RelayHub`. + function getConfiguration() external view returns (RelayHubConfig memory config); + + /** + * @param token An address of an ERC-20 compatible tokens. + * @return The minimum amount of a given `token` that needs to be staked so that the Relay Manager + * is considered to be 'staked' by this `RelayHub`. Zero value means this token is not allowed for staking. + */ + function getMinimumStakePerToken(IERC20 token) external view returns (uint256); + + /** + * @param worker An address of the Relay Worker. + * @return The address of its Relay Manager. + */ + function getWorkerManager(address worker) external view returns (address); + + /** + * @param manager An address of the Relay Manager. + * @return The count of Relay Workers associated with this Relay Manager. + */ + function getWorkerCount(address manager) external view returns (uint256); + + /// @return An account's balance. It can be either a deposit of a `Paymaster`, or a revenue of a Relay Manager. + function balanceOf(address target) external view returns (uint256); + + /// @return The `StakeManager` address for this `RelayHub`. + function getStakeManager() external view returns (IStakeManager); + + /// @return The `Penalizer` address for this `RelayHub`. + function getPenalizer() external view returns (address); + + /// @return The `RelayRegistrar` address for this `RelayHub`. + function getRelayRegistrar() external view returns (address); + + /// @return The `BatchGateway` address for this `RelayHub`. + function getBatchGateway() external view returns (address); + + /** + * @notice Uses `StakeManager` to decide if the Relay Manager can be considered staked or not. + * Returns if the stake's token, amount and delay satisfy all requirements, reverts otherwise. + */ + function verifyRelayManagerStaked(address relayManager) external view; + + /** + * @notice Uses `StakeManager` to check if the Relay Manager can be considered abandoned or not. + * Returns true if the stake's abandonment time is in the past including the escheatment delay, false otherwise. + */ + function isRelayEscheatable(address relayManager) external view returns (bool); + + /// @return `true` if the `RelayHub` is deprecated, `false` it it is not deprecated and can serve transactions. + function isDeprecated() external view returns (bool); + + /// @return The timestamp from which the hub no longer allows relaying calls. + function getDeprecationTime() external view returns (uint256); + + /// @return The block number in which the contract has been deployed. + function getCreationBlock() external view returns (uint256); + + /// @return a SemVer-compliant version of the `RelayHub` contract. + function versionHub() external view returns (string memory); + + /// @return A total measurable amount of gas left to current execution. Same as 'gasleft()' for pure EVMs. + function aggregateGasleft() external view returns (uint256); +} diff --git a/contracts/protocol/meta-tx/GSN/interfaces/IRelayRegistrar.sol b/contracts/protocol/meta-tx/GSN/interfaces/IRelayRegistrar.sol new file mode 100644 index 00000000..b608ee6c --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/interfaces/IRelayRegistrar.sol @@ -0,0 +1,86 @@ +//SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.6; + +import "@openzeppelin/contracts-0.8.x/interfaces/IERC165.sol"; + +/** + * @title The RelayRegistrar Interface + * @notice The on-chain registrar for all registered Relay Managers. + * + * @notice The client can use an implementation of a `RelayRegistrar` to find relay registration info. + * + */ +interface IRelayRegistrar is IERC165 { + /** + * @notice A struct containing all the information necessary to client to interact with the Relay Server. + */ + struct RelayInfo { + //last registration block number + uint32 lastSeenBlockNumber; + //last registration block timestamp + uint40 lastSeenTimestamp; + //stake (first registration) block number + uint32 firstSeenBlockNumber; + //stake (first registration) block timestamp + uint40 firstSeenTimestamp; + bytes32[3] urlParts; + address relayManager; + } + + /** + * @notice Emitted when a relay server registers or updates its details. + * Looking up these events allows a client to discover registered Relay Servers. + */ + event RelayServerRegistered(address indexed relayManager, address indexed relayHub, bytes32[3] relayUrl); + + /** + * @notice This function is called by Relay Servers in order to register or to update their registration. + * @param relayHub The address of the `RelayHub` contract for which this action is performed. + * @param url The URL of the Relay Server that is listening to the clients' requests. + */ + function registerRelayServer(address relayHub, bytes32[3] calldata url) external; + + /** + * @return The block number in which the contract has been deployed. + */ + function getCreationBlock() external view returns (uint256); + + /** + * @return The maximum age the relay is considered registered by default by this `RelayRegistrar`, in seconds. + */ + function getRelayRegistrationMaxAge() external view returns (uint256); + + /** + * @notice Change the maximum relay registration age. + */ + function setRelayRegistrationMaxAge(uint256) external; + + /** + * @param relayManager An address of a Relay Manager. + * @param relayHub The address of the `RelayHub` contract for which this action is performed. + * @return info All the details of the given Relay Manager's registration. Throws if relay not found for `RelayHub`. + */ + function getRelayInfo(address relayHub, address relayManager) external view returns (RelayInfo memory info); + + /** + * @notice Read relay info of registered Relay Server from an on-chain storage. + * @param relayHub The address of the `RelayHub` contract for which this action is performed. + * @return info The list of `RelayInfo`s of registered Relay Servers + */ + function readRelayInfos(address relayHub) external view returns (RelayInfo[] memory info); + + /** + * @notice Read relay info of registered Relay Server from an on-chain storage. + * @param relayHub The address of the `RelayHub` contract for which this action is performed. + * @param maxCount The maximum amount of relays to be returned by this function. + * @param oldestBlockNumber The latest block number in which a Relay Server may be registered. + * @param oldestBlockTimestamp The latest block timestamp in which a Relay Server may be registered. + * @return info The list of `RelayInfo`s of registered Relay Servers + */ + function readRelayInfosInRange( + address relayHub, + uint256 oldestBlockNumber, + uint256 oldestBlockTimestamp, + uint256 maxCount + ) external view returns (RelayInfo[] memory info); +} diff --git a/contracts/protocol/meta-tx/GSN/interfaces/IStakeManager.sol b/contracts/protocol/meta-tx/GSN/interfaces/IStakeManager.sol new file mode 100644 index 00000000..f74791d3 --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/interfaces/IStakeManager.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts-0.8.x/utils/introspection/ERC165.sol"; + +/** + * @title The StakeManager Interface + * @notice In order to prevent an attacker from registering a large number of unresponsive relays, the GSN requires + * the Relay Server to maintain a permanently locked stake in the system before being able to register. + * + * @notice Also, in some cases the behavior of a Relay Server may be found to be illegal by a `Penalizer` contract. + * In such case, the stake will never be returned to the Relay Server operator and will be slashed. + * + * @notice An implementation of this interface is tasked with keeping Relay Servers' stakes, made in any ERC-20 token. + * Note that the `RelayHub` chooses which ERC-20 tokens to support and how much stake is needed. + */ +interface IStakeManager is IERC165 { + /// @notice Emitted when a `stake` or `unstakeDelay` are initialized or increased. + event StakeAdded( + address indexed relayManager, + address indexed owner, + IERC20 token, + uint256 stake, + uint256 unstakeDelay + ); + + /// @notice Emitted once a stake is scheduled for withdrawal. + event StakeUnlocked(address indexed relayManager, address indexed owner, uint256 withdrawTime); + + /// @notice Emitted when owner withdraws `relayManager` funds. + event StakeWithdrawn(address indexed relayManager, address indexed owner, IERC20 token, uint256 amount); + + /// @notice Emitted when an authorized `RelayHub` penalizes a `relayManager`. + event StakePenalized(address indexed relayManager, address indexed beneficiary, IERC20 token, uint256 reward); + + /// @notice Emitted when a `relayManager` adds a new `RelayHub` to a list of authorized. + event HubAuthorized(address indexed relayManager, address indexed relayHub); + + /// @notice Emitted when a `relayManager` removes a `RelayHub` from a list of authorized. + event HubUnauthorized(address indexed relayManager, address indexed relayHub, uint256 removalTime); + + /// @notice Emitted when a `relayManager` sets its `owner`. This is necessary to prevent stake hijacking. + event OwnerSet(address indexed relayManager, address indexed owner); + + /// @notice Emitted when a `burnAddress` is changed. + event BurnAddressSet(address indexed burnAddress); + + /// @notice Emitted when a `devAddress` is changed. + event DevAddressSet(address indexed devAddress); + + /// @notice Emitted if Relay Server is inactive for an `abandonmentDelay` and contract owner initiates its removal. + event RelayServerAbandoned(address indexed relayManager, uint256 abandonedTime); + + /// @notice Emitted to indicate an action performed by a relay server to prevent it from being marked as abandoned. + event RelayServerKeepalive(address indexed relayManager, uint256 keepaliveTime); + + /// @notice Emitted when the stake of an abandoned relayer has been confiscated and transferred to the `devAddress`. + event AbandonedRelayManagerStakeEscheated( + address indexed relayManager, + address indexed owner, + IERC20 token, + uint256 amount + ); + + /** + * @param stake - amount of ether staked for this relay + * @param unstakeDelay - number of seconds to elapse before the owner can retrieve the stake after calling 'unlock' + * @param withdrawTime - timestamp in seconds when 'withdraw' will be callable, or zero if the unlock has not been called + * @param owner - address that receives revenue and manages relayManager's stake + */ + struct StakeInfo { + uint256 stake; + uint256 unstakeDelay; + uint256 withdrawTime; + uint256 abandonedTime; + uint256 keepaliveTime; + IERC20 token; + address owner; + } + + struct RelayHubInfo { + uint256 removalTime; + } + + /** + * @param devAddress - the address that will receive the 'abandoned' stake + * @param abandonmentDelay - the amount of time after which the relay can be marked as 'abandoned' + * @param escheatmentDelay - the amount of time after which the abandoned relay's stake and balance may be withdrawn to the `devAddress` + */ + struct AbandonedRelayServerConfig { + address devAddress; + uint256 abandonmentDelay; + uint256 escheatmentDelay; + } + + /** + * @notice Set the owner of a Relay Manager. Called only by the RelayManager itself. + * Note that owners cannot transfer ownership - if the entry already exists, reverts. + * @param owner - owner of the relay (as configured off-chain) + */ + function setRelayManagerOwner(address owner) external; + + /** + * @notice Put a stake for a relayManager and set its unstake delay. + * Only the owner can call this function. If the entry does not exist, reverts. + * The owner must give allowance of the ERC-20 token to the StakeManager before calling this method. + * It is the RelayHub who has a configurable list of minimum stakes per token. StakeManager accepts all tokens. + * @param token The address of an ERC-20 token that is used by the relayManager as a stake + * @param relayManager The address that represents a stake entry and controls relay registrations on relay hubs + * @param unstakeDelay The number of seconds to elapse before an owner can retrieve the stake after calling `unlock` + * @param amount The amount of tokens to be taken from the relayOwner and locked in the StakeManager as a stake + */ + function stakeForRelayManager( + IERC20 token, + address relayManager, + uint256 unstakeDelay, + uint256 amount + ) external; + + /** + * @notice Schedule the unlocking of the stake. The `unstakeDelay` must pass before owner can call `withdrawStake`. + * @param relayManager The address of a Relay Manager whose stake is to be unlocked. + */ + function unlockStake(address relayManager) external; + + /** + * @notice Withdraw the unlocked stake. + * @param relayManager The address of a Relay Manager whose stake is to be withdrawn. + */ + function withdrawStake(address relayManager) external; + + /** + * @notice Add the `RelayHub` to a list of authorized by this Relay Manager. + * This allows the RelayHub to penalize this Relay Manager. The `RelayHub` cannot trust a Relay it cannot penalize. + * @param relayManager The address of a Relay Manager whose stake is to be authorized for the new `RelayHub`. + * @param relayHub The address of a `RelayHub` to be authorized. + */ + function authorizeHubByOwner(address relayManager, address relayHub) external; + + /** + * @notice Same as `authorizeHubByOwner` but can be called by the RelayManager itself. + */ + function authorizeHubByManager(address relayHub) external; + + /** + * @notice Remove the `RelayHub` from a list of authorized by this Relay Manager. + * @param relayManager The address of a Relay Manager. + * @param relayHub The address of a `RelayHub` to be unauthorized. + */ + function unauthorizeHubByOwner(address relayManager, address relayHub) external; + + /** + * @notice Same as `unauthorizeHubByOwner` but can be called by the RelayManager itself. + */ + function unauthorizeHubByManager(address relayHub) external; + + /** + * Slash the stake of the relay relayManager. In order to prevent stake kidnapping, burns part of stake on the way. + * @param relayManager The address of a Relay Manager to be penalized. + * @param beneficiary The address that receives part of the penalty amount. + * @param amount A total amount of penalty to be withdrawn from stake. + */ + function penalizeRelayManager( + address relayManager, + address beneficiary, + uint256 amount + ) external; + + /** + * @notice Allows the contract owner to set the given `relayManager` as abandoned after a configurable delay. + * Its entire stake and balance will be taken from a relay if it does not respond to being marked as abandoned. + */ + function markRelayAbandoned(address relayManager) external; + + /** + * @notice If more than `abandonmentDelay` has passed since the last Keepalive transaction, and relay manager + * has been marked as abandoned, and after that more that `escheatmentDelay` have passed, entire stake and + * balance will be taken from this relay. + */ + function escheatAbandonedRelayStake(address relayManager) external; + + /** + * @notice Sets a new `keepaliveTime` for the given `relayManager`, preventing it from being marked as abandoned. + * Can be called by an authorized `RelayHub` or by the `relayOwner` address. + */ + function updateRelayKeepaliveTime(address relayManager) external; + + /** + * @notice Check if the Relay Manager can be considered abandoned or not. + * Returns true if the stake's abandonment time is in the past including the escheatment delay, false otherwise. + */ + function isRelayEscheatable(address relayManager) external view returns (bool); + + /** + * @notice Get the stake details information for the given Relay Manager. + * @param relayManager The address of a Relay Manager. + * @return stakeInfo The `StakeInfo` structure. + * @return isSenderAuthorizedHub `true` if the `msg.sender` for this call was a `RelayHub` that is authorized now. + * `false` if the `msg.sender` for this call is not authorized. + */ + function getStakeInfo(address relayManager) + external + view + returns (StakeInfo memory stakeInfo, bool isSenderAuthorizedHub); + + /** + * @return The maximum unstake delay this `StakeManger` allows. This is to prevent locking money forever by mistake. + */ + function getMaxUnstakeDelay() external view returns (uint256); + + /** + * @notice Change the address that will receive the 'burned' part of the penalized stake. + * This is done to prevent malicious Relay Server from penalizing itself and breaking even. + */ + function setBurnAddress(address _burnAddress) external; + + /** + * @return The address that will receive the 'burned' part of the penalized stake. + */ + function getBurnAddress() external view returns (address); + + /** + * @notice Change the address that will receive the 'abandoned' stake. + * This is done to prevent Relay Servers that lost their keys from losing access to funds. + */ + function setDevAddress(address _burnAddress) external; + + /** + * @return The structure that contains all configuration values for the 'abandoned' stake. + */ + function getAbandonedRelayServerConfig() external view returns (AbandonedRelayServerConfig memory); + + /** + * @return the block number in which the contract has been deployed. + */ + function getCreationBlock() external view returns (uint256); + + /** + * @return a SemVer-compliant version of the `StakeManager` contract. + */ + function versionSM() external view returns (string memory); +} diff --git a/contracts/protocol/meta-tx/GSN/libraries/GsnEip712Library.sol b/contracts/protocol/meta-tx/GSN/libraries/GsnEip712Library.sol new file mode 100644 index 00000000..81addb39 --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/libraries/GsnEip712Library.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "./GsnTypes.sol"; +import "../../interfaces/IERC2771Recipient.sol"; +import "../../interfaces/IForwarder.sol"; + +import "./GsnUtils.sol"; + +/** + * @title The ERC-712 Library for GSN + * @notice Bridge Library to convert a GSN RelayRequest into a valid `ForwardRequest` for a `Forwarder`. + */ +library GsnEip712Library { + // maximum length of return value/revert reason for 'execute' method. Will truncate result if exceeded. + uint256 private constant MAX_RETURN_SIZE = 1024; + + //copied from Forwarder (can't reference string constants even from another library) + string public constant GENERIC_PARAMS = + "address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime"; + + bytes public constant RELAYDATA_TYPE = + "RelayData(uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,uint256 transactionCalldataGasUsed,address relayWorker,address paymaster,address forwarder,bytes paymasterData,uint256 clientId)"; + + string public constant RELAY_REQUEST_NAME = "RelayRequest"; + string public constant RELAY_REQUEST_SUFFIX = string(abi.encodePacked("RelayData relayData)", RELAYDATA_TYPE)); + + bytes public constant RELAY_REQUEST_TYPE = + abi.encodePacked(RELAY_REQUEST_NAME, "(", GENERIC_PARAMS, ",", RELAY_REQUEST_SUFFIX); + + bytes32 public constant RELAYDATA_TYPEHASH = keccak256(RELAYDATA_TYPE); + bytes32 public constant RELAY_REQUEST_TYPEHASH = keccak256(RELAY_REQUEST_TYPE); + + struct EIP712Domain { + string name; + string version; + uint256 chainId; + address verifyingContract; + } + + bytes32 public constant EIP712DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + function splitRequest(GsnTypes.RelayRequest calldata req) internal pure returns (bytes memory suffixData) { + suffixData = abi.encode(hashRelayData(req.relayData)); + } + + //verify that the recipient trusts the given forwarder + // MUST be called by paymaster + function verifyForwarderTrusted(GsnTypes.RelayRequest calldata relayRequest) internal view { + (bool success, bytes memory ret) = + relayRequest.request.to.staticcall( + abi.encodeWithSelector(IERC2771Recipient.isTrustedForwarder.selector, relayRequest.relayData.forwarder) + ); + } + + function verifySignature( + string memory domainSeparatorName, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) internal view { + bytes memory suffixData = splitRequest(relayRequest); + bytes32 _domainSeparator = domainSeparator(domainSeparatorName, relayRequest.relayData.forwarder); + IForwarder forwarder = IForwarder(payable(relayRequest.relayData.forwarder)); + forwarder.verify(relayRequest.request, _domainSeparator, RELAY_REQUEST_TYPEHASH, suffixData, signature); + } + + function verify( + string memory domainSeparatorName, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) internal view { + verifyForwarderTrusted(relayRequest); + verifySignature(domainSeparatorName, relayRequest, signature); + } + + function execute( + string memory domainSeparatorName, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) + internal + returns ( + bool forwarderSuccess, + bool callSuccess, + bytes memory ret + ) + { + bytes memory suffixData = splitRequest(relayRequest); + bytes32 _domainSeparator = domainSeparator(domainSeparatorName, relayRequest.relayData.forwarder); + /* solhint-disable-next-line avoid-low-level-calls */ + (forwarderSuccess, ret) = relayRequest.relayData.forwarder.call( + abi.encodeWithSelector( + IForwarder.execute.selector, + relayRequest.request, + _domainSeparator, + RELAY_REQUEST_TYPEHASH, + suffixData, + signature + ) + ); + if (forwarderSuccess) { + //decode return value of execute: + (callSuccess, ret) = abi.decode(ret, (bool, bytes)); + } + truncateInPlace(ret); + } + + //truncate the given parameter (in-place) if its length is above the given maximum length + // do nothing otherwise. + //NOTE: solidity warns unless the method is marked "pure", but it DOES modify its parameter. + function truncateInPlace(bytes memory data) internal pure { + MinLibBytes.truncateInPlace(data, MAX_RETURN_SIZE); + } + + function domainSeparator(string memory name, address forwarder) internal view returns (bytes32) { + return + hashDomain(EIP712Domain({ name: name, version: "3", chainId: getChainID(), verifyingContract: forwarder })); + } + + function getChainID() internal view returns (uint256 id) { + /* solhint-disable no-inline-assembly */ + assembly { + id := chainid() + } + } + + function hashDomain(EIP712Domain memory req) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + EIP712DOMAIN_TYPEHASH, + keccak256(bytes(req.name)), + keccak256(bytes(req.version)), + req.chainId, + req.verifyingContract + ) + ); + } + + function hashRelayData(GsnTypes.RelayData calldata req) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + RELAYDATA_TYPEHASH, + req.maxFeePerGas, + req.maxPriorityFeePerGas, + req.transactionCalldataGasUsed, + req.relayWorker, + req.paymaster, + req.forwarder, + keccak256(req.paymasterData), + req.clientId + ) + ); + } +} diff --git a/contracts/protocol/meta-tx/GSN/libraries/GsnTypes.sol b/contracts/protocol/meta-tx/GSN/libraries/GsnTypes.sol new file mode 100644 index 00000000..6f2b8bee --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/libraries/GsnTypes.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier:MIT + +pragma solidity ^0.8.0; + +import "../../interfaces/IForwarder.sol"; + +library GsnTypes { + struct RelayData { + uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; + uint256 transactionCalldataGasUsed; + address relayWorker; + address paymaster; + address forwarder; + bytes paymasterData; + uint256 clientId; + } + + struct RelayRequest { + IForwarder.ForwardRequest request; + RelayData relayData; + } +} diff --git a/contracts/protocol/meta-tx/GSN/libraries/GsnUtils.sol b/contracts/protocol/meta-tx/GSN/libraries/GsnUtils.sol new file mode 100644 index 00000000..6e67004a --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/libraries/GsnUtils.sol @@ -0,0 +1,57 @@ +/* solhint-disable no-inline-assembly */ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; + +import "./MinLibBytes.sol"; +import "./GsnTypes.sol"; + +/** + * @title The GSN Solidity Utils Library + * @notice Some library functions used throughout the GSN Solidity codebase. + */ +library GsnUtils { + bytes32 private constant RELAY_REQUEST_ID_MASK = 0x00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + + /** + * @notice Calculate an identifier for the meta-transaction in a format similar to a transaction hash. + * Note that uniqueness relies on signature and may not be enforced if meta-transactions are verified + * with a different algorithm, e.g. when batching. + * @param relayRequest The `RelayRequest` for which an ID is being calculated. + * @param signature The signature for the `RelayRequest`. It is not validated here and may even remain empty. + */ + function getRelayRequestID(GsnTypes.RelayRequest calldata relayRequest, bytes calldata signature) + internal + pure + returns (bytes32) + { + return + keccak256(abi.encode(relayRequest.request.from, relayRequest.request.nonce, signature)) & + RELAY_REQUEST_ID_MASK; + } + + /** + * @notice Extract the method identifier signature from the encoded function call. + */ + function getMethodSig(bytes memory msgData) internal pure returns (bytes4) { + return MinLibBytes.readBytes4(msgData, 0); + } + + /** + * @notice Extract a parameter from encoded-function block. + * see: https://solidity.readthedocs.io/en/develop/abi-spec.html#formal-specification-of-the-encoding + * The return value should be casted to the right type (`uintXXX`/`bytesXXX`/`address`/`bool`/`enum`). + * @param msgData Byte array containing a uint256 value. + * @param index Index in byte array of uint256 value. + * @return result uint256 value from byte array. + */ + function getParam(bytes memory msgData, uint256 index) internal pure returns (uint256 result) { + return MinLibBytes.readUint256(msgData, 4 + index * 32); + } + + /// @notice Re-throw revert with the same revert data. + function revertWithData(bytes memory data) internal pure { + assembly { + revert(add(data, 32), mload(data)) + } + } +} diff --git a/contracts/protocol/meta-tx/GSN/libraries/MinLibBytes.sol b/contracts/protocol/meta-tx/GSN/libraries/MinLibBytes.sol new file mode 100644 index 00000000..94360b16 --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/libraries/MinLibBytes.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +// minimal bytes manipulation required by GSN +// a minimal subset from 0x/LibBytes +/* solhint-disable no-inline-assembly */ +pragma solidity ^0.8.0; + +library MinLibBytes { + //truncate the given parameter (in-place) if its length is above the given maximum length + // do nothing otherwise. + //NOTE: solidity warns unless the method is marked "pure", but it DOES modify its parameter. + function truncateInPlace(bytes memory data, uint256 maxlen) internal pure { + if (data.length > maxlen) { + assembly { + mstore(data, maxlen) + } + } + } + + /// @dev Reads an address from a position in a byte array. + /// @param b Byte array containing an address. + /// @param index Index in byte array of address. + /// @return result address from byte array. + function readAddress(bytes memory b, uint256 index) internal pure returns (address result) { + require(b.length >= index + 20, "readAddress: data too short"); + + // Add offset to index: + // 1. Arrays are prefixed by 32-byte length parameter (add 32 to index) + // 2. Account for size difference between address length and 32-byte storage word (subtract 12 from index) + index += 20; + + // Read address from array memory + assembly { + // 1. Add index to address of bytes array + // 2. Load 32-byte word from memory + // 3. Apply 20-byte mask to obtain address + result := and(mload(add(b, index)), 0xffffffffffffffffffffffffffffffffffffffff) + } + return result; + } + + function readBytes32(bytes memory b, uint256 index) internal pure returns (bytes32 result) { + require(b.length >= index + 32, "readBytes32: data too short"); + + // Read the bytes32 from array memory + assembly { + result := mload(add(b, add(index, 32))) + } + return result; + } + + /// @dev Reads a uint256 value from a position in a byte array. + /// @param b Byte array containing a uint256 value. + /// @param index Index in byte array of uint256 value. + /// @return result uint256 value from byte array. + function readUint256(bytes memory b, uint256 index) internal pure returns (uint256 result) { + result = uint256(readBytes32(b, index)); + return result; + } + + function readBytes4(bytes memory b, uint256 index) internal pure returns (bytes4 result) { + require(b.length >= index + 4, "readBytes4: data too short"); + + // Read the bytes4 from array memory + assembly { + result := mload(add(b, add(index, 32))) + // Solidity does not require us to clean the trailing bytes. + // We do it anyway + result := and(result, 0xFFFFFFFF00000000000000000000000000000000000000000000000000000000) + } + return result; + } +} diff --git a/contracts/protocol/meta-tx/GSN/libraries/RLPReader.sol b/contracts/protocol/meta-tx/GSN/libraries/RLPReader.sol new file mode 100644 index 00000000..afeac46c --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/libraries/RLPReader.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier:APACHE-2.0 +/* + * Taken from https://github.com/hamdiallam/Solidity-RLP + */ +/* solhint-disable */ +pragma solidity ^0.8.0; + +library RLPReader { + uint8 constant STRING_SHORT_START = 0x80; + uint8 constant STRING_LONG_START = 0xb8; + uint8 constant LIST_SHORT_START = 0xc0; + uint8 constant LIST_LONG_START = 0xf8; + uint8 constant WORD_SIZE = 32; + + struct RLPItem { + uint256 len; + uint256 memPtr; + } + + using RLPReader for bytes; + using RLPReader for uint256; + using RLPReader for RLPReader.RLPItem; + + // helper function to decode rlp encoded legacy ethereum transaction + /* + * @param rawTransaction RLP encoded legacy ethereum transaction rlp([nonce, gasPrice, gasLimit, to, value, data])) + * @return tuple (nonce,gasLimit,to,value,data) + */ + + function decodeLegacyTransaction(bytes calldata rawTransaction) + internal + pure + returns ( + uint256, + uint256, + address, + uint256, + bytes memory + ) + { + RLPReader.RLPItem[] memory values = rawTransaction.toRlpItem().toList(); // must convert to an rlpItem first! + return (values[0].toUint(), values[2].toUint(), values[3].toAddress(), values[4].toUint(), values[5].toBytes()); + } + + /* + * @param rawTransaction format: 0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, access_list])) + * @return tuple (nonce,gasLimit,to,value,data) + */ + function decodeTransactionType1(bytes calldata rawTransaction) + internal + pure + returns ( + uint256, + uint256, + address, + uint256, + bytes memory + ) + { + bytes memory payload = rawTransaction[1:rawTransaction.length]; + RLPReader.RLPItem[] memory values = payload.toRlpItem().toList(); // must convert to an rlpItem first! + return (values[1].toUint(), values[3].toUint(), values[4].toAddress(), values[5].toUint(), values[6].toBytes()); + } + + /* + * @param rawTransaction format: 0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list])) + * @return tuple (nonce,gasLimit,to,value,data) + */ + function decodeTransactionType2(bytes calldata rawTransaction) + internal + pure + returns ( + uint256, + uint256, + address, + uint256, + bytes memory + ) + { + bytes memory payload = rawTransaction[1:rawTransaction.length]; + RLPReader.RLPItem[] memory values = payload.toRlpItem().toList(); // must convert to an rlpItem first! + return (values[1].toUint(), values[4].toUint(), values[5].toAddress(), values[6].toUint(), values[7].toBytes()); + } + + /* + * @param item RLP encoded bytes + */ + function toRlpItem(bytes memory item) internal pure returns (RLPItem memory) { + if (item.length == 0) return RLPItem(0, 0); + uint256 memPtr; + assembly { + memPtr := add(item, 0x20) + } + return RLPItem(item.length, memPtr); + } + + /* + * @param item RLP encoded list in bytes + */ + function toList(RLPItem memory item) internal pure returns (RLPItem[] memory result) { + require(isList(item), "isList failed"); + uint256 items = numItems(item); + result = new RLPItem[](items); + uint256 memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 dataLen; + for (uint256 i = 0; i < items; i++) { + dataLen = _itemLength(memPtr); + result[i] = RLPItem(dataLen, memPtr); + memPtr = memPtr + dataLen; + } + } + + /* + * Helpers + */ + // @return indicator whether encoded payload is a list. negate this function call for isData. + function isList(RLPItem memory item) internal pure returns (bool) { + uint8 byte0; + uint256 memPtr = item.memPtr; + assembly { + byte0 := byte(0, mload(memPtr)) + } + if (byte0 < LIST_SHORT_START) return false; + return true; + } + + // @return number of payload items inside an encoded list. + function numItems(RLPItem memory item) internal pure returns (uint256) { + uint256 count = 0; + uint256 currPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 endPtr = item.memPtr + item.len; + while (currPtr < endPtr) { + currPtr = currPtr + _itemLength(currPtr); + // skip over an item + count++; + } + return count; + } + + // @return entire rlp item byte length + function _itemLength(uint256 memPtr) internal pure returns (uint256 len) { + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + if (byte0 < STRING_SHORT_START) return 1; + else if (byte0 < STRING_LONG_START) return byte0 - STRING_SHORT_START + 1; + else if (byte0 < LIST_SHORT_START) { + assembly { + let byteLen := sub(byte0, 0xb7) // number of bytes the actual length is + memPtr := add(memPtr, 1) // skip over the first byte + /* 32 byte word size */ + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len + len := add(dataLen, add(byteLen, 1)) + } + } else if (byte0 < LIST_LONG_START) { + return byte0 - LIST_SHORT_START + 1; + } else { + assembly { + let byteLen := sub(byte0, 0xf7) + memPtr := add(memPtr, 1) + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length + len := add(dataLen, add(byteLen, 1)) + } + } + } + + // @return number of bytes until the data + function _payloadOffset(uint256 memPtr) internal pure returns (uint256) { + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + if (byte0 < STRING_SHORT_START) return 0; + else if (byte0 < STRING_LONG_START || (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START)) return 1; + else if (byte0 < LIST_SHORT_START) + // being explicit + return byte0 - (STRING_LONG_START - 1) + 1; + else return byte0 - (LIST_LONG_START - 1) + 1; + } + + /** RLPItem conversions into data types **/ + // @returns raw rlp encoding in bytes + function toRlpBytes(RLPItem memory item) internal pure returns (bytes memory) { + bytes memory result = new bytes(item.len); + uint256 ptr; + assembly { + ptr := add(0x20, result) + } + copy(item.memPtr, ptr, item.len); + return result; + } + + function toBoolean(RLPItem memory item) internal pure returns (bool) { + require(item.len == 1, "Invalid RLPItem. Booleans are encoded in 1 byte"); + uint256 result; + uint256 memPtr = item.memPtr; + assembly { + result := byte(0, mload(memPtr)) + } + return result == 0 ? false : true; + } + + function toAddress(RLPItem memory item) internal pure returns (address) { + // 1 byte for the length prefix according to RLP spec + require(item.len <= 21, "Invalid RLPItem. Addresses are encoded in 20 bytes or less"); + return address(uint160(toUint(item))); + } + + function toUint(RLPItem memory item) internal pure returns (uint256) { + uint256 offset = _payloadOffset(item.memPtr); + uint256 len = item.len - offset; + uint256 memPtr = item.memPtr + offset; + uint256 result; + assembly { + result := div(mload(memPtr), exp(256, sub(32, len))) // shift to the correct location + } + return result; + } + + function toBytes(RLPItem memory item) internal pure returns (bytes memory) { + uint256 offset = _payloadOffset(item.memPtr); + uint256 len = item.len - offset; + // data length + bytes memory result = new bytes(len); + uint256 destPtr; + assembly { + destPtr := add(0x20, result) + } + copy(item.memPtr + offset, destPtr, len); + return result; + } + + /* + * @param src Pointer to source + * @param dest Pointer to destination + * @param len Amount of memory to copy from the source + */ + function copy( + uint256 src, + uint256 dest, + uint256 len + ) internal pure { + if (len == 0) return; + + // copy as many word sizes as possible + for (; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + if (len > 0) { + // left over bytes. Mask is used to remove unwanted bytes from the word + uint256 mask = 256**(WORD_SIZE - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + } + } +} diff --git a/contracts/protocol/meta-tx/GSN/libraries/RelayHubValidator.sol b/contracts/protocol/meta-tx/GSN/libraries/RelayHubValidator.sol new file mode 100644 index 00000000..333ef44f --- /dev/null +++ b/contracts/protocol/meta-tx/GSN/libraries/RelayHubValidator.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "./GsnTypes.sol"; + +/** + * @title The RelayHub Validator Library + * @notice Validates the `msg.data` received by the `RelayHub` does not contain unnecessary bytes. + * Including these extra bytes would allow the Relay Server to inflate transaction costs and overcharge the client. + */ +library RelayHubValidator { + /// @notice Validate that encoded `relayCall` is properly packed without any extra bytes + function verifyTransactionPacking( + string calldata domainSeparatorName, + GsnTypes.RelayRequest calldata relayRequest, + bytes calldata signature, + bytes calldata approvalData + ) internal pure { + // abicoder v2: https://docs.soliditylang.org/en/latest/abi-spec.html + // each static param/member is 1 word + // struct (with dynamic members) has offset to struct which is 1 word + // dynamic member is 1 word offset to actual value, which is 1-word length and ceil(length/32) words for data + // relayCall has 5 method params, + // relayRequest: 2 members + // relayData 8 members + // ForwardRequest: 7 members + // total 21 32-byte words if all dynamic params are zero-length. + uint256 expectedMsgDataLen = + 4 + + 22 * + 32 + + dynamicParamSize(bytes(domainSeparatorName)) + + dynamicParamSize(signature) + + dynamicParamSize(approvalData) + + dynamicParamSize(relayRequest.request.data) + + dynamicParamSize(relayRequest.relayData.paymasterData); + // zero-length signature is allowed in a batch relay transaction + require(signature.length <= 65, "invalid signature length"); + require(expectedMsgDataLen == msg.data.length, "extra msg.data bytes"); + } + + // helper method for verifyTransactionPacking: + // size (in bytes) of the given "bytes" parameter. size include the length (32-byte word), + // and actual data size, rounded up to full 32-byte words + function dynamicParamSize(bytes calldata buf) internal pure returns (uint256) { + return 32 + ((buf.length + 31) & (type(uint256).max - 31)); + } +} diff --git a/contracts/protocol/meta-tx/TokenPaymaster.sol b/contracts/protocol/meta-tx/TokenPaymaster.sol new file mode 100644 index 00000000..96abe53a --- /dev/null +++ b/contracts/protocol/meta-tx/TokenPaymaster.sol @@ -0,0 +1,319 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +//contracts +import { ERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/ERC20.sol"; +import { BasePaymaster } from "./GSN/BasePaymaster.sol"; + +//libraries +import { SafeMath } from "@openzeppelin/contracts-0.8.x/utils/math/SafeMath.sol"; +import { GsnTypes } from "./GSN/libraries/GsnTypes.sol"; + +//interfaces +import { IOptyFiOracle } from "../optyfi-oracle/contracts/interfaces/IOptyFiOracle.sol"; +import { IERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-0.8.x/token/ERC20/extensions/draft-IERC20Permit.sol"; +import { IERC20PermitLegacy } from "../../interfaces/opty/IERC20PermitLegacy-0.8.x.sol"; +import { IUniswapV2Router01 } from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router01.sol"; +import { ISwapRouter } from "../../interfaces/uniswap/ISwapRouter.sol"; + +/** + * @title A Token-based paymaster + * @author opty.fi + * @notice - each request is paid for by the caller. + * - preRelayedCall - pre-pay the maximum possible price for the tx + * - postRelayedCall - refund the caller for the unused gas + */ +contract TokenPaymaster is BasePaymaster { + using SafeMath for uint256; + + /** @dev ETH address */ + address private constant ETH = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + + /** WETH ERC20 token address */ + address public constant WETH = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + struct PaymasterData { + address token; + address dex; + bool isUniV3; + bytes permit; + bytes approve; + bytes pathUniv3; + address[] pathUniV2; + uint256 deadline; + } + + IOptyFiOracle optyFiOracle; + uint256 public gasUsedByPost; + + constructor(address _optyFiOracle) { + optyFiOracle = IOptyFiOracle(_optyFiOracle); + } + + function versionPaymaster() external view virtual override returns (string memory) { + return "2.2.3+opengsn.token.ipaymaster"; + } + + /** + * @notice set gas used by postRelayedCall, for proper gas calculation. + * You can use TokenGasCalculator to calculate these values (they depend on actual code of postRelayedCall, + * but also the gas usage of the token and the swap) + * @param _gasUsedByPost gas used by postRelayedCall + */ + function setPostGasUsage(uint256 _gasUsedByPost) external onlyOwner { + gasUsedByPost = _gasUsedByPost; + } + + /** + * @notice Sets the OptyFi Oracle contract + * @param _optyFiOracle OptyFi Oracle contract address + */ + function setOptyFiOracle(address _optyFiOracle) external onlyOwner { + optyFiOracle = IOptyFiOracle(_optyFiOracle); + } + + /** + * @notice return the payer of a given RelayRequest + * @param _relayRequest GSN RelayRequest + */ + function getPayer(GsnTypes.RelayRequest calldata _relayRequest) public view virtual returns (address) { + return _relayRequest.request.from; + } + + /** + * @dev get the user's input token + * @param _paymasterData paymaster data containing the swap data + */ + function _getToken(bytes memory _paymasterData) internal view returns (IERC20 token) { + PaymasterData memory paymasterData = abi.decode(_paymasterData, (PaymasterData)); + token = IERC20(paymasterData.token); + } + + /** + * @dev decode paymasterData + * @param _paymasterData swap data (token, callees, exchangeData, startIndexes, values, permit, deadline) + */ + function _getPaymasterData(bytes memory _paymasterData) internal view returns (PaymasterData memory paymasterData) { + paymasterData = abi.decode(_paymasterData, (PaymasterData)); + } + + /** + * @dev return the pre charge value + * @param _token token used to pay for gas + * @param _relayRequest forward request and relay data + * @param _maxPossibleGas max possible gas to spent + */ + function _calculatePreCharge( + address _token, + GsnTypes.RelayRequest calldata _relayRequest, + uint256 _maxPossibleGas + ) internal view returns (address payer, uint256 tokenPreCharge) { + payer = this.getPayer(_relayRequest); + uint256 ethMaxCharge = relayHub.calculateCharge(_maxPossibleGas, _relayRequest.relayData); + ethMaxCharge += _relayRequest.request.value; + tokenPreCharge = _getETHInToken(_token, ethMaxCharge); + } + + /** + * @dev calculates pre charge value and transfers to paymaster + * @param _relayRequest forward request and relay data + * @param _signature payer signature + * @param _approvalData token approvals + * @param _maxPossibleGas max possible gas to spent + */ + function _preRelayedCall( + GsnTypes.RelayRequest calldata _relayRequest, + bytes calldata _signature, + bytes calldata _approvalData, + uint256 _maxPossibleGas + ) internal virtual override returns (bytes memory context, bool revertOnRecipientRevert) { + (_signature); + IERC20 token = _getToken(_relayRequest.relayData.paymasterData); + _permit(address(token), _approvalData); + (address payer, uint256 tokenPrecharge) = _calculatePreCharge(address(token), _relayRequest, _maxPossibleGas); + bool success = token.transferFrom(payer, address(this), tokenPrecharge); + return (abi.encode(payer, tokenPrecharge, token), false); + } + + /** + * @dev calculates actual gas spent, refund relayer for gas the spent, and refund payer for unspent tokens + * @param _context context bytes + * @param _gasUseWithoutPost actual gas used without post relayed call cost + * @param _relayData relay data + */ + function _postRelayedCall( + bytes calldata _context, + bool, + uint256 _gasUseWithoutPost, + GsnTypes.RelayData calldata _relayData + ) internal virtual override { + (address payer, uint256 tokenPrecharge, IERC20 token) = abi.decode(_context, (address, uint256, IERC20)); + _postRelayedCallInternal(payer, tokenPrecharge, 0, _gasUseWithoutPost, _relayData, token); + } + + /** + * @dev calculates actual gas spent, refund relayer for gas the spent, and refund payer for unspent tokens + * @param _payer original payer address + * @param _tokenPrecharge max gas spent in token unit + * @param _valueRequested value requested + * @param _gasUseWithoutPost actual gas used without post relayed call cost + * @param _relayData relay data + * @param _token token used to pay for gas + */ + function _postRelayedCallInternal( + address _payer, + uint256 _tokenPrecharge, + uint256 _valueRequested, + uint256 _gasUseWithoutPost, + GsnTypes.RelayData calldata _relayData, + IERC20 _token + ) internal { + uint256 ethActualCharge = relayHub.calculateCharge(_gasUseWithoutPost.add(gasUsedByPost), _relayData); + uint256 tokenActualCharge = _getETHInToken(address(_token), _valueRequested.add(ethActualCharge)); + uint256 tokenRefund = _tokenPrecharge.sub(tokenActualCharge); + _refundPayer(_payer, _token, tokenRefund); + _depositProceedsToHub(_payer, ethActualCharge, _relayData.paymasterData); + emit TokensCharged(_gasUseWithoutPost, gasUsedByPost, ethActualCharge, tokenActualCharge); + } + + /** + * @dev refund payer for the unspent tokens + * @param _payer address + * @param _token token used to pay for gas + * @param _tokenRefund amount to refund + */ + function _refundPayer( + address _payer, + IERC20 _token, + uint256 _tokenRefund + ) private { + require(_token.transfer(_payer, _tokenRefund), "failed refund"); + } + + /** + * @dev refund relayer for gas the spent + * @param _ethActualCharge gas cost paid by the relayer + * @param _paymasterData swap data (token, callees, exchangeData, startIndexes, values, permit, deadline) + */ + function _depositProceedsToHub( + address _payer, + uint256 _ethActualCharge, + bytes calldata _paymasterData + ) private { + uint256 receivedAmount = _swapToETH(_payer, _ethActualCharge, _paymasterData); + relayHub.depositFor{ value: receivedAmount }(address(this)); + } + + /** + * @dev swap token for ETH + * @param _ethActualCharge gas cost paid by the relayer + * @param _paymasterData swap data (token, router, permit) + * @return receivedAmount amount received from the swap + */ + function _swapToETH( + address _payer, + uint256 _ethActualCharge, + bytes calldata _paymasterData + ) private returns (uint256 receivedAmount) { + PaymasterData memory pd = _getPaymasterData(_paymasterData); + _approve(pd.token, pd.approve); + _permit(pd.token, pd.permit); + uint256 balanceBefore = address(this).balance; + if (pd.isUniV3) { + ISwapRouter(pd.dex).exactInput( + ISwapRouter.ExactInputParams({ + path: pd.pathUniv3, + recipient: address(this), + deadline: pd.deadline, + amountIn: _getETHInToken(pd.token, _ethActualCharge).mul(101).div(100), + amountOutMinimum: 0 //_ethActualCharge + }) + ); + } else { + IUniswapV2Router01(pd.dex).swapExactTokensForETH( + _getETHInToken(pd.token, _ethActualCharge).mul(101).div(100), + 0, //_ethActualCharge, + pd.pathUniV2, + address(this), + pd.deadline + ); + } + receivedAmount = address(this).balance.sub(balanceBefore); + } + + /** + * @dev Get the expected amount to receive of _token1 after swapping _token0 + * @param _swapInAmount Amount of _token0 to be swapped for _token1 + * @param _token0 Contract address of one of the liquidity pool's underlying tokens + * @param _token1 Contract address of one of the liquidity pool's underlying tokens + * @return _swapOutAmount oracle price + */ + function _calculateSwapOutAmount( + uint256 _swapInAmount, + address _token0, + address _token1 + ) internal view returns (uint256 _swapOutAmount) { + uint256 price = optyFiOracle.getTokenPrice(_token0, _token1); + require(price > uint256(0), "!price"); + uint256 decimals0 = ERC20(_token0).decimals(); + uint256 decimals1 = ERC20(_token1).decimals(); + _swapOutAmount = ((_swapInAmount * price * 10**decimals1) / 10**(18 + decimals0)); + } + + /** + * @dev Get the _token equivalent amount of WETH + * @param _token token address + * @param _amount amount in WETH + * @return amount token equivalent amount + */ + function _getETHInToken(address _token, uint256 _amount) internal view returns (uint256 amount) { + _token == WETH ? amount = _amount : amount = _calculateSwapOutAmount(_amount, WETH, _token); + } + + /* solhint-disable avoid-low-level-calls*/ + /** + * @dev execute the permit according to the permit data + * @param _permitData data + */ + function _permit(address _token, bytes memory _permitData) internal { + if (_permitData.length == 32 * 7) { + (bool success, ) = _token.call(abi.encodePacked(IERC20Permit.permit.selector, _permitData)); + require(success, "PERMIT_FAILED"); + } + + if (_permitData.length == 32 * 8) { + (bool success, ) = _token.call(abi.encodePacked(IERC20PermitLegacy.permit.selector, _permitData)); + require(success, "PERMIT_LEGACY_FAILED"); + } + } + + /** + * @dev execute the approve according to the approve data + * @param _approveData data + */ + function _approve(address _token, bytes memory _approveData) internal { + if (_approveData.length < 32 * 7) { + (bool success, ) = _token.call(abi.encodePacked(IERC20.approve.selector, _approveData)); + require(success, "APPROVE_FAILED"); + } + } + + function _verifyPaymasterData(GsnTypes.RelayRequest calldata relayRequest) internal view override { + require(relayRequest.relayData.paymasterData.length == 544, "invalid paymasterData"); + } + + receive() external payable override { + emit Received(msg.value); + } + + event Received(uint256 eth); + + event TokensCharged( + uint256 gasUseWithoutPost, + uint256 gasJustPost, + uint256 ethActualCharge, + uint256 tokenActualCharge + ); +} diff --git a/contracts/protocol/meta-tx/VaultGateway.sol b/contracts/protocol/meta-tx/VaultGateway.sol new file mode 100644 index 00000000..3dbdd524 --- /dev/null +++ b/contracts/protocol/meta-tx/VaultGateway.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { ERC2771Context } from "@openzeppelin/contracts-0.8.x/metatx/ERC2771Context.sol"; +import { SafeERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/utils/SafeERC20.sol"; +import { IVault } from "./interfaces/IVault.sol"; +import { IERC20 } from "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-0.8.x/token/ERC20/extensions/draft-IERC20Permit.sol"; +import { IERC20PermitLegacy } from "./interfaces/IERC20PermitLegacy.sol"; + +/** + * @title Vault contract inspired by AAVE V3's AToken.sol + * @author opty.fi + * @notice Implementation of the risk specific interest bearing vault + */ +contract VaultGateway is ERC2771Context { + using SafeERC20 for IERC20; + + // solhint-disable-next-line no-empty-blocks + constructor(address forwarder) ERC2771Context(forwarder) {} + + /** + * @notice Deposit underlying tokens to the a given OptyFi vault + * @param _vault the address of the target vault, + * @param _userDepositUT Amount in underlying token + * @param _expectedOutput Minimum amount of vault tokens minted after fees + * @param _permitParams permit parameters: amount, deadline, v, s, r + * @param _accountsProof merkle proof for caller + */ + function deposit( + address _vault, + uint256 _userDepositUT, + uint256 _expectedOutput, + bytes calldata _permitParams, + bytes32[] calldata _accountsProof + ) public returns (uint256) { + address _underlyingToken = IVault(_vault).underlyingToken(); + + _permit(_underlyingToken, _permitParams); + IERC20(_underlyingToken).safeTransferFrom(_msgSender(), address(this), _userDepositUT); + IERC20(_underlyingToken).approve(_vault, _userDepositUT); + + return IVault(_vault).userDepositVault(_msgSender(), _userDepositUT, _expectedOutput, "0x", _accountsProof); + } + + /* solhint-disable avoid-low-level-calls*/ + /** + * @dev execute the permit according to the permit param + * @param _permitParams data + */ + function _permit(address _token, bytes calldata _permitParams) internal { + if (_permitParams.length == 32 * 7) { + (bool success, ) = _token.call(abi.encodePacked(IERC20Permit.permit.selector, _permitParams)); + require(success, "!permit"); + } + + if (_permitParams.length == 32 * 8) { + (bool success, ) = _token.call(abi.encodePacked(IERC20PermitLegacy.permit.selector, _permitParams)); + require(success, "!legacy_permit"); + } + } +} diff --git a/contracts/protocol/meta-tx/interfaces/IERC20PermitLegacy.sol b/contracts/protocol/meta-tx/interfaces/IERC20PermitLegacy.sol new file mode 100644 index 00000000..0bdbe7cc --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IERC20PermitLegacy.sol @@ -0,0 +1,15 @@ +//SPDX-license-identifier: MIT +pragma solidity ^0.8.15; + +interface IERC20PermitLegacy { + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/contracts/protocol/meta-tx/interfaces/IERC20Token.sol b/contracts/protocol/meta-tx/interfaces/IERC20Token.sol new file mode 100644 index 00000000..76b5913f --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IERC20Token.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC20/IERC20.sol) + +pragma solidity >=0.7.6; + +import "@openzeppelin/contracts-0.8.x/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts-0.8.x/token/ERC20/extensions/IERC20Metadata.sol"; + +/** + * @notice Extended ERC-20 token interface used internally in OpenGSN modules. + * Renamed to avoid conflict with OZ namespace. Includes IERC20, ERC20Metadata. + * added semi-standard "wrapped eth" access methods deposit() and "withdraw()" + */ +interface IERC20Token is IERC20, IERC20Metadata { + function deposit() external payable; + + function withdraw(uint256 amount) external; +} diff --git a/contracts/protocol/meta-tx/interfaces/IERC2771Recipient.sol b/contracts/protocol/meta-tx/interfaces/IERC2771Recipient.sol new file mode 100644 index 00000000..c037cc48 --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IERC2771Recipient.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.0; + +/** + * @title The ERC-2771 Recipient Base Abstract Class - Declarations + * + * @notice A contract must implement this interface in order to support relayed transaction. + * + * @notice It is recommended that your contract inherits from the ERC2771Recipient contract. + */ +abstract contract IERC2771Recipient { + /** + * :warning: **Warning** :warning: The Forwarder can have a full control over your Recipient. Only trust verified Forwarder. + * @param forwarder The address of the Forwarder contract that is being used. + * @return isTrustedForwarder `true` if the Forwarder is trusted to forward relayed transactions by this Recipient. + */ + function isTrustedForwarder(address forwarder) public view virtual returns (bool); + + /** + * @notice Use this method the contract anywhere instead of msg.sender to support relayed transactions. + * @return sender The real sender of this call. + * For a call that came through the Forwarder the real sender is extracted from the last 20 bytes of the `msg.data`. + * Otherwise simply returns `msg.sender`. + */ + function _msgSender() internal view virtual returns (address); + + /** + * @notice Use this method in the contract instead of `msg.data` when difference matters (hashing, signature, etc.) + * @return data The real `msg.data` of this call. + * For a call that came through the Forwarder, the real sender address was appended as the last 20 bytes + * of the `msg.data` - so this method will strip those 20 bytes off. + * Otherwise (if the call was made directly and not through the forwarder) simply returns `msg.data`. + */ + function _msgData() internal view virtual returns (bytes calldata); +} diff --git a/contracts/protocol/meta-tx/interfaces/IForwarder.sol b/contracts/protocol/meta-tx/interfaces/IForwarder.sol new file mode 100644 index 00000000..1cec711e --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IForwarder.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts-0.8.x/interfaces/IERC165.sol"; + +interface IForwarder is IERC165 { + /** + * @notice A representation of a request for a `Forwarder` to send `data` on behalf of a `from` to a target (`to`). + */ + struct ForwardRequest { + address from; + address to; + uint256 value; + uint256 gas; + uint256 nonce; + bytes data; + uint256 validUntilTime; + } + + event DomainRegistered(bytes32 indexed domainSeparator, bytes domainValue); + + event RequestTypeRegistered(bytes32 indexed typeHash, string typeStr); + + /** + * @param from The address of a sender. + * @return The nonce for this address. + */ + function getNonce(address from) external view returns (uint256); + + /** + * @notice Verify the transaction is valid and can be executed. + * Implementations must validate the signature and the nonce of the request are correct. + * Does not revert and returns successfully if the input is valid. + * Reverts if any validation has failed. For instance, if either signature or nonce are incorrect. + * Reverts if `domainSeparator` or `requestTypeHash` are not registered as well. + */ + function verify( + ForwardRequest calldata forwardRequest, + bytes32 domainSeparator, + bytes32 requestTypeHash, + bytes calldata suffixData, + bytes calldata signature + ) external view; + + /** + * @notice Executes a transaction specified by the `ForwardRequest`. + * The transaction is first verified and then executed. + * The success flag and returned bytes array of the `CALL` are returned as-is. + * + * This method would revert only in case of a verification error. + * + * All the target errors are reported using the returned success flag and returned bytes array. + * + * @param forwardRequest All requested transaction parameters. + * @param domainSeparator The domain used when signing this request. + * @param requestTypeHash The request type used when signing this request. + * @param suffixData The ABI-encoded extension data for the current `RequestType` used when signing this request. + * @param signature The client signature to be validated. + * + * @return success The success flag of the underlying `CALL` to the target address. + * @return ret The byte array returned by the underlying `CALL` to the target address. + */ + function execute( + ForwardRequest calldata forwardRequest, + bytes32 domainSeparator, + bytes32 requestTypeHash, + bytes calldata suffixData, + bytes calldata signature + ) external payable returns (bool success, bytes memory ret); + + /** + * @notice Register a new Request typehash. + * + * @notice This is necessary for the Forwarder to be able to verify the signatures conforming to the ERC-712. + * + * @param typeName The name of the request type. + * @param typeSuffix Any extra data after the generic params. Must contain add at least one param. + * The generic ForwardRequest type is always registered by the constructor. + */ + function registerRequestType(string calldata typeName, string calldata typeSuffix) external; + + /** + * @notice Register a new domain separator. + * + * @notice This is necessary for the Forwarder to be able to verify the signatures conforming to the ERC-712. + * + * @notice The domain separator must have the following fields: `name`, `version`, `chainId`, `verifyingContract`. + * The `chainId` is the current network's `chainId`, and the `verifyingContract` is this Forwarder's address. + * This method accepts the domain name and version to create and register the domain separator value. + * @param name The domain's display name. + * @param version The domain/protocol version. + */ + function registerDomainSeparator(string calldata name, string calldata version) external; +} diff --git a/contracts/protocol/meta-tx/interfaces/IVault.sol b/contracts/protocol/meta-tx/interfaces/IVault.sol new file mode 100644 index 00000000..22712285 --- /dev/null +++ b/contracts/protocol/meta-tx/interfaces/IVault.sol @@ -0,0 +1,42 @@ +//SPDX-license-identifier: MIT +pragma solidity ^0.8.15; + +interface IVault { + /** + * @notice Deposit underlying tokens to the vault + * @dev Mint the shares right away as per oracle based price per full share value + * @param _beneficiary the address of the deposit beneficiary, + * if _beneficiary = address(0) => _beneficiary = msg.sender + * @param _userDepositUT Amount in underlying token + * @param _expectedOutput expected output + * @param _permitParams permit parameters: amount, deadline, v, s, r + * @param _accountsProof merkle proof for caller + */ + function userDepositVault( + address _beneficiary, + uint256 _userDepositUT, + uint256 _expectedOutput, + bytes calldata _permitParams, + bytes32[] calldata _accountsProof + ) external returns (uint256); + + /** + * @notice redeems the vault shares and transfers underlying token to `_beneficiary` + * @dev Burn the shares right away as per oracle based price per full share value + * @param _receiver the address which will receive the underlying tokens + * @param _userWithdrawVT amount in vault token + * @param _expectedOutput expected output + * @param _accountsProof merkle proof for caller + */ + function userWithdrawVault( + address _receiver, + uint256 _userWithdrawVT, + uint256 _expectedOutput, + bytes32[] calldata _accountsProof + ) external returns (uint256); + + /** + * @dev the vault underlying token contract address + */ + function underlyingToken() external returns (address); +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 95edff58..e85a3151 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -186,7 +186,7 @@ const config: HardhatUserConfig = { initialBaseFeePerGas: 1_00_000_000, gasPrice: "auto", forking: buildForkConfig(FORK as eEVMNetwork, FORK_BLOCK_NUMBER), - allowUnlimitedContractSize: false, + allowUnlimitedContractSize: true, chainId: NETWORKS_CHAIN_ID[NETWORK_NAME as eEVMNetwork], accounts: { mnemonic, diff --git a/package.json b/package.json index 666ef7d9..c877264f 100644 --- a/package.json +++ b/package.json @@ -137,9 +137,15 @@ }, "dependencies": { "@chainlink/contracts": "^0.3.1", + "@opengsn/cli": "^3.0.0-beta.2", + "@opengsn/common": "^3.0.0-beta.2", + "@opengsn/contracts": "^3.0.0-beta.2", + "@opengsn/dev": "^3.0.0-beta.2", + "@opengsn/provider": "^3.0.0-beta.2", "@openzeppelin/contracts": "3.4.0", "@openzeppelin/contracts-0.8.x": "npm:@openzeppelin/contracts@4.4.1", "@optyfi/defi-legos": "v0.1.0-rc.38", + "@solidstate/contracts": "^0.0.35", "@uniswap/v2-core": "^1.0.1", "@uniswap/v2-periphery": "^1.1.0-beta.0", "@uniswap/v3-core": "^1.0.1", diff --git a/test/test-opty/token-paymaster.spec.ts b/test/test-opty/token-paymaster.spec.ts new file mode 100644 index 00000000..6b326383 --- /dev/null +++ b/test/test-opty/token-paymaster.spec.ts @@ -0,0 +1,307 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; +import chai, { expect } from "chai"; +import { solidity } from "ethereum-waffle"; +import { BigNumber, BytesLike } from "ethers"; +import { deployments, ethers, getChainId, network } from "hardhat"; +import { eEVMNetwork } from "../../helper-hardhat-config"; +import { getAccountsMerkleProof, getAccountsMerkleRoot, Signers, to_10powNumber_BN } from "../../helpers/utils"; +import { deployTestHub, getVaultDeploymentAndConfigure, signTypedData } from "./utils"; +import { + ERC20Permit, + ERC20Permit__factory, + VaultGateway, + Vault, + TokenPaymaster, + RelayHub, + StakeManager, + IForwarder, + TestHub, +} from "../../typechain"; +import { getPermitSignature, setTokenBalanceInStorage } from "./utils"; +import { GsnTestEnvironment } from "@opengsn/cli/dist/GsnTestEnvironment"; +import { RelayRequest, defaultEnvironment, decodeRevertReason, constants } from "@opengsn/common"; +import { calculatePostGas, registerAsRelayServer, deployHub } from "./utils"; +import { Penalizer } from "../../typechain/Penalizer"; +import { defaultGsnConfig } from "@opengsn/provider"; +import { parseUnits } from "ethers/lib/utils"; +import { TypedRequestData, GsnDomainSeparatorType, GsnRequestType } from "@opengsn/common/dist/EIP712/TypedRequestData"; + +chai.use(solidity); +const fork = process.env.FORK as eEVMNetwork; + +async function deploy(name: string, ...params: any[]) { + const Contract = await ethers.getContractFactory(name); + return await Contract.deploy(...params).then(f => f.deployed()); +} + +describe("TokenPaymaster", function () { + let owner: SignerWithAddress; + let relayer: SignerWithAddress; + let alice: SignerWithAddress; + + let paymaster: TokenPaymaster; + let relayHub: RelayHub; + let forwarder: IForwarder; + let usdc: ERC20Permit; + let vaultGateway: VaultGateway; + let vault: Vault; + let stakeManager: StakeManager; + let penalizer: Penalizer; + let relayRequest: RelayRequest; + let paymasterData: string; + let metaTxSignature: BytesLike; + let depositAmountUSDC: BigNumber; + let permitData: string; + + const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; + const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; + const UniswapV2Router02Address = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"; + + before(async function () { + await deployments.fixture(); + [owner, relayer, alice] = await ethers.getSigners(); + + //Get Oracle deployment + const oracle = await deployments.get("OptyFiOracle"); + + //Deploy Relay Hub + stakeManager = ( + await deploy("StakeManager", defaultEnvironment.maxUnstakeDelay, 0, 0, owner.address, owner.address) + ); + penalizer = ( + await deploy( + "Penalizer", + defaultEnvironment.penalizerConfiguration.penalizeBlockDelay, + defaultEnvironment.penalizerConfiguration.penalizeBlockExpiration, + ) + ); + relayHub = ( + await deployHub( + stakeManager.address, + penalizer.address, + constants.ZERO_ADDRESS, + constants.ZERO_ADDRESS, + "0", + owner, + ) + ); + + //Deploy Forwarder and set request type and domain separator + forwarder = await deploy("Forwarder"); + await forwarder.registerRequestType(GsnRequestType.typeName, GsnRequestType.typeSuffix); + await forwarder.registerDomainSeparator(defaultGsnConfig.domainSeparatorName, GsnDomainSeparatorType.version); + + //Deploy Paymaster, set RelayHub and Forwarder + paymaster = await deploy("TokenPaymaster", oracle.address); + await paymaster.connect(owner).setRelayHub(relayHub.address); + await paymaster.setTrustedForwarder(forwarder.address); + + //Deploy Vault Gateway + vaultGateway = await deploy("VaultGateway", forwarder.address); + + //Get USDC instance + usdc = await ethers.getContractAt(ERC20Permit__factory.abi, USDC); + + //Set balance for Owner and Alice + await setTokenBalanceInStorage(usdc, owner.address, "20000"); + await setTokenBalanceInStorage(usdc, alice.address, "40000"); + + const context = ethers.utils.defaultAbiCoder.encode( + ["address", "uint256", "address"], + [relayer.address, 5000000, usdc.address], + ); + + //Set deposit amount + depositAmountUSDC = ethers.utils.parseUnits("10000", 6); + + //Set swap deadline + const swapDeadline = BigNumber.from("1000000000000000000000000000000000000"); + + //Encode dex approval and paymaster data (used to refund relayer) + const approveData = ethers.utils.defaultAbiCoder.encode( + ["address", "uint256"], + [UniswapV2Router02Address, depositAmountUSDC], + ); + paymasterData = ethers.utils.defaultAbiCoder.encode( + ["tuple(address, address, bool, bytes, bytes, bytes, address[], uint256)"], + [[usdc.address, UniswapV2Router02Address, false, "0x", approveData, "0x", [USDC, WETH], swapDeadline]], + ); + + //Calculate gas spent in postRelayCall (swap tokens to refund relayer) + const gasUsedByPost = await calculatePostGas(usdc, paymaster, paymasterData, owner, depositAmountUSDC, context); + + //Set postRelayCall gas usage + await paymaster.connect(owner).setPostGasUsage(gasUsedByPost); + + console.log("paymaster post with precharge=", (await paymaster.gasUsedByPost()).toString()); + + //Get Vault deployment and set configuration + vault = await getVaultDeploymentAndConfigure( + "opUSDC-Save", + "908337486804231642580332837833655270430560746049134246454386846501909299200", + "10000000000", + "1000000000", + "1000000000000", + ); + + //Get accounts proof + const _accountsRoot = getAccountsMerkleRoot([alice.address, vaultGateway.address]); + await vault.connect(owner).setWhitelistedAccountsRoot(_accountsRoot); + let accountsProof = getAccountsMerkleProof([alice.address, vaultGateway.address], vaultGateway.address); + + //Sign VaultGateway permit to use Alice's USDC + const deadline = ethers.constants.MaxUint256; + const sig = await getPermitSignature(alice, usdc, vaultGateway.address, depositAmountUSDC, deadline, { + version: "2", + nonce: BigNumber.from("1"), + }); + let permitVaultGateway = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"], + [alice.address, vaultGateway.address, depositAmountUSDC, deadline, sig.v, sig.r, sig.s], + ); + + //Vault Gateway deposit() Call + const dataCall = vaultGateway.interface.encodeFunctionData("deposit", [ + vault.address, + depositAmountUSDC, + BigNumber.from("0"), + permitVaultGateway, + accountsProof, + ]); + + //RelayRequest Call + const provider = ethers.getDefaultProvider(); + const feeData = await provider.getFeeData(); + const gasPrice = feeData.gasPrice ? feeData.gasPrice.toString() : ""; + + relayRequest = { + request: { + from: alice.address, + to: vaultGateway.address, + value: "0", + gas: "1000000", + nonce: (await forwarder.getNonce(alice.address)).toString(), + data: dataCall, + validUntilTime: ethers.constants.MaxUint256.toString(), + }, + relayData: { + maxFeePerGas: gasPrice, + maxPriorityFeePerGas: gasPrice, + transactionCalldataGasUsed: "0", + relayWorker: relayer.address, + paymaster: paymaster.address, + forwarder: forwarder.address, + paymasterData, + clientId: "1", + }, + }; + + //Sign USDC permit + const sig2 = await getPermitSignature(alice, usdc, paymaster.address, depositAmountUSDC, deadline, { + version: "2", + }); + + permitData = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"], + [alice.address, paymaster.address, depositAmountUSDC, deadline, sig2.v, sig2.r, sig2.s], + ); + }); + + after(async function () { + await GsnTestEnvironment.stopGsn(); + }); + + describe("#relayedCall()", function () { + const paymasterDeposit = (1e18).toString(); + + before(async () => { + await setTokenBalanceInStorage(usdc, owner.address, "40000"); + + const stake = parseUnits("10000", 6).toString(); + await usdc.connect(owner).approve(stakeManager.address, stake); + await registerAsRelayServer(usdc, stakeManager, relayer, owner, stake, relayHub); + await relayHub.depositFor(paymaster.address, { value: paymasterDeposit }); + await paymaster.setRelayHub(relayHub.address); + }); + + it("should reject if incorrect signature", async () => { + const dataToSign = new TypedRequestData( + defaultGsnConfig.domainSeparatorName, + 222, //wrong chainId + forwarder.address, + relayRequest, + ); + + //Sign Meta Transaction + const wrongSignature = await signTypedData(relayer.provider, relayer.address, dataToSign); + + const externalGasLimit = (5e6).toString(); + const relayCall = await relayHub + .connect(relayer) + .callStatic.relayCall( + defaultGsnConfig.domainSeparatorName, + (10e6).toString(), + relayRequest, + wrongSignature, + permitData, + { + gasLimit: externalGasLimit, + }, + ); + + expect(decodeRevertReason(relayCall.returnValue)).eq("FWD: signature mismatch"); + }); + + it("should pay with token to make a call", async function () { + const chainId = +(await getChainId()); + const dataToSign = new TypedRequestData( + defaultGsnConfig.domainSeparatorName, + chainId, + forwarder.address, + relayRequest, + ); + + //Sign Meta Transaction + metaTxSignature = await signTypedData(alice.provider, alice.address, dataToSign); + + const preRelayAliceVaultBalance = await vault.balanceOf(alice.address); + const preRelayAliceETHBalance = await alice.getBalance(); + const relayerPreBalance = await relayer.getBalance(); + + //Execute Relay Call + const externalGasLimit = (5e6).toString(); + expect( + await relayHub + .connect(relayer) + .relayCall( + defaultGsnConfig.domainSeparatorName, + (10e6).toString(), + relayRequest, + metaTxSignature, + permitData, + { + gasLimit: externalGasLimit, + }, + ), + ) + .to.emit(relayHub, "TransactionRelayed") + .to.emit(paymaster, "TokensCharged"); + + const postRelayAliceVaultBalance = await vault.balanceOf(alice.address); + const postRelayAliceETHBalance = await alice.getBalance(); + + //Alice Vault tokens increased + expect(postRelayAliceVaultBalance).gt(preRelayAliceVaultBalance); + + //Alice ETH balance don't change + expect(postRelayAliceETHBalance).eq(preRelayAliceETHBalance); + + //Relayer receiced more ETH than spent + const ethReceived = await relayHub.balanceOf(relayer.address); + expect(ethReceived).gt(0); + await relayHub.connect(relayer).withdraw(relayer.address, ethReceived); + const relayerPostBalance = await relayer.getBalance(); + expect(relayerPostBalance).gt(relayerPreBalance); + }); + }); +}); diff --git a/test/test-opty/utils.ts b/test/test-opty/utils.ts index fb644df5..534076f6 100644 --- a/test/test-opty/utils.ts +++ b/test/test-opty/utils.ts @@ -1,15 +1,40 @@ // !! Important !! // Please do not keep this file under helpers/utils as it imports hre from hardhat -import { BigNumber, BigNumberish, Contract, Signature } from "ethers"; +import { BigNumber, BigNumberish, Contract, Signature, Signer } from "ethers"; import { getAddress, parseEther, splitSignature } from "ethers/lib/utils"; -import hre, { ethers } from "hardhat"; +import hre, { deployments, ethers } from "hardhat"; import ethTokens from "@optyfi/defi-legos/ethereum/tokens/wrapped_tokens"; import polygonTokens from "@optyfi/defi-legos/polygon/tokens"; import avaxTokens from "@optyfi/defi-legos/avalanche/tokens"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; import { StrategyStepType } from "../../helpers/type"; -import { ERC20, IAdapterFull, IWETH, Registry, ERC20Permit } from "../../typechain"; +import { + ERC20, + IAdapterFull, + IWETH, + Registry, + ERC20Permit, + RelayHub__factory, + RelayRegistrar__factory, + Vault, + Vault__factory, +} from "../../typechain"; import { fundWalletToken, getBlockTimestamp } from "../../helpers/contracts-actions"; +import { StakeManager, TokenGasCalculator, RelayHub, RelayRegistrar } from "../../typechain"; +import { deployContract } from "../../helpers/helpers"; +import { PrefixedHexString } from "ethereumjs-util"; +import { GasUsedEvent } from "../../typechain/TokenGasCalculator"; +import { + Environment, + IntString, + RelayHubConfiguration, + constants, + ForwardRequest, + RelayData, + RelayRequest, + defaultEnvironment, + splitRelayUrlForRegistrar, +} from "@opengsn/common"; const setStorageAt = (address: string, slot: string, val: string): Promise => hre.network.provider.send("hardhat_setStorageAt", [address, slot, val]); @@ -300,6 +325,75 @@ export async function getPermitSignature( ); } +const EIP712Domain = [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, +]; + +const RelayRequest = [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "gas", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "data", type: "bytes" }, + { name: "validUntilTime", type: "uint256" }, +]; + +const RelayData = [ + { name: "maxFeePerGas", type: "uint256" }, + { name: "maxPriorityFeePerGas", type: "uint256" }, + { name: "transactionCalldataGasUsed", type: "uint256" }, + { name: "relayWorker", type: "address" }, + { name: "paymaster", type: "address" }, + { name: "forwarder", type: "address" }, + { name: "paymasterData", type: "bytes" }, + { name: "clientId", type: "uint256" }, +]; + +function getMetaTxTypeData(chainId: number, verifyingContract: string) { + return { + types: { + EIP712Domain, + RelayRequest, + RelayData, + }, + domain: { + name: "GSN Relayed Transaction", + version: 3, + chainId, + verifyingContract, + }, + primaryType: "RelayRequest", + }; +} + +export async function signTypedData(signer: any, from: string, data: any) { + const isHardhat = data.domain.chainId == 31337; + const [method, argData] = isHardhat ? ["eth_signTypedData", data] : ["eth_signTypedData_v4", JSON.stringify(data)]; + return await signer.send(method, [from, argData]); +} + +export async function buildRequest(forwarder: Contract, input: any) { + const nonce = (await forwarder.getNonce(input.from)).toString(); + return { value: 0, gas: 1e6, nonce, validUntilTime: ethers.constants.MaxUint256, ...input }; +} + +export async function buildTypedData(forwarder: Contract, request: any) { + const chainId = (await forwarder.provider.getNetwork()).chainId; + const typeData = getMetaTxTypeData(chainId, forwarder.address); + return { ...typeData, message: request }; +} + +export async function signMetaTxRequest(signer: any, forwarder: Contract, input: any) { + const request = await buildRequest(forwarder, input); + const toSign = await buildTypedData(forwarder, request); + const signature = signTypedData(signer, input.from, toSign); + return { signature, request }; +} + export async function getPermitLegacySignature( signer: SignerWithAddress, token: ERC20Permit, @@ -356,3 +450,124 @@ export async function getPermitLegacySignature( ), ); } + +export async function revertReason(func: Promise): Promise { + try { + await func; + return "ok"; // no revert + } catch (e: any) { + return e.message.replace(/.*reverted with reason string /, ""); + } +} + +export async function registerAsRelayServer( + token: ERC20Permit, + stakeManager: StakeManager, + relay: SignerWithAddress, + relayOwner: SignerWithAddress, + stake: string, + hub: RelayHub, +): Promise { + await stakeManager.connect(relay).setRelayManagerOwner(relayOwner.address); + await stakeManager + .connect(relayOwner) + .stakeForRelayManager(token.address, relay.address, 7 * 24 * 3600, stake.toString()); + await stakeManager.connect(relayOwner).authorizeHubByOwner(relay.address, hub.address); + await hub.setMinimumStakes([token.address], [stake]); + await hub.connect(relay).addRelayWorkers([relay.address]); + const relayRegistrar = ( + await hre.ethers.getContractAt("RelayRegistrar", await hub.getRelayRegistrar()) + ); + await relayRegistrar.connect(relay).registerRelayServer(hub.address, splitRelayUrlForRegistrar("url")); +} + +export async function deployTestHub(calculator: boolean = false, signer: Signer): Promise { + const contract = calculator ? "TokenGasCalculator" : "TestHub"; + return deployContract(hre, contract, false, signer, [ + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + defaultEnvironment.relayHubConfiguration, + ]); +} + +export function mergeRelayRequest( + req: RelayRequest, + overrideData: Partial, + overrideRequest: Partial = {}, +): RelayRequest { + return { + relayData: { ...req.relayData, ...overrideData }, + request: { ...req.request, ...overrideRequest }, + }; +} + +export async function calculatePostGas( + token: any, + paymaster: any, + paymasterData: string, + account: Signer, + tokenAmount: BigNumber, + context: PrefixedHexString, +): Promise { + const calc = (await deployTestHub(true, account)) as TokenGasCalculator; + await paymaster.connect(account).setRelayHub(calc.address); + await token.connect(account).transfer(paymaster.address, tokenAmount); + const res = await calc.calculatePostGas(paymaster.address, context, paymasterData); + const receipt = await res.wait(); + const event = receipt.events?.find(it => it.event === "GasUsed") as unknown as GasUsedEvent; + return event.args.gasUsedByPost; +} + +export async function deployHub( + stakeManager: string, + penalizer: string, + batchGateway: string, + testToken: string, + testTokenMinimumStake: IntString, + signer: Signer, + configOverride: Partial = {}, + environment: Environment = defaultEnvironment, + relayRegistrationMaxAge = constants.yearInSec, +): Promise { + const relayHubConfiguration: RelayHubConfiguration = { + ...environment.relayHubConfiguration, + ...configOverride, + }; + const relayRegistrar = await new RelayRegistrar__factory(signer).deploy(relayRegistrationMaxAge); + const hub: RelayHub = await new RelayHub__factory(signer).deploy( + stakeManager, + penalizer, + batchGateway, + relayRegistrar.address, + relayHubConfiguration, + ); + + await hub.connect(signer).setMinimumStakes([testToken], [testTokenMinimumStake]); + + // @ts-ignore + hub._secretRegistrarInstance = relayRegistrar; + return hub; +} + +export async function getVaultDeploymentAndConfigure( + vaultName: string, + vaultConfiguration: string, + maxUserdepoist: string, + minUserDeposit: string, + maxVaultTVL: string, +): Promise { + const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get(vaultName)).address; + let vault = await ethers.getContractAt(Vault__factory.abi, OPUSDCGROW_VAULT_ADDRESS); + //set vault config + const _vaultConfiguration = BigNumber.from(vaultConfiguration); + await vault.setVaultConfiguration(_vaultConfiguration); + await vault.setValueControlParams( + maxUserdepoist, // 10,000 USDC + minUserDeposit, // 1000 USDC + maxVaultTVL, // 1,000,000 USDC + ); + + return vault; +} diff --git a/test/test-opty/vault-gateway.spec.ts b/test/test-opty/vault-gateway.spec.ts new file mode 100644 index 00000000..5727f668 --- /dev/null +++ b/test/test-opty/vault-gateway.spec.ts @@ -0,0 +1,247 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; +import chai, { expect } from "chai"; +import { solidity } from "ethereum-waffle"; +import { BigNumber } from "ethers"; +import { deployments, ethers, getChainId } from "hardhat"; +import { eEVMNetwork } from "../../helper-hardhat-config"; +import { MULTI_CHAIN_VAULT_TOKENS } from "../../helpers/constants/tokens"; +import { getAccountsMerkleProof, getAccountsMerkleRoot, Signers, to_10powNumber_BN } from "../../helpers/utils"; +import { signTypedData } from "./utils"; +import { + ERC20Permit, + ERC20Permit__factory, + Forwarder, + VaultGateway, + Registry, + Registry__factory, + Vault, + Vault__factory, +} from "../../typechain"; +import { getPermitSignature, setTokenBalanceInStorage } from "./utils"; +import { GsnDomainSeparatorType, GsnRequestType, TypedRequestData } from "@opengsn/common"; +import { defaultGsnConfig } from "@opengsn/provider"; +import { SignTypedDataVersion, TypedDataUtils } from "@metamask/eth-sig-util"; +import keccak256 from "keccak256"; + +chai.use(solidity); +const fork = process.env.FORK as eEVMNetwork; + +async function deploy(name: string, ...params: any[]) { + const Contract = await ethers.getContractFactory(name); + return await Contract.deploy(...params).then(f => f.deployed()); +} + +const EIP712Domain = [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, +]; + +const RelayRequest = [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "gas", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "data", type: "bytes" }, + { name: "validUntilTime", type: "uint256" }, + { name: "relayData", type: "RelayData" }, +]; + +const RelayData = [ + { name: "maxFeePerGas", type: "uint256" }, + { name: "maxPriorityFeePerGas", type: "uint256" }, + { name: "transactionCalldataGasUsed", type: "uint256" }, + { name: "relayWorker", type: "address" }, + { name: "paymaster", type: "address" }, + { name: "forwarder", type: "address" }, + { name: "paymasterData", type: "bytes" }, + { name: "clientId", type: "uint256" }, +]; + +describe("VaultGateway", function () { + before(async function () { + //deploy contracts + await deployments.fixture(); + this.forwarder = await deploy("Forwarder"); + await this.forwarder.registerRequestType(GsnRequestType.typeName, GsnRequestType.typeSuffix); + await this.forwarder.registerDomainSeparator(defaultGsnConfig.domainSeparatorName, GsnDomainSeparatorType.version); + this.vaultGateway = await deploy("VaultGateway", this.forwarder.address); + + const OPUSDCGROW_VAULT_ADDRESS = (await deployments.get("opUSDC-Save")).address; + const REGISTRY_PROXY_ADDRESS = (await deployments.get("RegistryProxy")).address; + + this.vault = await ethers.getContractAt(Vault__factory.abi, OPUSDCGROW_VAULT_ADDRESS); + this.registry = await ethers.getContractAt(Registry__factory.abi, REGISTRY_PROXY_ADDRESS); + + //Get signers + const financeOperatorAddress = await this.registry.getFinanceOperator(); + const governanceAddress = await this.registry.getGovernance(); + const signers: SignerWithAddress[] = await ethers.getSigners(); + this.signers = {} as Signers; + this.signers.financeOperator = await ethers.getSigner(financeOperatorAddress); + this.signers.governance = await ethers.getSigner(governanceAddress); + this.signers.alice = signers[0]; + this.relayer = signers[1]; + console.log("financeOperator", this.signers.financeOperator.address); + console.log("governance", this.signers.governance.address); + console.log("alice", this.signers.alice.address); + console.log("relayer", this.relayer.address); + + //set vault config + const _vaultConfiguration = BigNumber.from( + "908337486804231642580332837833655270430560746049134246454386846501909299200", + ); + await this.vault.connect(this.signers.governance).setVaultConfiguration(_vaultConfiguration); + await this.vault.connect(this.signers.financeOperator).setValueControlParams( + "10000000000", // 10,000 USDC + "1000000000", // 1000 USDC + "1000000000000", // 1,000,000 USDC + ); + + const _accountsRoot = getAccountsMerkleRoot([this.signers.alice.address, this.vaultGateway.address]); + await this.vault.connect(this.signers.governance).setWhitelistedAccountsRoot(_accountsRoot); + + //add USDC balance to user + this.usdc = ( + await ethers.getContractAt(ERC20Permit__factory.abi, MULTI_CHAIN_VAULT_TOKENS[fork].USDC.address) + ); + await setTokenBalanceInStorage(this.usdc, this.signers.alice.address, "20000"); + + this.accountsProof = getAccountsMerkleProof( + [this.signers.alice.address, this.vaultGateway.address], + this.vaultGateway.address, + ); + + this.depositAmountUSDC = BigNumber.from("1000").mul(to_10powNumber_BN("6")); + }); + describe("VaultGateway Test", function () { + beforeEach(async function () { + const deadline = ethers.constants.MaxUint256; + const sig = await getPermitSignature( + this.signers.alice, + this.usdc, + this.vaultGateway.address, + this.depositAmountUSDC, + deadline, + { version: "2" }, + ); + this.dataMetaVaultPermit = ethers.utils.defaultAbiCoder.encode( + ["address", "address", "uint256", "uint256", "uint8", "bytes32", "bytes32"], + [this.signers.alice.address, this.vaultGateway.address, this.depositAmountUSDC, deadline, sig.v, sig.r, sig.s], + ); + }); + + it("deposit using VaultGateway directly", async function () { + await expect( + this.vaultGateway + .connect(this.signers.alice) + .deposit( + this.vault.address, + this.depositAmountUSDC, + BigNumber.from("0"), + this.dataMetaVaultPermit, + this.accountsProof, + ), + ) + .to.emit(this.vault, "Transfer") + .withArgs(ethers.constants.AddressZero, this.signers.alice.address, this.depositAmountUSDC); + }); + + it("deposits via a meta-tx", async function () { + const forwarder = this.forwarder.connect(this.relayer); + + const dataCall = this.vaultGateway.interface.encodeFunctionData("deposit", [ + this.vault.address, + this.depositAmountUSDC, + BigNumber.from("0"), + this.dataMetaVaultPermit, + this.accountsProof, + ]); + + const provider = ethers.getDefaultProvider(); + const feeData = await provider.getFeeData(); + const gasPrice = feeData.gasPrice ? feeData.gasPrice.toString() : ""; + const relayRequest = { + request: { + from: this.signers.alice.address, + to: this.vaultGateway.address, + value: "0", + gas: "1000000", + nonce: (await forwarder.getNonce(this.signers.alice.address)).toString(), + data: dataCall, + validUntilTime: ethers.constants.MaxUint256.toString(), + }, + relayData: { + maxFeePerGas: gasPrice, + maxPriorityFeePerGas: gasPrice, + transactionCalldataGasUsed: "0", + relayWorker: ethers.constants.AddressZero, + paymaster: ethers.constants.AddressZero, + forwarder: forwarder.address, + paymasterData: "0x", + clientId: "1", + }, + }; + + const chainId = +(await getChainId()); + + const typeData = new TypedRequestData( + defaultGsnConfig.domainSeparatorName, + chainId, + forwarder.address, + relayRequest, + ); + + const signature = await signTypedData(this.signers.alice.provider, this.signers.alice.address, typeData); + + const data = { + domain: { + name: "GSN Relayed Transaction", + version: "3", + chainId, + verifyingContract: forwarder.address, + }, + primaryType: "RelayRequest", + types: { + EIP712Domain: EIP712Domain, + RelayRequest: RelayRequest, + RelayData: RelayData, + }, + message: typeData.message, + }; + + const data2 = { + types: { + RelayData: RelayData, + }, + }; + + const GENERIC_PARAMS = + "address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntilTime"; + const typeSuffix = + "RelayData relayData)RelayData(uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,uint256 transactionCalldataGasUsed,address relayWorker,address paymaster,address forwarder,bytes paymasterData,uint256 clientId)"; + const typeName = `RelayRequest(${GENERIC_PARAMS},${typeSuffix}`; + const typeHash = keccak256(typeName); + + const hashSuffix = TypedDataUtils.hashStruct( + "RelayData", + relayRequest.relayData, + data2.types, + SignTypedDataVersion.V4, + ); + + const domainSeparator = TypedDataUtils.hashStruct( + "EIP712Domain", + data.domain, + data.types, + SignTypedDataVersion.V4, + ); + + await expect(forwarder.execute(relayRequest.request, domainSeparator, typeHash, hashSuffix, signature)) + .to.emit(this.vault, "Transfer") + .withArgs(ethers.constants.AddressZero, this.signers.alice.address, this.depositAmountUSDC); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 6bb8c289..5e3fe99e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2007,6 +2007,16 @@ __metadata: languageName: node linkType: hard +"@opengsn/contracts@npm:^3.0.0-beta.1": + version: 3.0.0-beta.2 + resolution: "@opengsn/contracts@npm:3.0.0-beta.2" + dependencies: + "@openzeppelin/contracts": ^4.2.0 + web3-eth-contract: ^1.7.4 + checksum: c44562d4d34b32b31755c2d18fe380584d23618d8d280fb47c9e20c053d0e479274aa3026628575ddf64eaba398c021961c429d940c9cb4cfc801534fdf517b2 + languageName: node + linkType: hard + "@openzeppelin/contracts-0.8.x@npm:@openzeppelin/contracts@4.4.1": version: 4.4.1 resolution: "@openzeppelin/contracts@npm:4.4.1" @@ -2226,6 +2236,13 @@ __metadata: languageName: node linkType: hard +"@solidstate/contracts@npm:^0.0.35": + version: 0.0.35 + resolution: "@solidstate/contracts@npm:0.0.35" + checksum: db0a88d6c01dd99f40cc8074d3df429d8485fa997eb38171c18464253a4d7512b2f04c925efd0ef2373691a875c063c52cc7f382232995f67984fd4fc7cb9974 + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^1.1.2": version: 1.1.2 resolution: "@szmarczak/http-timer@npm:1.1.2" @@ -5942,6 +5959,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^3.1.4": + version: 3.1.5 + resolution: "cross-fetch@npm:3.1.5" + dependencies: + node-fetch: 2.6.7 + checksum: f6b8c6ee3ef993ace6277fd789c71b6acf1b504fd5f5c7128df4ef2f125a429e29cd62dc8c127523f04a5f2fa4771ed80e3f3d9695617f441425045f505cf3bb + languageName: node + linkType: hard + "cross-spawn@npm:^6.0.0, cross-spawn@npm:^6.0.5": version: 6.0.5 resolution: "cross-spawn@npm:6.0.5" @@ -6599,9 +6625,11 @@ __metadata: "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@0.3.0-beta.12" "@nomiclabs/hardhat-etherscan": ^3.0.1 "@nomiclabs/hardhat-waffle": ^2.0.1 + "@opengsn/contracts": ^3.0.0-beta.1 "@openzeppelin/contracts": 3.4.0 "@openzeppelin/contracts-0.8.x": "npm:@openzeppelin/contracts@4.4.1" "@optyfi/defi-legos": v0.1.0-rc.38 + "@solidstate/contracts": ^0.0.35 "@tenderly/hardhat-tenderly": 1.1.0-beta.6 "@typechain/ethers-v5": ^7.1.2 "@typechain/hardhat": ^2.3.0 @@ -6895,6 +6923,13 @@ __metadata: languageName: node linkType: hard +"es6-promise@npm:^4.2.8": + version: 4.2.8 + resolution: "es6-promise@npm:4.2.8" + checksum: 95614a88873611cb9165a85d36afa7268af5c03a378b35ca7bda9508e1d4f1f6f19a788d4bc755b3fd37c8ebba40782018e02034564ff24c9d6fa37e959ad57d + languageName: node + linkType: hard + "es6-symbol@npm:^3.1.1, es6-symbol@npm:~3.1.3": version: 3.1.3 resolution: "es6-symbol@npm:3.1.3" @@ -12567,6 +12602,20 @@ fsevents@~2.3.2: languageName: node linkType: hard +"node-fetch@npm:2.6.7": + version: 2.6.7 + resolution: "node-fetch@npm:2.6.7" + dependencies: + whatwg-url: ^5.0.0 + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 8d816ffd1ee22cab8301c7756ef04f3437f18dace86a1dae22cf81db8ef29c0bf6655f3215cb0cdb22b420b6fe141e64b26905e7f33f9377a7fa59135ea3e10b + languageName: node + linkType: hard + "node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1": version: 2.6.5 resolution: "node-fetch@npm:2.6.5" @@ -17264,6 +17313,16 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core-helpers@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core-helpers@npm:1.8.0" + dependencies: + web3-eth-iban: 1.8.0 + web3-utils: 1.8.0 + checksum: f0af1cfb790b2c51ac29d19d0c77cc4caf8e363ffc069b4943bbd4ebf602bd04835af6c5c4f8b2601ee3c8157b8c658783a7ddf3cd82a9c1d9c15091a624249a + languageName: node + linkType: hard + "web3-core-method@npm:1.2.11": version: 1.2.11 resolution: "web3-core-method@npm:1.2.11" @@ -17305,6 +17364,19 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core-method@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core-method@npm:1.8.0" + dependencies: + "@ethersproject/transactions": ^5.6.2 + web3-core-helpers: 1.8.0 + web3-core-promievent: 1.8.0 + web3-core-subscriptions: 1.8.0 + web3-utils: 1.8.0 + checksum: 5b73af8a34c94cfaee8dd69435fe78cc6eccde2e30c2d9632b4efa96d18b8ee13012d14ed6cfe4350db6f9ea2e04c53e55a47ac31db013728812427828338290 + languageName: node + linkType: hard + "web3-core-promievent@npm:1.2.11": version: 1.2.11 resolution: "web3-core-promievent@npm:1.2.11" @@ -17332,6 +17404,15 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core-promievent@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core-promievent@npm:1.8.0" + dependencies: + eventemitter3: 4.0.4 + checksum: 0c39987322104e9243c79a33effbeeb9380dfc4ae711a7a07704bdd1610cf23b46dcb5a50164df0065159c5fb2f336f5700c522feff2299de8a12f431e1884b7 + languageName: node + linkType: hard + "web3-core-requestmanager@npm:1.2.11": version: 1.2.11 resolution: "web3-core-requestmanager@npm:1.2.11" @@ -17371,6 +17452,19 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core-requestmanager@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core-requestmanager@npm:1.8.0" + dependencies: + util: ^0.12.0 + web3-core-helpers: 1.8.0 + web3-providers-http: 1.8.0 + web3-providers-ipc: 1.8.0 + web3-providers-ws: 1.8.0 + checksum: df4d8295a9b0e328c63498c5cc8a828d2f1d7f7e6f6893daa1f4b1ce2de5ed9fd428838606ec267f47c83b610340909c30975e51ab7e07564a1ec598043fb3ba + languageName: node + linkType: hard + "web3-core-subscriptions@npm:1.2.11": version: 1.2.11 resolution: "web3-core-subscriptions@npm:1.2.11" @@ -17402,6 +17496,16 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core-subscriptions@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core-subscriptions@npm:1.8.0" + dependencies: + eventemitter3: 4.0.4 + web3-core-helpers: 1.8.0 + checksum: 934d3b823b7ca7859509f806c81d236bd0e93224e91b9928538a669e6a42e95aa0d3117f36f54f2d4c5e5f429748eb011f9e99022edf363a48d1c5c1e40da212 + languageName: node + linkType: hard + "web3-core@npm:1.2.11": version: 1.2.11 resolution: "web3-core@npm:1.2.11" @@ -17432,6 +17536,21 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-core@npm:1.8.0": + version: 1.8.0 + resolution: "web3-core@npm:1.8.0" + dependencies: + "@types/bn.js": ^5.1.0 + "@types/node": ^12.12.6 + bignumber.js: ^9.0.0 + web3-core-helpers: 1.8.0 + web3-core-method: 1.8.0 + web3-core-requestmanager: 1.8.0 + web3-utils: 1.8.0 + checksum: a524cc2b23af54650166af07d30a242ec740fe8274bec72e8f2b1757deaf733de295eb6c71594e2332b46a32f69e060414b138a5a4e7598e503188c96a0efa44 + languageName: node + linkType: hard + "web3-core@npm:^1.7.1": version: 1.7.3 resolution: "web3-core@npm:1.7.3" @@ -17468,6 +17587,16 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-eth-abi@npm:1.8.0": + version: 1.8.0 + resolution: "web3-eth-abi@npm:1.8.0" + dependencies: + "@ethersproject/abi": ^5.6.3 + web3-utils: 1.8.0 + checksum: c9559abc6e928017a561e619863735f5d0c04efe93aee484cff5a376d780be4af40ff7a67723e4478c2e174ad8f35d2a102dd7c442cc976fa374c60c7607445b + languageName: node + linkType: hard + "web3-eth-accounts@npm:1.2.11": version: 1.2.11 resolution: "web3-eth-accounts@npm:1.2.11" @@ -17539,6 +17668,22 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-eth-contract@npm:^1.7.4": + version: 1.8.0 + resolution: "web3-eth-contract@npm:1.8.0" + dependencies: + "@types/bn.js": ^5.1.0 + web3-core: 1.8.0 + web3-core-helpers: 1.8.0 + web3-core-method: 1.8.0 + web3-core-promievent: 1.8.0 + web3-core-subscriptions: 1.8.0 + web3-eth-abi: 1.8.0 + web3-utils: 1.8.0 + checksum: d433984796c2ac5a4a436d5e9cc93b89e3d4669ec332730c628d471fc23981d19be3a8cea1cfe8634f17469da7e33fa6302cb3d886c2a7d0322bc2e03d8d6a59 + languageName: node + linkType: hard + "web3-eth-ens@npm:1.2.11": version: 1.2.11 resolution: "web3-eth-ens@npm:1.2.11" @@ -17602,6 +17747,16 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-eth-iban@npm:1.8.0": + version: 1.8.0 + resolution: "web3-eth-iban@npm:1.8.0" + dependencies: + bn.js: ^5.2.1 + web3-utils: 1.8.0 + checksum: 3877b18da7c4a965a8cf180fdff95bc3de9a94f671e336403cb11c45b646257fb4c26b3f1e18a51a37f5865190c74f769354a5719517324997bc6a43e2ac2834 + languageName: node + linkType: hard + "web3-eth-personal@npm:1.2.11": version: 1.2.11 resolution: "web3-eth-personal@npm:1.2.11" @@ -17751,6 +17906,18 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-providers-http@npm:1.8.0": + version: 1.8.0 + resolution: "web3-providers-http@npm:1.8.0" + dependencies: + abortcontroller-polyfill: ^1.7.3 + cross-fetch: ^3.1.4 + es6-promise: ^4.2.8 + web3-core-helpers: 1.8.0 + checksum: 2af709826ae806c02db1936ccfd25bc28be04403c2e41a02a5e8ebb120ba910cf97dd8c8203c1d08d110a819f7a6548cca813227f4bb567a733d92c4a3ebc930 + languageName: node + linkType: hard + "web3-providers-ipc@npm:1.2.11": version: 1.2.11 resolution: "web3-providers-ipc@npm:1.2.11" @@ -17782,6 +17949,16 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-providers-ipc@npm:1.8.0": + version: 1.8.0 + resolution: "web3-providers-ipc@npm:1.8.0" + dependencies: + oboe: 2.1.5 + web3-core-helpers: 1.8.0 + checksum: b65d7f0f37c0d477d46f7e6db7c2ddad0d9bf3ff9a37cdb0b220788779bba551086e11af16ee2488d397d7f54cb16795bddf6b96435ed2527c495391a70b8993 + languageName: node + linkType: hard + "web3-providers-ws@npm:1.2.11": version: 1.2.11 resolution: "web3-providers-ws@npm:1.2.11" @@ -17816,6 +17993,17 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-providers-ws@npm:1.8.0": + version: 1.8.0 + resolution: "web3-providers-ws@npm:1.8.0" + dependencies: + eventemitter3: 4.0.4 + web3-core-helpers: 1.8.0 + websocket: ^1.0.32 + checksum: 61124ae1c6e68713553083a2d652169d399e368c6a9f49b4966cf5c59bcaee2a47e55d993726b3702a605ed48d8b18d79398712cd5b67ac237ec46715033e7fc + languageName: node + linkType: hard + "web3-shh@npm:1.2.11": version: 1.2.11 resolution: "web3-shh@npm:1.2.11" @@ -17886,6 +18074,21 @@ typescript@^4.4.3: languageName: node linkType: hard +"web3-utils@npm:1.8.0": + version: 1.8.0 + resolution: "web3-utils@npm:1.8.0" + dependencies: + bn.js: ^5.2.1 + ethereum-bloom-filters: ^1.0.6 + ethereumjs-util: ^7.1.0 + ethjs-unit: 0.1.6 + number-to-bn: 1.7.0 + randombytes: ^2.1.0 + utf8: 3.0.0 + checksum: 9ac6b8be14fd1feb8f6744d97f94a043716f360035f07de5a6ab414b25838922ac06e09141af39f66fbf3e76de5f1c3e07807929adf754a28ac2159e32dacedf + languageName: node + linkType: hard + "web3-utils@npm:^1.0.0-beta.31, web3-utils@npm:^1.3.0": version: 1.6.0 resolution: "web3-utils@npm:1.6.0"