diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1f1ee0f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +build/ +dist/ +.idea/ +package-lock.json diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 00000000..ecbdb870 --- /dev/null +++ b/.solhint.json @@ -0,0 +1,8 @@ +{ + "extends": "solhint:recommended", + "rules": { + "compiler-version": ["error","^0.6.12"], + "mark-callable-contracts": ["off"], + "reason-string": ["warn",{"maxLength":32}] + } +} diff --git a/.solhintignore b/.solhintignore new file mode 100644 index 00000000..3f33bc5f --- /dev/null +++ b/.solhintignore @@ -0,0 +1 @@ +contracts/0x diff --git a/Readme.md b/Readme.md new file mode 100644 index 00000000..7db45dac --- /dev/null +++ b/Readme.md @@ -0,0 +1,40 @@ +### Rif Relay Contracts + +This project is part of the rif relay ecosystem, it contains all the contracts that +the rif relay system use. + +#### How to deploy contracts + +To deploy you can use 2 ways: + +1. Configure the `truffle.js` file on the root of the project to set +your network and later run `npx truffle migrate --network `. + +2. Configure the `truffle.js` file on the root of the project to set the rsk +network and then run `npm run deploy `. + +That will start the migration, at the end you should see a summary with all the +contract addresses. + +#### How to compile contracts + +Just run `npx truffle compile` at the root of the project and that will compile the +contracts and generate a folder build with all the compiled contracts inside. + +#### How to generate a dist version +Run this command `npm run dist` to generate the dist folder with the distributable +version inside. + +#### How to use the contracts as library + +To use this project as a dependency you can install it on your project like any +other dependency like this `npm i --save @rsksmart/rif-relay-contracts`. That will +provide you with a way to get the contracts and interfaces. You can use them +like this: + +```javascript +import {RelayHub, IForwarder} from '@rsksmart/rif-relay-contracts'; + +const relayHubContractAbi = RelayHub.abi; +const iForwarderAbi = IForwarder.abi; +``` diff --git a/contracts/Migrations.sol b/contracts/Migrations.sol new file mode 100644 index 00000000..75657fb0 --- /dev/null +++ b/contracts/Migrations.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +contract Migrations { + address public owner; + // solhint-disable-next-line var-name-mixedcase + uint public last_completed_migration; + + constructor() public { + owner = msg.sender; + } + + modifier restricted() { + if (msg.sender == owner) _; + } + + function setCompleted(uint completed) public restricted { + last_completed_migration = completed; + } + + function upgrade(address newAddress) public restricted { + Migrations upgraded = Migrations(newAddress); + upgraded.setCompleted(last_completed_migration); + } +} diff --git a/contracts/Penalizer.sol b/contracts/Penalizer.sol new file mode 100644 index 00000000..82ff2276 --- /dev/null +++ b/contracts/Penalizer.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; + +import "./utils/RLPReader.sol"; +import "./utils/RSKAddrValidator.sol"; +import "./interfaces/IRelayHub.sol"; +import "./interfaces/IPenalizer.sol"; + +contract Penalizer is IPenalizer{ + + string public override versionPenalizer = "2.0.1+enveloping.penalizer.ipenalizer"; + + mapping(bytes32 => bool) public penalizedTransactions; + + using ECDSA for bytes32; + + function decodeTransaction(bytes memory rawTransaction) private pure returns (Transaction memory transaction) { + (transaction.nonce, + transaction.gasPrice, + transaction.gasLimit, + transaction.to, + transaction.value, + transaction.data) = RLPReader.decodeTransaction(rawTransaction); + return transaction; + } + + modifier relayManagerOnly(IRelayHub hub) { + require(hub.isRelayManagerStaked(msg.sender), "Unknown relay manager"); + _; + } + + function penalizeRepeatedNonce( + bytes memory unsignedTx1, + bytes memory signature1, + bytes memory unsignedTx2, + bytes memory signature2, + IRelayHub hub + ) + public + override + relayManagerOnly(hub) + { + // Can be called by a relay manager only. + // If a relay attacked the system by signing multiple transactions with the 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 (msg.sender), thus incentivizing anyone to report offending relays. + // If reported via a relay, the forfeited stake is split between + // msg.sender (the relay used for reporting) and the address that reported it. + + bytes32 txHash1 = keccak256(abi.encodePacked(unsignedTx1)); + bytes32 txHash2 = keccak256(abi.encodePacked(unsignedTx2)); + + // check that transactions were not already penalized + require(!penalizedTransactions[txHash1] || !penalizedTransactions[txHash2], "Transactions already penalized"); + + address addr1 = txHash1.recover(signature1); + address addr2 = txHash2.recover(signature2); + + require(addr1 == addr2, "Different signer"); + require(RSKAddrValidator.checkPKNotZero(addr1), "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"); + + penalizedTransactions[txHash1] = true; + penalizedTransactions[txHash2] = true; + + hub.penalize(addr1, msg.sender); + } +} diff --git a/contracts/RelayHub.sol b/contracts/RelayHub.sol new file mode 100644 index 00000000..17cb045d --- /dev/null +++ b/contracts/RelayHub.sol @@ -0,0 +1,389 @@ +/* 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:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "./utils/Eip712Library.sol"; +import "./interfaces/EnvelopingTypes.sol"; +import "./interfaces/IRelayHub.sol"; +import "./interfaces/IForwarder.sol"; + +contract RelayHub is IRelayHub { + using SafeMath for uint256; + + uint256 public override minimumStake; + uint256 public override minimumUnstakeDelay; + uint256 public override minimumEntryDepositValue; + uint256 public override maxWorkerCount; + address public override penalizer; + + string public override versionHub = "2.0.1+enveloping.hub.irelayhub"; + + // maps relay worker's address to its manager's address + mapping(address => bytes32) public override workerToManager; + + // maps relay managers to the number of their workers + mapping(address => uint256) public override workerCount; + + // maps relay managers to their stakes + mapping(address => StakeInfo) public stakes; + + constructor( + address _penalizer, + uint256 _maxWorkerCount, + uint256 _minimumEntryDepositValue, + uint256 _minimumUnstakeDelay, + uint256 _minimumStake + ) public { + require( + _maxWorkerCount > 0 && + _minimumStake > 0 && + _minimumEntryDepositValue > 0 && + _minimumUnstakeDelay > 0, "invalid hub init params" + ); + + penalizer = _penalizer; + maxWorkerCount = _maxWorkerCount; + minimumUnstakeDelay = _minimumUnstakeDelay; + minimumStake = _minimumStake; + minimumEntryDepositValue = _minimumEntryDepositValue; + } + + function registerRelayServer( + string calldata url + ) external override { + //relay manager is msg.sender + //Check if Relay Manager is staked + requireManagerStaked(msg.sender); + + require(workerCount[msg.sender] > 0, "no relay workers"); + emit RelayServerRegistered(msg.sender, url); + } + + function disableRelayWorkers(address[] calldata relayWorkers) + external + override + { + //relay manager is msg.sender + uint256 actualWorkerCount = workerCount[msg.sender]; + require( + actualWorkerCount >= relayWorkers.length, + "invalid quantity of workers" + ); + workerCount[msg.sender] = actualWorkerCount - relayWorkers.length; + + //Check if Relay Manager is staked + requireManagerStaked(msg.sender); + + bytes32 enabledWorker = + bytes32(uint256(msg.sender) << 4) | + 0x0000000000000000000000000000000000000000000000000000000000000001; + bytes32 disabledWorker = bytes32(uint256(msg.sender) << 4); + + for (uint256 i = 0; i < relayWorkers.length; i++) { + //The relay manager can only disable its relay workers and only if they are enabled (right-most nibble as 1) + require( + workerToManager[relayWorkers[i]] == enabledWorker, + "Incorrect Manager" + ); + //Disabling a worker means putting the right-most nibble to 0 + workerToManager[relayWorkers[i]] = disabledWorker; + } + + emit RelayWorkersDisabled( + msg.sender, + relayWorkers, + workerCount[msg.sender] + ); + } + + /** + New relay worker addresses can be added (as enabled workers) as long as they don't have a relay manager aldeady assigned. + */ + function addRelayWorkers(address[] calldata newRelayWorkers) + external + override + { + address relayManager = msg.sender; + workerCount[relayManager] = + workerCount[relayManager] + + newRelayWorkers.length; + require( + workerCount[relayManager] <= maxWorkerCount, + "too many workers" + ); + + //Check if Relay Manager is staked + requireManagerStaked(relayManager); + + bytes32 enabledWorker = + bytes32(uint256(relayManager) << 4) | + 0x0000000000000000000000000000000000000000000000000000000000000001; + for (uint256 i = 0; i < newRelayWorkers.length; i++) { + require( + workerToManager[newRelayWorkers[i]] == bytes32(0), + "this worker has a manager" + ); + workerToManager[newRelayWorkers[i]] = enabledWorker; + } + + emit RelayWorkersAdded( + relayManager, + newRelayWorkers, + workerCount[relayManager] + ); + } + + function deployCall( + EnvelopingTypes.DeployRequest calldata deployRequest, + bytes calldata signature + ) external override { + (signature); + + bytes32 managerEntry = workerToManager[msg.sender]; + + //read last nibble which stores the isWorkerEnabled flag, it must be 1 (true) + require( + managerEntry & + 0x0000000000000000000000000000000000000000000000000000000000000001 == + 0x0000000000000000000000000000000000000000000000000000000000000001, + "Not an enabled worker" + ); + + address manager = address(uint160(uint256(managerEntry >> 4))); + + require(msg.sender == tx.origin, "RelayWorker cannot be a contract"); + require( + msg.sender == deployRequest.relayData.relayWorker, + "Not a right worker" + ); + + requireManagerStaked(manager); + + require( + deployRequest.relayData.gasPrice <= tx.gasprice, + "Invalid gas price" + ); + + bool deploySuccess; + bytes memory ret; + ( deploySuccess, ret) = Eip712Library.deploy(deployRequest, signature); + + if (!deploySuccess) { + assembly { + revert( + add(ret, 32), + mload(ret) + ) + } + } + } + + function relayCall( + EnvelopingTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) external override returns (bool destinationCallSuccess){ + (signature); + + require(msg.sender == tx.origin, "RelayWorker cannot be a contract"); + require( + msg.sender == relayRequest.relayData.relayWorker, + "Not a right worker" + ); + require( + relayRequest.relayData.gasPrice <= tx.gasprice, + "Invalid gas price" + ); + + bytes32 managerEntry = workerToManager[msg.sender]; + //read last nibble which stores the isWorkerEnabled flag, it must be 1 (true) + require( + managerEntry & + 0x0000000000000000000000000000000000000000000000000000000000000001 == + 0x0000000000000000000000000000000000000000000000000000000000000001, + "Not an enabled worker" + ); + + address manager = address(uint160(uint256(managerEntry >> 4))); + + requireManagerStaked(manager); + + bool forwarderSuccess; + bytes memory relayedCallReturnValue; + //use succ as relay call success variable + (forwarderSuccess, destinationCallSuccess, relayedCallReturnValue) = Eip712Library + .execute(relayRequest, signature); + + if (!forwarderSuccess) { + assembly { + revert( + add(relayedCallReturnValue, 32), + mload(relayedCallReturnValue) + ) + } + } + + if (destinationCallSuccess) { + emit TransactionRelayed( + manager, + msg.sender, + keccak256(signature), + relayedCallReturnValue + ); + } else { + emit TransactionRelayedButRevertedByRecipient( + manager, + msg.sender, + keccak256(signature), + relayedCallReturnValue + ); + } + } + + modifier penalizerOnly() { + require(msg.sender == penalizer, "Not penalizer"); + _; + } + + /// Slash the stake of the relay relayManager. In order to prevent stake kidnapping, burns half of stake on the way. + /// @param relayWorker - worker whose manager will be penalized + /// @param beneficiary - address that receives half of the penalty amount + function penalize(address relayWorker, address payable beneficiary) + external + override + penalizerOnly + { + //Relay worker might be enabled or disabled + address relayManager = + address(uint160(uint256(workerToManager[relayWorker] >> 4))); + require(relayManager != address(0), "Unknown relay worker"); + + StakeInfo storage stakeInfo = stakes[relayManager]; + + uint256 amount = stakeInfo.stake; + + //In the case the stake owner have already withrawn their funds + require(amount > 0, "Unstaked relay manager"); + + // Half of the stake will be burned (sent to address 0) + stakeInfo.stake = 0; + + uint256 toBurn = SafeMath.div(amount, 2); + uint256 reward = SafeMath.sub(amount, toBurn); + + // RBTC is burned and transferred + address(0).transfer(toBurn); + beneficiary.transfer(reward); + emit StakePenalized(relayManager, beneficiary, reward); + } + + function getStakeInfo(address relayManager) + external + view + override + returns (StakeInfo memory stakeInfo) + { + return stakes[relayManager]; + } + // Put a stake for a relayManager and set its unstake delay. + // If the entry does not exist, it is created, and the caller of this function becomes its owner. + // If the entry already exists, only the owner can call this function. + // @param relayManager - address that represents a stake entry and controls relay registrations on relay hubs + // @param unstakeDelay - number of blocks to elapse before the owner can retrieve the stake after calling 'unlock' + function stakeForAddress(address relayManager, uint256 unstakeDelay) + external + payable + override + { + + StakeInfo storage stakeInfo = stakes[relayManager]; + + require( + stakeInfo.owner == address(0) || + stakeInfo.owner == msg.sender, + "not owner" + ); + require( + unstakeDelay >= stakeInfo.unstakeDelay, + "unstakeDelay cannot be decreased" + ); + require(msg.sender != relayManager, "caller is the relayManager"); + require( + stakes[msg.sender].owner == address(0), + "sender is a relayManager itself" + ); + + //If it is the initial stake, it must meet the entry value + if (stakeInfo.owner == address(0)) { + require( + msg.value >= minimumEntryDepositValue, + "Insufficient intitial stake" + ); + } + + stakeInfo.owner = msg.sender; + stakeInfo.stake += msg.value; + stakeInfo.unstakeDelay = unstakeDelay; + emit StakeAdded( + relayManager, + stakeInfo.owner, + stakeInfo.stake, + stakeInfo.unstakeDelay + ); + } + + function unlockStake(address relayManager) external override { + StakeInfo storage info = stakes[relayManager]; + require(info.owner == msg.sender, "not owner"); + require(info.withdrawBlock == 0, "already pending"); + info.withdrawBlock = block.number.add(info.unstakeDelay); + emit StakeUnlocked(relayManager, msg.sender, info.withdrawBlock); + } + + function withdrawStake(address relayManager) external override { + StakeInfo storage info = stakes[relayManager]; + require(info.owner == msg.sender, "not owner"); + require(info.withdrawBlock > 0, "Withdrawal is not scheduled"); + require(info.withdrawBlock <= block.number, "Withdrawal is not due"); + uint256 amount = info.stake; + delete stakes[relayManager]; + msg.sender.transfer(amount); + emit StakeWithdrawn(relayManager, msg.sender, amount); + } + + modifier ownerOnly(address relayManager) { + StakeInfo storage info = stakes[relayManager]; + require(info.owner == msg.sender, "not owner"); + _; + } + + modifier managerOnly() { + StakeInfo storage info = stakes[msg.sender]; + require(info.owner != address(0), "not manager"); + _; + } + + function requireManagerStaked(address relayManager) internal view { + StakeInfo storage info = stakes[relayManager]; + + require( + info.stake >= minimumStake && //isAmountSufficient + info.unstakeDelay >= minimumUnstakeDelay && //isDelaySufficient + info.withdrawBlock == 0, //isStakeLocked + "RelayManager not staked" + ); + } + + function isRelayManagerStaked(address relayManager) external view override returns (bool) { + StakeInfo storage info = stakes[relayManager]; + return info.stake >= minimumStake && //isAmountSufficient + info.unstakeDelay >= minimumUnstakeDelay && //isDelaySufficient + info.withdrawBlock == 0; //isStakeLocked + } +} diff --git a/contracts/factory/CustomSmartWalletFactory.sol b/contracts/factory/CustomSmartWalletFactory.sol new file mode 100644 index 00000000..29936757 --- /dev/null +++ b/contracts/factory/CustomSmartWalletFactory.sol @@ -0,0 +1,347 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "../interfaces/ICustomSmartWalletFactory.sol"; +import "../utils/RSKAddrValidator.sol"; + +/* solhint-disable no-inline-assembly */ +/* solhint-disable avoid-low-level-calls */ + +/** +==================================================================================================================================================== + Documentation of the Proxy Code being deployed +==================================================================================================================================================== +A simple proxy that delegates every call to an address. +This ProxyA is the one instantiated per smart wallet, and it will receive the Forwarder as MC. So every call +made to this proxy will end up in MC. +MC is controlled by the same developer who created the factory, and . +For the transaction execution (execute() call), MC will do the signature verification and payment. Then it will +execute the request, and, if a logic was defined, it will forward the flow to it before returning. + + + + +==================================================================================================================================================== + PROXY A +==================================================================================================================================================== + Constructor +==================================================================================================================================================== +PC | OPCODE| Mnemonic | Stack [top, bottom] | Comments +---------------------------------------------------------------------------------------------------------------------------------------------------- +0 | 60 2D | PUSH1 2D | [45] | Size of runtime code +2 | 3D | RETURNDATASIZE | [0, 45] | Before any external call, returdatasize = 0 (cheaper than PUSH1 00) +3 | 81 | DUP2 | [45, 0, 45] | +4 | 60 09 | PUSH1 09 | [9, 45, 0, 45] | Size of constructor code +6 | 3D | RETURNDATASIZE | [0, 9, 45, 0, 45] | +7 | 39 | CODECOPY | [0, 45] | Mem[0:44] = address(this).code[9:53] +8 | F3 | RETURN | [] | return Mem[0:44] + +==================================================================================================================================================== + Runtime Code +==================================================================================================================================================== +PC | OPCODE| Mnemonic | Stack [top, bottom] | Comments +---------------------------------------------------------------------------------------------------------------------------------- +0 | 36 | CALLDATASIZE | [msg.data.size] | +1 | 3D | RETURNDATASIZE | [0, msg.data.size] | +2 | 3D | RETURNDATASIZE | [0, 0, msg.data.size] | +3 | 37 | CALLDATACOPY | [] | Mem[0:msg.data.size-1] = msg.data[0:msg.data.size-1] +4 | 3D | RETURNDATASIZE | [0] | +5 | 3D | RETURNDATASIZE | [0, 0] | +6 | 3D | RETURNDATASIZE | [0, 0, 0] | +7 | 3D | RETURNDATASIZE | [0, 0, 0, 0] | +8 | 36 | CALLDATASIZE | [msg.data.size, 0, 0, 0, 0] | +9 | 3D | RETURNDATASIZE | [0, msg.data.size, 0, 0, 0, 0] | +10 | 73 MC | PUSH20 MC | [mcAddr,0, msg.data.size, 0, 0, 0, 0] | mcAddr = address of master Copy, injected by factory +31 | 5A | GAS | [rGas, mcAddr,0, msg.data.size, 0, 0, 0, 0] | rGas = remaining gas +32 | F4 | DELEGATECALL | [isSuccess, 0, 0] | isSuccess, Mem[0:0] = address(mcAddr).delegateCall.gas(rGas)(Mem[0:msg.data.size-1]) +33 | 3D | RETURNDATASIZE | [rds, isSuccess, 0, 0] | rds = size of what the logic called returned +34 | 92 | SWAP3 | [0, isSuccess, 0, rds] | +35 | 3D | RETURNDATASIZE | [rds, 0, isSuccess, 0, rds] | +36 | 90 | SWAP1 | [0, rds, isSuccess, 0, rds] | +37 | 80 | DUP1 | [0, 0, rds, isSuccess, 0, rds] | +38 | 3E | RETURNDATACOPY | [isSuccess, 0, rds] | Mem[0:rds-1] = RETURNDATA[0:rds-1] +39 | 60 2B | PUSH1 2B | [43, isSuccess, 0, rds] | +41 | 57 | JUMPI | [0, rds] | if(isSuccess) then jump to PC=43 +42 | FD | REVERT | [] | revert(Mem[0, rds-1]) +43 | 5B | JUMPDEST | [0, rds] | +44 | F3 | RETURN | [] | return(Mem[0, rds-1]) + */ + +/** Factory of Proxies to the CustomSmartWallet (Forwarder) +The Forwarder itself is a Template with portions delegated to a custom logic (it is also a proxy) */ +contract CustomSmartWalletFactory is ICustomSmartWalletFactory { + using ECDSA for bytes32; + + bytes11 private constant RUNTIME_START = hex"363D3D373D3D3D3D363D73"; + bytes14 private constant RUNTIME_END = hex"5AF43D923D90803E602B57FD5BF3"; + address public masterCopy; // this is the ForwarderProxy contract that will be proxied + bytes32 public constant DATA_VERSION_HASH = keccak256("2"); + + // Nonces of senders, used to prevent replay attacks + mapping(address => uint256) private nonces; + + /** + * @param forwarderTemplate It implements all the payment and execution needs, + * it pays for the deployment during initialization, and it pays for the transaction + * execution on each execute() call. + * It also acts a a proxy to a logic contract. Any unrecognized function will be forwarded to this custom logic (if it exists) + */ + constructor(address forwarderTemplate) public { + masterCopy = forwarderTemplate; + } + + + function runtimeCodeHash() external override view returns (bytes32){ + return keccak256( + abi.encodePacked(RUNTIME_START, masterCopy, RUNTIME_END) + ); + } + + function nonce(address from) public view override returns (uint256) { + return nonces[from]; + } + + function createUserSmartWallet( + address owner, + address recoverer, + address logic, + uint256 index, + bytes calldata initParams, + bytes calldata sig + ) external override { + bytes memory packed = + abi.encodePacked( + "\x19\x10", + owner, + recoverer, + logic, + index, + initParams + ); + (sig); + require( + RSKAddrValidator.safeEquals(keccak256(packed).recover(sig), owner), + "Invalid signature" + ); + + //e6ddc71a => initialize(address owner,address logic,address tokenAddr,address tokenRecipient,uint256 tokenAmount,uint256 tokenGas,bytes initParams) + bytes memory initData = + abi.encodeWithSelector( + hex"e6ddc71a", + owner, + logic, + address(0), // This "gas-funded" call does not pay with tokens + address(0), + 0, + 0, //No token transfer + initParams + ); + + deploy( + getCreationBytecode(), + keccak256( + abi.encodePacked( + owner, + recoverer, + logic, + keccak256(initParams), + index + ) + ), + initData + ); + } + + function relayedUserSmartWalletCreation( + IForwarder.DeployRequest memory req, + bytes32 domainSeparator, + bytes32 suffixData, + bytes calldata sig + ) external override { + (sig); + require(msg.sender == req.relayHub, "Invalid caller"); + _verifySig(req, domainSeparator, suffixData, sig); + nonces[req.from]++; + + //e6ddc71a => initialize(address owner,address logic,address tokenAddr,address tokenRecipient,uint256 tokenAmount,uint256 tokenGas,bytes initParams) + //a9059cbb = transfer(address _to, uint256 _value) public returns (bool success) + //initParams (req.data) must not contain the function selector for the logic initialization function + /* solhint-disable avoid-tx-origin */ + deploy( + getCreationBytecode(), + keccak256( + abi.encodePacked( + req.from, + req.recoverer, + req.to, + keccak256(req.data), + req.index + ) + ), + abi.encodeWithSelector( + hex"e6ddc71a", + req.from, + req.to, + req.tokenContract, + tx.origin, + req.tokenAmount, + req.tokenGas, + req.data + ) + ); + } + + /** + * Calculates the Smart Wallet address for an owner EOA, wallet logic, and specific initialization params + * @param owner - EOA of the owner of the smart wallet + * @param recoverer - Address of that can be used by some contracts to give specific roles to the caller (e.g, a recoverer) + * @param logic - Custom logic to use in the smart wallet (address(0) if no extra logic needed) + * @param initParamsHash - If there's a custom logic, these are the params to call initialize(bytes) (function sig must not be included). Only the hash value is passed + * @param index - Allows to create many addresses for the same owner|recoverer|logic|initParams + */ + function getSmartWalletAddress( + address owner, + address recoverer, + address logic, + bytes32 initParamsHash, + uint256 index + ) external view override returns (address) { + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + keccak256( + abi.encodePacked( + owner, + recoverer, + logic, + initParamsHash, + index + ) + ), + keccak256(getCreationBytecode()) + ) + ) + ) + ) + ); + } + + function deploy( + bytes memory code, + bytes32 salt, + bytes memory initdata + ) internal returns (address addr) { + //Deployment of the Smart Wallet + /* solhint-disable-next-line no-inline-assembly */ + assembly { + addr := create2(0, add(code, 0x20), mload(code), salt) + if iszero(extcodesize(addr)) { + revert(0, 0) + } + } + + //Since the init code determines the address of the smart wallet, any initialization + //required is done via the runtime code, to avoid the parameters impacting on the resulting address + + /* solhint-disable-next-line avoid-low-level-calls */ + (bool success, ) = addr.call(initdata); + + require(success, "Unable to initialize SW"); + + //No info is returned, an event is emitted to inform the new deployment + emit Deployed(addr, uint256(salt)); + } + + // Returns the proxy code to that is deployed on every Smart Wallet creation + function getCreationBytecode() public view override returns (bytes memory) { + //The code to install: constructor, runtime start, master copy, runtime end + return + abi.encodePacked( + hex"602D3D8160093D39F3", + RUNTIME_START, + masterCopy, + RUNTIME_END + ); + } + + function _getEncoded( + IForwarder.DeployRequest memory req, + bytes32 suffixData + ) public pure returns (bytes memory) { + return + abi.encodePacked( + keccak256( + "RelayRequest(address relayHub,address from,address to,address tokenContract,address recoverer,uint256 value,uint256 nonce,uint256 tokenAmount,uint256 tokenGas,uint256 index,bytes data,RelayData relayData)RelayData(uint256 gasPrice,bytes32 domainSeparator,address relayWorker,address callForwarder,address callVerifier)" + ), + abi.encode( + req.relayHub, + req.from, + req.to, + req.tokenContract, + req.recoverer, + req.value, + req.nonce, + req.tokenAmount, + req.tokenGas, + req.index, + keccak256(req.data) + ), + suffixData + ); + } + + function getChainID() internal pure returns (uint256 id) { + /* solhint-disable no-inline-assembly */ + assembly { + id := chainid() + } + } + + function _verifySig( + IForwarder.DeployRequest memory req, + bytes32 domainSeparator, + bytes32 suffixData, + bytes memory sig + ) internal view { + //Verify nonce + require(nonces[req.from] == req.nonce, "nonce mismatch"); + + //Verify Domain separator + require( + keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), //EIP712DOMAIN_TYPEHASH + keccak256("RSK Enveloping Transaction"), // DOMAIN_NAME + DATA_VERSION_HASH, + getChainID(), + address(this) + ) + ) == domainSeparator, + "Invalid domain separator" + ); + + require( + RSKAddrValidator.safeEquals( + keccak256( + abi.encodePacked( + "\x19\x01", + domainSeparator, + keccak256(_getEncoded(req, suffixData)) + ) + ) + .recover(sig), + req.from + ), + "signature mismatch" + ); + } +} diff --git a/contracts/factory/SmartWalletFactory.sol b/contracts/factory/SmartWalletFactory.sol new file mode 100644 index 00000000..f044cfee --- /dev/null +++ b/contracts/factory/SmartWalletFactory.sol @@ -0,0 +1,301 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "../interfaces/ISmartWalletFactory.sol"; +import "../utils/RSKAddrValidator.sol"; + +/* solhint-disable no-inline-assembly */ +/* solhint-disable avoid-low-level-calls */ + +/** +==================================================================================================================================================== + Documentation of the Proxy Code being deployed +==================================================================================================================================================== +A simple proxy that delegates every call to an address. +This ProxyA is the one instantiated per smart wallet, and it will receive the Forwarder as MC. So every call +made to this proxy will end up in MC. +MC is controlled by the same developer who created the factory, and . +For the transaction execution (execute() call), MC will do the signature verification and payment. Then it will +execute the request + + + + +==================================================================================================================================================== + PROXY A +==================================================================================================================================================== + Constructor +==================================================================================================================================================== +PC | OPCODE| Mnemonic | Stack [top, bottom] | Comments +---------------------------------------------------------------------------------------------------------------------------------------------------- +0 | 60 2D | PUSH1 2D | [45] | Size of runtime code +2 | 3D | RETURNDATASIZE | [0, 45] | Before any external call, returdatasize = 0 (cheaper than PUSH1 00) +3 | 81 | DUP2 | [45, 0, 45] | +4 | 60 09 | PUSH1 09 | [9, 45, 0, 45] | Size of constructor code +6 | 3D | RETURNDATASIZE | [0, 9, 45, 0, 45] | +7 | 39 | CODECOPY | [0, 45] | Mem[0:44] = address(this).code[9:53] +8 | F3 | RETURN | [] | return Mem[0:44] + +==================================================================================================================================================== + Runtime Code +==================================================================================================================================================== +PC | OPCODE| Mnemonic | Stack [top, bottom] | Comments +---------------------------------------------------------------------------------------------------------------------------------- +0 | 36 | CALLDATASIZE | [msg.data.size] | +1 | 3D | RETURNDATASIZE | [0, msg.data.size] | +2 | 3D | RETURNDATASIZE | [0, 0, msg.data.size] | +3 | 37 | CALLDATACOPY | [] | Mem[0:msg.data.size-1] = msg.data[0:msg.data.size-1] +4 | 3D | RETURNDATASIZE | [0] | +5 | 3D | RETURNDATASIZE | [0, 0] | +6 | 3D | RETURNDATASIZE | [0, 0, 0] | +7 | 3D | RETURNDATASIZE | [0, 0, 0, 0] | +8 | 36 | CALLDATASIZE | [msg.data.size, 0, 0, 0, 0] | +9 | 3D | RETURNDATASIZE | [0, msg.data.size, 0, 0, 0, 0] | +10 | 73 MC | PUSH20 MC | [mcAddr,0, msg.data.size, 0, 0, 0, 0] | mcAddr = address of master Copy, injected by factory +31 | 5A | GAS | [rGas, mcAddr,0, msg.data.size, 0, 0, 0, 0] | rGas = remaining gas +32 | F4 | DELEGATECALL | [isSuccess, 0, 0] | isSuccess, Mem[0:0] = address(mcAddr).delegateCall.gas(rGas)(Mem[0:msg.data.size-1]) +33 | 3D | RETURNDATASIZE | [rds, isSuccess, 0, 0] | rds = size of what the logic called returned +34 | 92 | SWAP3 | [0, isSuccess, 0, rds] | +35 | 3D | RETURNDATASIZE | [rds, 0, isSuccess, 0, rds] | +36 | 90 | SWAP1 | [0, rds, isSuccess, 0, rds] | +37 | 80 | DUP1 | [0, 0, rds, isSuccess, 0, rds] | +38 | 3E | RETURNDATACOPY | [isSuccess, 0, rds] | Mem[0:rds-1] = RETURNDATA[0:rds-1] +39 | 60 2B | PUSH1 2B | [43, isSuccess, 0, rds] | +41 | 57 | JUMPI | [0, rds] | if(isSuccess) then jump to PC=43 +42 | FD | REVERT | [] | revert(Mem[0, rds-1]) +43 | 5B | JUMPDEST | [0, rds] | +44 | F3 | RETURN | [] | return(Mem[0, rds-1]) + */ + +/** Factory of Proxies to the SmartWallet (Forwarder) + */ +contract SmartWalletFactory is ISmartWalletFactory { + using ECDSA for bytes32; + + bytes11 private constant RUNTIME_START = hex"363D3D373D3D3D3D363D73"; + bytes14 private constant RUNTIME_END = hex"5AF43D923D90803E602B57FD5BF3"; + address public masterCopy; // this is the ForwarderProxy contract that will be proxied + bytes32 public constant DATA_VERSION_HASH = keccak256("2"); + + // Nonces of senders, used to prevent replay attacks + mapping(address => uint256) private nonces; + + /** + * @param forwarderTemplate It implements all the payment and execution needs, + * it pays for the deployment during initialization, and it pays for the transaction + * execution on each execute() call. + */ + constructor(address forwarderTemplate) public { + masterCopy = forwarderTemplate; + } + + function runtimeCodeHash() external override view returns (bytes32){ + return keccak256( + abi.encodePacked(RUNTIME_START, masterCopy, RUNTIME_END) + ); + } + + function nonce(address from) public override view returns (uint256) { + return nonces[from]; + } + + function createUserSmartWallet( + address owner, + address recoverer, + uint256 index, + bytes calldata sig + ) external override { + bytes memory packed = abi.encodePacked( + "\x19\x10", + owner, + recoverer, + index + ); + + require(RSKAddrValidator.safeEquals(keccak256(packed).recover(sig),owner), "Invalid signature"); + + //a6b63eb8 => initialize(address owner,address tokenAddr,address tokenRecipient,uint256 tokenAmount,uint256 tokenGas) + bytes memory initData = abi.encodeWithSelector( + hex"a6b63eb8", + owner, + address(0), // This "gas-funded" call does not pay with tokens + address(0), + 0, + 0 //No token transfer + ); + + deploy(getCreationBytecode(), keccak256( + abi.encodePacked( + owner, + recoverer, + index + ) // salt + ), initData); + } + + function relayedUserSmartWalletCreation( + IForwarder.DeployRequest memory req, + bytes32 domainSeparator, + bytes32 suffixData, + bytes calldata sig + ) external override { + + require(msg.sender == req.relayHub, "Invalid caller"); + _verifySig(req, domainSeparator, suffixData, sig); + nonces[req.from]++; + + //a6b63eb8 => initialize(address owner,address tokenAddr,address tokenRecipient,uint256 tokenAmount,uint256 tokenGas) + //a9059cbb = transfer(address _to, uint256 _value) public returns (bool success) + /* solhint-disable avoid-tx-origin */ + deploy(getCreationBytecode(), keccak256( + abi.encodePacked( + req.from, + req.recoverer, + req.index + ) // salt + ), abi.encodeWithSelector( + hex"a6b63eb8", + req.from, + req.tokenContract, + tx.origin, + req.tokenAmount, + req.tokenGas + )); + } + + /** + * Calculates the Smart Wallet address for an owner EOA + * @param owner - EOA of the owner of the smart wallet + * @param recoverer - Address of that can be used by some contracts to give specific roles to the caller (e.g, a recoverer) + * @param index - Allows to create many addresses for the same owner|recoverer + */ + function getSmartWalletAddress( + address owner, + address recoverer, + uint256 index + ) external override view returns (address) { + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + keccak256( + abi.encodePacked( + owner, + recoverer, + index + ) + ), // salt + keccak256(getCreationBytecode()) + ) + ) + ) + ) + ); + } + + function deploy( + bytes memory code, + bytes32 salt, + bytes memory initdata + ) internal returns (address addr) { + + //Deployment of the Smart Wallet + /* solhint-disable-next-line no-inline-assembly */ + assembly { + addr := create2(0, add(code, 0x20), mload(code), salt) + if iszero(extcodesize(addr)) { + revert(0, 0) + } + } + + //Since the init code determines the address of the smart wallet, any initialization + //required is done via the runtime code, to avoid the parameters impacting on the resulting address + + /* solhint-disable-next-line avoid-low-level-calls */ + (bool success, ) = addr.call(initdata); + + /* solhint-disable-next-line reason-string */ + require(success, "Unable to initialize SW"); + + //No info is returned, an event is emitted to inform the new deployment + emit Deployed(addr, uint256(salt)); + } + + // Returns the proxy code to that is deployed on every Smart Wallet creation + function getCreationBytecode() public override view returns (bytes memory) { + //The code to install: constructor, runtime start, master copy, runtime end + return abi.encodePacked(hex"602D3D8160093D39F3", RUNTIME_START, masterCopy, RUNTIME_END); + } + + function _getEncoded( + IForwarder.DeployRequest memory req, + bytes32 suffixData + ) public pure returns (bytes memory) { + return + abi.encodePacked( + keccak256("RelayRequest(address relayHub,address from,address to,address tokenContract,address recoverer,uint256 value,uint256 nonce,uint256 tokenAmount,uint256 tokenGas,uint256 index,bytes data,RelayData relayData)RelayData(uint256 gasPrice,bytes32 domainSeparator,address relayWorker,address callForwarder,address callVerifier)"), + abi.encode( + req.relayHub, + req.from, + req.to, + req.tokenContract, + req.recoverer, + req.value, + req.nonce, + req.tokenAmount, + req.tokenGas, + req.index, + keccak256(req.data) + ), + suffixData + ); + } + + function getChainID() internal pure returns (uint256 id) { + /* solhint-disable no-inline-assembly */ + assembly { + id := chainid() + } + } + + function _verifySig( + IForwarder.DeployRequest memory req, + bytes32 domainSeparator, + bytes32 suffixData, + bytes memory sig + ) internal view { + + //Verify nonce + require(nonces[req.from] == req.nonce, "nonce mismatch"); + + //Verify Domain separator + require( + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),//EIP712DOMAIN_TYPEHASH + keccak256("RSK Enveloping Transaction"),// DOMAIN_NAME + DATA_VERSION_HASH, + getChainID(), + address(this) + ) + ) == domainSeparator, + "Invalid domain separator" + ); + + require( + RSKAddrValidator.safeEquals( + keccak256(abi.encodePacked( + "\x19\x01", + domainSeparator, + keccak256(_getEncoded(req, suffixData))) + ).recover(sig), req.from),"signature mismatch" + ); + } + +} diff --git a/contracts/interfaces/EnvelopingTypes.sol b/contracts/interfaces/EnvelopingTypes.sol new file mode 100644 index 00000000..bdc4a225 --- /dev/null +++ b/contracts/interfaces/EnvelopingTypes.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +import "./IForwarder.sol"; + +interface EnvelopingTypes { + struct RelayData { + uint256 gasPrice; + bytes32 domainSeparator; + address relayWorker; + address callForwarder; + address callVerifier; + } + + struct RelayRequest { + IForwarder.ForwardRequest request; + RelayData relayData; + } + + struct DeployRequest { + IForwarder.DeployRequest request; + RelayData relayData; + } +} diff --git a/contracts/interfaces/ICustomSmartWalletFactory.sol b/contracts/interfaces/ICustomSmartWalletFactory.sol new file mode 100644 index 00000000..861227b0 --- /dev/null +++ b/contracts/interfaces/ICustomSmartWalletFactory.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "./IWalletFactory.sol"; + +interface ICustomSmartWalletFactory is IWalletFactory{ + + function createUserSmartWallet( + address owner, + address recoverer, + address logic, + uint256 index, + bytes calldata initParams, + bytes calldata sig + ) external; + + function getSmartWalletAddress( + address owner, + address recoverer, + address logic, + bytes32 initParamsHash, + uint256 index + ) external view returns (address); + +} diff --git a/contracts/interfaces/IDeployVerifier.sol b/contracts/interfaces/IDeployVerifier.sol new file mode 100644 index 00000000..3f21720c --- /dev/null +++ b/contracts/interfaces/IDeployVerifier.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "./EnvelopingTypes.sol"; + +interface IDeployVerifier { + function versionVerifier() external view returns (string memory); + + /** + * Called by Relay to validate the parameters of the request + * + * + * @param relayRequest - the full relay request structure + * @param signature - user's EIP712-compatible signature of the {@link relayRequest}. + * Note that in most cases the verifier shouldn't try use it at all. It is always checked + * by the forwarder immediately after verifyRelayedCall returns. + */ + function verifyRelayedCall( + EnvelopingTypes.DeployRequest calldata relayRequest, + bytes calldata signature + ) external returns (bytes memory context); +} diff --git a/contracts/interfaces/IForwarder.sol b/contracts/interfaces/IForwarder.sol new file mode 100644 index 00000000..c6f5a99b --- /dev/null +++ b/contracts/interfaces/IForwarder.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +interface IForwarder { + + struct ForwardRequest { + address relayHub; + address from; + address to; + address tokenContract; + uint256 value; + uint256 gas; + uint256 nonce; + uint256 tokenAmount; + uint256 tokenGas; + bytes data; + } + + struct DeployRequest { + address relayHub; + address from; + address to; // In a deploy request, the to param inidicates an optional logic contract + address tokenContract; + address recoverer; // only used in SmartWallet deploy requests + uint256 value; + uint256 nonce; + uint256 tokenAmount; + uint256 tokenGas; + uint256 index; // only used in SmartWallet deploy requests + bytes data; + } + + + + function nonce() + external view + returns(uint256); + + /** + * verify the transaction would execute. + * validate the signature and the nonce of the request. + * revert if either signature or nonce are incorrect. + */ + function verify( + bytes32 domainSeparator, + bytes32 suffixData, + ForwardRequest calldata forwardRequest, + bytes calldata signature + ) external view; + + /** + * execute a transaction + * @param forwardRequest - all transaction parameters + * @param domainSeparator - domain used when signing this request + * @param suffixData - the extension data used when signing this request. + * @param signature - signature to validate. + * + * the transaction is verified, and then executed. + * the success and ret of "call" are returned. + * This method would revert only verification errors. target errors + * are reported using the returned "success" and ret string + */ + function execute( + bytes32 domainSeparator, + bytes32 suffixData, + ForwardRequest calldata forwardRequest, + bytes calldata signature + ) + external payable + returns (bool success, bytes memory ret); + + function directExecute(address to, bytes calldata data) external payable returns ( + bool success, + bytes memory ret + ); +} diff --git a/contracts/interfaces/IPenalizer.sol b/contracts/interfaces/IPenalizer.sol new file mode 100644 index 00000000..f1b7f6fc --- /dev/null +++ b/contracts/interfaces/IPenalizer.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +import "./IRelayHub.sol"; + +interface IPenalizer { + + struct Transaction { + uint256 nonce; + uint256 gasPrice; + uint256 gasLimit; + address to; + uint256 value; + bytes data; + } + + function penalizeRepeatedNonce( + bytes calldata unsignedTx1, + bytes calldata signature1, + bytes calldata unsignedTx2, + bytes calldata signature2, + IRelayHub hub + ) external; + + function versionPenalizer() external view returns (string memory); +} diff --git a/contracts/interfaces/IRelayHub.sol b/contracts/interfaces/IRelayHub.sol new file mode 100644 index 00000000..6eb8f294 --- /dev/null +++ b/contracts/interfaces/IRelayHub.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "./EnvelopingTypes.sol"; + +interface IRelayHub { + // Emitted when a relay server registers or updates its details + // Looking at these events lets a client discover relay servers + event RelayServerRegistered( + address indexed relayManager, + string relayUrl); + + // Emitted when relays are added by a relayManager + event RelayWorkersAdded( + address indexed relayManager, + address[] newRelayWorkers, + uint256 workersCount + ); + + // Emitted when relays are removed by a relayManager + event RelayWorkersDisabled( + address indexed relayManager, + address[] relayWorkers, + uint256 workersCount + ); + + // Emitted when a transaction is relayed. Note that the actual encoded function might be reverted: this will be + // indicated in the status field. + // Useful when monitoring a relay's operation and relayed calls to a contract. + // Charge is the ether value deducted from the recipient's balance, paid to the relay's manager. + event TransactionRelayed( + address indexed relayManager, + address relayWorker, + bytes32 relayRequestSigHash, + bytes relayedCallReturnValue); + + event TransactionRelayedButRevertedByRecipient( + address indexed relayManager, + address relayWorker, + bytes32 relayRequestSigHash, + bytes reason); + + event TransactionResult( + bytes returnValue + ); + + event Penalized( + address indexed relayWorker, + address sender, + uint256 reward + ); + + /// Add new worker addresses controlled by 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; + + // Disable a relayWorker account so it cannot relay calls anymore (e.g, if the account was compromised) + // Once disabled, a relay worker cannot be re-enabled + function disableRelayWorkers(address[] calldata relayWorkers) external; + + function registerRelayServer(string calldata url) external; + + /// Relays a transaction. For this to succeed, multiple conditions must be met: + /// - the sender must be a registered Relay Worker that the user signed + /// - the transaction's gas price must be equal or larger than the one that was signed by the sender + /// - the transaction must have enough gas to run all internal transactions if they use all gas available to them + /// + /// If all conditions are met, the call will be relayed and the recipient charged. + /// + /// Arguments: + /// @param relayRequest - all details of the requested relayed call + /// @param signature - client's EIP-712 signature over the relayRequest struct + /// + /// Emits a TransactionRelayed event. + /// destinationCallSuccess - indicates whether the call to the destination contract's function was successfull or not, + /// is can be false when TransactionRelayedButRevertedByRecipient is emitted. + function relayCall( + EnvelopingTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) + external returns (bool destinationCallSuccess); + + function deployCall( + EnvelopingTypes.DeployRequest calldata deployRequest, + bytes calldata signature ) + external; + + function penalize(address relayWorker, address payable beneficiary) external; + + /* getters */ + function penalizer() external view returns(address); + + // Minimum stake a relay can have. An attack to the network will never cost less than half this value. + function minimumStake() external view returns (uint256); + + // Minimum unstake delay blocks of a relay manager's stake + function minimumUnstakeDelay() external view returns (uint256); + + // maximum number of worker account allowed per manager + function maxWorkerCount() external view returns (uint256); + + function workerToManager(address worker) external view returns(bytes32); + + function workerCount(address manager) external view returns(uint256); + + function isRelayManagerStaked(address relayManager) external view returns(bool); + + function versionHub() external view returns (string memory); + + /// Emitted when a stake or unstakeDelay are initialized or increased + event StakeAdded( + address indexed relayManager, + address indexed owner, + uint256 stake, + uint256 unstakeDelay + ); + + /// Emitted once a stake is scheduled for withdrawal + event StakeUnlocked( + address indexed relayManager, + address indexed owner, + uint256 withdrawBlock + ); + + /// Emitted when owner withdraws relayManager funds + event StakeWithdrawn( + address indexed relayManager, + address indexed owner, + uint256 amount + ); + + /// Emitted when an authorized Relay Hub penalizes a relayManager + event StakePenalized( + address indexed relayManager, + address indexed beneficiary, + uint256 reward + ); + + // @param stake - amount of ether staked for this relay + // @param unstakeDelay - number of blocks to elapse before the owner can retrieve the stake after calling 'unlock' + // @param withdrawBlock - first block number '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 withdrawBlock; + address payable owner; + } + + // Put a stake for a relayManager and set its unstake delay. + // If the entry does not exist, it is created, and the caller of this function becomes its owner. + // If the entry already exists, only the owner can call this function. + // @param relayManager - address that represents a stake entry and controls relay registrations on relay hubs + // @param unstakeDelay - number of blocks to elapse before the owner can retrieve the stake after calling 'unlock' + function stakeForAddress(address relayManager, uint256 unstakeDelay) external payable; + + function unlockStake(address relayManager) external; + + function withdrawStake(address relayManager) external; + + function getStakeInfo(address relayManager) external view returns (StakeInfo memory stakeInfo); + + //For initial stakes, this is the minimum stake value allowed for taking ownership of this address' stake + function minimumEntryDepositValue() external view returns (uint256); +} diff --git a/contracts/interfaces/IRelayVerifier.sol b/contracts/interfaces/IRelayVerifier.sol new file mode 100644 index 00000000..ee07f8f4 --- /dev/null +++ b/contracts/interfaces/IRelayVerifier.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "./EnvelopingTypes.sol"; + +interface IRelayVerifier { + function versionVerifier() external view returns (string memory); + + /** + * Called by Relay to validate the parameters of the request + * + * + * @param relayRequest - the full relay request structure + * @param signature - user's EIP712-compatible signature of the {@link relayRequest}. + * Note that in most cases the verifier shouldn't try use it at all. It is always checked + * by the forwarder immediately after verifyRelayedCall returns. + */ + function verifyRelayedCall( + EnvelopingTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) external returns (bytes memory context); +} diff --git a/contracts/interfaces/ISmartWalletFactory.sol b/contracts/interfaces/ISmartWalletFactory.sol new file mode 100644 index 00000000..3fae89e3 --- /dev/null +++ b/contracts/interfaces/ISmartWalletFactory.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "./IWalletFactory.sol"; + +interface ISmartWalletFactory is IWalletFactory{ + function createUserSmartWallet( + address owner, + address recoverer, + uint256 index, + bytes calldata sig + ) external; + + function getSmartWalletAddress( + address owner, + address recoverer, + uint256 index + ) external view returns (address); +} diff --git a/contracts/interfaces/ITokenHandler.sol b/contracts/interfaces/ITokenHandler.sol new file mode 100644 index 00000000..b69cfb46 --- /dev/null +++ b/contracts/interfaces/ITokenHandler.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +interface ITokenHandler { + /** + * Returns a list of accepted tokens + */ + function getAcceptedTokens() external view returns (address[] memory); + + /** + * Returns a true if a token is accepted + */ + function acceptsToken(address token) external view returns (bool); +} diff --git a/contracts/interfaces/IVersionRegistry.sol b/contracts/interfaces/IVersionRegistry.sol new file mode 100644 index 00000000..341bc254 --- /dev/null +++ b/contracts/interfaces/IVersionRegistry.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +interface IVersionRegistry { + + //event emitted whenever a version is added + event VersionAdded(bytes32 indexed id, bytes32 version, string value, uint time); + + //event emitted whenever a version is canceled + event VersionCanceled(bytes32 indexed id, bytes32 version, string reason); + + /** + * add a version + * @param id the object-id to add a version (32-byte string) + * @param version the new version to add (32-byte string) + * @param value value to attach to this version + */ + function addVersion(bytes32 id, bytes32 version, string calldata value) external; + + /** + * cancel a version. + */ + function cancelVersion(bytes32 id, bytes32 version, string calldata reason) external; +} diff --git a/contracts/interfaces/IWalletCustomLogic.sol b/contracts/interfaces/IWalletCustomLogic.sol new file mode 100644 index 00000000..1e3350dc --- /dev/null +++ b/contracts/interfaces/IWalletCustomLogic.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "./IForwarder.sol"; + +/** + * Interface defining the methods that should be implemented + * in order to provide custom logic to a CustomSmartWallet + */ +interface IWalletCustomLogic { + + /** + * Setup any data required by the custom logic + * @param initParams Data required in order to initilize custom logic + */ + function initialize(bytes memory initParams) external; + + /** + * Lets a relayer execute the custom logic + */ + function execute( + bytes32 domainSeparator, + bytes32 suffixData, + IForwarder.ForwardRequest calldata forwardRequest, + bytes calldata signature + ) + external payable + returns (bytes memory ret); + + /** + * Lets an account with RBTC execute the custom logic + * @param to Target contract address + * @param data Destination function + */ + function directExecute(address to, bytes calldata data) external payable returns ( + bytes memory ret + ); +} \ No newline at end of file diff --git a/contracts/interfaces/IWalletFactory.sol b/contracts/interfaces/IWalletFactory.sol new file mode 100644 index 00000000..fa5ef46a --- /dev/null +++ b/contracts/interfaces/IWalletFactory.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "./IForwarder.sol"; + +interface IWalletFactory { + + function nonce (address from) external view returns(uint256); + + function runtimeCodeHash() external view returns (bytes32); + + function relayedUserSmartWalletCreation( + IForwarder.DeployRequest memory req, + bytes32 domainSeparator, + bytes32 suffixData, + bytes calldata sig + ) external; + + + function getCreationBytecode() external view returns (bytes memory); + + event Deployed(address indexed addr, uint256 salt); //Event triggered when a deploy is successful + +} \ No newline at end of file diff --git a/contracts/smartwallet/CustomSmartWallet.sol b/contracts/smartwallet/CustomSmartWallet.sol new file mode 100644 index 00000000..58a14895 --- /dev/null +++ b/contracts/smartwallet/CustomSmartWallet.sol @@ -0,0 +1,389 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "../interfaces/IForwarder.sol"; +import "../utils/RSKAddrValidator.sol"; + +/* solhint-disable no-inline-assembly */ +/* solhint-disable avoid-low-level-calls */ + +contract CustomSmartWallet is IForwarder { + using ECDSA for bytes32; + + uint256 public override nonce; + bytes32 public constant DATA_VERSION_HASH = keccak256("2"); + + function verify( + bytes32 domainSeparator, + bytes32 suffixData, + ForwardRequest memory req, + bytes calldata sig + ) external override view { + + _verifySig(domainSeparator, suffixData, req, sig); + } + + function getOwner() private view returns (bytes32 owner){ + assembly { + owner := sload( + 0xa7b53796fd2d99cb1f5ae019b54f9e024446c3d12b483f733ccc62ed04eb126a + ) + } + } + + function recover(address owner, address factory, address swTemplate, address destinationContract,address logic, uint256 index, bytes32 initParamsHash, bytes calldata data) external payable returns (bool success, bytes memory ret){ + + address wallet = + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + factory, + keccak256(abi.encodePacked(owner, msg.sender, logic, initParamsHash, index)), //salt + keccak256(abi.encodePacked(hex"602D3D8160093D39F3363D3D373D3D3D3D363D73", swTemplate, hex"5AF43D923D90803E602B57FD5BF3")) + ) + ) + ) + ) + ); + + + require(wallet == address(this), "Invalid recoverer"); + + if(destinationContract != address(0)){ + (success, ret) = destinationContract.call{value: msg.value}(data); + } + + //If any balance has been added then trasfer it to the owner EOA + if (address(this).balance > 0) { + //sent any value left to the recoverer account + payable(msg.sender).transfer(address(this).balance); + } + + } + + function directExecute(address to, bytes calldata data) external override payable returns ( + bool success, + bytes memory ret + ) + { + + //Verify Owner + require( + getOwner() == keccak256(abi.encodePacked(msg.sender)), + "Not the owner of the SmartWallet" + ); + + bytes32 logicStrg; + assembly { + logicStrg := sload( + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc + ) + } + + // If there's no extra logic, then call the destination contract + if (logicStrg == bytes32(0)) { + (success, ret) = to.call{value: msg.value}(data); + } else { + //If there's extra logic, delegate the execution + (success, ret) = (address(uint160(uint256(logicStrg)))).delegatecall(msg.data); + } + + //If any balance has been added then trasfer it to the owner EOA + if (address(this).balance > 0) { + //can't fail: req.from signed (off-chain) the request, so it must be an EOA... + payable(msg.sender).transfer(address(this).balance); + } + } + + function execute( + bytes32 domainSeparator, + bytes32 suffixData, + ForwardRequest memory req, + bytes calldata sig + ) + external + override + payable + returns ( + bool success, + bytes memory ret + ) + { + + (sig); + require(msg.sender == req.relayHub, "Invalid caller"); + + _verifySig(domainSeparator, suffixData, req, sig); + nonce++; + + if(req.tokenAmount > 0){ + /* solhint-disable avoid-tx-origin */ + (success, ret) = req.tokenContract.call{gas: req.tokenGas}( + abi.encodeWithSelector( + hex"a9059cbb", + tx.origin, + req.tokenAmount + ) + ); + + require( + success && (ret.length == 0 || abi.decode(ret, (bool))), + "Unable to pay for relay" + ); + } + + bytes32 logicStrg; + assembly { + logicStrg := sload( + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc + ) + } + + //Why this require is not needed: in the case that the EVM implementation + //sends gasleft() as req.gas if gasleft() < req.gas (see EIP-1930), which would end in the call reverting + //If the relayer made this on purpose in order to collect the payment, since all gasLeft() + //was sent to this call, then the next line would give an out of gas, and, as a consequence, will + //revert the whole transaction, and the payment will not happen + //But it could happen that the destination call makes a gasleft() check and decides to revert if it is + //not enough, in that case there might be enough gas to complete the relay and the token payment would be collected + //For that reason, the next require line must be left uncommented, to avoid malicious relayer attacks to destination contract + //methods that revert if the gasleft() is not enough to execute whatever logic they have. + + require(gasleft() > req.gas,"Not enough gas left"); + + // If there's no extra logic, then call the destination contract + if (logicStrg == bytes32(0)) { + (success, ret) = req.to.call{gas: req.gas, value: req.value}(req.data); + } else { + //If there's extra logic, delegate the execution + (success, ret) = (address(uint160(uint256(logicStrg)))).delegatecall(msg.data); + } + + //If any balance has been added then trasfer it to the owner EOA + uint256 balanceToTransfer = address(this).balance; + if ( balanceToTransfer > 0) { + //can't fail: req.from signed (off-chain) the request, so it must be an EOA... + payable(req.from).transfer(balanceToTransfer); + } + } + + function getChainID() private pure returns (uint256 id) { + assembly { + id := chainid() + } + } + + function _verifySig( + bytes32 domainSeparator, + bytes32 suffixData, + ForwardRequest memory req, + bytes memory sig + ) private view { + + //Verify Owner + require( + getOwner() == keccak256(abi.encodePacked(req.from)), + "Not the owner of the SmartWallet" + ); + + //Verify nonce + require(nonce == req.nonce, "nonce mismatch"); + + require( + keccak256(abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("RSK Enveloping Transaction"), //DOMAIN_NAME + DATA_VERSION_HASH, + getChainID(), + address(this))) == domainSeparator, + "Invalid domain separator" + ); + + require( + RSKAddrValidator.safeEquals( + keccak256(abi.encodePacked( + "\x19\x01", + domainSeparator, + keccak256(_getEncoded(suffixData, req))) + ).recover(sig), req.from), "signature mismatch" + ); + } + + function _getEncoded( + bytes32 suffixData, + ForwardRequest memory req + ) private pure returns (bytes memory) { + return + abi.encodePacked( + keccak256("RelayRequest(address relayHub,address from,address to,address tokenContract,uint256 value,uint256 gas,uint256 nonce,uint256 tokenAmount,uint256 tokenGas,bytes data,RelayData relayData)RelayData(uint256 gasPrice,bytes32 domainSeparator,address relayWorker,address callForwarder,address callVerifier)"), //requestTypeHash, + abi.encode( + req.relayHub, + req.from, + req.to, + req.tokenContract, + req.value, + req.gas, + req.nonce, + req.tokenAmount, + req.tokenGas, + keccak256(req.data) + ), + suffixData + ); + } + + function isInitialized() external view returns (bool) { + + if (getOwner() == bytes32(0)) { + return false; + } else { + return true; + } + } + + /** + * This Proxy will first charge for the deployment and then it will pass the + * initialization scope to the wallet logic. + * This function can only be called once, and it is called by the Factory during deployment + * @param owner - The EOA that will own the smart wallet + * @param logic - The address containing the custom logic where to delegate everything that is not payment-related + * @param tokenAddr - The Token used for payment of the deploy + * @param tokenRecipient - The recipient of the payment + * @param tokenAmount - The amount to pay + * @param initParams - Initialization data to pass to the custom logic's initialize(bytes) function + */ + + function initialize( + address owner, + address logic, + address tokenAddr, + address tokenRecipient, + uint256 tokenAmount, + uint256 tokenGas, + bytes memory initParams + ) external { + + require(getOwner() == bytes32(0), "already initialized"); + + //To avoid re-entrancy attacks by external contracts, the first thing we do is set + //the variable that controls "is initialized" + //We set this instance as initialized, by + //storing the logic address + //Set the owner of this Smart Wallet + //slot for owner = bytes32(uint256(keccak256('eip1967.proxy.owner')) - 1) = a7b53796fd2d99cb1f5ae019b54f9e024446c3d12b483f733ccc62ed04eb126a + bytes32 ownerCell = keccak256(abi.encodePacked(owner)); + + assembly { + sstore( + 0xa7b53796fd2d99cb1f5ae019b54f9e024446c3d12b483f733ccc62ed04eb126a, + ownerCell + ) + } + + //we need to initialize the contract + if (tokenAmount > 0) { + + (bool success, bytes memory ret ) = tokenAddr.call{gas:tokenGas}(abi.encodeWithSelector( + hex"a9059cbb", + tokenRecipient, + tokenAmount)); + + require( + success && (ret.length == 0 || abi.decode(ret, (bool))), + "Unable to pay for deployment"); + } + + //If no logic is injected at this point, then the Forwarder will never accept a custom logic (since + //the initialize function can only be called once) + if (logic != address(0) ) { + + //Initialize function of custom wallet logic must be initialize(bytes) = 439fab91 + (bool success, ) = logic.delegatecall(abi.encodeWithSelector( + hex"439fab91", + initParams + )); + + require( + success, + "initialize call in logic failed" + ); + + assembly { + //The slot used complies with EIP-1967, obtained as: + //bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) + sstore( + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc, + logic + ) + } + } + } + + function _fallback() private { + //Proxy code to the logic (if any) + + bytes32 logicStrg; + assembly { + logicStrg := sload( + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc + ) + } + + if (bytes32(0) != logicStrg) { + //If the storage cell is not empty + + address logic = address(uint160(uint256(logicStrg))); + + assembly { + let ptr := mload(0x40) + + // (1) copy incoming call data + calldatacopy(ptr, 0, calldatasize()) + + // (2) forward call to logic contract + let result := delegatecall( + gas(), + logic, + ptr, + calldatasize(), + 0, + 0 + ) + let size := returndatasize() + + // (3) retrieve return data + returndatacopy(ptr, 0, size) + + // (4) forward return data back to caller + switch result + case 0 { + revert(ptr, size) + } + default { + return(ptr, size) + } + } + } + } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data + * is empty. + */ + receive() payable external { + _fallback(); + } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other + * function in the contract matches the call data. + */ + fallback() payable external { + _fallback(); + } +} diff --git a/contracts/smartwallet/SmartWallet.sol b/contracts/smartwallet/SmartWallet.sol new file mode 100644 index 00000000..c3655b65 --- /dev/null +++ b/contracts/smartwallet/SmartWallet.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "../interfaces/IForwarder.sol"; +import "../utils/RSKAddrValidator.sol"; + +/* solhint-disable no-inline-assembly */ +/* solhint-disable avoid-low-level-calls */ + +contract SmartWallet is IForwarder { + using ECDSA for bytes32; + + uint256 public override nonce; + bytes32 public constant DATA_VERSION_HASH = keccak256("2"); + + function verify( + bytes32 domainSeparator, + bytes32 suffixData, + ForwardRequest memory req, + bytes calldata sig + ) external override view { + + _verifySig(domainSeparator, suffixData, req, sig); + } + + function getOwner() private view returns (bytes32 owner){ + assembly { + owner := sload( + 0xa7b53796fd2d99cb1f5ae019b54f9e024446c3d12b483f733ccc62ed04eb126a + ) + } + } + + + function recover(address owner, address factory, address swTemplate, address destinationContract, uint256 index, bytes calldata data) external payable returns (bool success, bytes memory ret){ + + address wallet = + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + factory, + keccak256(abi.encodePacked(owner, msg.sender, index)), //salt + keccak256(abi.encodePacked(hex"602D3D8160093D39F3363D3D373D3D3D3D363D73", swTemplate, hex"5AF43D923D90803E602B57FD5BF3")) + ) + ) + ) + ) + ); + + require(wallet == address(this), "Invalid recoverer"); + + if(destinationContract != address(0)){ + (success, ret) = destinationContract.call{value: msg.value}(data); + } + + //If any balance has been added then trasfer it to the owner EOA + if (address(this).balance > 0) { + //sent any value left to the recoverer account + payable(msg.sender).transfer(address(this).balance); + } + + } + + + function directExecute(address to, bytes calldata data) external override payable returns ( + bool success, + bytes memory ret + ) + { + + //Verify Owner + require( + getOwner() == keccak256(abi.encodePacked(msg.sender)), + "Not the owner of the SmartWallet" + ); + + (success, ret) = to.call{value: msg.value}(data); + + //If any balance has been added then trasfer it to the owner EOA + if (address(this).balance > 0) { + //can't fail: req.from signed (off-chain) the request, so it must be an EOA... + payable(msg.sender).transfer(address(this).balance); + } + + } + + function execute( + bytes32 domainSeparator, + bytes32 suffixData, + ForwardRequest memory req, + bytes calldata sig + ) + external + override + payable + returns ( + bool success, + bytes memory ret + ) + { + + (sig); + require(msg.sender == req.relayHub, "Invalid caller"); + + _verifySig(domainSeparator, suffixData, req, sig); + nonce++; + + if(req.tokenAmount > 0){ + /* solhint-disable avoid-tx-origin */ + (success, ret) = req.tokenContract.call{gas: req.tokenGas}( + abi.encodeWithSelector( + hex"a9059cbb", + tx.origin, + req.tokenAmount + ) + ); + + require( + success && (ret.length == 0 || abi.decode(ret, (bool))), + "Unable to pay for relay" + ); + } + + + //Why this require is not needed: in the case that the EVM implementation + //sends gasleft() as req.gas if gasleft() < req.gas (see EIP-1930), which would end in the call reverting + //If the relayer made this on purpose in order to collect the payment, since all gasLeft() + //was sent to this call, then the next line would give an out of gas, and, as a consequence, will + //revert the whole transaction, and the payment will not happen + //But it could happen that the destination call makes a gasleft() check and decides to revert if it is + //not enough, in that case there might be enough gas to complete the relay and the token payment would be collected + //For that reason, the next require line must be left uncommented, to avoid malicious relayer attacks to destination contract + //methods that revert if the gasleft() is not enough to execute whatever logic they have. + + require(gasleft() > req.gas,"Not enough gas left"); + (success, ret) = req.to.call{gas: req.gas, value: req.value}(req.data); + + //If any balance has been added then trasfer it to the owner EOA + uint256 balanceToTransfer = address(this).balance; + if ( balanceToTransfer > 0) { + //can't fail: req.from signed (off-chain) the request, so it must be an EOA... + payable(req.from).transfer(balanceToTransfer); + } + + } + + + + function getChainID() private pure returns (uint256 id) { + assembly { + id := chainid() + } + } + + function _verifySig( + bytes32 domainSeparator, + bytes32 suffixData, + ForwardRequest memory req, + bytes memory sig + ) private view { + + //Verify Owner + require( + getOwner() == keccak256(abi.encodePacked(req.from)), + "Not the owner of the SmartWallet" + ); + + //Verify nonce + require(nonce == req.nonce, "nonce mismatch"); + + require( + keccak256(abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), //hex"8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f", + keccak256("RSK Enveloping Transaction"), //DOMAIN_NAME hex"d41b7f69f4d7734774d21b5548d74704ad02f9f1545db63927a1d58479c576c8" + DATA_VERSION_HASH, + getChainID(), + address(this))) == domainSeparator, + "Invalid domain separator" + ); + + require( + RSKAddrValidator.safeEquals( + keccak256(abi.encodePacked( + "\x19\x01", + domainSeparator, + keccak256(_getEncoded(suffixData, req))) + ).recover(sig), req.from), "signature mismatch" + ); + } + + function _getEncoded( + bytes32 suffixData, + ForwardRequest memory req + ) private pure returns (bytes memory) { + return + abi.encodePacked( + keccak256("RelayRequest(address relayHub,address from,address to,address tokenContract,uint256 value,uint256 gas,uint256 nonce,uint256 tokenAmount,uint256 tokenGas,bytes data,RelayData relayData)RelayData(uint256 gasPrice,bytes32 domainSeparator,address relayWorker,address callForwarder,address callVerifier)"), //requestTypeHash, + abi.encode( + req.relayHub, + req.from, + req.to, + req.tokenContract, + req.value, + req.gas, + req.nonce, + req.tokenAmount, + req.tokenGas, + keccak256(req.data) + ), + suffixData + ); + } + + function isInitialized() external view returns (bool) { + + if (getOwner() == bytes32(0)) { + return false; + } else { + return true; + } + } + + /** + * This Proxy will first charge for the deployment and then it will pass the + * initialization scope to the wallet logic. + * This function can only be called once, and it is called by the Factory during deployment + * @param owner - The EOA that will own the smart wallet + * @param tokenAddr - The Token used for payment of the deploy + * @param tokenRecipient - Recipient of payment + * @param tokenAmount - Amount to pay + */ + + function initialize( + address owner, + address tokenAddr, + address tokenRecipient, + uint256 tokenAmount, + uint256 tokenGas + ) external { + + require(getOwner() == bytes32(0), "already initialized"); + + //Set the owner of this Smart Wallet + //slot for owner = bytes32(uint256(keccak256('eip1967.proxy.owner')) - 1) = a7b53796fd2d99cb1f5ae019b54f9e024446c3d12b483f733ccc62ed04eb126a + bytes32 ownerCell = keccak256(abi.encodePacked(owner)); + + assembly { + sstore( + 0xa7b53796fd2d99cb1f5ae019b54f9e024446c3d12b483f733ccc62ed04eb126a, + ownerCell + ) + } + + //we need to initialize the contract + if (tokenAmount > 0) { + (bool success, bytes memory ret ) = tokenAddr.call{gas: tokenGas}(abi.encodeWithSelector( + hex"a9059cbb", + tokenRecipient, + tokenAmount)); + + require( + success && (ret.length == 0 || abi.decode(ret, (bool))), + "Unable to pay for deployment"); + } + + } + + /* solhint-disable no-empty-blocks */ + receive() payable external { + + } +} diff --git a/contracts/test/ERC20Mod.sol b/contracts/test/ERC20Mod.sol new file mode 100644 index 00000000..1fb11b08 --- /dev/null +++ b/contracts/test/ERC20Mod.sol @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.12; + +import "@openzeppelin/contracts/GSN/Context.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin guidelines: functions revert instead + * of returning `false` on failure. This behavior is nonetheless conventional + * and does not conflict with the expectations of ERC20 applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +/* solhint-disable reason-string, state-visibility, no-empty-blocks */ +contract ERC20Mod is Context, IERC20 { + using SafeMath for uint256; + using Address for address; + + mapping (address => uint256) _balances; + + mapping (address => mapping (address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + uint8 private _decimals; + + /** + * @dev Sets the values for {name} and {symbol}, initializes {decimals} with + * a default value of 18. + * + * To select a different value for {decimals}, use {_setupDecimals}. + * + * All three of these values are immutable: they can only be set once during + * construction. + */ + constructor (string memory name, string memory symbol) public { + _name = name; + _symbol = symbol; + _decimals = 18; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5,05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the value {ERC20} uses, unless {_setupDecimals} is + * called. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view returns (uint8) { + return _decimals; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance")); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue, "ERC20: decreased allowance below zero")); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer(address sender, address recipient, uint256 amount) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Sets {decimals} to a value other than the default one of 18. + * + * WARNING: This function should only be called from the constructor. Most + * applications that interact with token contracts will not expect + * {decimals} to ever change, and may work incorrectly if it does. + */ + function _setupDecimals(uint8 decimals_) internal { + _decimals = decimals_; + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be to transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } +} diff --git a/contracts/test/FailureCustomLogic.sol b/contracts/test/FailureCustomLogic.sol new file mode 100644 index 00000000..93679841 --- /dev/null +++ b/contracts/test/FailureCustomLogic.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + + +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "../interfaces/IForwarder.sol"; +import "../interfaces/IWalletCustomLogic.sol"; +import "../utils/RSKAddrValidator.sol"; + +/* solhint-disable avoid-low-level-calls, no-unused-vars */ +/** +* Example custom logic which always fail +*/ +contract FailureCustomLogic is IWalletCustomLogic { + using ECDSA for bytes32; + + event LogicCalled(); + event InitCalled(); + + function initialize(bytes memory initParams) override public { + emit InitCalled(); + } + + function execute( + bytes32 domainSeparator, + bytes32 suffixData, + IForwarder.ForwardRequest memory req, + bytes calldata sig + ) override external payable returns (bytes memory ret) { + revert("always fail"); + } + + function directExecute(address to, bytes calldata data) override external payable returns ( + bytes memory ret + ) { + revert("always fail"); + } +} diff --git a/contracts/test/NonCompliantERC20.sol b/contracts/test/NonCompliantERC20.sol new file mode 100644 index 00000000..edb2a899 --- /dev/null +++ b/contracts/test/NonCompliantERC20.sol @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: MIT +/* solhint-disable reason-string, no-empty-blocks */ + +pragma solidity ^0.6.12; + +import "@openzeppelin/contracts/GSN/Context.sol"; +import "./NonCompliantIERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin guidelines: functions revert instead + * of returning `false` on failure. This behavior is nonetheless conventional + * and does not conflict with the expectations of ERC20 applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract NonCompliantERC20 is Context, NonCompliantIERC20 { + using SafeMath for uint256; + using Address for address; + + mapping (address => uint256) private _balances; + + mapping (address => mapping (address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + uint8 private _decimals; + + /** + * @dev Sets the values for {name} and {symbol}, initializes {decimals} with + * a default value of 18. + * + * To select a different value for {decimals}, use {_setupDecimals}. + * + * All three of these values are immutable: they can only be set once during + * construction. + */ + constructor (string memory name, string memory symbol) public { + _name = name; + _symbol = symbol; + _decimals = 18; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5,05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the value {ERC20} uses, unless {_setupDecimals} is + * called. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view returns (uint8) { + return _decimals; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public virtual override { + _transfer(_msgSender(), recipient, amount); + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance")); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue, "ERC20: decreased allowance below zero")); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer(address sender, address recipient, uint256 amount) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Sets {decimals} to a value other than the default one of 18. + * + * WARNING: This function should only be called from the constructor. Most + * applications that interact with token contracts will not expect + * {decimals} to ever change, and may work incorrectly if it does. + */ + function _setupDecimals(uint8 decimals_) internal { + _decimals = decimals_; + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be to transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } +} diff --git a/contracts/test/NonCompliantIERC20.sol b/contracts/test/NonCompliantIERC20.sol new file mode 100644 index 00000000..15ef25aa --- /dev/null +++ b/contracts/test/NonCompliantIERC20.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface NonCompliantIERC20 { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external; + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} diff --git a/contracts/test/PayableWithEmit.sol b/contracts/test/PayableWithEmit.sol new file mode 100644 index 00000000..dde299a3 --- /dev/null +++ b/contracts/test/PayableWithEmit.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +//make sure that "payable" function that uses _msgSender() still works +// (its not required to use _msgSender(), since the default function +// will never be called through GSN, but still, if someone uses it, +// it should work) +contract PayableWithEmit { + + string public versionRecipient = "2.0.1+enveloping.payablewithemit.irelayrecipient"; + + event Received(address sender, uint value, uint gasleft); + + receive () external payable { + + emit Received(msg.sender, msg.value, gasleft()); + } + + + //helper: send value to another contract + function doSend(address payable target) public payable { + + uint before = gasleft(); + // solhint-disable-next-line check-send-result + bool success = target.send(msg.value); + uint gasAfter = gasleft(); + emit GasUsed(before-gasAfter, success); + } + event GasUsed(uint gasUsed, bool success); +} diff --git a/contracts/test/ProxyCustomLogic.sol b/contracts/test/ProxyCustomLogic.sol new file mode 100644 index 00000000..a1ca35f3 --- /dev/null +++ b/contracts/test/ProxyCustomLogic.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + + +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "../interfaces/IForwarder.sol"; +import "../interfaces/IWalletCustomLogic.sol"; +import "../utils/RSKAddrValidator.sol"; + +/* solhint-disable avoid-low-level-calls, no-unused-vars */ +/** +* Example custom logic which proxies the call to the smart contract +*/ +contract ProxyCustomLogic is IWalletCustomLogic { + using ECDSA for bytes32; + + event LogicCalled(); + event InitCalled(); + + function initialize(bytes memory initParams) override public { + emit InitCalled(); + } + + function execute( + bytes32 domainSeparator, + bytes32 suffixData, + IForwarder.ForwardRequest memory req, + bytes calldata sig + ) override external payable returns (bytes memory ret) { + emit LogicCalled(); + bool success; + (success, ret) = req.to.call{gas: req.gas, value: req.value}(req.data); + require(success, "call failed"); + } + + function directExecute(address to, bytes calldata data) override external payable returns ( + bytes memory ret + ) { + emit LogicCalled(); + bool success; + (success, ret) = to.call{value: msg.value}(data); + require(success, "call failed"); + + } +} diff --git a/contracts/test/SuccessCustomLogic.sol b/contracts/test/SuccessCustomLogic.sol new file mode 100644 index 00000000..90301f0c --- /dev/null +++ b/contracts/test/SuccessCustomLogic.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + + +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "../interfaces/IForwarder.sol"; +import "../interfaces/IWalletCustomLogic.sol"; +import "../utils/RSKAddrValidator.sol"; + +/* solhint-disable avoid-low-level-calls, no-unused-vars */ + +/** +* Example custom logic which always succeed +*/ +contract SuccessCustomLogic is IWalletCustomLogic { + using ECDSA for bytes32; + + event LogicCalled(); + event InitCalled(); + + function initialize(bytes memory initParams) override public { + emit InitCalled(); + } + + function execute( + bytes32 domainSeparator, + bytes32 suffixData, + IForwarder.ForwardRequest memory req, + bytes calldata sig + ) override external payable returns (bytes memory ret) { + emit LogicCalled(); + ret = "success"; + } + + function directExecute(address to, bytes calldata data) override external payable returns ( + bytes memory ret + ) { + emit LogicCalled(); + ret = "success"; + } +} diff --git a/contracts/test/TestDeployVerifier.sol b/contracts/test/TestDeployVerifier.sol new file mode 100644 index 00000000..18532b16 --- /dev/null +++ b/contracts/test/TestDeployVerifier.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "../interfaces/IDeployVerifier.sol"; + +contract TestDeployVerifier { + + event Deposited(address indexed verifier, address indexed from, uint256 amount); + event Accepted(uint256 tokenAmount, address from); + + IDeployVerifier public verifierContract; + + constructor ( address verifier) public { + verifierContract = IDeployVerifier(verifier); + } + + function verifyRelayedCall( + EnvelopingTypes.DeployRequest calldata deployRequest, + bytes calldata signature + ) + external + virtual + returns (bytes memory context) { + (context) = verifierContract.verifyRelayedCall(deployRequest, signature); + emit Accepted(deployRequest.request.tokenAmount, deployRequest.request.from); + } + + function depositFor(address target) public payable { + emit Deposited(target, msg.sender, msg.value); + } +} diff --git a/contracts/test/TestDeployVerifierConfigurableMisbehavior.sol b/contracts/test/TestDeployVerifierConfigurableMisbehavior.sol new file mode 100644 index 00000000..65dd417d --- /dev/null +++ b/contracts/test/TestDeployVerifierConfigurableMisbehavior.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "./TestDeployVerifierEverythingAccepted.sol"; + +contract TestDeployVerifierConfigurableMisbehavior is TestDeployVerifierEverythingAccepted { + bool public withdrawDuringPreRelayedCall; + bool public returnInvalidErrorCode; + bool public revertPostRelayCall; + bool public overspendAcceptGas; + bool public revertPreRelayCall; + bool public expensiveGasLimits; + int public expensiveGasLimitsIterations; + + function setWithdrawDuringPreRelayedCall(bool val) public { + withdrawDuringPreRelayedCall = val; + } + function setReturnInvalidErrorCode(bool val) public { + returnInvalidErrorCode = val; + } + function setRevertPostRelayCall(bool val) public { + revertPostRelayCall = val; + } + function setRevertPreRelayCall(bool val) public { + revertPreRelayCall = val; + } + function setOverspendAcceptGas(bool val) public { + overspendAcceptGas = val; + } + + function setExpensiveGasLimits(bool val) public { + expensiveGasLimits = val; + } + function setExpensiveGasLimitsIterations(int val) public { + expensiveGasLimitsIterations = val; + } + + function verifyRelayedCall( + /* solhint-disable-next-line no-unused-vars */ + EnvelopingTypes.DeployRequest calldata relayRequest, + bytes calldata signature + ) + external + override + returns (bytes memory) { + (signature, relayRequest); + if (overspendAcceptGas) { + uint i = 0; + while (true) { + i++; + } + } + + require(!returnInvalidErrorCode, "invalid code"); + + if (revertPreRelayCall) { + revert("revertPreRelayCall: Reverting"); + } + return (""); + } + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} +} diff --git a/contracts/test/TestDeployVerifierEverythingAccepted.sol b/contracts/test/TestDeployVerifierEverythingAccepted.sol new file mode 100644 index 00000000..4330b652 --- /dev/null +++ b/contracts/test/TestDeployVerifierEverythingAccepted.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "../interfaces/IDeployVerifier.sol"; +import "../interfaces/ITokenHandler.sol"; + +contract TestDeployVerifierEverythingAccepted is IDeployVerifier, ITokenHandler { + + function versionVerifier() external view override virtual returns (string memory){ + return "2.0.1+enveloping.test-pea.iverifier"; + } + + event SampleRecipientPreCall(); + event SampleRecipientPostCall(bool success); + + mapping (address => bool) public tokens; + address[] public acceptedTokens; + + function verifyRelayedCall( + /* solhint-disable-next-line no-unused-vars */ + EnvelopingTypes.DeployRequest calldata relayRequest, + bytes calldata signature + ) + external + override + virtual + returns (bytes memory) { + (signature, relayRequest); + emit SampleRecipientPreCall(); + return ("no revert here"); + } + + function acceptToken(address token) external { + require(token != address(0), "Token cannot be zero address"); + require(tokens[token] == false, "Token is already accepted"); + tokens[token] = true; + acceptedTokens.push(token); + } + + function getAcceptedTokens() external override view returns (address[] memory){ + return acceptedTokens; + } + + function acceptsToken(address token) external override view returns (bool){ + return tokens[token]; + } +} diff --git a/contracts/test/TestForwarderTarget.sol b/contracts/test/TestForwarderTarget.sol new file mode 100644 index 00000000..58dfa3c8 --- /dev/null +++ b/contracts/test/TestForwarderTarget.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +contract TestForwarderTarget { + + string public versionRecipient = "2.0.1+enveloping.test.recipient"; + + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} + + event TestForwarderMessage(string message, address msgSender, address origin); + + function emitMessage(string memory message) public { + + // solhint-disable-next-line avoid-tx-origin + emit TestForwarderMessage(message, msg.sender, tx.origin); + } + + + function mustReceiveEth(uint value) public payable { + require( msg.value == value, "didn't receive value"); + } + + event Reverting(string message); + + function testRevert() public { + require(address(this) == address(0), "always fail"); + emit Reverting("if you see this revert failed..."); + } +} diff --git a/contracts/test/TestRSKAddressValidator.sol b/contracts/test/TestRSKAddressValidator.sol new file mode 100644 index 00000000..872426c1 --- /dev/null +++ b/contracts/test/TestRSKAddressValidator.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +import "../utils/RSKAddrValidator.sol"; +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; + +contract TestRSKAddressValidator { + using ECDSA for bytes32; + + function getAddress(bytes32 messageHash, bytes memory sig) public pure returns (address) { + return messageHash.recover(sig); + } + + function compareAddressWithZeroPK(bytes32 messageHash, bytes memory sig) external view returns (bool) { + address addr = this.getAddress(messageHash, sig); + return RSKAddrValidator.checkPKNotZero(addr); + } + + function compareAddressWithSigner(bytes32 messageHash, bytes memory sig, address addr2) external view returns (bool) { + address addr1 = this.getAddress(messageHash, sig); + return addr1 == addr2; + } +} \ No newline at end of file diff --git a/contracts/test/TestRecipient.sol b/contracts/test/TestRecipient.sol new file mode 100644 index 00000000..67b83f32 --- /dev/null +++ b/contracts/test/TestRecipient.sol @@ -0,0 +1,106 @@ +/* solhint-disable avoid-tx-origin */ +/* solhint-disable avoid-low-level-calls */ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +import "./TestVerifierConfigurableMisbehavior.sol"; + +contract TestRecipient { + + string public versionRecipient = "2.0.1+enveloping.test.irelayrecipient"; + bool public nextRevert; + + event Reverting(string message); + + function setNextRevert() public { + nextRevert = true; + } + + function testRevert() public { + require(address(this) == address(0), "always fail"); + emit Reverting("if you see this revert failed..."); + } + + function testNextRevert() public { + if(nextRevert){ + require(address(this) == address(0), "always fail"); + emit Reverting("if you see this revert failed..."); + } + else{ + nextRevert = true; + } + } + + address payable public verifier; + + function setWithdrawDuringRelayedCall(address payable _verifier) public { + verifier = _verifier; + } + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} + + event SampleRecipientEmitted(string message, address msgSender, address origin, uint256 msgValue, uint256 balance); + event SampleRecipientEmittedNew(string message, address msgSender, address origin, uint256 msgValue, uint256 balance); + + function emitMessage(string memory message) public payable returns (string memory) { + + emit SampleRecipientEmitted(message, msg.sender, tx.origin, msg.value, address(this).balance); + return "emitMessage return value"; + } + + + function emitMessage3(string memory message) public payable returns (string memory) { + + emit SampleRecipientEmitted(message, msg.sender, tx.origin, msg.value, address(this).balance); + emit SampleRecipientEmittedNew("result", msg.sender, tx.origin, msg.value, address(this).balance); + emit SampleRecipientEmittedNew("result2", msg.sender, tx.origin, msg.value, address(this).balance); + emit SampleRecipientEmittedNew("result3", msg.sender, tx.origin, msg.value, address(this).balance); + emit SampleRecipientEmittedNew("result4", msg.sender, tx.origin, msg.value, address(this).balance); + + return message; + } + + + function transferTokens(address tokenRecipient, address tokenAddr, uint256 tokenAmount) public payable { + + if (tokenAmount > 0) { + (bool success, bytes memory ret ) = tokenAddr.call(abi.encodeWithSelector( + hex"a9059cbb", + tokenRecipient, + tokenAmount)); + + require( + success && (ret.length == 0 || abi.decode(ret, (bool))), + "Unable to pay for deployment"); + } + emit SampleRecipientEmitted("Payment Successful", msg.sender, tx.origin, msg.value, address(this).balance); + } + + // solhint-disable-next-line no-empty-blocks + function dontEmitMessage(string memory message) public {} + + function emitMessageNoParams() public { + emit SampleRecipientEmitted("Method with no parameters", msg.sender, tx.origin, 0, address(this).balance); + } + + //return (or revert) with a string in the given length + function checkReturnValues(uint len, bool doRevert) public view returns (string memory) { + (this); + string memory mesg = "this is a long message that we are going to return a small part from. we don't use a loop since we want a fixed gas usage of the method itself."; + require( bytes(mesg).length>=len, "invalid len: too large"); + + /* solhint-disable no-inline-assembly */ + //cut the msg at that length + assembly { mstore(mesg, len) } + require(!doRevert, mesg); + return mesg; + } + + //function with no return value (also test revert with no msg. + function checkNoReturnValues(bool doRevert) public view { + (this); + /* solhint-disable-next-line reason-string */ + require(!doRevert); + } +} diff --git a/contracts/test/TestRelayVerifier.sol b/contracts/test/TestRelayVerifier.sol new file mode 100644 index 00000000..79f9cdab --- /dev/null +++ b/contracts/test/TestRelayVerifier.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "../interfaces/IRelayVerifier.sol"; + +contract TestRelayVerifier { + + event Deposited(address indexed verifier, address indexed from, uint256 amount); + event Accepted(uint256 tokenAmount, address from); + + IRelayVerifier public verifierContract; + + constructor ( address verifier) public { + verifierContract = IRelayVerifier(verifier); + } + + function verifyRelayedCall( + EnvelopingTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) + external + virtual + returns (bytes memory context) { + (context) = verifierContract.verifyRelayedCall(relayRequest, signature); + emit Accepted(relayRequest.request.tokenAmount, relayRequest.request.from); + } + + function depositFor(address target) public payable { + emit Deposited(target, msg.sender, msg.value); + } +} diff --git a/contracts/test/TestRelayWorkerContract.sol b/contracts/test/TestRelayWorkerContract.sol new file mode 100644 index 00000000..717e9200 --- /dev/null +++ b/contracts/test/TestRelayWorkerContract.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier:MIT +/* solhint-disable avoid-tx-origin */ +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "../interfaces/IRelayHub.sol"; + +contract TestRelayWorkerContract { + + function relayCall( + IRelayHub hub, + EnvelopingTypes.RelayRequest memory relayRequest, + bytes memory signature) + public + { + hub.relayCall(relayRequest, signature); + } + + function deployCall( + IRelayHub hub, + EnvelopingTypes.DeployRequest memory deployRequest, + bytes memory signature) + public + { + hub.deployCall(deployRequest, signature); + } +} diff --git a/contracts/test/TestSmartWallet.sol b/contracts/test/TestSmartWallet.sol new file mode 100644 index 00000000..3818f9cb --- /dev/null +++ b/contracts/test/TestSmartWallet.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "../interfaces/IForwarder.sol"; + +// helper class for testing the forwarder. +contract TestSmartWallet { + function callExecute(IForwarder sw, IForwarder.ForwardRequest memory req, + bytes32 domainSeparator, bytes32 suffixData, bytes memory sig) public payable { + (bool relaySuccess, bytes memory ret) = sw.execute{value:msg.value}(domainSeparator, suffixData, req, sig); + + emit Result(relaySuccess, relaySuccess ? "" : this.decodeErrorMessage(ret)); + } + + event Result(bool success, string error); + + function decodeErrorMessage(bytes calldata ret) external pure returns (string memory message) { + //decode evert string: assume it has a standard Error(string) signature: simply skip the (selector,offset,length) fields + if ( ret.length>4+32+32 ) { + return abi.decode(ret[4:], (string)); + } + //unknown buffer. return as-is + return string(ret); + } + + function getChainId() public pure returns (uint256 id){ + /* solhint-disable-next-line no-inline-assembly */ + assembly { id := chainid() } + } +} diff --git a/contracts/test/TestUtil.sol b/contracts/test/TestUtil.sol new file mode 100644 index 00000000..d7ce7a55 --- /dev/null +++ b/contracts/test/TestUtil.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier:MIT +// solhint-disable no-inline-assembly +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "../interfaces/EnvelopingTypes.sol"; +import "../utils/Eip712Library.sol"; + +contract TestUtil { + bytes32 public constant RELAY_REQUEST_TYPEHASH = keccak256("RelayRequest(address relayHub,address from,address to,address tokenContract,uint256 value,uint256 gas,uint256 nonce,uint256 tokenAmount,uint256 tokenGas,bytes data,RelayData relayData)RelayData(uint256 gasPrice,bytes32 domainSeparator,address relayWorker,address callForwarder,address callVerifier)"); + + //helpers for test to call the library funcs: + function callForwarderVerify( + EnvelopingTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) + external + view { + + //(bool callSuccess,) = + relayRequest.relayData.callForwarder.staticcall( + abi.encodeWithSelector( + IForwarder.verify.selector, + relayRequest.relayData.domainSeparator, + Eip712Library.hashRelayData(relayRequest.relayData), + relayRequest.request, + signature + ) + ); + } + + function callForwarderVerifyAndCall( + EnvelopingTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) + external + returns ( + bool success, + bytes memory ret + ) { + bool forwarderSuccess; + + (forwarderSuccess, success, ret) = Eip712Library.execute(relayRequest, signature); + + if (!forwarderSuccess) { + revertWithData(ret); + } + + emit Called(success, success == false ? ret : bytes("")); + } + + event Called(bool success, bytes error); + + //re-throw revert with the same revert data. + function revertWithData(bytes memory data) internal pure { + assembly { + revert(add(data,32), mload(data)) + } + } + + function splitRequest( + EnvelopingTypes.RelayRequest calldata req + ) + external + pure + returns ( + IForwarder.ForwardRequest memory forwardRequest, + bytes32 typeHash, + bytes32 suffixData + ) { + forwardRequest = IForwarder.ForwardRequest( + req.request.relayHub, + req.request.from, + req.request.to, + req.request.tokenContract, + req.request.value, + req.request.gas, + req.request.nonce, + req.request.tokenAmount, + req.request.tokenGas, + req.request.data + ); + suffixData = Eip712Library.hashRelayData(req.relayData); + typeHash = RELAY_REQUEST_TYPEHASH; + } + + function libGetChainID() public pure returns (uint256 id) { + assembly { + id := chainid() + } + } +} diff --git a/contracts/test/TestVerifierConfigurableMisbehavior.sol b/contracts/test/TestVerifierConfigurableMisbehavior.sol new file mode 100644 index 00000000..ed3837b3 --- /dev/null +++ b/contracts/test/TestVerifierConfigurableMisbehavior.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "./TestVerifierEverythingAccepted.sol"; + +contract TestVerifierConfigurableMisbehavior is TestVerifierEverythingAccepted { + + bool public withdrawDuringPostRelayedCall; + bool public withdrawDuringPreRelayedCall; + bool public returnInvalidErrorCode; + bool public revertPostRelayCall; + bool public overspendAcceptGas; + bool public revertPreRelayCall; + bool public expensiveGasLimits; + int public expensiveGasLimitsIterations; + + + function setWithdrawDuringPostRelayedCall(bool val) public { + withdrawDuringPostRelayedCall = val; + } + function setWithdrawDuringPreRelayedCall(bool val) public { + withdrawDuringPreRelayedCall = val; + } + function setReturnInvalidErrorCode(bool val) public { + returnInvalidErrorCode = val; + } + function setRevertPostRelayCall(bool val) public { + revertPostRelayCall = val; + } + function setRevertPreRelayCall(bool val) public { + revertPreRelayCall = val; + } + function setOverspendAcceptGas(bool val) public { + overspendAcceptGas = val; + } + function setExpensiveGasLimits(bool val) public { + expensiveGasLimits = val; + } + function setExpensiveGasLimitsIterations(int val) public { + expensiveGasLimitsIterations = val; + } + + function verifyRelayedCall( + /* solhint-disable-next-line no-unused-vars */ + EnvelopingTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) + external + override + returns (bytes memory) { + (signature, relayRequest); + if (overspendAcceptGas) { + uint i = 0; + while (true) { + i++; + } + } + + require(!returnInvalidErrorCode, "invalid code"); + + if (revertPreRelayCall) { + revert("revertPreRelayCall: Reverting"); + } + return (""); + } + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} +} diff --git a/contracts/test/TestVerifierEverythingAccepted.sol b/contracts/test/TestVerifierEverythingAccepted.sol new file mode 100644 index 00000000..cbaa9d14 --- /dev/null +++ b/contracts/test/TestVerifierEverythingAccepted.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "../interfaces/IRelayVerifier.sol"; +import "../interfaces/ITokenHandler.sol"; + +contract TestVerifierEverythingAccepted is IRelayVerifier, ITokenHandler { + event SampleRecipientPreCall(); + event SampleRecipientPostCall(bool success); + + mapping (address => bool) public tokens; + address[] public acceptedTokens; + + function versionVerifier() external view override virtual returns (string memory){ + return "2.0.1+enveloping.test-pea.iverifier"; + } + + function verifyRelayedCall( + /* solhint-disable-next-line no-unused-vars */ + EnvelopingTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) + external + override + virtual + returns (bytes memory) { + (signature, relayRequest); + emit SampleRecipientPreCall(); + return ("no revert here"); + } + + function acceptToken(address token) external { + require(token != address(0), "Token cannot be zero address"); + require(tokens[token] == false, "Token is already accepted"); + tokens[token] = true; + acceptedTokens.push(token); + } + + function getAcceptedTokens() external override view returns (address[] memory){ + return acceptedTokens; + } + + function acceptsToken(address token) external override view returns (bool){ + return tokens[token]; + } +} diff --git a/contracts/test/TestVerifierVariableGasLimits.sol b/contracts/test/TestVerifierVariableGasLimits.sol new file mode 100644 index 00000000..c02e8e7a --- /dev/null +++ b/contracts/test/TestVerifierVariableGasLimits.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "./TestVerifierEverythingAccepted.sol"; + +contract TestVerifierVariableGasLimits is TestVerifierEverythingAccepted { + + string public override versionVerifier = "2.0.1+enveloping.test-vgl.iverifier"; + + event SampleRecipientPreCallWithValues( + uint256 gasleft + ); + + event SampleRecipientPostCallWithValues( + uint256 gasleft + ); + + function verifyRelayedCall( + /* solhint-disable-next-line no-unused-vars */ + EnvelopingTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) + external + override + returns (bytes memory) { + (signature, relayRequest); + emit SampleRecipientPreCallWithValues(gasleft()); + return (""); + } +} diff --git a/contracts/test/TestVersions.sol b/contracts/test/TestVersions.sol new file mode 100644 index 00000000..7df8eb74 --- /dev/null +++ b/contracts/test/TestVersions.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +contract TestVersions { + function versionHub() external pure returns (string memory) { + return "3.0.0"; + } +} diff --git a/contracts/test/tokens/NonCompliantTestToken.sol b/contracts/test/tokens/NonCompliantTestToken.sol new file mode 100644 index 00000000..534c8621 --- /dev/null +++ b/contracts/test/tokens/NonCompliantTestToken.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +import "../NonCompliantERC20.sol"; + + +contract NonCompliantTestToken is NonCompliantERC20("NCTest Token", "NTKN"){ + + + function mint(uint amount, address to) public { + _mint(msg.sender, amount); + _transfer(msg.sender, to, amount); + } + +} \ No newline at end of file diff --git a/contracts/test/tokens/NonRevertTestToken.sol b/contracts/test/tokens/NonRevertTestToken.sol new file mode 100644 index 00000000..c3c6e50a --- /dev/null +++ b/contracts/test/tokens/NonRevertTestToken.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +import "../ERC20Mod.sol"; + +contract NonRevertTestToken is ERC20Mod("NonRevert Test Token", "NRTKN") { + function mint(uint amount, address to) public { + _mint(msg.sender, amount); + _transfer(msg.sender, to, amount); + } + + function transfer(address recipient, uint256 amount) public virtual override returns (bool success) { + address sender = _msgSender(); + if( (sender != address(0)) && (recipient != address(0)) && (_balances[sender] >= amount)){ + _balances[sender] = _balances[sender].sub(amount); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + success = true; + } + } +} \ No newline at end of file diff --git a/contracts/test/tokens/TestToken.sol b/contracts/test/tokens/TestToken.sol new file mode 100644 index 00000000..80266d3c --- /dev/null +++ b/contracts/test/tokens/TestToken.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestToken is ERC20("Test Token", "TKN") { + function mint(uint amount, address to) public { + _mint(msg.sender, amount); + _transfer(msg.sender, to, amount); + } +} \ No newline at end of file diff --git a/contracts/test/tokens/Tether.sol b/contracts/test/tokens/Tether.sol new file mode 100644 index 00000000..51e233e4 --- /dev/null +++ b/contracts/test/tokens/Tether.sol @@ -0,0 +1,382 @@ +pragma solidity 0.6.12; +// SPDX-License-Identifier: MIT +/* solhint-disable reason-string, visibility-modifier-order, var-name-mixedcase */ + +/** + * @title SafeMath + * @dev Math operations with safety checks that throw on error + */ +library SafeMath { + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + if (a == 0) { + return 0; + } + uint256 c = a * b; + assert(c / a == b); + return c; + } + function div(uint256 a, uint256 b) internal pure returns (uint256) { + // assert(b > 0); // Solidity automatically throws when dividing by 0 + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + return c; + } + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + assert(b <= a); + return a - b; + } + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + assert(c >= a); + return c; + } +} +/** + * @title Ownable + * @dev The Ownable contract has an owner address, and provides basic authorization control + * functions, this simplifies the implementation of "user permissions". + */ +contract Ownable { + address public owner; + /** + * @dev The Ownable constructor sets the original `owner` of the contract to the sender + * account. + */ + constructor() public { + owner = msg.sender; + } + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + /** + * @dev Allows the current owner to transfer control of the contract to a newOwner. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) public onlyOwner { + if (newOwner != address(0)) { + owner = newOwner; + } + } +} +/** + * @title ERC20Basic + * @dev Simpler version of ERC20 interface + * @dev see https://github.com/ethereum/EIPs/issues/20 + */ +abstract contract ERC20Basic { + uint public _totalSupply; + function totalSupply() public virtual returns (uint); + function balanceOf(address who) public virtual returns (uint); + function transfer(address to, uint value) public virtual; + event Transfer(address indexed from, address indexed to, uint value); +} +/** + * @title ERC20 interface + * @dev see https://github.com/ethereum/EIPs/issues/20 + */ +abstract contract ERC20 is ERC20Basic { + function allowance(address owner, address spender) public virtual returns (uint); + function transferFrom(address from, address to, uint value) public virtual; + function approve(address spender, uint value) public virtual; + event Approval(address indexed owner, address indexed spender, uint value); +} +/** + * @title Basic token + * @dev Basic version of StandardToken, with no allowances. + */ +abstract contract BasicToken is Ownable, ERC20Basic { + using SafeMath for uint; + mapping(address => uint) public balances; + // additional variables for use if transaction fees ever became necessary + uint public basisPointsRate = 0; + uint public maximumFee = 0; + /** + * @dev Fix for the ERC20 short address attack. + */ + modifier onlyPayloadSize(uint size) { + require(!(msg.data.length < size + 4)); + _; + } + /** + * @dev transfer token for a specified address + * @param _to The address to transfer to. + * @param _value The amount to be transferred. + */ + function transfer(address _to, uint _value) public override virtual onlyPayloadSize(2 * 32) { + uint fee = (_value.mul(basisPointsRate)).div(10000); + if (fee > maximumFee) { + fee = maximumFee; + } + uint sendAmount = _value.sub(fee); + balances[msg.sender] = balances[msg.sender].sub(_value); + balances[_to] = balances[_to].add(sendAmount); + if (fee > 0) { + balances[owner] = balances[owner].add(fee); + Transfer(msg.sender, owner, fee); + } + Transfer(msg.sender, _to, sendAmount); + } + /** + * @dev Gets the balance of the specified address. + * @param _owner The address to query the the balance of. + * @return balance An uint representing the amount owned by the passed address. + */ + function balanceOf(address _owner) public override virtual returns (uint balance) { + return balances[_owner]; + } +} +/** + * @title Standard ERC20 token + * + * @dev Implementation of the basic standard token. + * @dev https://github.com/ethereum/EIPs/issues/20 + * @dev Based oncode by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol + */ +abstract contract StandardToken is BasicToken, ERC20 { + using SafeMath for uint; + mapping (address => mapping (address => uint)) public allowed; + uint public MAX_UINT = 2**256 - 1; + /** + * @dev Transfer tokens from one address to another + * @param _from address The address which you want to send tokens from + * @param _to address The address which you want to transfer to + * @param _value uint the amount of tokens to be transferred + */ + function transferFrom(address _from, address _to, uint _value) public override virtual onlyPayloadSize(3 * 32) { + uint _allowance = allowed[_from][msg.sender]; + // Check is not needed because sub(_allowance, _value) will already throw if this condition is not met + // if (_value > _allowance) throw; + uint fee = (_value.mul(basisPointsRate)).div(10000); + if (fee > maximumFee) { + fee = maximumFee; + } + if (_allowance < MAX_UINT) { + allowed[_from][msg.sender] = _allowance.sub(_value); + } + uint sendAmount = _value.sub(fee); + balances[_from] = balances[_from].sub(_value); + balances[_to] = balances[_to].add(sendAmount); + if (fee > 0) { + balances[owner] = balances[owner].add(fee); + Transfer(_from, owner, fee); + } + Transfer(_from, _to, sendAmount); + } + /** + * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. + * @param _spender The address which will spend the funds. + * @param _value The amount of tokens to be spent. + */ + function approve(address _spender, uint _value) public virtual override onlyPayloadSize(2 * 32) { + // To change the approve amount you first have to reduce the addresses` + // allowance to zero by calling `approve(_spender, 0)` if it is not + // already 0 to mitigate the race condition described here: + // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + require(!((_value != 0) && (allowed[msg.sender][_spender] != 0))); + allowed[msg.sender][_spender] = _value; + Approval(msg.sender, _spender, _value); + } + /** + * @dev Function to check the amount of tokens than an owner allowed to a spender. + * @param _owner address The address which owns the funds. + * @param _spender address The address which will spend the funds. + * @return remaining A uint specifying the remaining amount of tokens still available for the spender. + */ + function allowance(address _owner, address _spender) public override virtual returns (uint remaining) { + return allowed[_owner][_spender]; + } +} +/** + * @title Pausable + * @dev Base contract which allows children to implement an emergency stop mechanism. + */ +contract Pausable is Ownable { + event Pause(); + event Unpause(); + bool public paused = false; + /** + * @dev Modifier to make a function callable only when the contract is not paused. + */ + modifier whenNotPaused() { + require(!paused); + _; + } + /** + * @dev Modifier to make a function callable only when the contract is paused. + */ + modifier whenPaused() { + require(paused); + _; + } + /** + * @dev called by the owner to pause, triggers stopped state + */ + function pause() onlyOwner whenNotPaused public { + paused = true; + Pause(); + } + /** + * @dev called by the owner to unpause, returns to normal state + */ + function unpause() onlyOwner whenPaused public { + paused = false; + Unpause(); + } +} +abstract contract BlackList is Ownable, BasicToken { + /////// Getters to allow the same blacklist to be used also by other contracts (including upgraded Tether) /////// + function getBlackListStatus(address _maker) external returns (bool) { + return isBlackListed[_maker]; + } + function getOwner() external returns (address) { + return owner; + } + mapping (address => bool) public isBlackListed; + function addBlackList (address _evilUser) public onlyOwner { + isBlackListed[_evilUser] = true; + AddedBlackList(_evilUser); + } + function removeBlackList (address _clearedUser) public onlyOwner { + isBlackListed[_clearedUser] = false; + RemovedBlackList(_clearedUser); + } + function destroyBlackFunds (address _blackListedUser) public onlyOwner { + require(isBlackListed[_blackListedUser]); + uint dirtyFunds = balanceOf(_blackListedUser); + balances[_blackListedUser] = 0; + _totalSupply -= dirtyFunds; + DestroyedBlackFunds(_blackListedUser, dirtyFunds); + } + event DestroyedBlackFunds(address _blackListedUser, uint _balance); + event AddedBlackList(address _user); + event RemovedBlackList(address _user); +} +abstract contract UpgradedStandardToken is StandardToken{ + // those methods are called by the legacy contract + // and they must ensure msg.sender to be the contract address + function transferByLegacy(address from, address to, uint value) public virtual; + function transferFromByLegacy(address sender, address from, address spender, uint value) public virtual; + function approveByLegacy(address from, address spender, uint value) public virtual; +} +contract TetherToken is Pausable, StandardToken, BlackList { + using SafeMath for uint; + string public name; + string public symbol; + uint public decimals; + address public upgradedAddress; + bool public deprecated; + // The contract can be initialized with a number of tokens + // All the tokens are deposited to the owner address + // + // @param _balance Initial supply of the contract + // @param _name Token Name + // @param _symbol Token symbol + // @param _decimals Token decimals + constructor(uint _initialSupply, string memory _name, string memory _symbol, uint _decimals) public { + _totalSupply = _initialSupply; + name = _name; + symbol = _symbol; + decimals = _decimals; + balances[owner] = _initialSupply; + deprecated = false; + } + // Forward ERC20 methods to upgraded contract if this one is deprecated + function transfer(address _to, uint _value) public override whenNotPaused { + require(!isBlackListed[msg.sender]); + if (deprecated) { + return UpgradedStandardToken(upgradedAddress).transferByLegacy(msg.sender, _to, _value); + } else { + return super.transfer(_to, _value); + } + } + // Forward ERC20 methods to upgraded contract if this one is deprecated + function transferFrom(address _from, address _to, uint _value) public override whenNotPaused { + require(!isBlackListed[_from]); + if (deprecated) { + return UpgradedStandardToken(upgradedAddress).transferFromByLegacy(msg.sender, _from, _to, _value); + } else { + return super.transferFrom(_from, _to, _value); + } + } + // Forward ERC20 methods to upgraded contract if this one is deprecated + function balanceOf(address who) public override returns (uint) { + if (deprecated) { + return UpgradedStandardToken(upgradedAddress).balanceOf(who); + } else { + return super.balanceOf(who); + } + } + // Forward ERC20 methods to upgraded contract if this one is deprecated + function approve(address _spender, uint _value) public override onlyPayloadSize(2 * 32) { + if (deprecated) { + return UpgradedStandardToken(upgradedAddress).approveByLegacy(msg.sender, _spender, _value); + } else { + return super.approve(_spender, _value); + } + } + // Forward ERC20 methods to upgraded contract if this one is deprecated + function allowance(address _owner, address _spender) public override returns (uint remaining) { + if (deprecated) { + return StandardToken(upgradedAddress).allowance(_owner, _spender); + } else { + return super.allowance(_owner, _spender); + } + } + // deprecate current contract in favour of a new one + function deprecate(address _upgradedAddress) public onlyOwner { + deprecated = true; + upgradedAddress = _upgradedAddress; + Deprecate(_upgradedAddress); + } + // deprecate current contract if favour of a new one + function totalSupply() public override returns (uint) { + if (deprecated) { + return StandardToken(upgradedAddress).totalSupply(); + } else { + return _totalSupply; + } + } + // Issue a new amount of tokens + // these tokens are deposited into the owner address + // + // @param _amount Number of tokens to be issued + function issue(uint amount) public onlyOwner { + require(_totalSupply + amount > _totalSupply); + require(balances[owner] + amount > balances[owner]); + balances[owner] += amount; + _totalSupply += amount; + Issue(amount); + } + // Redeem tokens. + // These tokens are withdrawn from the owner address + // if the balance must be enough to cover the redeem + // or the call will fail. + // @param _amount Number of tokens to be issued + function redeem(uint amount) public onlyOwner { + require(_totalSupply >= amount); + require(balances[owner] >= amount); + _totalSupply -= amount; + balances[owner] -= amount; + Redeem(amount); + } + function setParams(uint newBasisPoints, uint newMaxFee) public onlyOwner { + // Ensure transparency by hardcoding limit beyond which fees can never be added + require(newBasisPoints < 20); + require(newMaxFee < 50); + basisPointsRate = newBasisPoints; + maximumFee = newMaxFee.mul(10**decimals); + Params(basisPointsRate, maximumFee); + } + // Called when new token are issued + event Issue(uint amount); + // Called when tokens are redeemed + event Redeem(uint amount); + // Called when contract is deprecated + event Deprecate(address newAddress); + // Called if contract ever adds fees + event Params(uint feeBasisPoints, uint maxFee); +} \ No newline at end of file diff --git a/contracts/test/webinar/HeavyTask.sol b/contracts/test/webinar/HeavyTask.sol new file mode 100644 index 00000000..08c4efc9 --- /dev/null +++ b/contracts/test/webinar/HeavyTask.sol @@ -0,0 +1,53 @@ +/* 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:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + + +contract HeavyTask { + using SafeMath for uint256; + + + event ValueCalculated(bytes32 value); + + // maps relay managers to the number of their workers + mapping(uint256 => mapping(bytes32 => bytes32) ) public dataStorage; + + bool public greenCode; + + constructor( + ) public { + greenCode = false; + } + + + function resetGreenCode() external{ + greenCode = false; + } + + /** + New relay worker addresses can be added (as enabled workers) as long as they don't have a relay manager aldeady assigned. + */ + function performTask(address infoA, uint256 infoB, bytes memory infoC) + external + { + require(greenCode == false, "Only runs when not in green"); + + for(uint256 steps = 0; steps < 10; steps++){ + bytes32 value = keccak256(abi.encodePacked(infoA,keccak256(abi.encodePacked(infoB,keccak256(abi.encodePacked(infoC)))))); + bytes32 key = keccak256(abi.encodePacked("Key_",value)); + dataStorage[steps][key] = value; + emit ValueCalculated(value); + } + + greenCode = true; + } + + +} diff --git a/contracts/utils/Eip712Library.sol b/contracts/utils/Eip712Library.sol new file mode 100644 index 00000000..4c7722ea --- /dev/null +++ b/contracts/utils/Eip712Library.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "../interfaces/EnvelopingTypes.sol"; +import "../interfaces/IForwarder.sol"; +import "../interfaces/IWalletFactory.sol"; +import "./MinLibBytes.sol"; +/** + * Bridge Library to map Enveloping RelayRequest into a call of a SmartWallet + */ +library Eip712Library { + function deploy(EnvelopingTypes.DeployRequest calldata relayRequest, bytes calldata signature) internal returns (bool deploySuccess, bytes memory ret) { + + // The gas limit for the deploy creation is injected here, since the gasCalculation + // estimate is done against the whole relayedUserSmartWalletCreation function in + // the relayClient + + /* solhint-disable-next-line avoid-low-level-calls */ + (deploySuccess, ret) = relayRequest.relayData.callForwarder.call( + abi.encodeWithSelector(IWalletFactory.relayedUserSmartWalletCreation.selector, + relayRequest.request, relayRequest.relayData.domainSeparator, + hashRelayData(relayRequest.relayData), signature + )); + + } + + //forwarderSuccess = Did the call to IForwarder.execute() revert or not? + //relaySuccess = Did the destination-contract call revert or not? + //ret = if !forwarderSuccess it is the revert reason of IForwarder, otherwise it is the destination-contract return data, wich might be + // a revert reason if !relaySuccess + function execute(EnvelopingTypes.RelayRequest calldata relayRequest, bytes calldata signature) internal returns (bool forwarderSuccess, bool relaySuccess, bytes memory ret) { + /* solhint-disable-next-line avoid-low-level-calls */ + (forwarderSuccess, ret) = relayRequest.relayData.callForwarder.call( + abi.encodeWithSelector(IForwarder.execute.selector, relayRequest.relayData.domainSeparator, + hashRelayData(relayRequest.relayData), relayRequest.request, signature + )); + + if ( forwarderSuccess ) { + (relaySuccess, ret) = abi.decode(ret, (bool, bytes)); // decode return value of execute: + } + + MinLibBytes.truncateInPlace(ret, 1024); // maximum length of return value/revert reason for 'execute' method. Will truncate result if exceeded. + } + + function hashRelayData(EnvelopingTypes.RelayData calldata req) internal pure returns (bytes32) { + return keccak256(abi.encode( + keccak256("RelayData(uint256 gasPrice,bytes32 domainSeparator,address relayWorker,address callForwarder,address callVerifier)"), // RELAYDATA_TYPEHASH + req.gasPrice, + req.domainSeparator, + req.relayWorker, + req.callForwarder, + req.callVerifier + )); + } +} diff --git a/contracts/utils/MinLibBytes.sol b/contracts/utils/MinLibBytes.sol new file mode 100644 index 00000000..cc3c11a7 --- /dev/null +++ b/contracts/utils/MinLibBytes.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier:MIT +// minimal bytes manipulation required by Enveloping +// a minimal subset from 0x/LibBytes +/* solhint-disable no-inline-assembly */ +pragma solidity ^0.6.12; + +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/utils/RLPReader.sol b/contracts/utils/RLPReader.sol new file mode 100644 index 00000000..16256cac --- /dev/null +++ b/contracts/utils/RLPReader.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier:APACHE-2.0 +/* +* Taken from https://github.com/hamdiallam/Solidity-RLP +*/ +/* solhint-disable */ +pragma solidity ^0.6.12; + +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 { + uint len; + uint memPtr; + } + + using RLPReader for bytes; + using RLPReader for uint; + using RLPReader for RLPReader.RLPItem; + + // helper function to decode rlp encoded ethereum transaction + /* + * @param rawTransaction RLP encoded ethereum transaction + * @return tuple (nonce,gasPrice,gasLimit,to,value,data) + */ + + function decodeTransaction(bytes memory rawTransaction) internal pure returns (uint, uint, uint, address, uint, bytes memory){ + RLPReader.RLPItem[] memory values = rawTransaction.toRlpItem().toList(); // must convert to an rlpItem first! + return (values[0].toUint(), values[1].toUint(), values[2].toUint(), values[3].toAddress(), values[4].toUint(), values[5].toBytes()); + } + + /* + * @param item RLP encoded bytes + */ + function toRlpItem(bytes memory item) internal pure returns (RLPItem memory) { + if (item.length == 0) + return RLPItem(0, 0); + uint 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"); + uint items = numItems(item); + result = new RLPItem[](items); + uint memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint dataLen; + for (uint 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; + uint 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 (uint) { + uint count = 0; + uint currPtr = item.memPtr + _payloadOffset(item.memPtr); + uint 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(uint memPtr) internal pure returns (uint len) { + uint 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) // # 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(uint memPtr) internal pure returns (uint) { + uint 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); + uint 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"); + uint result; + uint 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(toUint(item)); + } + + function toUint(RLPItem memory item) internal pure returns (uint) { + uint offset = _payloadOffset(item.memPtr); + uint len = item.len - offset; + uint memPtr = item.memPtr + offset; + uint 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) { + uint offset = _payloadOffset(item.memPtr); + uint len = item.len - offset; + // data length + bytes memory result = new bytes(len); + uint 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(uint src, uint dest, uint len) internal pure { + // 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; + } + // left over bytes. Mask is used to remove unwanted bytes from the word + uint 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/utils/RSKAddrValidator.sol b/contracts/utils/RSKAddrValidator.sol new file mode 100644 index 00000000..e652e83b --- /dev/null +++ b/contracts/utils/RSKAddrValidator.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; + +library RSKAddrValidator { + + /* + * @param addr it is an address to check that it does not originates from + * signing with PK = ZERO. RSK has a small difference in which @ZERO_PK_ADDR is + * also an address from PK = ZERO. So we check for both of them. + */ + function checkPKNotZero(address addr) internal pure returns (bool){ + return (addr != 0xdcc703c0E500B653Ca82273B7BFAd8045D85a470 && addr != address(0)); + } + + /* + * Safely compares two addresses, checking they do not originate from + * a zero private key + */ + function safeEquals(address addr1, address addr2) internal pure returns (bool){ + return (addr1 == addr2 && + addr1 != 0xdcc703c0E500B653Ca82273B7BFAd8045D85a470 && + addr1 != address(0)); + } +} \ No newline at end of file diff --git a/contracts/utils/VersionRegistry.sol b/contracts/utils/VersionRegistry.sol new file mode 100644 index 00000000..a89bd5e4 --- /dev/null +++ b/contracts/utils/VersionRegistry.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +// solhint-disable not-rely-on-time + +import "../interfaces/IVersionRegistry.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract VersionRegistry is IVersionRegistry, Ownable { + + function addVersion(bytes32 id, bytes32 version, string calldata value) external override onlyOwner { + require(id != bytes32(0), "missing id"); + require(version != bytes32(0), "missing version"); + emit VersionAdded(id, version, value, block.timestamp); + } + + function cancelVersion(bytes32 id, bytes32 version, string calldata reason) external override onlyOwner { + emit VersionCanceled(id, version, reason); + } +} diff --git a/contracts/verifier/CustomSmartWalletDeployVerifier.sol b/contracts/verifier/CustomSmartWalletDeployVerifier.sol new file mode 100644 index 00000000..d7b407a4 --- /dev/null +++ b/contracts/verifier/CustomSmartWalletDeployVerifier.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier:MIT +// solhint-disable no-inline-assembly +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "../factory/CustomSmartWalletFactory.sol"; +import "../interfaces/IDeployVerifier.sol"; +import "../interfaces/ITokenHandler.sol"; +import "../interfaces/EnvelopingTypes.sol"; + +/** + * A Verifier to be used on deploys. + */ +contract CustomSmartWalletDeployVerifier is IDeployVerifier, ITokenHandler, Ownable { + + address private factory; + mapping (address => bool) public tokens; + address[] public acceptedTokens; + + constructor(address walletFactory) public { + factory = walletFactory; + } + + function versionVerifier() external override virtual view returns (string memory){ + return "rif.enveloping.token.iverifier@2.0.1"; + } + + /* solhint-disable no-unused-vars */ + function verifyRelayedCall( + EnvelopingTypes.DeployRequest calldata relayRequest, + bytes calldata signature + ) + external + override + virtual + returns (bytes memory context) + { + require(tokens[relayRequest.request.tokenContract], "Token contract not allowed"); + require(relayRequest.relayData.callForwarder == factory, "Invalid factory"); + + address contractAddr = CustomSmartWalletFactory(relayRequest.relayData.callForwarder) + .getSmartWalletAddress( + relayRequest.request.from, + relayRequest.request.recoverer, + relayRequest.request.to, + keccak256(relayRequest.request.data), + relayRequest.request.index); + + require(!_isContract(contractAddr), "Address already created!"); + + if(relayRequest.request.tokenContract != address(0)){ + require(relayRequest.request.tokenAmount <= IERC20(relayRequest.request.tokenContract).balanceOf(contractAddr), "balance too low"); + } + + return (abi.encode(contractAddr, relayRequest.request.tokenAmount, relayRequest.request.tokenContract)); + } + + function acceptToken(address token) external onlyOwner { + require(token != address(0), "Token cannot be zero address"); + require(tokens[token] == false, "Token is already accepted"); + tokens[token] = true; + acceptedTokens.push(token); + } + + function getAcceptedTokens() external override view returns (address[] memory){ + return acceptedTokens; + } + + function acceptsToken(address token) external override view returns (bool){ + return tokens[token]; + } + + /** + * Check if a contract has code in it + * Should NOT be used in a contructor, it fails + * See: https://stackoverflow.com/a/54056854 + */ + function _isContract(address _addr) internal view returns (bool){ + uint32 size; + assembly { + size := extcodesize(_addr) + } + return (size > 0); + } +} diff --git a/contracts/verifier/DeployVerifier.sol b/contracts/verifier/DeployVerifier.sol new file mode 100644 index 00000000..e9cf43a8 --- /dev/null +++ b/contracts/verifier/DeployVerifier.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier:MIT +// solhint-disable no-inline-assembly +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "../factory/SmartWalletFactory.sol"; +import "../interfaces/IDeployVerifier.sol"; +import "../interfaces/ITokenHandler.sol"; +import "../interfaces/EnvelopingTypes.sol"; + +/** + * A Verifier to be used on deploys. + */ +contract DeployVerifier is IDeployVerifier, ITokenHandler, Ownable { + + address private factory; + mapping (address => bool) public tokens; + address[] public acceptedTokens; + + constructor(address walletFactory) public { + factory = walletFactory; + } + + function versionVerifier() external override virtual view returns (string memory){ + return "rif.enveloping.token.iverifier@2.0.1"; + } + + /* solhint-disable no-unused-vars */ + function verifyRelayedCall( + EnvelopingTypes.DeployRequest calldata relayRequest, + bytes calldata signature + ) + external + override + virtual + returns (bytes memory context) + { + require(tokens[relayRequest.request.tokenContract], "Token contract not allowed"); + require(relayRequest.relayData.callForwarder == factory, "Invalid factory"); + + address contractAddr = SmartWalletFactory(relayRequest.relayData.callForwarder) + .getSmartWalletAddress( + relayRequest.request.from, + relayRequest.request.recoverer, + relayRequest.request.index); + + require(!_isContract(contractAddr), "Address already created!"); + + if(relayRequest.request.tokenContract != address(0)){ + require(relayRequest.request.tokenAmount <= IERC20(relayRequest.request.tokenContract).balanceOf(contractAddr), "balance too low"); + } + + return (abi.encode(contractAddr, relayRequest.request.tokenAmount, relayRequest.request.tokenContract)); + } + + function acceptToken(address token) external onlyOwner { + require(token != address(0), "Token cannot be zero address"); + require(tokens[token] == false, "Token is already accepted"); + tokens[token] = true; + acceptedTokens.push(token); + } + + function getAcceptedTokens() external override view returns (address[] memory){ + return acceptedTokens; + } + + function acceptsToken(address token) external override view returns (bool){ + return tokens[token]; + } + + /** + * Check if a contract has code in it + * Should NOT be used in a contructor, it fails + * See: https://stackoverflow.com/a/54056854 + */ + function _isContract(address _addr) internal view returns (bool){ + uint32 size; + assembly { + size := extcodesize(_addr) + } + return (size > 0); + } +} diff --git a/contracts/verifier/RelayVerifier.sol b/contracts/verifier/RelayVerifier.sol new file mode 100644 index 00000000..352e603a --- /dev/null +++ b/contracts/verifier/RelayVerifier.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier:MIT +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "../interfaces/IWalletFactory.sol"; +import "../interfaces/IRelayVerifier.sol"; +import "../interfaces/ITokenHandler.sol"; +import "../interfaces/EnvelopingTypes.sol"; + +/* solhint-disable no-inline-assembly */ +/* solhint-disable avoid-low-level-calls */ + +/** + * A verifier for relay transactions. + */ +contract RelayVerifier is IRelayVerifier, ITokenHandler, Ownable { + using SafeMath for uint256; + + address private factory; + + constructor(address walletFactory) public { + factory = walletFactory; + } + + function versionVerifier() external override virtual view returns (string memory){ + return "rif.enveloping.token.iverifier@2.0.1"; + } + + mapping (address => bool) public tokens; + address[] public acceptedTokens; + + /* solhint-disable no-unused-vars */ + function verifyRelayedCall( + EnvelopingTypes.RelayRequest calldata relayRequest, + bytes calldata signature + ) + external + override + virtual + returns (bytes memory context) { + require(tokens[relayRequest.request.tokenContract], "Token contract not allowed"); + + address payer = relayRequest.relayData.callForwarder; + if(relayRequest.request.tokenContract != address(0)){ + require(relayRequest.request.tokenAmount <= IERC20(relayRequest.request.tokenContract).balanceOf(payer), "balance too low"); + } + + // Check for the codehash of the smart wallet sent + bytes32 smartWalletCodeHash; + assembly { smartWalletCodeHash := extcodehash(payer) } + + require(IWalletFactory(factory).runtimeCodeHash() == smartWalletCodeHash, "SW different to template"); + + return (abi.encode(payer, relayRequest.request.tokenAmount, relayRequest.request.tokenContract)); + } + + function acceptToken(address token) external onlyOwner { + require(token != address(0), "Token cannot be zero address"); + require(tokens[token] == false, "Token is already accepted"); + tokens[token] = true; + acceptedTokens.push(token); + } + + function getAcceptedTokens() external override view returns (address[] memory){ + return acceptedTokens; + } + + function acceptsToken(address token) external override view returns (bool){ + return tokens[token]; + } +} diff --git a/custom.d.ts b/custom.d.ts new file mode 100644 index 00000000..f52a2347 --- /dev/null +++ b/custom.d.ts @@ -0,0 +1,4 @@ +declare module '*.json' { + const content: any; + export default content; +} diff --git a/index.ts b/index.ts new file mode 100644 index 00000000..f9136859 --- /dev/null +++ b/index.ts @@ -0,0 +1,56 @@ +// interfaces +import * as EnvelopingTypes from './build/contracts/EnvelopingTypes.json'; +import * as ICustomSmartWalletFactory from './build/contracts/ICustomSmartWalletFactory.json'; +import * as IDeployVerifier from './build/contracts/IDeployVerifier.json'; +import * as IForwarder from './build/contracts/IForwarder.json'; +import * as IPenalizer from './build/contracts/IPenalizer.json'; +import * as IRelayHub from './build/contracts/IRelayHub.json'; +import * as IRelayVerifier from './build/contracts/IRelayVerifier.json'; +import * as ISmartWalletFactory from './build/contracts/ISmartWalletFactory.json'; +import * as ITokenHandler from './build/contracts/ITokenHandler.json'; +import * as IVersionRegistry from './build/contracts/IVersionRegistry.json'; +import * as IWalletCustomLogic from './build/contracts/IWalletCustomLogic.json'; +import * as IWalletFactory from './build/contracts/IWalletFactory.json'; + +// contracts +import * as CustomSmartWallet from './build/contracts/CustomSmartWallet.json'; +import * as CustomSmartWalletDeployVerifier from './build/contracts/CustomSmartWalletDeployVerifier.json'; +import * as CustomSmartWalletFactory from './build/contracts/CustomSmartWalletFactory.json'; +import * as DeployVerifier from './build/contracts/DeployVerifier.json'; +import * as Penalizer from './build/contracts/Penalizer.json'; +import * as RelayHub from './build/contracts/RelayHub.json'; +import * as RelayVerifier from './build/contracts/RelayVerifier.json'; +import * as SmartWallet from './build/contracts/SmartWallet.json'; +import * as SmartWalletFactory from './build/contracts/SmartWalletFactory.json'; +import * as TestDeployVerifierEverythingAccepted from './build/contracts/TestDeployVerifierEverythingAccepted.json'; +import * as TestVerifierEverythingAccepted from './build/contracts/TestVerifierEverythingAccepted.json'; +import * as VersionRegistry from './build/contracts/VersionRegistry.json'; + +export { + // interfaces + EnvelopingTypes, + ICustomSmartWalletFactory, + IDeployVerifier, + IForwarder, + IPenalizer, + IRelayHub, + IRelayVerifier, + ISmartWalletFactory, + ITokenHandler, + IVersionRegistry, + IWalletCustomLogic, + IWalletFactory, + // contracts + CustomSmartWallet, + CustomSmartWalletDeployVerifier, + CustomSmartWalletFactory, + DeployVerifier, + Penalizer, + RelayHub, + RelayVerifier, + SmartWallet, + SmartWalletFactory, + TestDeployVerifierEverythingAccepted, + TestVerifierEverythingAccepted, + VersionRegistry +} diff --git a/migrations/1_initial_migration.js b/migrations/1_initial_migration.js new file mode 100644 index 00000000..42968952 --- /dev/null +++ b/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +var Migrations = artifacts.require('./Migrations.sol') + +module.exports = function (deployer) { + deployer.deploy(Migrations) +} diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js new file mode 100644 index 00000000..de0d031a --- /dev/null +++ b/migrations/2_deploy_contracts.js @@ -0,0 +1,59 @@ +// Primary contracts +const RelayHub = artifacts.require('RelayHub') +const Penalizer = artifacts.require('Penalizer') +const SmartWallet = artifacts.require('SmartWallet') +const SmartWalletFactory = artifacts.require('SmartWalletFactory') +const DeployVerifier = artifacts.require('DeployVerifier') +const RelayVerifier = artifacts.require('RelayVerifier') +const TestToken = artifacts.require('TestToken') + +// For testing purposes +const SampleRecipient = artifacts.require('TestRecipient') + +// For CustomSmartWallet support +const CustomSmartWallet = artifacts.require('CustomSmartWallet') +const CustomSmartWalletFactory = artifacts.require('CustomSmartWalletFactory') +const CustomSmartWalletDeployVerifier = artifacts.require('CustomSmartWalletDeployVerifier') + +module.exports = async function (deployer) { + await deployer.deploy(Penalizer) + await deployer.deploy(RelayHub, Penalizer.address, 1, 1, 1, 1) + await deployer.deploy(SmartWallet) + await deployer.deploy(SmartWalletFactory, SmartWallet.address) + await deployer.deploy(DeployVerifier, SmartWalletFactory.address) + await deployer.deploy(RelayVerifier, SmartWalletFactory.address) + + const smartWalletRelayVerifierAddress = RelayVerifier.address + + await deployer.deploy(CustomSmartWallet) + await deployer.deploy(CustomSmartWalletFactory, CustomSmartWallet.address) + await deployer.deploy(CustomSmartWalletDeployVerifier, CustomSmartWalletFactory.address) + await deployer.deploy(RelayVerifier, CustomSmartWalletFactory.address) + + const customSmartWalletRelayVerifierAddress = RelayVerifier.address + + await deployer.deploy(TestToken) + await deployer.deploy(SampleRecipient) + + console.log('Done! Summary:') + + console.log('|===================================|============================================|') + console.log('| Contract | Address |') + console.log('|===================================|============================================|') + console.log(`| Penalizer | ${Penalizer.address} |`) + console.log(`| RelayHub | ${RelayHub.address} |`) + console.log('| Smart Wallet Contracts ========================================================|') + console.log(`| SmartWallet | ${SmartWallet.address} |`) + console.log(`| SmartWalletFactory | ${SmartWalletFactory.address} |`) + console.log(`| SmartWalletDeployVerifier | ${DeployVerifier.address} |`) + console.log(`| SmartWalletRelayVerifier | ${smartWalletRelayVerifierAddress} |`) + console.log('| Custom Smart Wallet Contracts =================================================|') + console.log(`| CustomSmartWallet | ${CustomSmartWallet.address} |`) + console.log(`| CustomSmartWalletFactory | ${CustomSmartWalletFactory.address} |`) + console.log(`| CustomSmartWalletDeployVerifier | ${CustomSmartWalletDeployVerifier.address} |`) + console.log(`| CustomSmartWalletRelayVerifier | ${customSmartWalletRelayVerifierAddress} |`) + console.log('| Testing Contracts =============================================================|') + console.log(`| SampleRecipient | ${SampleRecipient.address} |`) + console.log(`| TestToken | ${TestToken.address} |`) + console.log('|===================================|============================================|\n') +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..20305b70 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "rif-relay-contracts", + "version": "1.0.0", + "description": "This project contains all the contracts needed for the rif relay system.", + "main": "index.ts", + "scripts": { + "dist": "scripts/dist", + "deploy": "scripts/deploy" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@openeth/truffle-typings": "0.0.6", + "@openzeppelin/contracts": "^3.2.0", + "@truffle/contract": "^4.2.23", + "@truffle/hdwallet-provider": "1.2.3", + "@types/node": "^13.0.0", + "truffle-hdwallet-provider": "^1.0.17" + }, + "devDependencies": { + "solhint": "3.0.0", + "truffle": "^5.1.52", + "ts-loader": "^9.2.3", + "ts-node": "8.6.2", + "typechain": "1.0.5", + "typechain-target-trufflehotfix": "0.0.4-alpha", + "typescript": "^4.3.5", + "webpack": "^5.43.0", + "webpack-cli": "^4.7.2" + } +} diff --git a/scripts/deploy b/scripts/deploy new file mode 100755 index 00000000..268142ef --- /dev/null +++ b/scripts/deploy @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ $# -eq 0 ]; then + echo "You need to specify the network to use for deploy, should be one inside of the truffle.js file." + exit 1 +fi + +NETWORK=$1 + +npx truffle migrate --network "${NETWORK}" diff --git a/scripts/dist b/scripts/dist new file mode 100755 index 00000000..d7c664ec --- /dev/null +++ b/scripts/dist @@ -0,0 +1,3 @@ +#!/bin/bash + +npx truffle compile && webpack diff --git a/truffle.js b/truffle.js new file mode 100644 index 00000000..9792cf4d --- /dev/null +++ b/truffle.js @@ -0,0 +1,79 @@ +require('ts-node/register/transpile-only'); + +const HDWalletProvider = require('@truffle/hdwallet-provider'); +const mnemonic = 'digital unknown jealous mother legal hedgehog save glory december universe spread figure custom found six'; + +const secretMnemonicFile = './secret_mnemonic'; +const fs = require('fs'); +let secretMnemonic; +if (fs.existsSync(secretMnemonicFile)) { + secretMnemonic = fs.readFileSync(secretMnemonicFile, { encoding: 'utf8' }); +} + +module.exports = { + networks: { + development: { + verbose: process.env.VERBOSE, + host: '127.0.0.1', + port: 4444, + network_id: 33, + gas: 6300000, + gasPrice: 60000000 // 0.06 gwei + }, + rskdocker: { + verbose: process.env.VERBOSE, + host: 'enveloping-rskj', + port: 4444, + network_id: 33, + gas: 6300000, + gasPrice: 60000000 // 0.06 gwei + }, + rsk: { + verbose: process.env.VERBOSE, + host: '127.0.0.1', + port: 4444, + network_id: 33, + gas: 6300000, + gasPrice: 60000000 // 0.06 gwei + }, + rsktestnet: { + provider: function () { + return new HDWalletProvider(mnemonic, 'https://public-node.testnet.rsk.co') + }, + network_id: 31, + gas: 6300000, + gasPrice: 60000000 // 0.06 gwei + }, + rskmainnet: { + provider: function () { + return new HDWalletProvider(secretMnemonic, 'https://public-node.rsk.co') + }, + network_id: 30, + gas: 6300000, + gasPrice: 60000000 // 0.06 gwei + } + }, + mocha: { + slow: 1000, + reporter: 'eth-gas-reporter', + reporterOptions: { + currency: 'USD', + onlyCalledMethods: true, + showTimeSpent: true, + excludeContracts: [] + } + }, + compilers: { + solc: { + version: '0.6.12', + settings: { + evmVersion: 'istanbul', + optimizer: { + enabled: true, + runs: 200 // Optimize for how many times you intend to run the code + } + } + } + }, + migrations_directory: './migrations' +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..b3fde5ed --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "noImplicitAny": true, + "sourceMap": true, + "module": "es6", + "target": "es5", + "allowJs": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports" : true, + "esModuleInterop" : true + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..b6f19b05 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,27 @@ +const path = require('path'); + +module.exports = { + entry: './index.ts', + devtool: 'inline-source-map', + mode: 'production', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, + performance: { + maxEntrypointSize: 5120000, + maxAssetSize: 5120000 + } +};