Skip to content

Instantly share code, notes, and snippets.

@sohkai
Created December 5, 2019 16:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sohkai/37c2bcd32718c89ac71f93c9bca6432e to your computer and use it in GitHub Desktop.
Save sohkai/37c2bcd32718c89ac71f93c9bca6432e to your computer and use it in GitHub Desktop.
Aragon 0.7 - Finance
// See https://etherscan.io/address/0x836835289A2E81B66AE5d95b7c8dBC0480dCf9da#code
/**
*Submitted for verification at Etherscan.io on 2019-04-17
*/
// File: @aragon/os/contracts/common/UnstructuredStorage.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
library UnstructuredStorage {
function getStorageBool(bytes32 position) internal view returns (bool data) {
assembly { data := sload(position) }
}
function getStorageAddress(bytes32 position) internal view returns (address data) {
assembly { data := sload(position) }
}
function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
assembly { data := sload(position) }
}
function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
assembly { data := sload(position) }
}
function setStorageBool(bytes32 position, bool data) internal {
assembly { sstore(position, data) }
}
function setStorageAddress(bytes32 position, address data) internal {
assembly { sstore(position, data) }
}
function setStorageBytes32(bytes32 position, bytes32 data) internal {
assembly { sstore(position, data) }
}
function setStorageUint256(bytes32 position, uint256 data) internal {
assembly { sstore(position, data) }
}
}
// File: @aragon/os/contracts/acl/IACL.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
interface IACL {
function initialize(address permissionsCreator) external;
// TODO: this should be external
// See https://github.com/ethereum/solidity/issues/4832
function hasPermission(address who, address where, bytes32 what, bytes how) public view returns (bool);
}
// File: @aragon/os/contracts/common/IVaultRecoverable.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
interface IVaultRecoverable {
event RecoverToVault(address indexed vault, address indexed token, uint256 amount);
function transferToVault(address token) external;
function allowRecoverability(address token) external view returns (bool);
function getRecoveryVault() external view returns (address);
}
// File: @aragon/os/contracts/kernel/IKernel.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
interface IKernelEvents {
event SetApp(bytes32 indexed namespace, bytes32 indexed appId, address app);
}
// This should be an interface, but interfaces can't inherit yet :(
contract IKernel is IKernelEvents, IVaultRecoverable {
function acl() public view returns (IACL);
function hasPermission(address who, address where, bytes32 what, bytes how) public view returns (bool);
function setApp(bytes32 namespace, bytes32 appId, address app) public;
function getApp(bytes32 namespace, bytes32 appId) public view returns (address);
}
// File: @aragon/os/contracts/apps/AppStorage.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract AppStorage {
using UnstructuredStorage for bytes32;
/* Hardcoded constants to save gas
bytes32 internal constant KERNEL_POSITION = keccak256("aragonOS.appStorage.kernel");
bytes32 internal constant APP_ID_POSITION = keccak256("aragonOS.appStorage.appId");
*/
bytes32 internal constant KERNEL_POSITION = 0x4172f0f7d2289153072b0a6ca36959e0cbe2efc3afe50fc81636caa96338137b;
bytes32 internal constant APP_ID_POSITION = 0xd625496217aa6a3453eecb9c3489dc5a53e6c67b444329ea2b2cbc9ff547639b;
function kernel() public view returns (IKernel) {
return IKernel(KERNEL_POSITION.getStorageAddress());
}
function appId() public view returns (bytes32) {
return APP_ID_POSITION.getStorageBytes32();
}
function setKernel(IKernel _kernel) internal {
KERNEL_POSITION.setStorageAddress(address(_kernel));
}
function setAppId(bytes32 _appId) internal {
APP_ID_POSITION.setStorageBytes32(_appId);
}
}
// File: @aragon/os/contracts/acl/ACLSyntaxSugar.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract ACLSyntaxSugar {
function arr() internal pure returns (uint256[]) {
return new uint256[](0);
}
function arr(bytes32 _a) internal pure returns (uint256[] r) {
return arr(uint256(_a));
}
function arr(bytes32 _a, bytes32 _b) internal pure returns (uint256[] r) {
return arr(uint256(_a), uint256(_b));
}
function arr(address _a) internal pure returns (uint256[] r) {
return arr(uint256(_a));
}
function arr(address _a, address _b) internal pure returns (uint256[] r) {
return arr(uint256(_a), uint256(_b));
}
function arr(address _a, uint256 _b, uint256 _c) internal pure returns (uint256[] r) {
return arr(uint256(_a), _b, _c);
}
function arr(address _a, uint256 _b, uint256 _c, uint256 _d) internal pure returns (uint256[] r) {
return arr(uint256(_a), _b, _c, _d);
}
function arr(address _a, uint256 _b) internal pure returns (uint256[] r) {
return arr(uint256(_a), uint256(_b));
}
function arr(address _a, address _b, uint256 _c, uint256 _d, uint256 _e) internal pure returns (uint256[] r) {
return arr(uint256(_a), uint256(_b), _c, _d, _e);
}
function arr(address _a, address _b, address _c) internal pure returns (uint256[] r) {
return arr(uint256(_a), uint256(_b), uint256(_c));
}
function arr(address _a, address _b, uint256 _c) internal pure returns (uint256[] r) {
return arr(uint256(_a), uint256(_b), uint256(_c));
}
function arr(uint256 _a) internal pure returns (uint256[] r) {
r = new uint256[](1);
r[0] = _a;
}
function arr(uint256 _a, uint256 _b) internal pure returns (uint256[] r) {
r = new uint256[](2);
r[0] = _a;
r[1] = _b;
}
function arr(uint256 _a, uint256 _b, uint256 _c) internal pure returns (uint256[] r) {
r = new uint256[](3);
r[0] = _a;
r[1] = _b;
r[2] = _c;
}
function arr(uint256 _a, uint256 _b, uint256 _c, uint256 _d) internal pure returns (uint256[] r) {
r = new uint256[](4);
r[0] = _a;
r[1] = _b;
r[2] = _c;
r[3] = _d;
}
function arr(uint256 _a, uint256 _b, uint256 _c, uint256 _d, uint256 _e) internal pure returns (uint256[] r) {
r = new uint256[](5);
r[0] = _a;
r[1] = _b;
r[2] = _c;
r[3] = _d;
r[4] = _e;
}
}
contract ACLHelpers {
function decodeParamOp(uint256 _x) internal pure returns (uint8 b) {
return uint8(_x >> (8 * 30));
}
function decodeParamId(uint256 _x) internal pure returns (uint8 b) {
return uint8(_x >> (8 * 31));
}
function decodeParamsList(uint256 _x) internal pure returns (uint32 a, uint32 b, uint32 c) {
a = uint32(_x);
b = uint32(_x >> (8 * 4));
c = uint32(_x >> (8 * 8));
}
}
// File: @aragon/os/contracts/common/Uint256Helpers.sol
pragma solidity ^0.4.24;
library Uint256Helpers {
uint256 private constant MAX_UINT64 = uint64(-1);
string private constant ERROR_NUMBER_TOO_BIG = "UINT64_NUMBER_TOO_BIG";
function toUint64(uint256 a) internal pure returns (uint64) {
require(a <= MAX_UINT64, ERROR_NUMBER_TOO_BIG);
return uint64(a);
}
}
// File: @aragon/os/contracts/common/TimeHelpers.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract TimeHelpers {
using Uint256Helpers for uint256;
/**
* @dev Returns the current block number.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber() internal view returns (uint256) {
return block.number;
}
/**
* @dev Returns the current block number, converted to uint64.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber64() internal view returns (uint64) {
return getBlockNumber().toUint64();
}
/**
* @dev Returns the current timestamp.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp() internal view returns (uint256) {
return block.timestamp; // solium-disable-line security/no-block-members
}
/**
* @dev Returns the current timestamp, converted to uint64.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp64() internal view returns (uint64) {
return getTimestamp().toUint64();
}
}
// File: @aragon/os/contracts/common/Initializable.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract Initializable is TimeHelpers {
using UnstructuredStorage for bytes32;
// keccak256("aragonOS.initializable.initializationBlock")
bytes32 internal constant INITIALIZATION_BLOCK_POSITION = 0xebb05b386a8d34882b8711d156f463690983dc47815980fb82aeeff1aa43579e;
string private constant ERROR_ALREADY_INITIALIZED = "INIT_ALREADY_INITIALIZED";
string private constant ERROR_NOT_INITIALIZED = "INIT_NOT_INITIALIZED";
modifier onlyInit {
require(getInitializationBlock() == 0, ERROR_ALREADY_INITIALIZED);
_;
}
modifier isInitialized {
require(hasInitialized(), ERROR_NOT_INITIALIZED);
_;
}
/**
* @return Block number in which the contract was initialized
*/
function getInitializationBlock() public view returns (uint256) {
return INITIALIZATION_BLOCK_POSITION.getStorageUint256();
}
/**
* @return Whether the contract has been initialized by the time of the current block
*/
function hasInitialized() public view returns (bool) {
uint256 initializationBlock = getInitializationBlock();
return initializationBlock != 0 && getBlockNumber() >= initializationBlock;
}
/**
* @dev Function to be called by top level contract after initialization has finished.
*/
function initialized() internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(getBlockNumber());
}
/**
* @dev Function to be called by top level contract after initialization to enable the contract
* at a future block number rather than immediately.
*/
function initializedAt(uint256 _blockNumber) internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(_blockNumber);
}
}
// File: @aragon/os/contracts/common/Petrifiable.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract Petrifiable is Initializable {
// Use block UINT256_MAX (which should be never) as the initializable date
uint256 internal constant PETRIFIED_BLOCK = uint256(-1);
function isPetrified() public view returns (bool) {
return getInitializationBlock() == PETRIFIED_BLOCK;
}
/**
* @dev Function to be called by top level contract to prevent being initialized.
* Useful for freezing base contracts when they're used behind proxies.
*/
function petrify() internal onlyInit {
initializedAt(PETRIFIED_BLOCK);
}
}
// File: @aragon/os/contracts/common/Autopetrified.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract Autopetrified is Petrifiable {
constructor() public {
// Immediately petrify base (non-proxy) instances of inherited contracts on deploy.
// This renders them uninitializable (and unusable without a proxy).
petrify();
}
}
// File: @aragon/os/contracts/common/ConversionHelpers.sol
pragma solidity ^0.4.24;
library ConversionHelpers {
string private constant ERROR_IMPROPER_LENGTH = "CONVERSION_IMPROPER_LENGTH";
function dangerouslyCastUintArrayToBytes(uint256[] memory _input) internal pure returns (bytes memory output) {
// Force cast the uint256[] into a bytes array, by overwriting its length
// Note that the bytes array doesn't need to be initialized as we immediately overwrite it
// with the input and a new length. The input becomes invalid from this point forward.
uint256 byteLength = _input.length * 32;
assembly {
output := _input
mstore(output, byteLength)
}
}
function dangerouslyCastBytesToUintArray(bytes memory _input) internal pure returns (uint256[] memory output) {
// Force cast the bytes array into a uint256[], by overwriting its length
// Note that the uint256[] doesn't need to be initialized as we immediately overwrite it
// with the input and a new length. The input becomes invalid from this point forward.
uint256 intsLength = _input.length / 32;
require(_input.length == intsLength * 32, ERROR_IMPROPER_LENGTH);
assembly {
output := _input
mstore(output, intsLength)
}
}
}
// File: @aragon/os/contracts/common/ReentrancyGuard.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract ReentrancyGuard {
using UnstructuredStorage for bytes32;
/* Hardcoded constants to save gas
bytes32 internal constant REENTRANCY_MUTEX_POSITION = keccak256("aragonOS.reentrancyGuard.mutex");
*/
bytes32 private constant REENTRANCY_MUTEX_POSITION = 0xe855346402235fdd185c890e68d2c4ecad599b88587635ee285bce2fda58dacb;
string private constant ERROR_REENTRANT = "REENTRANCY_REENTRANT_CALL";
modifier nonReentrant() {
// Ensure mutex is unlocked
require(!REENTRANCY_MUTEX_POSITION.getStorageBool(), ERROR_REENTRANT);
// Lock mutex before function call
REENTRANCY_MUTEX_POSITION.setStorageBool(true);
// Perform function call
_;
// Unlock mutex after function call
REENTRANCY_MUTEX_POSITION.setStorageBool(false);
}
}
// File: @aragon/os/contracts/lib/token/ERC20.sol
// See https://github.com/OpenZeppelin/openzeppelin-solidity/blob/a9f910d34f0ab33a1ae5e714f69f9596a02b4d91/contracts/token/ERC20/ERC20.sol
pragma solidity ^0.4.24;
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
contract ERC20 {
function totalSupply() public view returns (uint256);
function balanceOf(address _who) public view returns (uint256);
function allowance(address _owner, address _spender)
public view returns (uint256);
function transfer(address _to, uint256 _value) public returns (bool);
function approve(address _spender, uint256 _value)
public returns (bool);
function transferFrom(address _from, address _to, uint256 _value)
public returns (bool);
event Transfer(
address indexed from,
address indexed to,
uint256 value
);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}
// File: @aragon/os/contracts/common/EtherTokenConstant.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
// aragonOS and aragon-apps rely on address(0) to denote native ETH, in
// contracts where both tokens and ETH are accepted
contract EtherTokenConstant {
address internal constant ETH = address(0);
}
// File: @aragon/os/contracts/common/IsContract.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract IsContract {
/*
* NOTE: this should NEVER be used for authentication
* (see pitfalls: https://github.com/fergarrui/ethereum-security/tree/master/contracts/extcodesize).
*
* This is only intended to be used as a sanity check that an address is actually a contract,
* RATHER THAN an address not being a contract.
*/
function isContract(address _target) internal view returns (bool) {
if (_target == address(0)) {
return false;
}
uint256 size;
assembly { size := extcodesize(_target) }
return size > 0;
}
}
// File: @aragon/os/contracts/common/SafeERC20.sol
// Inspired by AdEx (https://github.com/AdExNetwork/adex-protocol-eth/blob/b9df617829661a7518ee10f4cb6c4108659dd6d5/contracts/libs/SafeERC20.sol)
// and 0x (https://github.com/0xProject/0x-monorepo/blob/737d1dc54d72872e24abce5a1dbe1b66d35fa21a/contracts/protocol/contracts/protocol/AssetProxy/ERC20Proxy.sol#L143)
pragma solidity ^0.4.24;
library SafeERC20 {
// Before 0.5, solidity has a mismatch between `address.transfer()` and `token.transfer()`:
// https://github.com/ethereum/solidity/issues/3544
bytes4 private constant TRANSFER_SELECTOR = 0xa9059cbb;
string private constant ERROR_TOKEN_BALANCE_REVERTED = "SAFE_ERC_20_BALANCE_REVERTED";
string private constant ERROR_TOKEN_ALLOWANCE_REVERTED = "SAFE_ERC_20_ALLOWANCE_REVERTED";
function invokeAndCheckSuccess(address _addr, bytes memory _calldata)
private
returns (bool)
{
bool ret;
assembly {
let ptr := mload(0x40) // free memory pointer
let success := call(
gas, // forward all gas
_addr, // address
0, // no value
add(_calldata, 0x20), // calldata start
mload(_calldata), // calldata length
ptr, // write output over free memory
0x20 // uint256 return
)
if gt(success, 0) {
// Check number of bytes returned from last function call
switch returndatasize
// No bytes returned: assume success
case 0 {
ret := 1
}
// 32 bytes returned: check if non-zero
case 0x20 {
// Only return success if returned data was true
// Already have output in ptr
ret := eq(mload(ptr), 1)
}
// Not sure what was returned: don't mark as success
default { }
}
}
return ret;
}
function staticInvoke(address _addr, bytes memory _calldata)
private
view
returns (bool, uint256)
{
bool success;
uint256 ret;
assembly {
let ptr := mload(0x40) // free memory pointer
success := staticcall(
gas, // forward all gas
_addr, // address
add(_calldata, 0x20), // calldata start
mload(_calldata), // calldata length
ptr, // write output over free memory
0x20 // uint256 return
)
if gt(success, 0) {
ret := mload(ptr)
}
}
return (success, ret);
}
/**
* @dev Same as a standards-compliant ERC20.transfer() that never reverts (returns false).
* Note that this makes an external call to the token.
*/
function safeTransfer(ERC20 _token, address _to, uint256 _amount) internal returns (bool) {
bytes memory transferCallData = abi.encodeWithSelector(
TRANSFER_SELECTOR,
_to,
_amount
);
return invokeAndCheckSuccess(_token, transferCallData);
}
/**
* @dev Same as a standards-compliant ERC20.transferFrom() that never reverts (returns false).
* Note that this makes an external call to the token.
*/
function safeTransferFrom(ERC20 _token, address _from, address _to, uint256 _amount) internal returns (bool) {
bytes memory transferFromCallData = abi.encodeWithSelector(
_token.transferFrom.selector,
_from,
_to,
_amount
);
return invokeAndCheckSuccess(_token, transferFromCallData);
}
/**
* @dev Same as a standards-compliant ERC20.approve() that never reverts (returns false).
* Note that this makes an external call to the token.
*/
function safeApprove(ERC20 _token, address _spender, uint256 _amount) internal returns (bool) {
bytes memory approveCallData = abi.encodeWithSelector(
_token.approve.selector,
_spender,
_amount
);
return invokeAndCheckSuccess(_token, approveCallData);
}
/**
* @dev Static call into ERC20.balanceOf().
* Reverts if the call fails for some reason (should never fail).
*/
function staticBalanceOf(ERC20 _token, address _owner) internal view returns (uint256) {
bytes memory balanceOfCallData = abi.encodeWithSelector(
_token.balanceOf.selector,
_owner
);
(bool success, uint256 tokenBalance) = staticInvoke(_token, balanceOfCallData);
require(success, ERROR_TOKEN_BALANCE_REVERTED);
return tokenBalance;
}
/**
* @dev Static call into ERC20.allowance().
* Reverts if the call fails for some reason (should never fail).
*/
function staticAllowance(ERC20 _token, address _owner, address _spender) internal view returns (uint256) {
bytes memory allowanceCallData = abi.encodeWithSelector(
_token.allowance.selector,
_owner,
_spender
);
(bool success, uint256 allowance) = staticInvoke(_token, allowanceCallData);
require(success, ERROR_TOKEN_ALLOWANCE_REVERTED);
return allowance;
}
}
// File: @aragon/os/contracts/common/VaultRecoverable.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract VaultRecoverable is IVaultRecoverable, EtherTokenConstant, IsContract {
using SafeERC20 for ERC20;
string private constant ERROR_DISALLOWED = "RECOVER_DISALLOWED";
string private constant ERROR_VAULT_NOT_CONTRACT = "RECOVER_VAULT_NOT_CONTRACT";
string private constant ERROR_TOKEN_TRANSFER_FAILED = "RECOVER_TOKEN_TRANSFER_FAILED";
/**
* @notice Send funds to recovery Vault. This contract should never receive funds,
* but in case it does, this function allows one to recover them.
* @param _token Token balance to be sent to recovery vault.
*/
function transferToVault(address _token) external {
require(allowRecoverability(_token), ERROR_DISALLOWED);
address vault = getRecoveryVault();
require(isContract(vault), ERROR_VAULT_NOT_CONTRACT);
uint256 balance;
if (_token == ETH) {
balance = address(this).balance;
vault.transfer(balance);
} else {
ERC20 token = ERC20(_token);
balance = token.staticBalanceOf(this);
require(token.safeTransfer(vault, balance), ERROR_TOKEN_TRANSFER_FAILED);
}
emit RecoverToVault(vault, _token, balance);
}
/**
* @dev By default deriving from AragonApp makes it recoverable
* @param token Token address that would be recovered
* @return bool whether the app allows the recovery
*/
function allowRecoverability(address token) public view returns (bool) {
return true;
}
// Cast non-implemented interface to be public so we can use it internally
function getRecoveryVault() public view returns (address);
}
// File: @aragon/os/contracts/evmscript/IEVMScriptExecutor.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
interface IEVMScriptExecutor {
function execScript(bytes script, bytes input, address[] blacklist) external returns (bytes);
function executorType() external pure returns (bytes32);
}
// File: @aragon/os/contracts/evmscript/IEVMScriptRegistry.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract EVMScriptRegistryConstants {
/* Hardcoded constants to save gas
bytes32 internal constant EVMSCRIPT_REGISTRY_APP_ID = apmNamehash("evmreg");
*/
bytes32 internal constant EVMSCRIPT_REGISTRY_APP_ID = 0xddbcfd564f642ab5627cf68b9b7d374fb4f8a36e941a75d89c87998cef03bd61;
}
interface IEVMScriptRegistry {
function addScriptExecutor(IEVMScriptExecutor executor) external returns (uint id);
function disableScriptExecutor(uint256 executorId) external;
// TODO: this should be external
// See https://github.com/ethereum/solidity/issues/4832
function getScriptExecutor(bytes script) public view returns (IEVMScriptExecutor);
}
// File: @aragon/os/contracts/kernel/KernelConstants.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract KernelAppIds {
/* Hardcoded constants to save gas
bytes32 internal constant KERNEL_CORE_APP_ID = apmNamehash("kernel");
bytes32 internal constant KERNEL_DEFAULT_ACL_APP_ID = apmNamehash("acl");
bytes32 internal constant KERNEL_DEFAULT_VAULT_APP_ID = apmNamehash("vault");
*/
bytes32 internal constant KERNEL_CORE_APP_ID = 0x3b4bf6bf3ad5000ecf0f989d5befde585c6860fea3e574a4fab4c49d1c177d9c;
bytes32 internal constant KERNEL_DEFAULT_ACL_APP_ID = 0xe3262375f45a6e2026b7e7b18c2b807434f2508fe1a2a3dfb493c7df8f4aad6a;
bytes32 internal constant KERNEL_DEFAULT_VAULT_APP_ID = 0x7e852e0fcfce6551c13800f1e7476f982525c2b5277ba14b24339c68416336d1;
}
contract KernelNamespaceConstants {
/* Hardcoded constants to save gas
bytes32 internal constant KERNEL_CORE_NAMESPACE = keccak256("core");
bytes32 internal constant KERNEL_APP_BASES_NAMESPACE = keccak256("base");
bytes32 internal constant KERNEL_APP_ADDR_NAMESPACE = keccak256("app");
*/
bytes32 internal constant KERNEL_CORE_NAMESPACE = 0xc681a85306374a5ab27f0bbc385296a54bcd314a1948b6cf61c4ea1bc44bb9f8;
bytes32 internal constant KERNEL_APP_BASES_NAMESPACE = 0xf1f3eb40f5bc1ad1344716ced8b8a0431d840b5783aea1fd01786bc26f35ac0f;
bytes32 internal constant KERNEL_APP_ADDR_NAMESPACE = 0xd6f028ca0e8edb4a8c9757ca4fdccab25fa1e0317da1188108f7d2dee14902fb;
}
// File: @aragon/os/contracts/evmscript/EVMScriptRunner.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
contract EVMScriptRunner is AppStorage, Initializable, EVMScriptRegistryConstants, KernelNamespaceConstants {
string private constant ERROR_EXECUTOR_UNAVAILABLE = "EVMRUN_EXECUTOR_UNAVAILABLE";
string private constant ERROR_PROTECTED_STATE_MODIFIED = "EVMRUN_PROTECTED_STATE_MODIFIED";
/* This is manually crafted in assembly
string private constant ERROR_EXECUTOR_INVALID_RETURN = "EVMRUN_EXECUTOR_INVALID_RETURN";
*/
event ScriptResult(address indexed executor, bytes script, bytes input, bytes returnData);
function getEVMScriptExecutor(bytes _script) public view returns (IEVMScriptExecutor) {
return IEVMScriptExecutor(getEVMScriptRegistry().getScriptExecutor(_script));
}
function getEVMScriptRegistry() public view returns (IEVMScriptRegistry) {
address registryAddr = kernel().getApp(KERNEL_APP_ADDR_NAMESPACE, EVMSCRIPT_REGISTRY_APP_ID);
return IEVMScriptRegistry(registryAddr);
}
function runScript(bytes _script, bytes _input, address[] _blacklist)
internal
isInitialized
protectState
returns (bytes)
{
IEVMScriptExecutor executor = getEVMScriptExecutor(_script);
require(address(executor) != address(0), ERROR_EXECUTOR_UNAVAILABLE);
bytes4 sig = executor.execScript.selector;
bytes memory data = abi.encodeWithSelector(sig, _script, _input, _blacklist);
bytes memory output;
assembly {
let success := delegatecall(
gas, // forward all gas
executor, // address
add(data, 0x20), // calldata start
mload(data), // calldata length
0, // don't write output (we'll handle this ourselves)
0 // don't write output
)
output := mload(0x40) // free mem ptr get
switch success
case 0 {
// If the call errored, forward its full error data
returndatacopy(output, 0, returndatasize)
revert(output, returndatasize)
}
default {
switch gt(returndatasize, 0x3f)
case 0 {
// Need at least 0x40 bytes returned for properly ABI-encoded bytes values,
// revert with "EVMRUN_EXECUTOR_INVALID_RETURN"
// See remix: doing a `revert("EVMRUN_EXECUTOR_INVALID_RETURN")` always results in
// this memory layout
mstore(output, 0x08c379a000000000000000000000000000000000000000000000000000000000) // error identifier
mstore(add(output, 0x04), 0x0000000000000000000000000000000000000000000000000000000000000020) // starting offset
mstore(add(output, 0x24), 0x000000000000000000000000000000000000000000000000000000000000001e) // reason length
mstore(add(output, 0x44), 0x45564d52554e5f4558454355544f525f494e56414c49445f52455455524e0000) // reason
revert(output, 100) // 100 = 4 + 3 * 32 (error identifier + 3 words for the ABI encoded error)
}
default {
// Copy result
//
// Needs to perform an ABI decode for the expected `bytes` return type of
// `executor.execScript()` as solidity will automatically ABI encode the returned bytes as:
// [ position of the first dynamic length return value = 0x20 (32 bytes) ]
// [ output length (32 bytes) ]
// [ output content (N bytes) ]
//
// Perform the ABI decode by ignoring the first 32 bytes of the return data
let copysize := sub(returndatasize, 0x20)
returndatacopy(output, 0x20, copysize)
mstore(0x40, add(output, copysize)) // free mem ptr set
}
}
}
emit ScriptResult(address(executor), _script, _input, output);
return output;
}
modifier protectState {
address preKernel = address(kernel());
bytes32 preAppId = appId();
_; // exec
require(address(kernel()) == preKernel, ERROR_PROTECTED_STATE_MODIFIED);
require(appId() == preAppId, ERROR_PROTECTED_STATE_MODIFIED);
}
}
// File: @aragon/os/contracts/apps/AragonApp.sol
/*
* SPDX-License-Identitifer: MIT
*/
pragma solidity ^0.4.24;
// Contracts inheriting from AragonApp are, by default, immediately petrified upon deployment so
// that they can never be initialized.
// Unless overriden, this behaviour enforces those contracts to be usable only behind an AppProxy.
// ReentrancyGuard, EVMScriptRunner, and ACLSyntaxSugar are not directly used by this contract, but
// are included so that they are automatically usable by subclassing contracts
contract AragonApp is AppStorage, Autopetrified, VaultRecoverable, ReentrancyGuard, EVMScriptRunner, ACLSyntaxSugar {
string private constant ERROR_AUTH_FAILED = "APP_AUTH_FAILED";
modifier auth(bytes32 _role) {
require(canPerform(msg.sender, _role, new uint256[](0)), ERROR_AUTH_FAILED);
_;
}
modifier authP(bytes32 _role, uint256[] _params) {
require(canPerform(msg.sender, _role, _params), ERROR_AUTH_FAILED);
_;
}
/**
* @dev Check whether an action can be performed by a sender for a particular role on this app
* @param _sender Sender of the call
* @param _role Role on this app
* @param _params Permission params for the role
* @return Boolean indicating whether the sender has the permissions to perform the action.
* Always returns false if the app hasn't been initialized yet.
*/
function canPerform(address _sender, bytes32 _role, uint256[] _params) public view returns (bool) {
if (!hasInitialized()) {
return false;
}
IKernel linkedKernel = kernel();
if (address(linkedKernel) == address(0)) {
return false;
}
return linkedKernel.hasPermission(
_sender,
address(this),
_role,
ConversionHelpers.dangerouslyCastUintArrayToBytes(_params)
);
}
/**
* @dev Get the recovery vault for the app
* @return Recovery vault address for the app
*/
function getRecoveryVault() public view returns (address) {
// Funds recovery via a vault is only available when used with a kernel
return kernel().getRecoveryVault(); // if kernel is not set, it will revert
}
}
// File: @aragon/os/contracts/lib/math/SafeMath.sol
// See https://github.com/OpenZeppelin/openzeppelin-solidity/blob/d51e38758e1d985661534534d5c61e27bece5042/contracts/math/SafeMath.sol
// Adapted to use pragma ^0.4.24 and satisfy our linter rules
pragma solidity ^0.4.24;
/**
* @title SafeMath
* @dev Math operations with safety checks that revert on error
*/
library SafeMath {
string private constant ERROR_ADD_OVERFLOW = "MATH_ADD_OVERFLOW";
string private constant ERROR_SUB_UNDERFLOW = "MATH_SUB_UNDERFLOW";
string private constant ERROR_MUL_OVERFLOW = "MATH_MUL_OVERFLOW";
string private constant ERROR_DIV_ZERO = "MATH_DIV_ZERO";
/**
* @dev Multiplies two numbers, reverts on overflow.
*/
function mul(uint256 _a, uint256 _b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (_a == 0) {
return 0;
}
uint256 c = _a * _b;
require(c / _a == _b, ERROR_MUL_OVERFLOW);
return c;
}
/**
* @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
*/
function div(uint256 _a, uint256 _b) internal pure returns (uint256) {
require(_b > 0, ERROR_DIV_ZERO); // Solidity only automatically asserts when dividing by 0
uint256 c = _a / _b;
// assert(_a == _b * c + _a % _b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 _a, uint256 _b) internal pure returns (uint256) {
require(_b <= _a, ERROR_SUB_UNDERFLOW);
uint256 c = _a - _b;
return c;
}
/**
* @dev Adds two numbers, reverts on overflow.
*/
function add(uint256 _a, uint256 _b) internal pure returns (uint256) {
uint256 c = _a + _b;
require(c >= _a, ERROR_ADD_OVERFLOW);
return c;
}
/**
* @dev Divides two numbers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0, ERROR_DIV_ZERO);
return a % b;
}
}
// File: @aragon/os/contracts/lib/math/SafeMath64.sol
// See https://github.com/OpenZeppelin/openzeppelin-solidity/blob/d51e38758e1d985661534534d5c61e27bece5042/contracts/math/SafeMath.sol
// Adapted for uint64, pragma ^0.4.24, and satisfying our linter rules
// Also optimized the mul() implementation, see https://github.com/aragon/aragonOS/pull/417
pragma solidity ^0.4.24;
/**
* @title SafeMath64
* @dev Math operations for uint64 with safety checks that revert on error
*/
library SafeMath64 {
string private constant ERROR_ADD_OVERFLOW = "MATH64_ADD_OVERFLOW";
string private constant ERROR_SUB_UNDERFLOW = "MATH64_SUB_UNDERFLOW";
string private constant ERROR_MUL_OVERFLOW = "MATH64_MUL_OVERFLOW";
string private constant ERROR_DIV_ZERO = "MATH64_DIV_ZERO";
/**
* @dev Multiplies two numbers, reverts on overflow.
*/
function mul(uint64 _a, uint64 _b) internal pure returns (uint64) {
uint256 c = uint256(_a) * uint256(_b);
require(c < 0x010000000000000000, ERROR_MUL_OVERFLOW); // 2**64 (less gas this way)
return uint64(c);
}
/**
* @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
*/
function div(uint64 _a, uint64 _b) internal pure returns (uint64) {
require(_b > 0, ERROR_DIV_ZERO); // Solidity only automatically asserts when dividing by 0
uint64 c = _a / _b;
// assert(_a == _b * c + _a % _b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint64 _a, uint64 _b) internal pure returns (uint64) {
require(_b <= _a, ERROR_SUB_UNDERFLOW);
uint64 c = _a - _b;
return c;
}
/**
* @dev Adds two numbers, reverts on overflow.
*/
function add(uint64 _a, uint64 _b) internal pure returns (uint64) {
uint64 c = _a + _b;
require(c >= _a, ERROR_ADD_OVERFLOW);
return c;
}
/**
* @dev Divides two numbers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint64 a, uint64 b) internal pure returns (uint64) {
require(b != 0, ERROR_DIV_ZERO);
return a % b;
}
}
// File: @aragon/os/contracts/common/DepositableStorage.sol
pragma solidity 0.4.24;
contract DepositableStorage {
using UnstructuredStorage for bytes32;
// keccak256("aragonOS.depositableStorage.depositable")
bytes32 internal constant DEPOSITABLE_POSITION = 0x665fd576fbbe6f247aff98f5c94a561e3f71ec2d3c988d56f12d342396c50cea;
function isDepositable() public view returns (bool) {
return DEPOSITABLE_POSITION.getStorageBool();
}
function setDepositable(bool _depositable) internal {
DEPOSITABLE_POSITION.setStorageBool(_depositable);
}
}
// File: @aragon/apps-vault/contracts/Vault.sol
pragma solidity 0.4.24;
contract Vault is EtherTokenConstant, AragonApp, DepositableStorage {
using SafeERC20 for ERC20;
bytes32 public constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE");
string private constant ERROR_DATA_NON_ZERO = "VAULT_DATA_NON_ZERO";
string private constant ERROR_NOT_DEPOSITABLE = "VAULT_NOT_DEPOSITABLE";
string private constant ERROR_DEPOSIT_VALUE_ZERO = "VAULT_DEPOSIT_VALUE_ZERO";
string private constant ERROR_TRANSFER_VALUE_ZERO = "VAULT_TRANSFER_VALUE_ZERO";
string private constant ERROR_SEND_REVERTED = "VAULT_SEND_REVERTED";
string private constant ERROR_VALUE_MISMATCH = "VAULT_VALUE_MISMATCH";
string private constant ERROR_TOKEN_TRANSFER_FROM_REVERTED = "VAULT_TOKEN_TRANSFER_FROM_REVERT";
string private constant ERROR_TOKEN_TRANSFER_REVERTED = "VAULT_TOKEN_TRANSFER_REVERTED";
event VaultTransfer(address indexed token, address indexed to, uint256 amount);
event VaultDeposit(address indexed token, address indexed sender, uint256 amount);
/**
* @dev On a normal send() or transfer() this fallback is never executed as it will be
* intercepted by the Proxy (see aragonOS#281)
*/
function () external payable isInitialized {
require(msg.data.length == 0, ERROR_DATA_NON_ZERO);
_deposit(ETH, msg.value);
}
/**
* @notice Initialize Vault app
* @dev As an AragonApp it needs to be initialized in order for roles (`auth` and `authP`) to work
*/
function initialize() external onlyInit {
initialized();
setDepositable(true);
}
/**
* @notice Deposit `_value` `_token` to the vault
* @param _token Address of the token being transferred
* @param _value Amount of tokens being transferred
*/
function deposit(address _token, uint256 _value) external payable isInitialized {
_deposit(_token, _value);
}
/**
* @notice Transfer `_value` `_token` from the Vault to `_to`
* @param _token Address of the token being transferred
* @param _to Address of the recipient of tokens
* @param _value Amount of tokens being transferred
*/
/* solium-disable-next-line function-order */
function transfer(address _token, address _to, uint256 _value)
external
authP(TRANSFER_ROLE, arr(_token, _to, _value))
{
require(_value > 0, ERROR_TRANSFER_VALUE_ZERO);
if (_token == ETH) {
require(_to.send(_value), ERROR_SEND_REVERTED);
} else {
require(ERC20(_token).safeTransfer(_to, _value), ERROR_TOKEN_TRANSFER_REVERTED);
}
emit VaultTransfer(_token, _to, _value);
}
function balance(address _token) public view returns (uint256) {
if (_token == ETH) {
return address(this).balance;
} else {
return ERC20(_token).staticBalanceOf(address(this));
}
}
/**
* @dev Disable recovery escape hatch, as it could be used
* maliciously to transfer funds away from the vault
*/
function allowRecoverability(address) public view returns (bool) {
return false;
}
function _deposit(address _token, uint256 _value) internal {
require(isDepositable(), ERROR_NOT_DEPOSITABLE);
require(_value > 0, ERROR_DEPOSIT_VALUE_ZERO);
if (_token == ETH) {
// Deposit is implicit in this case
require(msg.value == _value, ERROR_VALUE_MISMATCH);
} else {
require(
ERC20(_token).safeTransferFrom(msg.sender, address(this), _value),
ERROR_TOKEN_TRANSFER_FROM_REVERTED
);
}
emit VaultDeposit(_token, msg.sender, _value);
}
}
// File: contracts/Finance.sol
/*
* SPDX-License-Identitifer: GPL-3.0-or-later
*/
pragma solidity 0.4.24;
contract Finance is EtherTokenConstant, IsContract, AragonApp {
using SafeMath for uint256;
using SafeMath64 for uint64;
using SafeERC20 for ERC20;
bytes32 public constant CREATE_PAYMENTS_ROLE = keccak256("CREATE_PAYMENTS_ROLE");
bytes32 public constant CHANGE_PERIOD_ROLE = keccak256("CHANGE_PERIOD_ROLE");
bytes32 public constant CHANGE_BUDGETS_ROLE = keccak256("CHANGE_BUDGETS_ROLE");
bytes32 public constant EXECUTE_PAYMENTS_ROLE = keccak256("EXECUTE_PAYMENTS_ROLE");
bytes32 public constant MANAGE_PAYMENTS_ROLE = keccak256("MANAGE_PAYMENTS_ROLE");
uint256 internal constant NO_SCHEDULED_PAYMENT = 0;
uint256 internal constant NO_TRANSACTION = 0;
uint256 internal constant MAX_SCHEDULED_PAYMENTS_PER_TX = 20;
uint256 internal constant MAX_UINT256 = uint256(-1);
uint64 internal constant MAX_UINT64 = uint64(-1);
uint64 internal constant MINIMUM_PERIOD = uint64(1 days);
string private constant ERROR_COMPLETE_TRANSITION = "FINANCE_COMPLETE_TRANSITION";
string private constant ERROR_NO_SCHEDULED_PAYMENT = "FINANCE_NO_SCHEDULED_PAYMENT";
string private constant ERROR_NO_TRANSACTION = "FINANCE_NO_TRANSACTION";
string private constant ERROR_NO_PERIOD = "FINANCE_NO_PERIOD";
string private constant ERROR_VAULT_NOT_CONTRACT = "FINANCE_VAULT_NOT_CONTRACT";
string private constant ERROR_SET_PERIOD_TOO_SHORT = "FINANCE_SET_PERIOD_TOO_SHORT";
string private constant ERROR_NEW_PAYMENT_AMOUNT_ZERO = "FINANCE_NEW_PAYMENT_AMOUNT_ZERO";
string private constant ERROR_NEW_PAYMENT_INTERVAL_ZERO = "FINANCE_NEW_PAYMENT_INTRVL_ZERO";
string private constant ERROR_NEW_PAYMENT_EXECS_ZERO = "FINANCE_NEW_PAYMENT_EXECS_ZERO";
string private constant ERROR_NEW_PAYMENT_IMMEDIATE = "FINANCE_NEW_PAYMENT_IMMEDIATE";
string private constant ERROR_RECOVER_AMOUNT_ZERO = "FINANCE_RECOVER_AMOUNT_ZERO";
string private constant ERROR_DEPOSIT_AMOUNT_ZERO = "FINANCE_DEPOSIT_AMOUNT_ZERO";
string private constant ERROR_ETH_VALUE_MISMATCH = "FINANCE_ETH_VALUE_MISMATCH";
string private constant ERROR_BUDGET = "FINANCE_BUDGET";
string private constant ERROR_EXECUTE_PAYMENT_NUM = "FINANCE_EXECUTE_PAYMENT_NUM";
string private constant ERROR_EXECUTE_PAYMENT_TIME = "FINANCE_EXECUTE_PAYMENT_TIME";
string private constant ERROR_PAYMENT_RECEIVER = "FINANCE_PAYMENT_RECEIVER";
string private constant ERROR_TOKEN_TRANSFER_FROM_REVERTED = "FINANCE_TKN_TRANSFER_FROM_REVERT";
string private constant ERROR_TOKEN_APPROVE_FAILED = "FINANCE_TKN_APPROVE_FAILED";
string private constant ERROR_PAYMENT_INACTIVE = "FINANCE_PAYMENT_INACTIVE";
string private constant ERROR_REMAINING_BUDGET = "FINANCE_REMAINING_BUDGET";
// Order optimized for storage
struct ScheduledPayment {
address token;
address receiver;
address createdBy;
bool inactive;
uint256 amount;
uint64 initialPaymentTime;
uint64 interval;
uint64 maxExecutions;
uint64 executions;
}
// Order optimized for storage
struct Transaction {
address token;
address entity;
bool isIncoming;
uint256 amount;
uint256 paymentId;
uint64 paymentExecutionNumber;
uint64 date;
uint64 periodId;
}
struct TokenStatement {
uint256 expenses;
uint256 income;
}
struct Period {
uint64 startTime;
uint64 endTime;
uint256 firstTransactionId;
uint256 lastTransactionId;
mapping (address => TokenStatement) tokenStatement;
}
struct Settings {
uint64 periodDuration;
mapping (address => uint256) budgets;
mapping (address => bool) hasBudget;
}
Vault public vault;
Settings internal settings;
// We are mimicing arrays, we use mappings instead to make app upgrade more graceful
mapping (uint256 => ScheduledPayment) internal scheduledPayments;
// Payments start at index 1, to allow us to use scheduledPayments[0] for transactions that are not
// linked to a scheduled payment
uint256 public paymentsNextIndex;
mapping (uint256 => Transaction) internal transactions;
uint256 public transactionsNextIndex;
mapping (uint64 => Period) internal periods;
uint64 public periodsLength;
event NewPeriod(uint64 indexed periodId, uint64 periodStarts, uint64 periodEnds);
event SetBudget(address indexed token, uint256 amount, bool hasBudget);
event NewPayment(uint256 indexed paymentId, address indexed recipient, uint64 maxExecutions, string reference);
event NewTransaction(uint256 indexed transactionId, bool incoming, address indexed entity, uint256 amount, string reference);
event ChangePaymentState(uint256 indexed paymentId, bool active);
event ChangePeriodDuration(uint64 newDuration);
event PaymentFailure(uint256 paymentId);
// Modifier used by all methods that impact accounting to make sure accounting period
// is changed before the operation if needed
// NOTE: its use **MUST** be accompanied by an initialization check
modifier transitionsPeriod {
bool completeTransition = _tryTransitionAccountingPeriod(getMaxPeriodTransitions());
require(completeTransition, ERROR_COMPLETE_TRANSITION);
_;
}
modifier scheduledPaymentExists(uint256 _paymentId) {
require(_paymentId > 0 && _paymentId < paymentsNextIndex, ERROR_NO_SCHEDULED_PAYMENT);
_;
}
modifier transactionExists(uint256 _transactionId) {
require(_transactionId > 0 && _transactionId < transactionsNextIndex, ERROR_NO_TRANSACTION);
_;
}
modifier periodExists(uint64 _periodId) {
require(_periodId < periodsLength, ERROR_NO_PERIOD);
_;
}
/**
* @notice Deposit ETH to the Vault, to avoid locking them in this Finance app forever
* @dev Send ETH to Vault. Send all the available balance.
*/
function () external payable isInitialized transitionsPeriod {
require(msg.value > 0, ERROR_DEPOSIT_AMOUNT_ZERO);
_deposit(
ETH,
msg.value,
"Ether transfer to Finance app",
msg.sender,
true
);
}
/**
* @notice Initialize Finance app for Vault at `_vault` with period length of `@transformTime(_periodDuration)`
* @param _vault Address of the vault Finance will rely on (non changeable)
* @param _periodDuration Duration in seconds of each period
*/
function initialize(Vault _vault, uint64 _periodDuration) external onlyInit {
initialized();
require(isContract(_vault), ERROR_VAULT_NOT_CONTRACT);
vault = _vault;
require(_periodDuration >= MINIMUM_PERIOD, ERROR_SET_PERIOD_TOO_SHORT);
settings.periodDuration = _periodDuration;
// Reserve the first scheduled payment index as an unused index for transactions not linked
// to a scheduled payment
scheduledPayments[0].inactive = true;
paymentsNextIndex = 1;
// Reserve the first transaction index as an unused index for periods with no transactions
transactionsNextIndex = 1;
// Start the first period
_newPeriod(getTimestamp64());
}
/**
* @notice Deposit `@tokenAmount(_token, _amount)`
* @dev Deposit for approved ERC20 tokens or ETH
* @param _token Address of deposited token
* @param _amount Amount of tokens sent
* @param _reference Reason for payment
*/
function deposit(address _token, uint256 _amount, string _reference) external payable isInitialized transitionsPeriod {
require(_amount > 0, ERROR_DEPOSIT_AMOUNT_ZERO);
if (_token == ETH) {
// Ensure that the ETH sent with the transaction equals the amount in the deposit
require(msg.value == _amount, ERROR_ETH_VALUE_MISMATCH);
}
_deposit(
_token,
_amount,
_reference,
msg.sender,
true
);
}
/**
* @notice Create a new payment of `@tokenAmount(_token, _amount)` to `_receiver` for '`_reference`'
* @dev Note that this function is protected by the `CREATE_PAYMENTS_ROLE` but uses `MAX_UINT256`
* as its interval auth parameter (as a sentinel value for "never repeating").
* While this protects against most cases (you typically want to set a baseline requirement
* for interval time), it does mean users will have to explicitly check for this case when
* granting a permission that includes a upperbound requirement on the interval time.
* @param _token Address of token for payment
* @param _receiver Address that will receive payment
* @param _amount Tokens that are paid every time the payment is due
* @param _reference String detailing payment reason
*/
function newImmediatePayment(address _token, address _receiver, uint256 _amount, string _reference)
external
// Use MAX_UINT256 as the interval parameter, as this payment will never repeat
// Payment time parameter is left as the last param as it was added later
authP(CREATE_PAYMENTS_ROLE, _arr(_token, _receiver, _amount, MAX_UINT256, uint256(1), getTimestamp()))
transitionsPeriod
{
require(_amount > 0, ERROR_NEW_PAYMENT_AMOUNT_ZERO);
_makePaymentTransaction(
_token,
_receiver,
_amount,
NO_SCHEDULED_PAYMENT, // unrelated to any payment id; it isn't created
0, // also unrelated to any payment executions
_reference
);
}
/**
* @notice Create a new payment of `@tokenAmount(_token, _amount)` to `_receiver` for `_reference`, executing `_maxExecutions` times at intervals of `@transformTime(_interval)`
* @dev See `newImmediatePayment()` for limitations on how the interval auth parameter can be used
* @param _token Address of token for payment
* @param _receiver Address that will receive payment
* @param _amount Tokens that are paid every time the payment is due
* @param _initialPaymentTime Timestamp for when the first payment is done
* @param _interval Number of seconds that need to pass between payment transactions
* @param _maxExecutions Maximum instances a payment can be executed
* @param _reference String detailing payment reason
*/
function newScheduledPayment(
address _token,
address _receiver,
uint256 _amount,
uint64 _initialPaymentTime,
uint64 _interval,
uint64 _maxExecutions,
string _reference
)
external
// Payment time parameter is left as the last param as it was added later
authP(CREATE_PAYMENTS_ROLE, _arr(_token, _receiver, _amount, uint256(_interval), uint256(_maxExecutions), uint256(_initialPaymentTime)))
transitionsPeriod
returns (uint256 paymentId)
{
require(_amount > 0, ERROR_NEW_PAYMENT_AMOUNT_ZERO);
require(_interval > 0, ERROR_NEW_PAYMENT_INTERVAL_ZERO);
require(_maxExecutions > 0, ERROR_NEW_PAYMENT_EXECS_ZERO);
// Token budget must not be set at all or allow at least one instance of this payment each period
require(!settings.hasBudget[_token] || settings.budgets[_token] >= _amount, ERROR_BUDGET);
// Don't allow creating single payments that are immediately executable, use `newImmediatePayment()` instead
if (_maxExecutions == 1) {
require(_initialPaymentTime > getTimestamp64(), ERROR_NEW_PAYMENT_IMMEDIATE);
}
paymentId = paymentsNextIndex++;
emit NewPayment(paymentId, _receiver, _maxExecutions, _reference);
ScheduledPayment storage payment = scheduledPayments[paymentId];
payment.token = _token;
payment.receiver = _receiver;
payment.amount = _amount;
payment.initialPaymentTime = _initialPaymentTime;
payment.interval = _interval;
payment.maxExecutions = _maxExecutions;
payment.createdBy = msg.sender;
// We skip checking how many times the new payment was executed to allow creating new
// scheduled payments before having enough vault balance
_executePayment(paymentId);
}
/**
* @notice Change period duration to `@transformTime(_periodDuration)`, effective for next accounting period
* @param _periodDuration Duration in seconds for accounting periods
*/
function setPeriodDuration(uint64 _periodDuration)
external
authP(CHANGE_PERIOD_ROLE, arr(uint256(_periodDuration), uint256(settings.periodDuration)))
transitionsPeriod
{
require(_periodDuration >= MINIMUM_PERIOD, ERROR_SET_PERIOD_TOO_SHORT);
settings.periodDuration = _periodDuration;
emit ChangePeriodDuration(_periodDuration);
}
/**
* @notice Set budget for `_token.symbol(): string` to `@tokenAmount(_token, _amount, false)`, effective immediately
* @param _token Address for token
* @param _amount New budget amount
*/
function setBudget(
address _token,
uint256 _amount
)
external
authP(CHANGE_BUDGETS_ROLE, arr(_token, _amount, settings.budgets[_token], uint256(settings.hasBudget[_token] ? 1 : 0)))
transitionsPeriod
{
settings.budgets[_token] = _amount;
if (!settings.hasBudget[_token]) {
settings.hasBudget[_token] = true;
}
emit SetBudget(_token, _amount, true);
}
/**
* @notice Remove spending limit for `_token.symbol(): string`, effective immediately
* @param _token Address for token
*/
function removeBudget(address _token)
external
authP(CHANGE_BUDGETS_ROLE, arr(_token, uint256(0), settings.budgets[_token], uint256(settings.hasBudget[_token] ? 1 : 0)))
transitionsPeriod
{
settings.budgets[_token] = 0;
settings.hasBudget[_token] = false;
emit SetBudget(_token, 0, false);
}
/**
* @notice Execute pending payment #`_paymentId`
* @dev Executes any payment (requires role)
* @param _paymentId Identifier for payment
*/
function executePayment(uint256 _paymentId)
external
authP(EXECUTE_PAYMENTS_ROLE, arr(_paymentId, scheduledPayments[_paymentId].amount))
scheduledPaymentExists(_paymentId)
transitionsPeriod
{
_executePaymentAtLeastOnce(_paymentId);
}
/**
* @notice Execute pending payment #`_paymentId`
* @dev Always allow receiver of a payment to trigger execution
* Initialization check is implicitly provided by `scheduledPaymentExists()` as new
* scheduled payments can only be created via `newScheduledPayment(),` which requires initialization
* @param _paymentId Identifier for payment
*/
function receiverExecutePayment(uint256 _paymentId) external scheduledPaymentExists(_paymentId) transitionsPeriod {
require(scheduledPayments[_paymentId].receiver == msg.sender, ERROR_PAYMENT_RECEIVER);
_executePaymentAtLeastOnce(_paymentId);
}
/**
* @notice `_active ? 'Activate' : 'Disable'` payment #`_paymentId`
* @dev Note that we do not require this action to transition periods, as it doesn't directly
* impact any accounting periods.
* Not having to transition periods also makes disabling payments easier to prevent funds
* from being pulled out in the event of a breach.
* @param _paymentId Identifier for payment
* @param _active Whether it will be active or inactive
*/
function setPaymentStatus(uint256 _paymentId, bool _active)
external
authP(MANAGE_PAYMENTS_ROLE, arr(_paymentId, uint256(_active ? 1 : 0)))
scheduledPaymentExists(_paymentId)
{
scheduledPayments[_paymentId].inactive = !_active;
emit ChangePaymentState(_paymentId, _active);
}
/**
* @notice Send tokens held in this contract to the Vault
* @dev Allows making a simple payment from this contract to the Vault, to avoid locked tokens.
* This contract should never receive tokens with a simple transfer call, but in case it
* happens, this function allows for their recovery.
* @param _token Token whose balance is going to be transferred.
*/
function recoverToVault(address _token) external isInitialized transitionsPeriod {
uint256 amount = _token == ETH ? address(this).balance : ERC20(_token).staticBalanceOf(address(this));
require(amount > 0, ERROR_RECOVER_AMOUNT_ZERO);
_deposit(
_token,
amount,
"Recover to Vault",
address(this),
false
);
}
/**
* @notice Transition accounting period if needed
* @dev Transitions accounting periods if needed. For preventing OOG attacks, a maxTransitions
* param is provided. If more than the specified number of periods need to be transitioned,
* it will return false.
* @param _maxTransitions Maximum periods that can be transitioned
* @return success Boolean indicating whether the accounting period is the correct one (if false,
* maxTransitions was surpased and another call is needed)
*/
function tryTransitionAccountingPeriod(uint64 _maxTransitions) external isInitialized returns (bool success) {
return _tryTransitionAccountingPeriod(_maxTransitions);
}
// Getter fns
/**
* @dev Disable recovery escape hatch if the app has been initialized, as it could be used
* maliciously to transfer funds in the Finance app to another Vault
* finance#recoverToVault() should be used to recover funds to the Finance's vault
*/
function allowRecoverability(address) public view returns (bool) {
return !hasInitialized();
}
function getPayment(uint256 _paymentId)
public
view
scheduledPaymentExists(_paymentId)
returns (
address token,
address receiver,
uint256 amount,
uint64 initialPaymentTime,
uint64 interval,
uint64 maxExecutions,
bool inactive,
uint64 executions,
address createdBy
)
{
ScheduledPayment storage payment = scheduledPayments[_paymentId];
token = payment.token;
receiver = payment.receiver;
amount = payment.amount;
initialPaymentTime = payment.initialPaymentTime;
interval = payment.interval;
maxExecutions = payment.maxExecutions;
executions = payment.executions;
inactive = payment.inactive;
createdBy = payment.createdBy;
}
function getTransaction(uint256 _transactionId)
public
view
transactionExists(_transactionId)
returns (
uint64 periodId,
uint256 amount,
uint256 paymentId,
uint64 paymentExecutionNumber,
address token,
address entity,
bool isIncoming,
uint64 date
)
{
Transaction storage transaction = transactions[_transactionId];
token = transaction.token;
entity = transaction.entity;
isIncoming = transaction.isIncoming;
date = transaction.date;
periodId = transaction.periodId;
amount = transaction.amount;
paymentId = transaction.paymentId;
paymentExecutionNumber = transaction.paymentExecutionNumber;
}
function getPeriod(uint64 _periodId)
public
view
periodExists(_periodId)
returns (
bool isCurrent,
uint64 startTime,
uint64 endTime,
uint256 firstTransactionId,
uint256 lastTransactionId
)
{
Period storage period = periods[_periodId];
isCurrent = _currentPeriodId() == _periodId;
startTime = period.startTime;
endTime = period.endTime;
firstTransactionId = period.firstTransactionId;
lastTransactionId = period.lastTransactionId;
}
function getPeriodTokenStatement(uint64 _periodId, address _token)
public
view
periodExists(_periodId)
returns (uint256 expenses, uint256 income)
{
TokenStatement storage tokenStatement = periods[_periodId].tokenStatement[_token];
expenses = tokenStatement.expenses;
income = tokenStatement.income;
}
/**
* @dev We have to check for initialization as periods are only valid after initializing
*/
function currentPeriodId() public view isInitialized returns (uint64) {
return _currentPeriodId();
}
/**
* @dev We have to check for initialization as periods are only valid after initializing
*/
function getPeriodDuration() public view isInitialized returns (uint64) {
return settings.periodDuration;
}
/**
* @dev We have to check for initialization as budgets are only valid after initializing
*/
function getBudget(address _token) public view isInitialized returns (uint256 budget, bool hasBudget) {
budget = settings.budgets[_token];
hasBudget = settings.hasBudget[_token];
}
/**
* @dev We have to check for initialization as budgets are only valid after initializing
*/
function getRemainingBudget(address _token) public view isInitialized returns (uint256) {
return _getRemainingBudget(_token);
}
/**
* @dev We have to check for initialization as budgets are only valid after initializing
*/
function canMakePayment(address _token, uint256 _amount) public view isInitialized returns (bool) {
return _canMakePayment(_token, _amount);
}
/**
* @dev Initialization check is implicitly provided by `scheduledPaymentExists()` as new
* scheduled payments can only be created via `newScheduledPayment(),` which requires initialization
*/
function nextPaymentTime(uint256 _paymentId) public view scheduledPaymentExists(_paymentId) returns (uint64) {
return _nextPaymentTime(_paymentId);
}
// Internal fns
function _deposit(address _token, uint256 _amount, string _reference, address _sender, bool _isExternalDeposit) internal {
_recordIncomingTransaction(
_token,
_sender,
_amount,
_reference
);
if (_token == ETH) {
vault.deposit.value(_amount)(ETH, _amount);
} else {
// First, transfer the tokens to Finance if necessary
// External deposit will be false when the assets were already in the Finance app
// and just need to be transferred to the Vault
if (_isExternalDeposit) {
// This assumes the sender has approved the tokens for Finance
require(
ERC20(_token).safeTransferFrom(msg.sender, address(this), _amount),
ERROR_TOKEN_TRANSFER_FROM_REVERTED
);
}
// Approve the tokens for the Vault (it does the actual transferring)
require(ERC20(_token).safeApprove(vault, _amount), ERROR_TOKEN_APPROVE_FAILED);
// Finally, initiate the deposit
vault.deposit(_token, _amount);
}
}
function _executePayment(uint256 _paymentId) internal returns (uint256) {
ScheduledPayment storage payment = scheduledPayments[_paymentId];
require(!payment.inactive, ERROR_PAYMENT_INACTIVE);
uint64 paid = 0;
while (_nextPaymentTime(_paymentId) <= getTimestamp64() && paid < MAX_SCHEDULED_PAYMENTS_PER_TX) {
if (!_canMakePayment(payment.token, payment.amount)) {
emit PaymentFailure(_paymentId);
break;
}
// The while() predicate prevents these two from ever overflowing
payment.executions += 1;
paid += 1;
// We've already checked the remaining budget with `_canMakePayment()`
_unsafeMakePaymentTransaction(
payment.token,
payment.receiver,
payment.amount,
_paymentId,
payment.executions,
""
);
}
return paid;
}
function _executePaymentAtLeastOnce(uint256 _paymentId) internal {
uint256 paid = _executePayment(_paymentId);
if (paid == 0) {
if (_nextPaymentTime(_paymentId) <= getTimestamp64()) {
revert(ERROR_EXECUTE_PAYMENT_NUM);
} else {
revert(ERROR_EXECUTE_PAYMENT_TIME);
}
}
}
function _makePaymentTransaction(
address _token,
address _receiver,
uint256 _amount,
uint256 _paymentId,
uint64 _paymentExecutionNumber,
string _reference
)
internal
{
require(_getRemainingBudget(_token) >= _amount, ERROR_REMAINING_BUDGET);
_unsafeMakePaymentTransaction(_token, _receiver, _amount, _paymentId, _paymentExecutionNumber, _reference);
}
/**
* @dev Unsafe version of _makePaymentTransaction that assumes you have already checked the
* remaining budget
*/
function _unsafeMakePaymentTransaction(
address _token,
address _receiver,
uint256 _amount,
uint256 _paymentId,
uint64 _paymentExecutionNumber,
string _reference
)
internal
{
_recordTransaction(
false,
_token,
_receiver,
_amount,
_paymentId,
_paymentExecutionNumber,
_reference
);
vault.transfer(_token, _receiver, _amount);
}
function _newPeriod(uint64 _startTime) internal returns (Period storage) {
// There should be no way for this to overflow since each period is at least one day
uint64 newPeriodId = periodsLength++;
Period storage period = periods[newPeriodId];
period.startTime = _startTime;
// Be careful here to not overflow; if startTime + periodDuration overflows, we set endTime
// to MAX_UINT64 (let's assume that's the end of time for now).
uint64 endTime = _startTime + settings.periodDuration - 1;
if (endTime < _startTime) { // overflowed
endTime = MAX_UINT64;
}
period.endTime = endTime;
emit NewPeriod(newPeriodId, period.startTime, period.endTime);
return period;
}
function _recordIncomingTransaction(
address _token,
address _sender,
uint256 _amount,
string _reference
)
internal
{
_recordTransaction(
true, // incoming transaction
_token,
_sender,
_amount,
NO_SCHEDULED_PAYMENT, // unrelated to any existing payment
0, // and no payment executions
_reference
);
}
function _recordTransaction(
bool _incoming,
address _token,
address _entity,
uint256 _amount,
uint256 _paymentId,
uint64 _paymentExecutionNumber,
string _reference
)
internal
{
uint64 periodId = _currentPeriodId();
TokenStatement storage tokenStatement = periods[periodId].tokenStatement[_token];
if (_incoming) {
tokenStatement.income = tokenStatement.income.add(_amount);
} else {
tokenStatement.expenses = tokenStatement.expenses.add(_amount);
}
uint256 transactionId = transactionsNextIndex++;
Transaction storage transaction = transactions[transactionId];
transaction.token = _token;
transaction.entity = _entity;
transaction.isIncoming = _incoming;
transaction.amount = _amount;
transaction.paymentId = _paymentId;
transaction.paymentExecutionNumber = _paymentExecutionNumber;
transaction.date = getTimestamp64();
transaction.periodId = periodId;
Period storage period = periods[periodId];
if (period.firstTransactionId == NO_TRANSACTION) {
period.firstTransactionId = transactionId;
}
emit NewTransaction(transactionId, _incoming, _entity, _amount, _reference);
}
function _tryTransitionAccountingPeriod(uint64 _maxTransitions) internal returns (bool success) {
Period storage currentPeriod = periods[_currentPeriodId()];
uint64 timestamp = getTimestamp64();
// Transition periods if necessary
while (timestamp > currentPeriod.endTime) {
if (_maxTransitions == 0) {
// Required number of transitions is over allowed number, return false indicating
// it didn't fully transition
return false;
}
// We're already protected from underflowing above
_maxTransitions -= 1;
// If there were any transactions in period, record which was the last
// In case 0 transactions occured, first and last tx id will be 0
if (currentPeriod.firstTransactionId != NO_TRANSACTION) {
currentPeriod.lastTransactionId = transactionsNextIndex.sub(1);
}
// New period starts at end time + 1
currentPeriod = _newPeriod(currentPeriod.endTime.add(1));
}
return true;
}
function _canMakePayment(address _token, uint256 _amount) internal view returns (bool) {
return _getRemainingBudget(_token) >= _amount && vault.balance(_token) >= _amount;
}
function _currentPeriodId() internal view returns (uint64) {
// There is no way for this to overflow if protected by an initialization check
return periodsLength - 1;
}
function _getRemainingBudget(address _token) internal view returns (uint256) {
if (!settings.hasBudget[_token]) {
return MAX_UINT256;
}
uint256 budget = settings.budgets[_token];
uint256 spent = periods[_currentPeriodId()].tokenStatement[_token].expenses;
// A budget decrease can cause the spent amount to be greater than period budget
// If so, return 0 to not allow more spending during period
if (spent >= budget) {
return 0;
}
// We're already protected from the overflow above
return budget - spent;
}
function _nextPaymentTime(uint256 _paymentId) internal view returns (uint64) {
ScheduledPayment storage payment = scheduledPayments[_paymentId];
if (payment.executions >= payment.maxExecutions) {
return MAX_UINT64; // re-executes in some billions of years time... should not need to worry
}
// Split in multiple lines to circumvent linter warning
uint64 increase = payment.executions.mul(payment.interval);
uint64 nextPayment = payment.initialPaymentTime.add(increase);
return nextPayment;
}
// Syntax sugar
function _arr(address _a, address _b, uint256 _c, uint256 _d, uint256 _e, uint256 _f) internal pure returns (uint256[] r) {
r = new uint256[](6);
r[0] = uint256(_a);
r[1] = uint256(_b);
r[2] = _c;
r[3] = _d;
r[4] = _e;
r[5] = _f;
}
// Mocked fns (overrided during testing)
// Must be view for mocking purposes
function getMaxPeriodTransitions() internal view returns (uint64) { return MAX_UINT64; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment