diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..adb20fe --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.sol linguist-language=Solidity +*.vy linguist-language=Python diff --git a/.gitignore b/.gitignore index 86aa801..3adf41c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,12 @@ yarn-error.log* # Hardhat artifacts/ cache/ -typechain-types/ \ No newline at end of file +typechain-types/ + +# Brownie giga-chad +__pycache__ +.env +.history +.hypothesis/ +build/ +reports/ diff --git a/brownie-config.yaml b/brownie-config.yaml new file mode 100644 index 0000000..35433b0 --- /dev/null +++ b/brownie-config.yaml @@ -0,0 +1,9 @@ +hypothesis: + max_examples: 50 + +compiler: + solc: + # version: 0.8.6 + remappings: + - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.3.1" + - "@uniswap=Uniswap/v3-core@1.0.0" \ No newline at end of file diff --git a/contracts/FundsRouter.sol b/contracts/FundsRouter.sol new file mode 100644 index 0000000..a34fc11 --- /dev/null +++ b/contracts/FundsRouter.sol @@ -0,0 +1,160 @@ +pragma solidity 0.8.6; + + +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "./autonomy/abstract/Shared.sol"; +import "../interfaces/autonomy/IForwarder.sol"; + + +/** +* @notice This contract serves as both a router for bundling actions +* to be automated, along with conditions under which those actions +* should only be executed under, aswell as a vault for storing ETH +* to pay the execution fees by Autonomy Network as part of +* Autonomy's Automation Station. Users can deposit and withdraw +* funds at any time. This system is designed to be extremely modular +* where users can use an arbitrary number of conditions with an +* arbitrary number of calls in an arbitrary order. +* @author @quantafire (James Key) +*/ +contract FundsRouter is ReentrancyGuard, Shared { + + event BalanceChanged(address indexed user, uint newBal); + + + // ETH balances to pay for execution fees + mapping(address => uint) public balances; + // The Autonomy Registry to send the execution fee to + address payable public immutable registry; + // The forwarder used by the Registry to guarantee that calls from it + // have the correct `user` and `feeAmount` arguments + IForwarder public immutable regUserFeeVeriForwarder; + // The forwarder used by this FundsRouter contract to guarantee that + // calls from it have the correct `user` argument, where the recipient + // of the call(s) know that the `user` argument is correct + IForwarder public immutable routerUserVeriForwarder; + // Targets that shouldn't be valid for security or other reasons + mapping(address => bool) private _invalidTargets; + + + struct FcnData { + address target; + bytes callData; + uint ethForCall; + bool verifyUser; + } + + + constructor( + address payable registry_, + IForwarder regUserFeeVeriForwarder_, + IForwarder routerUserVeriForwarder_ + ) ReentrancyGuard() { + registry = registry_; + regUserFeeVeriForwarder = regUserFeeVeriForwarder_; + routerUserVeriForwarder = routerUserVeriForwarder_; + + _invalidTargets[address(this)] = true; + _invalidTargets[address(routerUserVeriForwarder_)] = true; + _invalidTargets[address(0)] = true; + } + + /** + * @notice Deposit ETH to fund the execution of requests by `spender`. + * @param spender The address that should be credited with the funds. + */ + function depositETH(address spender) external payable { + uint newBal = balances[spender] + msg.value; + balances[spender] = newBal; + emit BalanceChanged(spender, newBal); + } + + /** + * @notice Withdraw ETH from `msg.sender`'s balance to send to `recipient`. + * @param recipient The address to receive the ETH. + * @param amount The amount of ETH to withdraw. + */ + function withdrawETH(address payable recipient, uint amount) external nonReentrant { + uint startBal = balances[msg.sender]; + require(startBal >= amount, "FRouter: not enough funds"); + + uint newBal = startBal - amount; + balances[msg.sender] = newBal; + recipient.transfer(amount); + emit BalanceChanged(msg.sender, newBal); + } + + /** + * @notice Forward an arbitrary number of calls. These could be to + * contracts that just test a condition, such as time or a price, + * or to contracts to execute an action and change the state of + * that contract, such as rebalancing a portfolio, or simply + * sending ETH. This function takes into account any ETH received + * during any of the calls and adds it to `user`'s balance, enabling + * requests to be made without any deposited funds if the receiving + * contract pays some kind of reward for calling it. + * @param user The address of the user who made the request. + * @param feeAmount The amount that Autonomy charges to cover the gas cost of + * executing the request each time, plus a small, deterministic + * incentive fee to the bot. Assumed to be denominated in ETH. + * @param fcnData An array of FcnData structs to be called in series. Each struct + * specifies everything needed to make each call independently: + * - target - the address to be called + * - callData - the calldata that specifies what function in the target + * should be called (if a contract) along with any input parameters. + * - ethForCall - any ETH that should be sent with the call + * - verifyUser - whether or not the 1st argument of `callData` should + * be guaranteed to be `user`. If `true`, then the call is routed + * through `routerUserVeriForwarder` so that `target` knows it can + * trust the 1st input parameter as correct if + * `msg.sender == routerUserVeriForwarder` from `target`'s perspective. + * The user should make sure that that is true when generating `callData`. + * If `verifyUser` is `false`, the call will just be sent directly from this + * `FundsRouter` contract. + */ + function forwardCalls( + address user, + uint feeAmount, + FcnData[] calldata fcnData + ) external nonReentrant returns (bool, bytes memory) { + require(msg.sender == address(regUserFeeVeriForwarder), "FRouter: not userFeeForw"); + + uint userBal = balances[user]; + uint routerStartBal = address(this).balance; + uint ethSent = 0; + + bool success; + bytes memory returnData; + // Iterate through conditions and make sure they're all met + for (uint i; i < fcnData.length; i++) { + require(!_invalidTargets[fcnData[i].target], "FRouter: nice try ;)"); + ethSent += fcnData[i].ethForCall; + + if (fcnData[i].verifyUser) { + // Ensure that the 1st argument in this call is the user + require(abi.decode(fcnData[i].callData[4:36], (address)) == user, "FRouter: calldata not user"); + (success, returnData) = routerUserVeriForwarder.forward{value: fcnData[i].ethForCall}(fcnData[i].target, fcnData[i].callData); + } else { + (success, returnData) = fcnData[i].target.call{value: fcnData[i].ethForCall}(fcnData[i].callData); + } + + revertFailedCall(success, returnData); + } + + uint routerEndBal = address(this).balance; + // Make sure that funds were siphoned out this contract somehow + require(routerEndBal + ethSent >= routerStartBal, "FRouter: funds missing"); + uint ethReceivedDuringForwards = routerEndBal + ethSent - routerStartBal; + + // Make sure that the user has enough balance + // Having both these checks is definitely overkill - need to get rid of 1 + require(userBal + ethReceivedDuringForwards >= ethSent + feeAmount, "FRouter: not enough funds - fee"); + require(userBal + ethReceivedDuringForwards - ethSent - feeAmount == userBal + routerEndBal - routerStartBal - feeAmount, "FRouter: something doesnt add up"); + balances[user] = userBal + ethReceivedDuringForwards - ethSent - feeAmount; + + registry.transfer(feeAmount); + } + + // Receive ETH from called contracts, perhaps if they have a reward for poking them + receive() external payable {} +} \ No newline at end of file diff --git a/contracts/actions/MockTarget.sol b/contracts/actions/MockTarget.sol new file mode 100644 index 0000000..63b9a60 --- /dev/null +++ b/contracts/actions/MockTarget.sol @@ -0,0 +1,43 @@ +pragma solidity 0.8.6; + + +import "../FundsRouter.sol"; + + +contract MockTarget { + + address public immutable routerUserVeriForwarder; + FundsRouter public immutable fundsRouter; + uint public x; + address public addr; + uint public y; + + + constructor(address routerUserVeriForwarder_, FundsRouter fundsRouter_) { + routerUserVeriForwarder = routerUserVeriForwarder_; + fundsRouter = fundsRouter_; + } + + function incrementX() external { + x++; + } + + function setX(uint newX) public payable { + x = newX; + } + + function setY(uint newY) public payable { + y = newY; + } + + function setYVerify(address user, uint newY) public payable { + require(msg.sender == routerUserVeriForwarder, "MockTarget: not userForw"); + y = newY; + } + + function callWithdrawETH(uint amount) public { + fundsRouter.withdrawETH(payable(address(this)), amount); + } + + receive() external payable {} +} \ No newline at end of file diff --git a/contracts/autonomy/AUTO.sol b/contracts/autonomy/AUTO.sol new file mode 100644 index 0000000..ea49be0 --- /dev/null +++ b/contracts/autonomy/AUTO.sol @@ -0,0 +1,34 @@ +pragma solidity 0.8.6; + + +import "@openzeppelin/contracts/token/ERC777/ERC777.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + + +/** +* @title AUTO contract +* @notice The AUTO utility token which is used to stake in Autonomy and pay for +* execution fees with +* @author Quantaf1re (James Key) +*/ +contract AUTO is ERC777, Ownable { + + constructor( + string memory name, + string memory symbol, + address[] memory defaultOperators, + address receiver, + uint256 mintAmount + ) ERC777(name, symbol, defaultOperators) Ownable() { + _mint(receiver, mintAmount, "", ""); + } + + function mint( + address receiver, + uint amount, + bytes memory userData, + bytes memory operatorData + ) external onlyOwner { + _mint(receiver, amount, userData, operatorData); + } +} diff --git a/contracts/autonomy/EVMMaths.sol b/contracts/autonomy/EVMMaths.sol new file mode 100644 index 0000000..1077e92 --- /dev/null +++ b/contracts/autonomy/EVMMaths.sol @@ -0,0 +1,20 @@ +pragma solidity 0.8.6; + + +contract EVMMaths { + function mul5div1(uint a, uint b, uint c, uint d, uint e) external pure returns (uint) { + return a * b * c * d / e; + } + + function mul4Div2(uint a, uint b, uint c, uint d, uint e, uint f) external pure returns (uint) { + return a * b * c * d / (e * f); + } + + function mul3div1(uint a, uint b, uint c, uint d) external pure returns (uint) { + return a * b * c / d; + } + + function getRemainder(uint a, uint b) public pure returns (uint) { + return a % b; + } +} \ No newline at end of file diff --git a/contracts/autonomy/Forwarder.sol b/contracts/autonomy/Forwarder.sol new file mode 100644 index 0000000..461f6a5 --- /dev/null +++ b/contracts/autonomy/Forwarder.sol @@ -0,0 +1,31 @@ +pragma solidity 0.8.6; + + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "../../interfaces/autonomy/IForwarder.sol"; + + +contract Forwarder is IForwarder, Ownable { + + mapping(address => bool) private _canCall; + + + constructor() Ownable() {} + + + function forward( + address target, + bytes calldata callData + ) external override payable returns (bool success, bytes memory returnData) { + require(_canCall[msg.sender], "Forw: caller not the Registry"); + (success, returnData) = target.call{value: msg.value}(callData); + } + + function canCall(address caller) external view returns (bool) { + return _canCall[caller]; + } + + function setCaller(address caller, bool b) external onlyOwner { + _canCall[caller] = b; + } +} \ No newline at end of file diff --git a/contracts/autonomy/Miner.sol b/contracts/autonomy/Miner.sol new file mode 100644 index 0000000..2419a80 --- /dev/null +++ b/contracts/autonomy/Miner.sol @@ -0,0 +1,169 @@ +pragma solidity 0.8.6; + + +import "../../interfaces/autonomy/IRegistry.sol"; +import "./abstract/Shared.sol"; + + +contract Miner is Shared { + + IERC20 private _AUTO; + IRegistry private _reg; + uint private _AUTOPerReq; + uint private _AUTOPerExec; + uint private _AUTOPerReferal; + // 1k AUTO + uint public constant MAX_UPDATE_BAL = 1000 * _E_18; + // 1 AUTO + uint public constant MIN_REWARD = _E_18; + // 10k AUTO + uint public constant MIN_FUND = 10000 * _E_18; + // This counts the number of executed requests that the requester + // has mined rewards for + mapping(address => uint) private _minedReqCounts; + // This counts the number of executions that the executor has + // mined rewards for + mapping(address => uint) private _minedExecCounts; + // This counts the number of executed requests that the requester + // has mined rewards for + mapping(address => uint) private _minedReferalCounts; + + + event RatesUpdated(uint newAUTOPerReq, uint newAUTOPerExec, uint newAUTOPerReferal); + + + constructor( + IERC20 AUTO, + IRegistry reg, + uint AUTOPerReq, + uint AUTOPerExec, + uint AUTOPerReferal + ) { + _AUTO = AUTO; + _reg = reg; + _AUTOPerReq = AUTOPerReq; + _AUTOPerExec = AUTOPerExec; + _AUTOPerReferal = AUTOPerReferal; + } + + + ////////////////////////////////////////////////////////////// + // // + // Claiming // + // // + ////////////////////////////////////////////////////////////// + + function claimMiningRewards() external { + (uint reqRewardCount, uint execRewardCount, uint referalRewardCount, uint rewards) = + getAvailableMiningRewards(msg.sender); + require(rewards > 0, "Miner: no pending rewards"); + + _minedReqCounts[msg.sender] += reqRewardCount; + _minedExecCounts[msg.sender] += execRewardCount; + _minedReferalCounts[msg.sender] += referalRewardCount; + require(_AUTO.transfer(msg.sender, rewards)); + } + + function claimReqMiningReward(uint claimCount) external nzUint(claimCount) { + _claimSpecificMiningReward(_minedReqCounts, _reg.getReqCountOf(msg.sender), claimCount, _AUTOPerReq); + } + + function claimExecMiningReward(uint claimCount) external nzUint(claimCount) { + _claimSpecificMiningReward(_minedExecCounts, _reg.getExecCountOf(msg.sender), claimCount, _AUTOPerExec); + } + + function claimReferalMiningReward(uint claimCount) external nzUint(claimCount) { + _claimSpecificMiningReward(_minedReferalCounts, _reg.getReferalCountOf(msg.sender), claimCount, _AUTOPerReferal); + } + + function _claimSpecificMiningReward( + mapping(address => uint) storage counter, + uint regCount, + uint claimCount, + uint rate + ) private { + require( + claimCount <= regCount - counter[msg.sender], + "Miner: claim too large" + ); + + counter[msg.sender] += claimCount; + require(_AUTO.transfer(msg.sender, claimCount * rate)); + } + + + ////////////////////////////////////////////////////////////// + // // + // Updating params // + // // + ////////////////////////////////////////////////////////////// + + function updateAndFund( + uint newAUTOPerReq, + uint newAUTOPerExec, + uint newAUTOPerReferal, + uint amountToFund + ) external { + require(_AUTO.balanceOf(address(this)) <= MAX_UPDATE_BAL, "Miner: AUTO bal too high"); + // So that nobody updates with a small amount of AUTO and makes the rates + // 1 wei, effectively bricking the contract + require( + newAUTOPerReq >= MIN_REWARD && + newAUTOPerExec >= MIN_REWARD && + newAUTOPerReferal >= MIN_REWARD, + "Miner: new rates too low" + ); + require(amountToFund >= MIN_FUND, "Miner: funding too low, peasant"); + + // Update rates and fund the Miner + _AUTOPerReq = newAUTOPerReq; + _AUTOPerExec = newAUTOPerExec; + _AUTOPerReferal = newAUTOPerReferal; + require(_AUTO.transferFrom(msg.sender, address(this), amountToFund)); + emit RatesUpdated(newAUTOPerReq, newAUTOPerExec, newAUTOPerReferal); + } + + + ////////////////////////////////////////////////////////////// + // // + // Getters // + // // + ////////////////////////////////////////////////////////////// + + function getAUTOPerReq() external view returns (uint) { + return _AUTOPerReq; + } + + function getAUTOPerExec() external view returns (uint) { + return _AUTOPerExec; + } + + function getAUTOPerReferal() external view returns (uint) { + return _AUTOPerReferal; + } + + function getMinedReqCountOf(address addr) external view returns (uint) { + return _minedReqCounts[addr]; + } + + function getMinedExecCountOf(address addr) external view returns (uint) { + return _minedExecCounts[addr]; + } + + function getMinedReferalCountOf(address addr) external view returns (uint) { + return _minedReferalCounts[addr]; + } + + function getAvailableMiningRewards(address addr) public view returns (uint, uint, uint, uint) { + uint reqRewardCount = _reg.getReqCountOf(addr) - _minedReqCounts[addr]; + uint execRewardCount = _reg.getExecCountOf(addr) - _minedExecCounts[addr]; + uint referalRewardCount = _reg.getReferalCountOf(addr) - _minedReferalCounts[addr]; + + uint rewards = + (reqRewardCount * _AUTOPerReq) + + (execRewardCount * _AUTOPerExec) + + (referalRewardCount * _AUTOPerReferal); + + return (reqRewardCount, execRewardCount, referalRewardCount, rewards); + } +} \ No newline at end of file diff --git a/contracts/autonomy/Oracle.sol b/contracts/autonomy/Oracle.sol new file mode 100644 index 0000000..0c6027f --- /dev/null +++ b/contracts/autonomy/Oracle.sol @@ -0,0 +1,48 @@ +pragma solidity 0.8.6; + + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "../../interfaces/autonomy/IOracle.sol"; +import "../../interfaces/autonomy/IPriceOracle.sol"; + + +contract Oracle is IOracle, Ownable { + + IPriceOracle private _priceOracle; + bool private _defaultPayIsAUTO; + + + constructor(IPriceOracle priceOracle, bool defaultPayIsAUTO) Ownable() { + _priceOracle = priceOracle; + _defaultPayIsAUTO = defaultPayIsAUTO; + } + + + function getRandNum(uint seed) external override view returns (uint) { + return uint(blockhash(seed)); + } + + function getPriceOracle() external override view returns (IPriceOracle) { + return _priceOracle; + } + + function getAUTOPerETH() external override view returns (uint) { + return _priceOracle.getAUTOPerETH(); + } + + function getGasPriceFast() external override view returns (uint) { + return _priceOracle.getGasPriceFast(); + } + + function setPriceOracle(IPriceOracle newPriceOracle) external override onlyOwner { + _priceOracle = newPriceOracle; + } + + function defaultPayIsAUTO() external override view returns (bool) { + return _defaultPayIsAUTO; + } + + function setDefaultPayIsAUTO(bool newDefaultPayIsAUTO) external override onlyOwner { + _defaultPayIsAUTO = newDefaultPayIsAUTO; + } +} \ No newline at end of file diff --git a/contracts/autonomy/PriceOracle.sol b/contracts/autonomy/PriceOracle.sol new file mode 100644 index 0000000..16ca03b --- /dev/null +++ b/contracts/autonomy/PriceOracle.sol @@ -0,0 +1,35 @@ +pragma solidity 0.8.6; + + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "../../interfaces/autonomy/IPriceOracle.sol"; + + +contract PriceOracle is IPriceOracle, Ownable { + + + uint private _AUTOPerETH; + uint private _gasPrice; + + + constructor(uint AUTOPerETH, uint gasPrice) Ownable() { + _AUTOPerETH = AUTOPerETH; + _gasPrice = gasPrice; + } + + function getAUTOPerETH() external override view returns (uint) { + return _AUTOPerETH; + } + + function updateAUTOPerETH(uint AUTOPerETH) external onlyOwner { + _AUTOPerETH = AUTOPerETH; + } + + function getGasPriceFast() external override view returns (uint) { + return _gasPrice; + } + + function updateGasPriceFast(uint gasPrice) external onlyOwner { + _gasPrice = gasPrice; + } +} \ No newline at end of file diff --git a/contracts/autonomy/Registry.sol b/contracts/autonomy/Registry.sol new file mode 100644 index 0000000..cf57ea2 --- /dev/null +++ b/contracts/autonomy/Registry.sol @@ -0,0 +1,503 @@ +pragma solidity 0.8.6; + + +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "../../interfaces/autonomy/IRegistry.sol"; +import "../../interfaces/autonomy/IStakeManager.sol"; +import "../../interfaces/autonomy/IOracle.sol"; +import "../../interfaces/autonomy/IForwarder.sol"; +import "./abstract/Shared.sol"; +import "./AUTO.sol"; + + +contract Registry is IRegistry, Shared, ReentrancyGuard { + + // Constant public + uint public constant GAS_OVERHEAD_AUTO = 16000; + uint public constant GAS_OVERHEAD_ETH = 6000; + uint public constant BASE_BPS = 10000; + uint public constant PAY_AUTO_BPS = 11000; + uint public constant PAY_ETH_BPS = 13000; + + // Constant private + bytes private constant _EMPTY_BYTES = ""; + + AUTO private immutable _AUTO; + IStakeManager private immutable _stakeMan; + IOracle private immutable _oracle; + IForwarder private immutable _userForwarder; + IForwarder private immutable _gasForwarder; + IForwarder private immutable _userGasForwarder; + + // Used to make sure that `target` can't be something that affects + // the Autonomy system itself + mapping(address => bool) private _invalidTargets; + // This counts the number of times each user has had a request executed + mapping(address => uint) private _reqCounts; + // This counts the number of times each staker has executed a request + mapping(address => uint) private _execCounts; + // This counts the number of times each referer has been identified in an + // executed tx + mapping(address => uint) private _referalCounts; + bytes32[] private _hashedReqs; + + + // This is defined in IRegistry. Here for convenience + // The address vars are 20b, total 60, calldata is 4b + n*32b usually, which + // has a factor of 32. uint112 since the current ETH supply of ~115m can fit + // into that and it's the highest such that 2 * uint112 + 2 * bool is < 256b + // struct Request { + // address payable user; + // address target; + // address payable referer; + // bytes callData; + // uint112 initEthSent; + // uint112 ethForCall; + // bool verifyUser; + // bool insertFeeAmount; + // bool payWithAUTO; + // bool isAlive; + // } + + // Easier to parse when using native types rather than structs + event HashedReqAdded( + uint indexed id, + address indexed user, + address target, + address payable referer, + bytes callData, + uint112 initEthSent, + uint112 ethForCall, + bool verifyUser, + bool insertFeeAmount, + bool payWithAUTO, + bool isAlive + ); + event HashedReqExecuted(uint indexed id, bool wasRemoved); + event HashedReqCancelled(uint indexed id); + + + constructor( + IStakeManager stakeMan, + IOracle oracle, + IForwarder userForwarder, + IForwarder gasForwarder, + IForwarder userGasForwarder, + string memory tokenName, + string memory tokenSymbol, + uint totalAUTOSupply + ) ReentrancyGuard() { + // ERC777 token + address[] memory defaultOperators = new address[](2); + defaultOperators[0] = address(this); + defaultOperators[1] = address(stakeMan); + AUTO aut = new AUTO(tokenName, tokenSymbol, defaultOperators, msg.sender, totalAUTOSupply); + + _AUTO = aut; + _stakeMan = stakeMan; + _oracle = oracle; + _userForwarder = userForwarder; + _gasForwarder = gasForwarder; + _userGasForwarder = userGasForwarder; + _invalidTargets[address(this)] = true; + _invalidTargets[address(userForwarder)] = true; + _invalidTargets[address(gasForwarder)] = true; + _invalidTargets[address(userGasForwarder)] = true; + _invalidTargets[address(aut)] = true; + _invalidTargets[0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24] = true; + _invalidTargets[address(stakeMan)] = true; + _invalidTargets[_ADDR_0] = true; + } + + + ////////////////////////////////////////////////////////////// + // // + // Hashed Requests // + // // + ////////////////////////////////////////////////////////////// + + function newReq( + address target, + address payable referer, + bytes calldata callData, + uint112 ethForCall, + bool verifyUser, + bool insertFeeAmount, + bool isAlive + ) external payable override targetNotThis(target) returns (uint id) { + return _newReq( + target, + referer, + callData, + ethForCall, + verifyUser, + insertFeeAmount, + _oracle.defaultPayIsAUTO(), + isAlive + ); + } + + function newReqPaySpecific( + address target, + address payable referer, + bytes calldata callData, + uint112 ethForCall, + bool verifyUser, + bool insertFeeAmount, + bool payWithAUTO, + bool isAlive + ) external payable override targetNotThis(target) returns (uint id) { + return _newReq( + target, + referer, + callData, + ethForCall, + verifyUser, + insertFeeAmount, + payWithAUTO, + isAlive + ); + } + + function _newReq( + address target, + address payable referer, + bytes calldata callData, + uint112 ethForCall, + bool verifyUser, + bool insertFeeAmount, + bool payWithAUTO, + bool isAlive + ) + private + validEth(payWithAUTO, ethForCall) + returns (uint id) + { + if (isAlive) { + require(msg.value == 0, "Reg: no ETH while alive"); + } + + Request memory r = Request( + payable(msg.sender), + target, + referer, + callData, + uint112(msg.value), + ethForCall, + verifyUser, + insertFeeAmount, + payWithAUTO, + isAlive + ); + bytes32 hashedIpfsReq = keccak256(getReqBytes(r)); + + id = _hashedReqs.length; + emit HashedReqAdded( + id, + r.user, + r.target, + r.referer, + r.callData, + r.initEthSent, + r.ethForCall, + r.verifyUser, + r.insertFeeAmount, + r.payWithAUTO, + r.isAlive + ); + _hashedReqs.push(hashedIpfsReq); + } + + function getHashedReqs() external view override returns (bytes32[] memory) { + return _hashedReqs; + } + + function getHashedReqsSlice(uint startIdx, uint endIdx) external override view returns (bytes32[] memory) { + bytes32[] memory slice = new bytes32[](endIdx - startIdx); + uint sliceIdx = 0; + for (uint arrIdx = startIdx; arrIdx < endIdx; arrIdx++) { + slice[sliceIdx] = _hashedReqs[arrIdx]; + sliceIdx++; + } + + return slice; + } + + function getHashedReqsLen() external view override returns (uint) { + return _hashedReqs.length; + } + + function getHashedReq(uint id) external view override returns (bytes32) { + return _hashedReqs[id]; + } + + + ////////////////////////////////////////////////////////////// + // // + // Bytes Helpers // + // // + ////////////////////////////////////////////////////////////// + + function getReqBytes(Request memory r) public pure override returns (bytes memory) { + return abi.encode(r); + } + + function insertToCallData(bytes calldata callData, uint expectedGas, uint startIdx) public pure override returns (bytes memory) { + bytes memory cd = callData; + bytes memory expectedGasBytes = abi.encode(expectedGas); + for (uint i = 0; i < 32; i++) { + cd[startIdx+i] = expectedGasBytes[i]; + } + + return cd; + } + + + ////////////////////////////////////////////////////////////// + // // + // Executions // + // // + ////////////////////////////////////////////////////////////// + + /** + * @dev validCalldata needs to be before anything that would convert it to memory + * since that is persistent and would prevent validCalldata, that requires + * calldata, from working. Can't do the check in _execute for the same reason. + * Note: targetNotThis and validEth are used in newReq. + * validCalldata is only used here because it causes an unknown + * 'InternalCompilerError' when using it with newReq + */ + function executeHashedReq( + uint id, + Request calldata r, + uint expectedGas + ) + external + override + validExec + nonReentrant + validCalldata(r) + verReq(id, r) + returns (uint gasUsed) + { + uint startGas = gasleft(); + + // We are the gods now + if (r.isAlive) { + emit HashedReqExecuted(id, false); + } else { + emit HashedReqExecuted(id, true); + delete _hashedReqs[id]; + } + _execute(r, expectedGas); + + gasUsed = startGas - gasleft() + (r.payWithAUTO == true ? GAS_OVERHEAD_AUTO : GAS_OVERHEAD_ETH); + // Make sure that the expected gas used is within 10% of the actual gas used + require(expectedGas * 10 <= gasUsed * 11, "Reg: expectedGas too high"); + } + + function _execute(Request calldata r, uint expectedGas) private { + IOracle orac = _oracle; + uint ethStartBal = address(this).balance; + uint feeTotal; + if (r.payWithAUTO) { + feeTotal = expectedGas * orac.getGasPriceFast() * orac.getAUTOPerETH() * PAY_AUTO_BPS / (BASE_BPS * _E_18); + } else { + feeTotal = expectedGas * orac.getGasPriceFast() * PAY_ETH_BPS / BASE_BPS; + } + + // Make the call that the user requested + bool success; + bytes memory returnData; + if (r.verifyUser && !r.insertFeeAmount) { + (success, returnData) = _userForwarder.forward{value: r.ethForCall}(r.target, r.callData); + } else if (!r.verifyUser && r.insertFeeAmount) { + (success, returnData) = _gasForwarder.forward{value: r.ethForCall}( + r.target, + insertToCallData(r.callData, feeTotal, 4) + ); + } else if (r.verifyUser && r.insertFeeAmount) { + (success, returnData) = _userGasForwarder.forward{value: r.ethForCall}( + r.target, + insertToCallData(r.callData, feeTotal, 36) + ); + } else { + (success, returnData) = r.target.call{value: r.ethForCall}(r.callData); + } + // Need this if statement because if the call succeeds, the tx will revert + // with an EVM error because it can't decode 0x00. If a tx fails with no error + // message, maybe that's a problem? But if it failed without a message then it's + // gonna be hard to know what went wrong regardless + if (!success) { + // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/d57593c148dad16abe675083464787ca10f789ec/contracts/utils/Address.sol#L210 + if (returnData.length > 0) { + assembly { + let returndata_size := mload(returnData) + revert(add(32, returnData), returndata_size) + } + } else { + revert("No error message!"); + } + } + + // // Store AUTO rewards + // // It's cheaper to store the cumulative rewards than it is to send + // // an AUTO transfer directly since the former changes 1 storage + // // slot whereas the latter changes 2. The rewards are actually stored + // // in a different contract that reads the reward storage of this contract + // // because of the danger of someone using call to call to AUTO and transfer + // // out tokens. It could be prevented by preventing r.target being set to AUTO, + // // but it's better to be paranoid and totally separate the contracts. + // // Need to include these storages in the gas cost that the user pays since + // // they benefit from part of it and the costs can vary depending on whether + // // the amounts changed from were 0 or non-0 + // _reqCounts[r.user] += 1; + // _execCounts[msg.sender] += 1; + // if (r.referer != _ADDR_0) { + // _referalCounts[r.referer] += 1; + // } + + // If ETH was somehow siphoned from this contract during the request, + // this will revert because of an `Integer overflow` underflow - a security feature + uint ethReceivedDuringRequest = address(this).balance + r.ethForCall - ethStartBal; + if (r.payWithAUTO) { + // Send the executor their bounty + _AUTO.operatorSend(r.user, msg.sender, feeTotal, "", ""); + } else { + uint ethReceived = r.initEthSent - r.ethForCall + ethReceivedDuringRequest; + // Send the executor their bounty + require(ethReceived >= feeTotal, "Reg: not enough eth sent"); + payable(msg.sender).transfer(feeTotal); + + // Refund excess to the user + uint excess = ethReceived - feeTotal; + if (excess > 0) { + r.user.transfer(excess); + } + } + } + + + ////////////////////////////////////////////////////////////// + // // + // Cancellations // + // // + ////////////////////////////////////////////////////////////// + + function cancelHashedReq( + uint id, + Request memory r + ) + external + override + nonReentrant + verReq(id, r) + { + require(msg.sender == r.user, "Reg: not the user"); + + // Cancel the request + emit HashedReqCancelled(id); + delete _hashedReqs[id]; + + // Send refund + if (r.initEthSent > 0) { + r.user.transfer(r.initEthSent); + } + } + + + ////////////////////////////////////////////////////////////// + // // + // Getters // + // // + ////////////////////////////////////////////////////////////// + + function getAUTOAddr() external view override returns (address) { + return address(_AUTO); + } + + function getStakeManager() external view override returns (address) { + return address(_stakeMan); + } + + function getOracle() external view override returns (address) { + return address(_oracle); + } + + function getUserForwarder() external view override returns (address) { + return address(_userForwarder); + } + + function getGasForwarder() external view override returns (address) { + return address(_gasForwarder); + } + + function getUserGasForwarder() external view override returns (address) { + return address(_userGasForwarder); + } + + function getReqCountOf(address addr) external view override returns (uint) { + return _reqCounts[addr]; + } + + function getExecCountOf(address addr) external view override returns (uint) { + return _execCounts[addr]; + } + + function getReferalCountOf(address addr) external view override returns (uint) { + return _referalCounts[addr]; + } + + + ////////////////////////////////////////////////////////////// + // // + // Modifiers // + // // + ////////////////////////////////////////////////////////////// + + modifier targetNotThis(address target) { + require(!_invalidTargets[target], "Reg: nice try ;)"); + _; + } + + modifier validEth(bool payWithAUTO, uint ethForCall) { + if (payWithAUTO) { + // When paying with AUTO, there's no reason to send more ETH than will + // be used in the future call + require(ethForCall == msg.value, "Reg: ethForCall not msg.value"); + } else { + // When paying with ETH, ethForCall needs to be lower than msg.value + // since some ETH is needed to be left over for paying the fee + bounty + require(ethForCall <= msg.value, "Reg: ethForCall too high"); + } + _; + } + + modifier validCalldata(Request calldata r) { + if (r.verifyUser) { + require(abi.decode(r.callData[4:36], (address)) == r.user, "Reg: calldata not verified"); + } + _; + } + + modifier validExec() { + require(_stakeMan.isUpdatedExec(msg.sender), "Reg: not executor or expired"); + _; + } + + // Verify that a request is the same as the one initially stored. This also + // implicitly checks that the request hasn't been deleted as the hash of the + // request isn't going to be address(0) + modifier verReq( + uint id, + Request memory r + ) { + require( + keccak256(getReqBytes(r)) == _hashedReqs[id], + "Reg: request not the same" + ); + _; + } + + receive() external payable {} +} diff --git a/contracts/autonomy/StakeManager.sol b/contracts/autonomy/StakeManager.sol new file mode 100644 index 0000000..1388977 --- /dev/null +++ b/contracts/autonomy/StakeManager.sol @@ -0,0 +1,246 @@ +pragma solidity 0.8.6; + + +import "@openzeppelin/contracts/token/ERC777/IERC777.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC1820Registry.sol"; +import "../../interfaces/autonomy/IStakeManager.sol"; +import "../../interfaces/autonomy/IOracle.sol"; +import "./abstract/Shared.sol"; + + +contract StakeManager is IStakeManager, Shared, ReentrancyGuard, IERC777Recipient { + + uint public constant STAN_STAKE = 10000 * _E_18; + uint public constant BLOCKS_IN_EPOCH = 100; + bytes private constant _stakingIndicator = "staking"; + + IOracle private immutable _oracle; + // AUTO ERC777 + IERC777 private _AUTO; + bool private _AUTOSet = false; + IERC1820Registry constant private _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24); + bytes32 constant private TOKENS_RECIPIENT_INTERFACE_HASH = keccak256('ERC777TokensRecipient'); + uint private _totalStaked = 0; + // Needed so that receiving AUTO is rejected unless it's indicated + // that's it's used for staking and therefore not an accident (protect users) + Executor private _executor; + mapping(address => uint) private _stakerToStakedAmount; + address[] private _stakes; + + + // Pasted for convenience here, defined in IStakeManager + // struct Executor{ + // address addr; + // uint96 forEpoch; + // } + + + event Staked(address staker, uint amount); + event Unstaked(address staker, uint amount); + + + constructor(IOracle oracle) { + _oracle = oracle; + _ERC1820_REGISTRY.setInterfaceImplementer(address(this), TOKENS_RECIPIENT_INTERFACE_HASH, address(this)); + } + + + function setAUTO(IERC777 AUTO) external { + require(!_AUTOSet, "SM: AUTO already set"); + _AUTOSet = true; + _AUTO = AUTO; + } + + + ////////////////////////////////////////////////////////////// + // // + // Getters // + // // + ////////////////////////////////////////////////////////////// + + function getOracle() external view override returns (IOracle) { + return _oracle; + } + + function getAUTOAddr() external view override returns (address) { + return address(_AUTO); + } + + function getTotalStaked() external view override returns (uint) { + return _totalStaked; + } + + function getStake(address staker) external view override returns (uint) { + return _stakerToStakedAmount[staker]; + } + + function getStakes() external view override returns (address[] memory) { + return _stakes; + } + + function getStakesLength() external view override returns (uint) { + return _stakes.length; + } + + function getStakesSlice(uint startIdx, uint endIdx) external view override returns (address[] memory) { + address[] memory slice = new address[](endIdx - startIdx); + uint sliceIdx = 0; + for (uint stakeIdx = startIdx; stakeIdx < endIdx; stakeIdx++) { + slice[sliceIdx] = _stakes[stakeIdx]; + sliceIdx++; + } + + return slice; + } + + function getCurEpoch() public view override returns (uint96) { + return uint96((block.number / BLOCKS_IN_EPOCH) * BLOCKS_IN_EPOCH); + } + + function getExecutor() external view override returns (Executor memory) { + return _executor; + } + + function isCurExec(address addr) external view override returns (bool) { + // So that the storage is only loaded once + Executor memory ex = _executor; + if (ex.forEpoch == getCurEpoch()) { + if (ex.addr == addr) { + return true; + } else { + return false; + } + } + // If there're no stakes, allow anyone to be the executor so that a random + // person can bootstrap the network and nobody needs to be sent any coins + if (_stakes.length == 0) { return true; } + + return false; + } + + function getUpdatedExecRes() public view override returns (uint96 epoch, uint randNum, uint idxOfExecutor, address exec) { + epoch = getCurEpoch(); + // So that the storage is only loaded once + uint stakesLen = _stakes.length; + // If the executor is out of date and the system already has stake, + // choose a new executor. This will do nothing if the system is starting + // and allow someone to stake without needing there to already be existing stakes + if (_executor.forEpoch != epoch && stakesLen > 0) { + // -1 because blockhash(seed) in Oracle will return 0x00 if the + // seed == this block's height + randNum = _oracle.getRandNum(epoch - 1); + idxOfExecutor = randNum % stakesLen; + exec = _stakes[idxOfExecutor]; + } + } + + + ////////////////////////////////////////////////////////////// + // // + // Staking // + // // + ////////////////////////////////////////////////////////////// + + function updateExecutor() external override nonReentrant noFish returns (uint, uint, uint, address) { + return _updateExecutor(); + } + + function isUpdatedExec(address addr) external override nonReentrant noFish returns (bool) { + // So that the storage is only loaded once + Executor memory ex = _executor; + if (ex.forEpoch == getCurEpoch()) { + if (ex.addr == addr) { + return true; + } else { + return false; + } + } else { + (, , , address exec) = _updateExecutor(); + if (exec == addr) { return true; } + } + if (_stakes.length == 0) { return true; } + + return false; + } + + // The 1st stake/unstake of an epoch shouldn't change the executor, otherwise + // a staker could precalculate the effect of how much they stake in order to + // game the staker selection algo + function stake(uint numStakes) external nzUint(numStakes) nonReentrant updateExec noFish override { + uint amount = numStakes * STAN_STAKE; + _stakerToStakedAmount[msg.sender] += amount; + // So that the storage is only loaded once + IERC777 AUTO = _AUTO; + + // Deposit the coins + uint balBefore = AUTO.balanceOf(address(this)); + AUTO.operatorSend(msg.sender, address(this), amount, "", _stakingIndicator); + // This check is a bit unnecessary, but better to be paranoid than r3kt + require(AUTO.balanceOf(address(this)) - balBefore == amount, "SM: transfer bal check failed"); + + for (uint i; i < numStakes; i++) { + _stakes.push(msg.sender); + } + + _totalStaked += amount; + emit Staked(msg.sender, amount); + } + + function unstake(uint[] calldata idxs) external nzUintArr(idxs) nonReentrant updateExec noFish override { + uint amount = idxs.length * STAN_STAKE; + require(amount <= _stakerToStakedAmount[msg.sender], "SM: not enough stake, peasant"); + + for (uint i = 0; i < idxs.length; i++) { + require(_stakes[idxs[i]] == msg.sender, "SM: idx is not you"); + require(idxs[i] < _stakes.length, "SM: idx out of bounds"); + // Update stakes by moving the last element to the + // element we're wanting to delete (so it doesn't leave gaps, which is + // necessary for the _updateExecutor algo) + _stakes[idxs[i]] = _stakes[_stakes.length-1]; + _stakes.pop(); + } + + _stakerToStakedAmount[msg.sender] -= amount; + _AUTO.send(msg.sender, amount, _stakingIndicator); + _totalStaked -= amount; + emit Unstaked(msg.sender, amount); + } + + function _updateExecutor() private returns (uint96 epoch, uint randNum, uint idxOfExecutor, address exec) { + (epoch, randNum, idxOfExecutor, exec) = getUpdatedExecRes(); + if (exec != _ADDR_0) { + _executor = Executor(exec, epoch); + } + } + + modifier updateExec() { + // Need to update executor at the start of stake/unstake as opposed to the + // end of the fcns because otherwise, for the 1st stake/unstake tx in an + // epoch, someone could influence the outcome of the executor by precalculating + // the outcome based on how much they stake and unfairly making themselves the executor + _updateExecutor(); + _; + } + + // Ensure the contract is fully collateralised every time + modifier noFish() { + _; + // >= because someone could send some tokens to this contract and disable it if it was == + require(_AUTO.balanceOf(address(this)) >= _totalStaked, "SM: something fishy here"); + } + + function tokensReceived( + address _operator, + address _from, + address _to, + uint256 _amount, + bytes calldata _data, + bytes calldata _operatorData + ) external override { + require(msg.sender == address(_AUTO), "SM: non-AUTO token"); + require(keccak256(_operatorData) == keccak256(_stakingIndicator), "SM: sending by mistake"); + } + +} \ No newline at end of file diff --git a/contracts/autonomy/Timelock.sol b/contracts/autonomy/Timelock.sol new file mode 100644 index 0000000..25e3c77 --- /dev/null +++ b/contracts/autonomy/Timelock.sol @@ -0,0 +1,110 @@ +pragma solidity 0.8.6; + + +// Taken from https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol +// and updated for solidity v0.8.6 +contract Timelock { + event NewAdmin(address indexed newAdmin); + event NewPendingAdmin(address indexed newPendingAdmin); + event NewDelay(uint indexed newDelay); + event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + + uint public constant GRACE_PERIOD = 14 days; + uint public constant MINIMUM_DELAY = 12 hours; + uint public constant MAXIMUM_DELAY = 30 days; + + address public admin; + address public pendingAdmin; + uint public delay; + + mapping (bytes32 => bool) public queuedTransactions; + + + constructor(address admin_, uint delay_) { + require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay."); + require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); + + admin = admin_; + delay = delay_; + } + + receive() external payable {} + + function setDelay(uint delay_) public { + require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); + require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); + require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); + delay = delay_; + + emit NewDelay(delay); + } + + function acceptAdmin() public { + require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin."); + admin = msg.sender; + pendingAdmin = address(0); + + emit NewAdmin(admin); + } + + function setPendingAdmin(address pendingAdmin_) public { + require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock."); + pendingAdmin = pendingAdmin_; + + emit NewPendingAdmin(pendingAdmin); + } + + function queueTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public returns (bytes32) { + require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); + require(eta >= getBlockTimestamp() + delay, "Timelock::queueTransaction: Estimated execution block must satisfy delay."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = true; + + emit QueueTransaction(txHash, target, value, signature, data, eta); + return txHash; + } + + function cancelTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public { + require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = false; + + emit CancelTransaction(txHash, target, value, signature, data, eta); + } + + function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public payable returns (bytes memory) { + require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued."); + require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock."); + require(getBlockTimestamp() <= eta + GRACE_PERIOD, "Timelock::executeTransaction: Transaction is stale."); + + queuedTransactions[txHash] = false; + + bytes memory callData; + + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); + } + + // solium-disable-next-line security/no-call-value + (bool success, bytes memory returnData) = target.call{value: value}(callData); + require(success, "Timelock::executeTransaction: Transaction execution reverted."); + + emit ExecuteTransaction(txHash, target, value, signature, data, eta); + + return returnData; + } + + function getBlockTimestamp() internal view returns (uint) { + // solium-disable-next-line security/no-block-members + return block.timestamp; + } +} \ No newline at end of file diff --git a/contracts/autonomy/abstract/Shared.sol b/contracts/autonomy/abstract/Shared.sol new file mode 100644 index 0000000..da3188e --- /dev/null +++ b/contracts/autonomy/abstract/Shared.sol @@ -0,0 +1,43 @@ +pragma solidity 0.8.6; + + +/** +* @title Shared contract +* @notice Holds constants and modifiers that are used in multiple contracts +* @dev It would be nice if this could be a library, but modifiers can't be exported :( +* @author Quantaf1re (James Key) +*/ +abstract contract Shared { + address constant internal _ADDR_0 = address(0); + uint constant internal _E_18 = 10**18; + + function revertFailedCall(bool success, bytes memory returnData) internal { + // Need this if statement because if the call succeeds, the tx will revert + // with an EVM error because it can't decode 0x00. If a tx fails with no error + // message, maybe that's a problem? But if it failed without a message then it's + // gonna be hard to know what went wrong regardless + if (!success) { + // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/d57593c148dad16abe675083464787ca10f789ec/contracts/utils/Address.sol#L210 + if (returnData.length > 0) { + assembly { + let returndata_size := mload(returnData) + revert(add(32, returnData), returndata_size) + } + } else { + revert("No error message!"); + } + } + } + + /// @dev Checks that a uint isn't nonzero/empty + modifier nzUint(uint u) { + require(u != 0, "Shared: uint input is empty"); + _; + } + + /// @dev Checks that a uint array isn't nonzero/empty + modifier nzUintArr(uint[] calldata arr) { + require(arr.length > 0, "Shared: uint arr input is empty"); + _; + } +} \ No newline at end of file diff --git a/contracts/conditions/TimeConditions.sol b/contracts/conditions/TimeConditions.sol new file mode 100644 index 0000000..e5f93bf --- /dev/null +++ b/contracts/conditions/TimeConditions.sol @@ -0,0 +1,79 @@ +pragma solidity 0.8.6; + + +/** +* @notice This contract serves as a condition contract, to be used with Autonomy's +* Automation Station and bundled with other calls to add conditions to +* calls to contracts in a modular way without the need for duplicating +* this logic elsewhere. It allows the called to specify that a request +* is executed between 2 times (with `betweenTimes`, presumably for a +* non-recurring request), aswell as to be called periodically every +* X seconds (with `everyTimePeriod`, presumably for recurring requests). +* @author @pldespaigne (Pierre-Louis Despaigne), @quantafire (James Key) +*/ +contract TimeConditions { + + event Started(address indexed user, uint callId); + + + // Mapping a user to last execution date of its ongoing requests + // - because a user can have multiple requests, we introduce an arbitrary requestID (also refered as `callId`) + // - users can know their previous `callId`s by looking at emitted `Started` events + mapping(address => mapping(uint => uint)) public userToIdToLastExecTime; + // The forwarder address through which calls are forwarded that guarantee the 1st argument, `user`, is accurate + address public immutable routerUserVeriForwarder; + + + constructor(address routerUserVeriForwarder_) { + routerUserVeriForwarder = routerUserVeriForwarder_; + } + + /** + * @notice Ensure that the tx is being executed between 2 times (inclusive) + * + * @param afterTime The 1st unix time from which the execution will succeed (inclusive). + * @param beforeTime The last unix time from which the execution will succeed (inclusive). + */ + function betweenTimes(uint afterTime, uint beforeTime) external view { + require(block.timestamp >= afterTime, "TimeConditions: too early"); + require(block.timestamp <= beforeTime, "TimeConditions: too late"); + } + + /** + * @notice Ensure the tx is executed periodically, beginning from `startTime`, and + * occurring every `periodLength` after that. + * @dev The execution will never occur exactly at `startTime`, nor exactly at the + * beginning of the next period, but the average execution time should be + * around the same time with the proper period and should not drift because + * `block.timestamp` is never used to refer to the last execution time. + * @param user The address of the user who made the request. This is guaranteed to be + * accurate by `msg.sender` being `routerUserVeriForwarder` and is needed + * to ensure that different users can't affect the requests of other users. + * @param callId An arbitrary request ID, used to differentiate between different requests + * from the same `user`. + * @param startTime The unix time that the automation should start for the first time. + * @param periodLength The number of seconds that should have passed inbetween calls. + */ + function everyTimePeriod( + address user, + uint callId, + uint startTime, + uint periodLength + ) external { + require(msg.sender == routerUserVeriForwarder, "TimeConditions: not userForw"); + + uint lastExecTime = userToIdToLastExecTime[user][callId]; + + // immediately execute the first time + if (lastExecTime == 0) { + require(block.timestamp >= startTime, "TimeConditions: not passed start"); + userToIdToLastExecTime[user][callId] = startTime; + emit Started(user, callId); + + } else { + uint nextExecTime = lastExecTime + periodLength; + require(block.timestamp >= nextExecTime, "TimeConditions: too early period"); + userToIdToLastExecTime[user][callId] = nextExecTime; + } + } +} diff --git a/contracts/conditions/UniswapV2Price.sol b/contracts/conditions/UniswapV2Price.sol new file mode 100644 index 0000000..a07b35a --- /dev/null +++ b/contracts/conditions/UniswapV2Price.sol @@ -0,0 +1,50 @@ +pragma solidity 0.8.6; + + +interface IUniswapV2Router02 { + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); + function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); + function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); +} + + +/** +* @notice UniswapV2Amounts effectively checks the current price of +* trading pairs on UniswapV2 +* @author @quantafire (James Key) +*/ +contract UniswapV2Amounts { + + IUniswapV2Router02 public immutable uni; + + constructor(IUniswapV2Router02 uni_) { + uni = uni_; + } + + /** + * @notice This checks the result of UniswapV2's `getAmountsOut` + * for the given trading pair and requires that it's above + * `minAmountOut`. Note this does not actually perform a + * trade, only checks the resulting prices of a hypothetical + * trade. + * @param srcToken The token to be sold. + * @param destToken The token to be bought. + * @param amountIn The amount of `srcToken` to sell. + * @param minAmountOut The minimum amount of `destToken` to receive + * where the tx won't revert. + */ + function amountOutGreaterThan( + address srcToken, + address destToken, + uint amountIn, + uint minAmountOut + ) external view returns (uint[] memory amountsOut) { + address[] memory path = new address[](2); + path[0] = srcToken; + path[1] = destToken; + amountsOut = uni.getAmountsOut(amountIn, path); + + require(amountsOut[amountsOut.length-1] >= minAmountOut, "UniswapV2Amounts: output too low"); + } +} \ No newline at end of file diff --git a/contracts/conditions/UniswapV3TWAP.sol b/contracts/conditions/UniswapV3TWAP.sol new file mode 100644 index 0000000..cd47e27 --- /dev/null +++ b/contracts/conditions/UniswapV3TWAP.sol @@ -0,0 +1,32 @@ +pragma solidity =0.7.6; + + +import "@uniswap/contracts/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/contracts/libraries/TickMath.sol"; +import "@uniswap/contracts/libraries/FixedPoint96.sol"; +import "@uniswap/contracts/libraries/FullMath.sol"; + + +contract TwapGetter { + function getSqrtTwapX96(address uniswapV3Pool, uint32 twapInterval) public view returns (uint160 sqrtPriceX96) { + if (twapInterval == 0) { + // return the current price if twapInterval == 0 + (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(uniswapV3Pool).slot0(); + } else { + uint32[] memory secondsAgos = new uint32[](2); + secondsAgos[0] = twapInterval; // from (before) + secondsAgos[1] = 0; // to (now) + + (int56[] memory tickCumulatives, ) = IUniswapV3Pool(uniswapV3Pool).observe(secondsAgos); + + // tick(imprecise as it's an integer) to price + sqrtPriceX96 = TickMath.getSqrtRatioAtTick( + int24((tickCumulatives[1] - tickCumulatives[0]) / twapInterval) + ); + } + } + + function getPriceX96FromSqrtPriceX96(uint160 sqrtPriceX96) public pure returns(uint256 priceX96) { + return FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, FixedPoint96.Q96); + } +} \ No newline at end of file diff --git a/contracts/time-conditions.sol b/contracts/time-conditions.sol deleted file mode 100644 index ae3e7e5..0000000 --- a/contracts/time-conditions.sol +++ /dev/null @@ -1,87 +0,0 @@ - -// SPDX-License-Identifier: UNLICENSED - -pragma solidity ^0.8.11; - - -contract TimeConditions { - - // Mapping a user to last execution date of its ongoing requests - // - because a user can have multiple requests, we introduce an arbitrary requestID (also refered as `callID`) - // - user are responsible to manage & remember their used requestID - mapping(address => mapping(uint256 => uint256)) public userToIDtoLastExecTime; - - - /** @dev Forward an arbitrary call if the function is called at the correct time, - * i.e. after `notBeforeTime` but before `notAfterTime`. - * Note: any ETH sent to this function will be forwarded to the `target`. - * - * @param notBeforeTime if the function is called before this timestamp it will fail - * @param notAfterTime if the function is called after this timestamp it will fail - * @param target address that will be called if the requets is successfull. - * @param params encoded arguments that will be forwarded to the `target` address if the requets is successful. - * - * @return success wether or not the forwarded transaction has succeeded. - * @return returnedData the returned result of the forwarded transaction. - */ - function executeAt( - uint256 notBeforeTime, - uint256 notAfterTime, - address payable target, - bytes memory params - ) public payable returns(bool success, bytes memory returnedData) { - - /* solhint-disable not-rely-on-time */ - require(block.timestamp > notBeforeTime, "Error: too early!"); - require(block.timestamp < notAfterTime, "Error: too late!"); - /* solhint-enable not-rely-on-time */ - - // execute target function - // solhint-disable-next-line indent, bracket-align, avoid-low-level-calls - (success, returnedData) = target.call{value: msg.value}(params); - } - - /** @dev Forward an arbitrary call if the function has been called at least `secondsSinceLastCall` since last time. - * Note: any ETH sent to this function will be forwarded to the `target`. - * - * @param originalSender the address of the user calling, - * request with same `originalSender` & same `callID` are considered to be the same, - * this is used only to store & retreive the amount of time between request. - * @param callID an arbitrary request ID, - * request with same `originalSender` & same `callID` are considered to be the same, - * this is used only to store & retreive the amount of time between request. - * @param secondsSinceLastCall number of seconds that should have passed since last call for this requets to be successfully forwarded. - * @param target address that will be called if the requets is successfull. - * @param params encoded arguments that will be forwarded to the `target` address if the requets is successful. - * - * @return success wether or not the forwarded transaction has succeeded. - * @return returnedData the returned result of the forwarded transaction. - */ - function executeAfter( - address originalSender, - uint256 callID, - uint256 secondsSinceLastCall, - address payable target, - bytes memory params - ) public payable returns(bool success, bytes memory returnedData) { - - // immediately execute the first time - if (userToIDtoLastExecTime[originalSender][callID] == 0) { - - userToIDtoLastExecTime[originalSender][callID] = block.timestamp; // solhint-disable-line not-rely-on-time - - } else { - - uint256 nextExecTime = userToIDtoLastExecTime[originalSender][callID] + secondsSinceLastCall; - require(block.timestamp > nextExecTime, "Error: too early!"); // solhint-disable-line not-rely-on-time - - // update last execution time - userToIDtoLastExecTime[originalSender][callID] += secondsSinceLastCall; - } - - // execute target function - // solhint-disable-next-line indent, bracket-align, avoid-low-level-calls - (success, returnedData) = target.call{value: msg.value}(params); - } - -} diff --git a/interfaces/autonomy/IForwarder.sol b/interfaces/autonomy/IForwarder.sol new file mode 100644 index 0000000..003c243 --- /dev/null +++ b/interfaces/autonomy/IForwarder.sol @@ -0,0 +1,11 @@ +pragma solidity 0.8.6; + + +interface IForwarder { + + function forward( + address target, + bytes calldata callData + ) external payable returns (bool success, bytes memory returnData); + +} \ No newline at end of file diff --git a/interfaces/autonomy/IOracle.sol b/interfaces/autonomy/IOracle.sol new file mode 100644 index 0000000..b3a819b --- /dev/null +++ b/interfaces/autonomy/IOracle.sol @@ -0,0 +1,22 @@ +pragma solidity 0.8.6; + + +import "./IPriceOracle.sol"; + + +interface IOracle { + // Needs to output the same number for the whole epoch + function getRandNum(uint salt) external view returns (uint); + + function getPriceOracle() external view returns (IPriceOracle); + + function getAUTOPerETH() external view returns (uint); + + function getGasPriceFast() external view returns (uint); + + function setPriceOracle(IPriceOracle newPriceOracle) external; + + function defaultPayIsAUTO() external view returns (bool); + + function setDefaultPayIsAUTO(bool newDefaultPayIsAUTO) external; +} \ No newline at end of file diff --git a/interfaces/autonomy/IPriceOracle.sol b/interfaces/autonomy/IPriceOracle.sol new file mode 100644 index 0000000..49cb6ca --- /dev/null +++ b/interfaces/autonomy/IPriceOracle.sol @@ -0,0 +1,9 @@ +pragma solidity 0.8.6; + + +interface IPriceOracle { + + function getAUTOPerETH() external view returns (uint); + + function getGasPriceFast() external view returns (uint); +} \ No newline at end of file diff --git a/interfaces/autonomy/IRegistry.sol b/interfaces/autonomy/IRegistry.sol new file mode 100644 index 0000000..a3a2072 --- /dev/null +++ b/interfaces/autonomy/IRegistry.sol @@ -0,0 +1,243 @@ +pragma solidity 0.8.6; + + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + + +/** +* @title Registry +* @notice A contract which is essentially a glorified forwarder. +* It essentially brings together people who want things executed, +* and people who want to do that execution in return for a fee. +* Users register the details of what they want executed, which +* should always revert unless their execution condition is true, +* and executors execute the request when the condition is true. +* Only a specific executor is allowed to execute requests at any +* given time, as determined by the StakeManager, which requires +* staking AUTO tokens. This is infrastructure, and an integral +* piece of the future of web3. It also provides the spark of life +* for a new form of organism - cyber life. We are the gods now. +* @author Quantaf1re (James Key) +*/ +interface IRegistry { + + // The address vars are 20b, total 60, calldata is 4b + n*32b usually, which + // has a factor of 32. uint112 since the current ETH supply of ~115m can fit + // into that and it's the highest such that 2 * uint112 + 3 * bool is < 256b + struct Request { + address payable user; + address target; + address payable referer; + bytes callData; + uint112 initEthSent; + uint112 ethForCall; + bool verifyUser; + bool insertFeeAmount; + bool payWithAUTO; + bool isAlive; + } + + + ////////////////////////////////////////////////////////////// + // // + // Hashed Requests // + // // + ////////////////////////////////////////////////////////////// + + /** + * @notice Creates a new request, logs the request info in an event, then saves + * a hash of it on-chain in `_hashedReqs`. Uses the default for whether + * to pay in ETH or AUTO + * @param target The contract address that needs to be called + * @param referer The referer to get rewarded for referring the sender + * to using Autonomy. Usally the address of a dapp owner + * @param callData The calldata of the call that the request is to make, i.e. + * the fcn identifier + inputs, encoded + * @param ethForCall The ETH to send with the call + * @param verifyUser Whether the 1st input of the calldata equals the sender. + * Needed for dapps to know who the sender is whilst + * ensuring that the sender intended + * that fcn and contract to be called - dapps will + * require that msg.sender is the Verified Forwarder, + * and only requests that have `verifyUser` = true will + * be forwarded via the Verified Forwarder, so any calls + * coming from it are guaranteed to have the 1st argument + * be the sender + * @param insertFeeAmount Whether the gas estimate of the executor should be inserted + * into the callData + * @param isAlive Whether or not the request should be deleted after it's executed + * for the first time. If `true`, the request will exist permanently + * (tho it can be cancelled any time), therefore executing the same + * request repeatedly aslong as the request is executable, + * and can be used to create fully autonomous contracts - the + * first single-celled cyber life. We are the gods now + * @return id The id of the request, equal to the index in `_hashedReqs` + */ + function newReq( + address target, + address payable referer, + bytes calldata callData, + uint112 ethForCall, + bool verifyUser, + bool insertFeeAmount, + bool isAlive + ) external payable returns (uint id); + + /** + * @notice Creates a new request, logs the request info in an event, then saves + * a hash of it on-chain in `_hashedReqs` + * @param target The contract address that needs to be called + * @param referer The referer to get rewarded for referring the sender + * to using Autonomy. Usally the address of a dapp owner + * @param callData The calldata of the call that the request is to make, i.e. + * the fcn identifier + inputs, encoded + * @param ethForCall The ETH to send with the call + * @param verifyUser Whether the 1st input of the calldata equals the sender. + * Needed for dapps to know who the sender is whilst + * ensuring that the sender intended + * that fcn and contract to be called - dapps will + * require that msg.sender is the Verified Forwarder, + * and only requests that have `verifyUser` = true will + * be forwarded via the Verified Forwarder, so any calls + * coming from it are guaranteed to have the 1st argument + * be the sender + * @param insertFeeAmount Whether the gas estimate of the executor should be inserted + * into the callData + * @param payWithAUTO Whether the sender wants to pay for the request in AUTO + * or ETH. Paying in AUTO reduces the fee + * @param isAlive Whether or not the request should be deleted after it's executed + * for the first time. If `true`, the request will exist permanently + * (tho it can be cancelled any time), therefore executing the same + * request repeatedly aslong as the request is executable, + * and can be used to create fully autonomous contracts - the + * first single-celled cyber life. We are the gods now + * @return id The id of the request, equal to the index in `_hashedReqs` + */ + function newReqPaySpecific( + address target, + address payable referer, + bytes calldata callData, + uint112 ethForCall, + bool verifyUser, + bool insertFeeAmount, + bool payWithAUTO, + bool isAlive + ) external payable returns (uint id); + + /** + * @notice Gets all keccak256 hashes of encoded requests. Completed requests will be 0x00 + * @return [bytes32[]] An array of all hashes + */ + function getHashedReqs() external view returns (bytes32[] memory); + + /** + * @notice Gets part of the keccak256 hashes of encoded requests. Completed requests will be 0x00. + * Needed since the array will quickly grow to cost more gas than the block limit to retrieve. + * so it can be viewed in chunks. E.g. for an array of x = [4, 5, 6, 7], x[1, 2] returns [5], + * the same as lists in Python + * @param startIdx [uint] The starting index from which to start getting the slice (inclusive) + * @param endIdx [uint] The ending index from which to start getting the slice (exclusive) + * @return [bytes32[]] An array of all hashes + */ + function getHashedReqsSlice(uint startIdx, uint endIdx) external view returns (bytes32[] memory); + + /** + * @notice Gets the total number of requests that have been made, hashed, and stored + * @return [uint] The total number of hashed requests + */ + function getHashedReqsLen() external view returns (uint); + + /** + * @notice Gets a single hashed request + * @param id [uint] The id of the request, which is its index in the array + * @return [bytes32] The sha3 hash of the request + */ + function getHashedReq(uint id) external view returns (bytes32); + + + ////////////////////////////////////////////////////////////// + // // + // Bytes Helpers // + // // + ////////////////////////////////////////////////////////////// + + /** + * @notice Encode a request into bytes + * @param r [request] The request to be encoded + * @return [bytes] The bytes array of the encoded request + */ + function getReqBytes(Request memory r) external pure returns (bytes memory); + + function insertToCallData(bytes calldata callData, uint expectedGas, uint startIdx) external pure returns (bytes memory); + + + ////////////////////////////////////////////////////////////// + // // + // Executions // + // // + ////////////////////////////////////////////////////////////// + + /** + * @notice Execute a hashedReq. Calls the `target` with `callData`, then + * charges the user the fee, and gives it to the executor + * @param id [uint] The index of the request in `_hashedReqs` + * @param r [request] The full request struct that fully describes the request. + * Typically known by seeing the `HashedReqAdded` event emitted with `newReq` + * @param expectedGas [uint] The gas that the executor expects the execution to cost, + * known by simulating the the execution of this tx locally off-chain. + * This can be forwarded as part of the requested call such that the + * receiving contract knows how much gas the whole execution cost and + * can do something to compensate the exact amount (e.g. as part of a trade). + * Cannot be more than 10% above the measured gas cost by the end of execution + * @return gasUsed [uint] The gas that was used as part of the execution. Used to know `expectedGas` + */ + function executeHashedReq( + uint id, + Request calldata r, + uint expectedGas + ) external returns (uint gasUsed); + + + ////////////////////////////////////////////////////////////// + // // + // Cancellations // + // // + ////////////////////////////////////////////////////////////// + + /** + * @notice Execute a hashedReq. Calls the `target` with `callData`, then + * charges the user the fee, and gives it to the executor + * @param id [uint] The index of the request in `_hashedReqs` + * @param r [request] The full request struct that fully describes the request. + * Typically known by seeing the `HashedReqAdded` event emitted with `newReq` + */ + function cancelHashedReq( + uint id, + Request memory r + ) external; + + + ////////////////////////////////////////////////////////////// + // // + // Getters // + // // + ////////////////////////////////////////////////////////////// + + function getAUTOAddr() external view returns (address); + + function getStakeManager() external view returns (address); + + function getOracle() external view returns (address); + + function getUserForwarder() external view returns (address); + + function getGasForwarder() external view returns (address); + + function getUserGasForwarder() external view returns (address); + + function getReqCountOf(address addr) external view returns (uint); + + function getExecCountOf(address addr) external view returns (uint); + + function getReferalCountOf(address addr) external view returns (uint); +} diff --git a/interfaces/autonomy/IStakeManager.sol b/interfaces/autonomy/IStakeManager.sol new file mode 100644 index 0000000..cf12fe2 --- /dev/null +++ b/interfaces/autonomy/IStakeManager.sol @@ -0,0 +1,161 @@ +pragma solidity 0.8.6; + + +import "./IOracle.sol"; + + +/** +* @title StakeManager +* @notice A lightweight Proof of Stake contract that allows +* staking of AUTO tokens. Instead of a miner winning +* the ability to produce a block, the algorithm selects +* a staker for a period of 100 blocks to be the executor. +* the executor has the exclusive right to execute requests +* in the Registry contract. The Registry checks with StakeManager +* who is allowed to execute requests at any given time +* @author Quantaf1re (James Key) +*/ +interface IStakeManager { + + struct Executor{ + address addr; + uint96 forEpoch; + } + + + ////////////////////////////////////////////////////////////// + // // + // Getters // + // // + ////////////////////////////////////////////////////////////// + + function getOracle() external view returns (IOracle); + + function getAUTOAddr() external view returns (address); + + function getTotalStaked() external view returns (uint); + + function getStake(address staker) external view returns (uint); + + /** + * @notice Returns the array of stakes. Every element in the array represents + * `STAN_STAKE` amount of AUTO tokens staked for that address. Addresses + * can be in the array arbitrarily many times + */ + function getStakes() external view returns (address[] memory); + + /** + * @notice The length of `_stakes`, i.e. the total staked when multiplied by `STAN_STAKE` + */ + function getStakesLength() external view returns (uint); + + /** + * @notice The same as getStakes except it returns only part of the array - the + * array might grow so large that retrieving it costs more gas than the + * block gas limit and therefore brick the contract. E.g. for an array of + * x = [4, 5, 6, 7], x[1, 2] returns [5], the same as lists in Python + * @param startIdx [uint] The starting index from which to start getting the slice (inclusive) + * @param endIdx [uint] The ending index from which to start getting the slice (exclusive) + */ + function getStakesSlice(uint startIdx, uint endIdx) external view returns (address[] memory); + + /** + * @notice Returns the current epoch. Goes in increments of 100. E.g. the epoch + * for 420 is 400, and 42069 is 42000 + */ + function getCurEpoch() external view returns (uint96); + + /** + * @notice Returns the currently stored Executor - which might be old, + * i.e. for a previous epoch + */ + function getExecutor() external view returns (Executor memory); + + /** + * @notice Returns whether `addr` is the current executor for this epoch. If the executor + * is outdated (i.e. for a previous epoch), it'll return false regardless of `addr` + * @param addr [address] The address to check + * @return [bool] Whether or not `addr` is the current executor for this epoch + */ + function isCurExec(address addr) external view returns (bool); + + /** + * @notice Returns what the result of updating the executor would be, but doesn't actually + * make any changes + * @return epoch Returns the relevant variables for determining the new executor if the executor + * can be updated currently. It can only be updated currently if the stored executor + * is for a previous epoch, and there is some stake in the system. If the executor + * can't be updated currently, then everything execpt `epoch` will return 0 + */ + function getUpdatedExecRes() external view returns (uint96 epoch, uint randNum, uint idxOfExecutor, address exec); + + ////////////////////////////////////////////////////////////// + // // + // Staking // + // // + ////////////////////////////////////////////////////////////// + + /** + * @notice Updates the executor. Calls `getUpdatedExecRes` to know. Makes the changes + * only if the executor can be updated + * @return Returns the relevant variables for determining the new executor if the executor + * can be updated currently + */ + function updateExecutor() external returns (uint, uint, uint, address); + + /** + * @notice Checks if the stored executor is for the current epoch - if it is, + * then it returns whether `addr` is the current exec or not. If the epoch + * is old, then it updates the executor, then returns whether `addr` is the + * current executor or not. If there's no stake in the system, returns true + * @param addr [address] The address to check + * @return [bool] Returns whether or not `addr` is the current, updated executor + */ + function isUpdatedExec(address addr) external returns (bool); + + /** + * @notice Stake a set amount of AUTO tokens. A set amount of tokens needs to be used + * so that a random number can be used to look up a specific index in the array. + * We want the staker to be chosen proportional to their stake, which requires + * knowing their stake in relation to everyone else. If you could stake any + * amount of AUTO tokens, then the contract would have to store that amount + * along with the staker and, crucially, would require iteration over the + * whole array. E.g. if the random number in this PoS system was 0.2, then + * you could calculate the amount of proportional stake that translates to. + * If the total stakes was 10^6, then whichever staker in the array at token + * position 200,000 would be the winner, but that requires going through every + * piece of staking info in the first part of the array in order to calculate + * the running cumulative and know who happens to have the slot where the + * cumulative stake is 200,000. This has problems when the staking array is + * so large that it costs more than the block gas limit to iterate over, which + * would brick the contract, but also just generally costs alot of gas. Having + * a set amount of AUTO tokens means you already know everything about every + * element in the array therefore don't need to iterate over it. + * Calling this will add the caller to the array. Calling this will first try + * and set the executor so that the caller can't precalculate and affect the outcome + * by deciding the size of `numStakes` + * @param numStakes [uint] The number of `STAN_STAKE` to stake and therefore how many + * slots in the array to add the user to + */ + function stake(uint numStakes) external; + + /** + * @notice Unstake AUTO tokens. Calling this will first try and set the executor so that + * the caller can't precalculate and affect the outcome by deciding the size of + * `numStakes` + * @dev Instead of just deleting the array slot, this takes the last element, copies + * it to the slot being unstaked, and pops off the original copy of the replacement + * from the end of the array, so that there are no gaps left, such that 0x00...00 + * can never be chosen as an executor + * @param idxs [uint[]] The indices of the user's slots, in order of which they'll be + * removed, which is not necessariy the current indices. E.g. if the `_staking` + * array is [a, b, c, b], and `idxs` = [1, 3], then i=1 will first get + * replaced by i=3 and look like [a, b, c], then it would try and replace i=3 + * by the end of the array...but i=3 no longer exists, so it'll revert. In this + * case, `idxs` would need to be [1, 1], which would result in [a, c]. It's + * recommended to choose idxs in descending order so that you don't have to + * take account of this behaviour - that way you can just use indexes + * as they are already without alterations + */ + function unstake(uint[] calldata idxs) external; +} \ No newline at end of file diff --git a/scripts/addresses.py b/scripts/addresses.py new file mode 100644 index 0000000..78f2d36 --- /dev/null +++ b/scripts/addresses.py @@ -0,0 +1,18 @@ +from brownie import network + + +if network.show_active() == 'avax-mainnet': + registry_addr = '0x68FCbECa74A7E5D386f74E14682c94DE0e1bC56b' + uniV2_address = '0x60aE616a2155Ee3d9A68541Ba4544862310933d4' + reg_uff_addr = '0xc21E82fe258ABf9BC3Ef68fB38aecDA79e472964' + + +if network.show_active() == 'avax-testnet': + registry_addr = '0xA0F25b796dD59E504077F87Caea1c0472Cd6b7b4' + reg_uff_addr = '0x457B8Bb4d6c8Cc2e506789F768996ddae60CD4fd' + + +if network.show_active() == 'bsc-mainnet': + registry_addr = '0x18d087F8D22D409D3CD366AF00BD7AeF0BF225Db' + uniV2_address = '0x10ED43C718714eb63d5aA57B78B54704E256024E' + reg_uff_addr = '0x4F54277e6412504EBa0B259A9E4c69Dc7EE4bB9c' \ No newline at end of file diff --git a/scripts/deploy.py b/scripts/deploy.py new file mode 100644 index 0000000..8fe4ca6 --- /dev/null +++ b/scripts/deploy.py @@ -0,0 +1,29 @@ +from brownie import accounts, FundsRouter, TimeConditions, Forwarder, UniswapV2Amounts, MockTarget +import sys +import os +sys.path.append(os.path.abspath('scripts')) +from addresses import * +sys.path.append(os.path.abspath('tests')) +from consts import * +sys.path.pop() + + +deployer_priv = os.environ['DEPLOYER_PRIV'] +deployer = accounts.add(private_key=deployer_priv) +print(deployer) +PUBLISH_SOURCE = True + + +def main(): + fruf = deployer.deploy(Forwarder, publish_source=PUBLISH_SOURCE) + fr = deployer.deploy(FundsRouter, registry_addr, reg_uff_addr, fruf, publish_source=PUBLISH_SOURCE) + fruf.setCaller(fr, True, {'from': deployer}) + tc = deployer.deploy(TimeConditions, fruf, publish_source=PUBLISH_SOURCE) + mock_target = deployer.deploy(MockTarget, fruf, fr, publish_source=PUBLISH_SOURCE) + uniV2_amounts = deployer.deploy(UniswapV2Amounts, uniV2_address, publish_source=PUBLISH_SOURCE) + + print(f'Forwarder deployed at: {fruf}') + print(f'FundsRouter deployed at: {fr}') + print(f'TimeConditions deployed at: {tc}') + print(f'UniswapV2Amounts deployed at: {uniV2_amounts}') + print(f'MockTarget deployed at: {mock_target}') diff --git a/test/time-conditions.ts b/test/time-conditions.ts index 88d2a76..fe24d21 100644 --- a/test/time-conditions.ts +++ b/test/time-conditions.ts @@ -77,7 +77,7 @@ describe('TimeConditions contract', () => { const CALL_ID = 1; - const lastExecA = await timeConditions.userToIDtoLastExecTime(owner.address, CALL_ID); + const lastExecA = await timeConditions.userToIdToLastExecTime(owner.address, CALL_ID); expect(lastExecA).to.be.equal(0); // statically call the function to get access to the returned values @@ -93,7 +93,7 @@ describe('TimeConditions contract', () => { const block = await ethers.provider.getBlock('latest'); const now = block.timestamp; - const lastExecB = await timeConditions.userToIDtoLastExecTime(owner.address, CALL_ID); + const lastExecB = await timeConditions.userToIdToLastExecTime(owner.address, CALL_ID); expect(lastExecB).to.be.equal(now); }); @@ -103,7 +103,7 @@ describe('TimeConditions contract', () => { const block = await ethers.provider.getBlock('latest'); const now = block.timestamp; - const lastExecA = await timeConditions.userToIDtoLastExecTime(owner.address, CALL_ID); + const lastExecA = await timeConditions.userToIdToLastExecTime(owner.address, CALL_ID); expect(lastExecA).to.be.equal(now); expect(timeConditions.executeAfter(owner.address, CALL_ID, ONE_HOUR, ZERO_ADDRESS, '0x00')).to.be.revertedWith('Error: too early'); @@ -119,7 +119,7 @@ describe('TimeConditions contract', () => { await ethers.provider.send('evm_increaseTime', [ ONE_HOUR + 10 ]); // increase eth node time by some amount in seconds await ethers.provider.send('evm_mine', []); // force mine a block so that block.timestamp is set as we wanted above - const lastExecA = await timeConditions.userToIDtoLastExecTime(owner.address, CALL_ID); + const lastExecA = await timeConditions.userToIdToLastExecTime(owner.address, CALL_ID); expect(lastExecA).to.be.equal(previousTime); // statically call the function to get access to the returned values @@ -132,7 +132,7 @@ describe('TimeConditions contract', () => { const receipt = await tx.wait(); expect(receipt.status).to.be.equal(1); - const lastExecB = await timeConditions.userToIDtoLastExecTime(owner.address, CALL_ID); + const lastExecB = await timeConditions.userToIdToLastExecTime(owner.address, CALL_ID); expect(lastExecB).to.be.equal(previousTime + ONE_HOUR); }); diff --git a/tests/FundsRouter/test_FundsRouter_constructor.py b/tests/FundsRouter/test_FundsRouter_constructor.py new file mode 100644 index 0000000..e029a48 --- /dev/null +++ b/tests/FundsRouter/test_FundsRouter_constructor.py @@ -0,0 +1,6 @@ +def test_constructor(a, auto, fruf, fr, deployer): + assert fr.registry() == auto.r + assert fr.regUserFeeVeriForwarder() == auto.uff + assert fr.routerUserVeriForwarder() == fruf + for addr in a: + assert fr.balances(addr) == 0 \ No newline at end of file diff --git a/tests/FundsRouter/test_depositETH.py b/tests/FundsRouter/test_depositETH.py new file mode 100644 index 0000000..f97f697 --- /dev/null +++ b/tests/FundsRouter/test_depositETH.py @@ -0,0 +1,25 @@ +from consts import * + + +def test_depositETH_self(a, auto, fr, deployer, user_a): + amount = 100 + + tx = fr.depositETH(user_a, {'from': user_a, 'value': amount}) + + assert fr.balance() == amount + for addr in a[1:]: + assert fr.balances(addr) == (amount if addr == user_a else 0) + assert addr.balance() == (INIT_ETH_BAL - amount if addr == user_a else INIT_ETH_BAL) + assert tx.events["BalanceChanged"][0].values() == [user_a, amount] + + +def test_depositETH_other(a, auto, fr, deployer, user_a, user_b): + amount = 100 + + tx = fr.depositETH(user_b, {'from': user_a, 'value': amount}) + + assert fr.balance() == amount + for addr in a[1:]: + assert fr.balances(addr) == (amount if addr == user_b else 0) + assert addr.balance() == (INIT_ETH_BAL - amount if addr == user_a else INIT_ETH_BAL) + assert tx.events["BalanceChanged"][0].values() == [user_b, amount] \ No newline at end of file diff --git a/tests/FundsRouter/test_depositETH_twice.py b/tests/FundsRouter/test_depositETH_twice.py new file mode 100644 index 0000000..5599612 --- /dev/null +++ b/tests/FundsRouter/test_depositETH_twice.py @@ -0,0 +1,47 @@ +from consts import * + + +def test_depositETH_self_twice(a, auto, fr, deployer, user_a): + amount = 100 + + tx = fr.depositETH(user_a, {'from': user_a, 'value': amount}) + + assert fr.balance() == amount + for addr in a[1:]: + assert fr.balances(addr) == (amount if addr == user_a else 0) + assert addr.balance() == (INIT_ETH_BAL - amount if addr == user_a else INIT_ETH_BAL) + assert tx.events["BalanceChanged"][0].values() == [user_a, amount] + + tx = fr.depositETH(user_a, {'from': user_a, 'value': amount}) + + assert fr.balance() == amount*2 + for addr in a[1:]: + assert fr.balances(addr) == (amount*2 if addr == user_a else 0) + assert addr.balance() == (INIT_ETH_BAL - amount*2 if addr == user_a else INIT_ETH_BAL) + assert tx.events["BalanceChanged"][0].values() == [user_a, amount*2] + + +def test_depositETH_other_self(a, auto, fr, deployer, user_a, user_b): + amount = 100 + + tx = fr.depositETH(user_b, {'from': user_a, 'value': amount}) + + assert fr.balance() == amount + for addr in a[1:]: + assert fr.balances(addr) == (amount if addr == user_b else 0) + assert addr.balance() == (INIT_ETH_BAL - amount if addr == user_a else INIT_ETH_BAL) + assert tx.events["BalanceChanged"][0].values() == [user_b, amount] + + tx = fr.depositETH(user_a, {'from': user_a, 'value': amount}) + + assert fr.balance() == amount*2 + for addr in a[1:]: + router_bal = 0 + account_bal = INIT_ETH_BAL + if addr == user_a or addr == user_b: + router_bal = amount + if addr == user_a: + account_bal = INIT_ETH_BAL - amount*2 + assert fr.balances(addr) == router_bal + assert addr.balance() == account_bal + assert tx.events["BalanceChanged"][0].values() == [user_a, amount] \ No newline at end of file diff --git a/tests/FundsRouter/test_forwardCalls_TimeConditions.py b/tests/FundsRouter/test_forwardCalls_TimeConditions.py new file mode 100644 index 0000000..c580bb4 --- /dev/null +++ b/tests/FundsRouter/test_forwardCalls_TimeConditions.py @@ -0,0 +1,189 @@ +from consts import * +from utils import * +from brownie import reverts, chain + + +# Single use automation + +def test_forwardCalls_time_condition_betweenTimes(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + amount_dep, _ = fr_dep + tc_fcn_data = (tc.address, tc.betweenTimes.encode_input(START_TIME, START_TIME+PERIOD_LENGTH), 0, False) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [tc_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, False, {'from': user_a}) + + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, False) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + + # Should now be within the valid time range + chain.sleep(START_TIME - chain.time() + 10) + # Need to make a tx to have the changed chain timestamp be reflected on-chain + deployer.transfer(deployer, 0) + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + tx = auto.r.executeHashedReq(id, req, expected_gas, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + eth_for_exec = get_eth_for_exec(evm_maths, tx, INIT_GAS_PRICE_FAST) + assert fr.balances(user_a) == amount_dep - eth_for_exec + assert fr.balance() == amount_dep - eth_for_exec + assert auto.r.getHashedReqs() == [NULL_HASH] + assert tx.events["HashedReqExecuted"][0].values() == [id, True] + + +def test_forwardCalls_time_condition_betweenTimes_rev_before(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + tc_fcn_data = (tc.address, tc.betweenTimes.encode_input(START_TIME, START_TIME+PERIOD_LENGTH), 0, False) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [tc_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, False, {'from': user_a}) + + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, False) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + + # Being just before the `afterTime` should revert + chain.sleep(START_TIME - chain.time() - 10) + deployer.transfer(deployer, 0) + with reverts(REV_MSG_TOO_EARLY): + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + +def test_forwardCalls_time_condition_betweenTimes_rev_after(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + tc_fcn_data = (tc.address, tc.betweenTimes.encode_input(START_TIME, START_TIME+PERIOD_LENGTH), 0, False) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [tc_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, False, {'from': user_a}) + + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, False) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + + # Being just before the `afterTime` should revert + chain.sleep(START_TIME - chain.time() + 110) + deployer.transfer(deployer, 0) + with reverts(REV_MSG_TOO_LATE): + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + +# Recurring automation + +def test_forwardCalls_time_condition_everyTimePeriod(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + amount_dep, _ = fr_dep + call_id = 1 + tc_fcn_data = (tc.address, tc.everyTimePeriod.encode_input(user_a, call_id, START_TIME, PERIOD_LENGTH), 0, True) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [tc_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, True, {'from': user_a}) + + assert tc.userToIdToLastExecTime(user_a, call_id) == 0 + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, True) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + + # Being just before the `afterTime` should revert + chain.sleep(START_TIME - chain.time() - 10) + deployer.transfer(deployer, 0) + with reverts(REV_MSG_NOT_PASSED_START): + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + # Should now be within the valid time range + chain.sleep(START_TIME - chain.time() + 10) + # Need to make a tx to have the changed chain timestamp be reflected on-chain + deployer.transfer(deployer, 0) + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + tx = auto.r.executeHashedReq(id, req, expected_gas, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + assert tc.userToIdToLastExecTime(user_a, call_id) == START_TIME + eth_for_exec_1 = get_eth_for_exec(evm_maths, tx, INIT_GAS_PRICE_FAST) + assert fr.balances(user_a) == amount_dep - eth_for_exec_1 + assert fr.balance() == amount_dep - eth_for_exec_1 + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqExecuted"][0].values() == [id, False] + + # Being just before the next period should revert + with reverts(REV_MSG_TOO_EARLY_PERIOD): + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + # Should now be within the valid time range + chain.sleep(START_TIME + PERIOD_LENGTH - chain.time() + 10) + deployer.transfer(deployer, 0) + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + tx = auto.r.executeHashedReq(id, req, expected_gas, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + assert tc.userToIdToLastExecTime(user_a, call_id) == START_TIME + PERIOD_LENGTH + eth_for_exec_2 = get_eth_for_exec(evm_maths, tx, INIT_GAS_PRICE_FAST) + assert fr.balances(user_a) == amount_dep - eth_for_exec_1 - eth_for_exec_2 + assert fr.balance() == amount_dep - eth_for_exec_1 - eth_for_exec_2 + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqExecuted"][0].values() == [id, False] + + +def test_forwardCalls_time_condition_everyTimePeriod_multiple_callIds(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + amount_dep, _ = fr_dep + call_id_1 = 1 + tc_fcn_data = (tc.address, tc.everyTimePeriod.encode_input(user_a, call_id_1, START_TIME, PERIOD_LENGTH), 0, True) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [tc_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, True, {'from': user_a}) + + assert tc.userToIdToLastExecTime(user_a, call_id_1) == 0 + id = 0 + req_1 = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, True) + hashes = [keccakReq(auto, req_1)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req_1] + + # Being just before the `afterTime` should revert + chain.sleep(START_TIME - chain.time() - 10) + deployer.transfer(deployer, 0) + with reverts(REV_MSG_NOT_PASSED_START): + expected_gas = auto.r.executeHashedReq.call(id, req_1, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + # Should now be within the valid time range of the 1st call_id + chain.sleep(START_TIME - chain.time() + 10) + # Need to make a tx to have the changed chain timestamp be reflected on-chain + deployer.transfer(deployer, 0) + + # A 2nd call_id should not interfere with the 1st + + call_id_2 = 2 + tc_fcn_data = (tc.address, tc.everyTimePeriod.encode_input(user_a, call_id_2, START_TIME+PERIOD_LENGTH, PERIOD_LENGTH), 0, True) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [tc_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, True, {'from': user_a}) + + assert tc.userToIdToLastExecTime(user_a, call_id_1) == 0 + assert tc.userToIdToLastExecTime(user_a, call_id_2) == 0 + id = 1 + req_2 = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, True) + hashes = [*hashes, keccakReq(auto, req_2)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req_2] + + # Being before the start for the 2nd call_id should revert + with reverts(REV_MSG_NOT_PASSED_START): + expected_gas = auto.r.executeHashedReq.call(id, req_2, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + # Should now be within the valid time range of the 2nd call_id + chain.sleep(START_TIME+PERIOD_LENGTH - chain.time() + 10) + deployer.transfer(deployer, 0) + expected_gas = auto.r.executeHashedReq.call(id, req_2, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + tx = auto.r.executeHashedReq(id, req_2, expected_gas, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + assert tc.userToIdToLastExecTime(user_a, call_id_1) == 0 + assert tc.userToIdToLastExecTime(user_a, call_id_2) == START_TIME+PERIOD_LENGTH + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqExecuted"][0].values() == [id, False] \ No newline at end of file diff --git a/tests/FundsRouter/test_forwardCalls_multiple.py b/tests/FundsRouter/test_forwardCalls_multiple.py new file mode 100644 index 0000000..685830a --- /dev/null +++ b/tests/FundsRouter/test_forwardCalls_multiple.py @@ -0,0 +1,121 @@ +from consts import * +from utils import * +from brownie import reverts, chain +import time + + +# Single use automation + +def test_forwardCalls_time_condition_target_betweenTimes(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + amount_dep, _ = fr_dep + new_x = 5 + tc_fcn_data = (tc.address, tc.betweenTimes.encode_input(START_TIME, START_TIME+PERIOD_LENGTH), 0, False) + mock_target_fcn_data = (mock_target.address, mock_target.setX.encode_input(new_x), 0, False) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [tc_fcn_data, mock_target_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, False, {'from': user_a}) + + assert mock_target.x() == 0 + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, False) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + + # Should now be within the valid time range + chain.sleep(START_TIME - chain.time() + 10) + # Need to make a tx to have the changed chain timestamp be reflected on-chain + deployer.transfer(deployer, 0) + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + tx = auto.r.executeHashedReq(id, req, expected_gas, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + assert mock_target.x() == new_x + eth_for_exec = get_eth_for_exec(evm_maths, tx, INIT_GAS_PRICE_FAST) + assert fr.balances(user_a) == amount_dep - eth_for_exec + assert fr.balance() == amount_dep - eth_for_exec + assert auto.r.getHashedReqs() == [NULL_HASH] + assert tx.events["HashedReqExecuted"][0].values() == [id, True] + + +# with verifyUser + +# Recurring automation + +def test_forwardCalls_time_condition_everyTimePeriod(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + amount_dep, _ = fr_dep + call_id = 1 + tc_fcn_data = (tc.address, tc.everyTimePeriod.encode_input(user_a, call_id, START_TIME, PERIOD_LENGTH), 0, True) + new_y = 5 + eth_to_send = 100 + mock_target_fcn_data = (mock_target.address, mock_target.setYVerify.encode_input(user_a, new_y), eth_to_send, True) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [tc_fcn_data, mock_target_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, True, {'from': user_a}) + + assert tc.userToIdToLastExecTime(user_a, call_id) == 0 + assert mock_target.y() == 0 + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, True) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + + # Being just before the `afterTime` should revert + chain.sleep(START_TIME - chain.time() - 10) + deployer.transfer(deployer, 0) + with reverts(REV_MSG_NOT_PASSED_START): + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + # Should now be within the valid time range + chain.sleep(START_TIME - chain.time() + 10) + # Need to make a tx to have the changed chain timestamp be reflected on-chain + deployer.transfer(deployer, 0) + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + tx = auto.r.executeHashedReq(id, req, expected_gas, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + assert tc.userToIdToLastExecTime(user_a, call_id) == START_TIME + assert mock_target.y() == new_y + eth_for_exec_1 = get_eth_for_exec(evm_maths, tx, INIT_GAS_PRICE_FAST) + assert fr.balances(user_a) == amount_dep - eth_for_exec_1 - eth_to_send + assert fr.balance() == amount_dep - eth_for_exec_1 - eth_to_send + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqExecuted"][0].values() == [id, False] + + new_y_2 = 2 + mock_target.setY(new_y_2) + assert mock_target.y() == new_y_2 + + # Being just before the next period should revert + with reverts(REV_MSG_TOO_EARLY_PERIOD): + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + # Should now be within the valid time range + chain.sleep(START_TIME + PERIOD_LENGTH - chain.time() + 10) + deployer.transfer(deployer, 0) + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + tx = auto.r.executeHashedReq(id, req, expected_gas, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + assert tc.userToIdToLastExecTime(user_a, call_id) == START_TIME + PERIOD_LENGTH + assert mock_target.y() == new_y + eth_for_exec_2 = get_eth_for_exec(evm_maths, tx, INIT_GAS_PRICE_FAST) + assert fr.balances(user_a) == amount_dep - eth_for_exec_1 - eth_to_send - eth_for_exec_2 - eth_to_send + assert fr.balance() == amount_dep - eth_for_exec_1 - eth_to_send - eth_for_exec_2 - eth_to_send + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqExecuted"][0].values() == [id, False] + + +def test_forwardCalls_time_condition_everyTimePeriod_incrementX_tutorial_demo(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + amount_dep, _ = fr_dep + PERIOD_LENGTH = 300 + call_id = 1 + tc_fcn_data = (tc.address, tc.everyTimePeriod.encode_input(user_a, call_id, time.time(), PERIOD_LENGTH), 0, True) + mock_target_fcn_data = (mock_target.address, mock_target.incrementX.encode_input(), 0, False) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [tc_fcn_data, mock_target_fcn_data]) + + tx = auto.r.newReq(fr.address, ADDR_0, fr_callData, 0, True, True, True, {'from': user_a}) + + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, True) + tx = auto.r.executeHashedReq(id, req, 21000, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) \ No newline at end of file diff --git a/tests/FundsRouter/test_forwardCalls_reverts.py b/tests/FundsRouter/test_forwardCalls_reverts.py new file mode 100644 index 0000000..243e8e6 --- /dev/null +++ b/tests/FundsRouter/test_forwardCalls_reverts.py @@ -0,0 +1,102 @@ +from consts import * +from utils import * +from brownie import reverts, chain + + +def test_forwardCalls_rev_userFeeForw(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + for addr in a: + with reverts(REV_MSG_NOT_USER_FEE_FORW): + fr.forwardCalls(user_a, 100, [(user_a, "", 0, False)], {'from': addr}) + + +def test_forwardCalls_rev_calldata_not_user(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a, user_b): + amount_dep, _ = fr_dep + call_id = 1 + tc_fcn_data = (tc.address, tc.everyTimePeriod.encode_input(user_b, call_id, START_TIME, PERIOD_LENGTH), 0, True) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [tc_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, False, {'from': user_a}) + + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, False) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + + fr.withdrawETH(user_a, amount_dep-100, {'from': user_a}) + + # Should now be within the valid time range + chain.sleep(START_TIME - chain.time() + 10) + # Need to make a tx to have the changed chain timestamp be reflected on-chain + deployer.transfer(deployer, 0) + + with reverts(REV_MSG_CALLDATA_NOT_USER): + auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + +def test_forwardCalls_rev_funds_fee(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + amount_dep, _ = fr_dep + tc_fcn_data = (tc.address, tc.betweenTimes.encode_input(START_TIME, START_TIME+PERIOD_LENGTH), 0, False) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [tc_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, False, {'from': user_a}) + + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, False) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + + fr.withdrawETH(user_a, amount_dep-100, {'from': user_a}) + + # Should now be within the valid time range + chain.sleep(START_TIME - chain.time() + 10) + # Need to make a tx to have the changed chain timestamp be reflected on-chain + deployer.transfer(deployer, 0) + + with reverts(REV_MSG_NOT_ENOUGH_FUNDS_FEE): + auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + +def test_forwardCalls_rev_funds_fee_from_eth_sent_in_call(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + deployer.transfer(fr, E_18) + amount_dep, _ = fr_dep + mock_target_fcn_data = (mock_target.address, mock_target.setX.encode_input(5), amount_dep+1, False) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [mock_target_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, False, {'from': user_a}) + + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, False) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + + # Should now be within the valid time range + chain.sleep(START_TIME - chain.time() + 10) + # Need to make a tx to have the changed chain timestamp be reflected on-chain + deployer.transfer(deployer, 0) + + with reverts(REV_MSG_NOT_ENOUGH_FUNDS_FEE): + auto.r.executeHashedReq(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + +def test_forwardCalls_rev_reentering_withdrawETH(a, auto, evm_maths, fr, fr_dep, tc, mock_target, deployer, user_a): + amount_dep, _ = fr_dep + mock_target_fcn_data = (mock_target.address, mock_target.callWithdrawETH.encode_input(5), 0, False) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [mock_target_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, False, {'from': user_a}) + + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, False) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + + with reverts(REV_MSG_REENTRANT): + auto.r.executeHashedReq(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) \ No newline at end of file diff --git a/tests/FundsRouter/test_withdrawETH.py b/tests/FundsRouter/test_withdrawETH.py new file mode 100644 index 0000000..186c75d --- /dev/null +++ b/tests/FundsRouter/test_withdrawETH.py @@ -0,0 +1,49 @@ +from consts import * +from brownie import reverts + + +def test_withdrawETH_self(a, auto, fr, fr_dep, deployer, user_a): + amount_dep, _ = fr_dep + amount_withdraw = 50 + amount_diff = amount_dep - amount_withdraw + assert fr.balances(user_a) == amount_dep + + tx = fr.withdrawETH(user_a, amount_withdraw, {'from': user_a}) + + assert fr.balance() == amount_diff + for addr in a[1:]: + assert fr.balances(addr) == (amount_diff if addr == user_a else 0) + assert addr.balance() == (INIT_ETH_BAL - amount_diff if addr == user_a else INIT_ETH_BAL) + assert tx.events["BalanceChanged"][0].values() == [user_a, amount_diff] + + +def test_withdrawETH_other(a, auto, fr, fr_dep, deployer, user_a, user_b): + amount_dep, _ = fr_dep + amount_withdraw = 50 + amount_diff = amount_dep - amount_withdraw + + tx = fr.withdrawETH(user_b, amount_withdraw, {'from': user_a}) + + assert fr.balance() == amount_diff + for addr in a[1:]: + router_bal = 0 + account_bal = INIT_ETH_BAL + if addr == user_a: + router_bal = amount_diff + if addr == user_a: + account_bal = INIT_ETH_BAL - amount_dep + if addr == user_b: + account_bal = INIT_ETH_BAL + amount_withdraw + assert fr.balances(addr) == router_bal + assert addr.balance() == account_bal + assert tx.events["BalanceChanged"][0].values() == [user_a, amount_diff] + + +def test_withdrawETH_rev_funds(a, auto, fr, fr_dep, deployer, user_a): + amount_dep, _ = fr_dep + assert fr.balances(user_a) == amount_dep + + for addr in a: + amount_withdraw = (amount_dep+1 if addr == user_a else 1) + with reverts(REV_MSG_NOT_ENOUGH_FUNDS): + fr.withdrawETH(user_a, amount_withdraw, {'from': addr}) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..376bcfa --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,185 @@ +import pytest +from brownie import web3, chain, network, Contract +from consts import * +# from utils import * + + +# Test isolation +@pytest.fixture(autouse=True) +def isolation(fn_isolation): + pass + + +@pytest.fixture(scope="module") +def deployer(a): + return a[0] + + +@pytest.fixture(scope="module") +def user_a(a): + return a[1] + + +@pytest.fixture(scope="module") +def user_b(a): + return a[2] + + +@pytest.fixture(scope="module") +def user_c(a): + return a[3] + + +@pytest.fixture(scope="module") +def user_d(a): + return a[4] + + +@pytest.fixture(scope="module") +def user_e(a): + return a[5] + + +# Deploy the contracts for repeated tests without having to redeploy each time + +def deploy_initial_AUTO_contracts(a, AUTO, PriceOracle, Oracle, StakeManager, Registry, Forwarder, Timelock): + class Context: + pass + + auto = Context() + # It's a bit easier to not get mixed up with accounts if they're named + # Can't define this in consts because a needs to be imported into the test + auto.DEPLOYER = a[0] + auto.FR_DEPLOYER = {"from": auto.DEPLOYER} + auto.ALICE = a[1] + auto.FR_ALICE = {"from": auto.ALICE} + auto.BOB = a[2] + auto.FR_BOB = {"from": auto.BOB} + auto.CHARLIE = a[3] + auto.FR_CHARLIE = {"from": auto.CHARLIE} + auto.DENICE = a[4] + auto.FR_DENICE = {"from": auto.DENICE} + auto.EOAs = list(a)[:10] + auto.EOAsStr = [str(acc) for acc in auto.EOAs] + + # Calling `updateExecutor` requires the epoch to be > 0 + chain.mine(BLOCKS_IN_EPOCH) + + auto.po = auto.DEPLOYER.deploy(PriceOracle, INIT_AUTO_PER_ETH_WEI, INIT_GAS_PRICE_FAST) + auto.o = auto.DEPLOYER.deploy(Oracle, auto.po, False) + auto.sm = auto.DEPLOYER.deploy(StakeManager, auto.o) + auto.uf = auto.DEPLOYER.deploy(Forwarder) + auto.ff = auto.DEPLOYER.deploy(Forwarder) + auto.uff = auto.DEPLOYER.deploy(Forwarder) + auto.r = auto.DEPLOYER.deploy( + Registry, + auto.sm, + auto.o, + auto.uf, + auto.ff, + auto.uff, + "Autonomy Network", + "AUTO", + INIT_AUTO_SUPPLY + ) + auto.AUTO = AUTO.at(auto.r.getAUTOAddr()) + auto.sm.setAUTO(auto.AUTO, auto.FR_DEPLOYER) + auto.uf.setCaller(auto.r, True, auto.FR_DEPLOYER) + auto.ff.setCaller(auto.r, True, auto.FR_DEPLOYER) + auto.uff.setCaller(auto.r, True, auto.FR_DEPLOYER) + + # Create timelock for OP owner + auto.tl = auto.DEPLOYER.deploy(Timelock, auto.DEPLOYER, 2*DAY) + auto.po.transferOwnership(auto.tl, auto.FR_DEPLOYER) + auto.o.transferOwnership(auto.tl, auto.FR_DEPLOYER) + auto.uf.transferOwnership(auto.tl, auto.FR_DEPLOYER) + auto.ff.transferOwnership(auto.tl, auto.FR_DEPLOYER) + auto.uff.transferOwnership(auto.tl, auto.FR_DEPLOYER) + + auto.all = [auto.AUTO, auto.po, auto.o, auto.sm, auto.uf, auto.ff, auto.uff, auto.r, auto.tl] + auto.allStr = [str(x) for x in auto.all] + + return auto + + + +@pytest.fixture(scope="module") +def cleanAUTO(a, AUTO, PriceOracle, Oracle, StakeManager, Registry, Forwarder, Timelock): + if network.show_active() != "mainnet-fork": + ERC1820_DEPLOYER = '0xa990077c3205cbDf861e17Fa532eeB069cE9fF96' + ERC1820_PAYLOAD = '0xf90a388085174876e800830c35008080b909e5608060405234801561001057600080fd5b506109c5806100206000396000f3fe608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c00291ba01820182018201820182018201820182018201820182018201820182018201820a01820182018201820182018201820182018201820182018201820182018201820' + a[0].transfer(ERC1820_DEPLOYER, ERC1820_ETH_AMOUNT) + web3.eth.send_raw_transaction(ERC1820_PAYLOAD) + + return deploy_initial_AUTO_contracts(a, AUTO, PriceOracle, Oracle, StakeManager, Registry, Forwarder, Timelock) + + +@pytest.fixture(scope="module") +def auto(cleanAUTO): + auto = cleanAUTO + # For being able to test staking with + auto.AUTO.transfer(auto.ALICE, MAX_TEST_STAKE, auto.FR_DEPLOYER) + auto.AUTO.transfer(auto.BOB, MAX_TEST_STAKE, auto.FR_DEPLOYER) + auto.AUTO.transfer(auto.CHARLIE, MAX_TEST_STAKE, auto.FR_DEPLOYER) + + return auto + + +# routerUserVeriForwarder in FundsRouter +@pytest.fixture(scope="module") +def fruf(Forwarder, deployer): + return deployer.deploy(Forwarder) + + +@pytest.fixture(scope="module") +def fr(FundsRouter, deployer, fruf, auto): + fr = deployer.deploy(FundsRouter, auto.r, auto.uff, fruf) + fruf.setCaller(fr, True, {'from': deployer}) + + return fr + + +@pytest.fixture(scope="module") +def fr_dep(fr, user_a): + amount = E_18 + tx = fr.depositETH(user_a, {'from': user_a, 'value': amount}) + assert fr.balances(user_a) == amount + + return amount, tx + + +@pytest.fixture(scope="module") +def tc(TimeConditions, fruf, deployer): + return deployer.deploy(TimeConditions, fruf) + + +@pytest.fixture(scope="module") +def mock_target(MockTarget, fruf, fr, deployer): + return deployer.deploy(MockTarget, fruf, fr) + + +# Need to test gas usage with the exact same rounding profile that Solidity +# uses vs Python +@pytest.fixture(scope="module") +def evm_maths(EVMMaths, deployer): + return deployer.deploy(EVMMaths) + + +@pytest.fixture(scope="module") +def uniV3_pool(): + return Contract.from_explorer('0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8') + + +@pytest.fixture(scope="module") +def uniV3_twap_getter(TwapGetter, deployer): + return deployer.deploy(TwapGetter) + + +@pytest.fixture(scope="module") +def uniV2_router(): + return Contract.from_explorer('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D') + + +@pytest.fixture(scope="module") +def uni_amounts(UniswapV2Amounts, uniV2_router, deployer): + return deployer.deploy(UniswapV2Amounts, uniV2_router) \ No newline at end of file diff --git a/tests/consts.py b/tests/consts.py new file mode 100644 index 0000000..dedcf9a --- /dev/null +++ b/tests/consts.py @@ -0,0 +1,54 @@ +from brownie import web3 + + +# General/shared +ADDR_0 = "0x0000000000000000000000000000000000000000" +ADDR_1001 = "0x1000000000000000000000000000000000000001" +NULL_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000' +# NULL_BYTES = "0x" +E_18 = int(1e18) +INIT_ETH_BAL = 100 * E_18 +MAX_UINT = 2**256 - 1 +MAX_UINT64 = 2**64 - 1 +HOUR = 60 * 60 +DAY = HOUR * 24 + + +# Autonomy +ERC1820_ETH_AMOUNT = 0.08*E_18 +BLOCKS_IN_EPOCH = 100 +INIT_AUTO_PER_ETH = 2000 +INIT_AUTO_PER_ETH_WEI = INIT_AUTO_PER_ETH * E_18 +# Ah the good old days +INIT_GAS_PRICE_FAST = 3 * 10**9 +INIT_AUTO_SUPPLY = (10**27) + (420069*10**18) +STAN_STAKE = 10000 * E_18 +INIT_NUM_STAKES = 100 +MAX_TEST_STAKE = INIT_NUM_STAKES * STAN_STAKE +MIN_GAS = 21000 +BASE_BPS = 10000 +PAY_ETH_FACTOR = 1.3 +PAY_ETH_BPS = PAY_ETH_FACTOR * BASE_BPS + + +# FundsRouter +REV_MSG_NOT_ENOUGH_FUNDS = "FRouter: not enough funds" +REV_MSG_NOT_USER_FEE_FORW = "FRouter: not userFeeForw" +REV_MSG_CALLDATA_NOT_USER = "FRouter: calldata not user" +REV_MSG_NOT_ENOUGH_FUNDS_FEE = "FRouter: not enough funds - fee" +REV_MSG_REENTRANT = "ReentrancyGuard: reentrant call" + + +# TimeConditions +START_TIME = 2000000000 +PERIOD_LENGTH = 100 +REV_MSG_TOO_EARLY = "TimeConditions: too early" +REV_MSG_TOO_LATE = "TimeConditions: too late" +REV_MSG_NOT_PASSED_START = "TimeConditions: not passed start" +REV_MSG_TOO_EARLY_PERIOD = "TimeConditions: too early period" + + +# UniswapV2Amounts +WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" +USDC_ADDR = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" +REV_MSG_OUTPUT_TOO_LOW = "UniswapV2Amounts: output too low" \ No newline at end of file diff --git a/tests/test_Forwarder_constructor.py b/tests/test_Forwarder_constructor.py new file mode 100644 index 0000000..90ee76a --- /dev/null +++ b/tests/test_Forwarder_constructor.py @@ -0,0 +1,4 @@ +def test_constructor(a, fruf, deployer): + assert fruf.owner() == deployer + for addr in a: + assert fruf.canCall(addr) == False \ No newline at end of file diff --git a/tests/test_TimeConditions_constructor.py b/tests/test_TimeConditions_constructor.py new file mode 100644 index 0000000..6fef5ce --- /dev/null +++ b/tests/test_TimeConditions_constructor.py @@ -0,0 +1,5 @@ +def test_constructor(a, fruf, tc, deployer): + assert tc.routerUserVeriForwarder() == fruf + for addr in a: + for i in range(10): + assert tc.userToIdToLastExecTime(addr, i) == 0 \ No newline at end of file diff --git a/tests/test_uniV2_price.py b/tests/test_uniV2_price.py new file mode 100644 index 0000000..78601d1 --- /dev/null +++ b/tests/test_uniV2_price.py @@ -0,0 +1,44 @@ +from consts import * +from utils import * +from brownie import reverts + + +def test_forwardCalls_uniV2_price_condition(a, auto, evm_maths, fr, fr_dep, uniV2_router, uni_amounts, deployer, user_a): + amount_dep, _ = fr_dep + uni_amounts_fcn_data = (uni_amounts.address, uni_amounts.amountOutGreaterThan.encode_input(WETH_ADDR, USDC_ADDR, E_18, 100), 0, False) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [uni_amounts_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, False, {'from': user_a}) + + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, False) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + expected_gas = auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + tx = auto.r.executeHashedReq(id, req, expected_gas, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) + + eth_for_exec = get_eth_for_exec(evm_maths, tx, INIT_GAS_PRICE_FAST) + assert fr.balances(user_a) == amount_dep - eth_for_exec + assert fr.balance() == amount_dep - eth_for_exec + assert auto.r.getHashedReqs() == [NULL_HASH] + assert tx.events["HashedReqExecuted"][0].values() == [id, True] + + +def test_forwardCalls_uniV2_price_condition_rev_output(a, auto, evm_maths, fr, fr_dep, uniV2_router, uni_amounts, deployer, user_a): + amount_dep, _ = fr_dep + uni_amounts_fcn_data = (uni_amounts.address, uni_amounts.amountOutGreaterThan.encode_input(WETH_ADDR, USDC_ADDR, E_18, E_18), 0, False) + fr_callData = fr.forwardCalls.encode_input(user_a, 0, [uni_amounts_fcn_data]) + + tx = auto.r.newReq(fr, ADDR_0, fr_callData, 0, True, True, False, {'from': user_a}) + + id = 0 + req = (user_a, fr, ADDR_0, fr_callData, 0, 0, True, True, False, False) + hashes = [keccakReq(auto, req)] + assert tx.return_value == id + assert auto.r.getHashedReqs() == hashes + assert tx.events["HashedReqAdded"][0].values() == [id, *req] + + with reverts(REV_MSG_OUTPUT_TOO_LOW): + auto.r.executeHashedReq.call(id, req, MIN_GAS, {'from': deployer, 'gasPrice': INIT_GAS_PRICE_FAST}) \ No newline at end of file diff --git a/tests/test_uniV3_twap.py b/tests/test_uniV3_twap.py new file mode 100644 index 0000000..ea0730b --- /dev/null +++ b/tests/test_uniV3_twap.py @@ -0,0 +1,5 @@ +# def test_univ3_twap(uniV3_pool, uniV3_twap_getter): +# print(uniV3_pool) +# price = uniV3_twap_getter.getSqrtTwapX96(uniV3_pool, 1) +# print(price) +# print(uniV3_twap_getter.getPriceX96FromSqrtPriceX96(price)) \ No newline at end of file diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..fc557b3 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,10 @@ +from brownie import web3 +from consts import * + + +def keccakReq(auto, req): + return web3.keccak(auto.r.getReqBytes(req)).hex() + + +def get_eth_for_exec(evm_maths, tx, gas_price_fast): + return evm_maths.mul3div1(tx.return_value, gas_price_fast, PAY_ETH_BPS, BASE_BPS) \ No newline at end of file