├── .env.example ├── .gitattributes ├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── foundry.toml ├── script ├── LitePsmJobDeploy.s.sol ├── VestedRewardsDistributionJobDeploy.s.sol ├── VestedRewardsDistributionJobInit.s.sol ├── VestedRewardsDistributionJobSetDist.s.sol ├── input │ ├── 1 │ │ ├── README.md │ │ ├── template-lite-psm-job-deploy.json │ │ ├── template-vested-rewards-distribution-job-init.json │ │ └── template-vested-rewards-distribution-job-set-dist.json │ └── 314311 │ │ ├── README.md │ │ ├── template-vested-rewards-distribution-job-init.json │ │ └── template-vested-rewards-distribution-job-set-dist.json └── output │ ├── 1 │ └── README.md │ └── 314311 │ └── README.md ├── shell.nix ├── src ├── AutoLineJob.sol ├── ClipperMomJob.sol ├── D3MJob.sol ├── FlapJob.sol ├── LerpJob.sol ├── LiquidatorJob.sol ├── LitePsmJob.sol ├── NetworkPaymentAdapter.sol ├── OracleJob.sol ├── Sequencer.sol ├── VestedRewardsDistributionJob.sol ├── base │ └── TimedJob.sol ├── deployment │ ├── LitePsmJob │ │ ├── LitePsmJobDeploy.sol │ │ └── LitePsmJobInstance.sol │ └── VestedRewardsDistributionJob │ │ ├── VestedRewardsDistributionJobDeploy.sol │ │ ├── VestedRewardsDistributionJobDeploy.t.integration.sol │ │ ├── VestedRewardsDistributionJobInit.sol │ │ └── VestedRewardsDistributionJobInit.t.integration.sol ├── interfaces │ ├── IJob.sol │ └── INetworkTreasury.sol ├── tests │ ├── AutoLineJob-integration.t.sol │ ├── ClipperMomJob-integration.t.sol │ ├── D3MJob.t.sol │ ├── DssCronBase.t.sol │ ├── FlapJob-integration.t.sol │ ├── LerpJob-integration.t.sol │ ├── LiquidatorJob-integration.t.sol │ ├── LitePsmJob-integration.t.sol │ ├── NetworkPaymentAdapter.t.sol │ ├── OracleJob-integration.t.sol │ ├── Sequencer.t.sol │ ├── VestedRewardsDistribution-integration.t.sol │ └── mocks │ │ ├── DaiJoinMock.sol │ │ ├── DaiMock.sol │ │ └── VatMock.sol └── utils │ └── EnumerableSet.sol └── test.sh /.env.example: -------------------------------------------------------------------------------- 1 | FOUNDRY_ROOT_CHAINID='number: ID of the main repo chain' 2 | FOUNDRY_EXPORTS_OVERWRITE_LATEST='boolean' 3 | ETH_RPC_URL='string: JSON RPC URL for the respective chain' 4 | 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores broadcast logs 6 | broadcast/ 7 | 8 | # Ignores script config 9 | script/input/**/*.json 10 | !script/input/**/template-*.json 11 | script/output/**/*.json 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/dss-test"] 2 | path = lib/dss-test 3 | url = https://github.com/makerdao/dss-test 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all :; forge build 2 | clean :; forge clean 3 | test :; ./test.sh $(match) 4 | deploy :; make && forge create Sequencer 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dss-cron 2 | 3 | Keeper jobs for Maker protocol. Designed to support multiple Keeper Networks. All jobs will be deployed contracts which implement the `IJob` interface. 4 | 5 | Keeper Networks will be required to watch the `activeJobs` array in the `Sequencer` and find all instances of available jobs. Helper methods `getNextJobs(...)` can be used to check a subsection (or everything) of the array all at once. Each job is safe to be executed in parallel. 6 | 7 | Funding of keeper networks is done through `dss-vest`. 8 | 9 | It is important that the `work` function succeeds IF AND ONLY IF the `workable` function returns a valid execution. It is tempting to save gas by allowing execution if the internal function itself passes, but this opens an attack vector where keeper networks can spam the function to collect the DAI payout. Furthermore, care should be taken to ensure keeper networks cannot mess with the state to produce valid job executions in rapid succession as this opens up a spam attack vector too. If jobs are susceptable to this they should include a cooldown period to prevent these types of spam. 10 | 11 | # Deployed Contracts 12 | 13 | Sequencer: [0x238b4E35dAed6100C6162fAE4510261f88996EC9](https://etherscan.io/address/0x238b4E35dAed6100C6162fAE4510261f88996EC9#code) 14 | 15 | ## Active Jobs 16 | 17 | AutoLineJob [thi=1000 bps, tlo=5000 bps]: [0x67AD4000e73579B9725eE3A149F85C4Af0A61361](https://etherscan.io/address/0x67AD4000e73579B9725eE3A149F85C4Af0A61361#code) 18 | LerpJob [maxDuration=1 day]: [0x8F8f2FC1F0380B9Ff4fE5c3142d0811aC89E32fB](https://etherscan.io/address/0x8F8f2FC1F0380B9Ff4fE5c3142d0811aC89E32fB#code) 19 | D3MJob [threshold=500 bps, ttl=10 minutes]: [0x2Ea4aDE144485895B923466B4521F5ebC03a0AeF](https://etherscan.io/address/0x2Ea4aDE144485895B923466B4521F5ebC03a0AeF#code) 20 | ClipperMomJob: [0x7E93C4f61C8E8874e7366cDbfeFF934Ed089f9fF](https://etherscan.io/address/0x7E93C4f61C8E8874e7366cDbfeFF934Ed089f9fF#code) 21 | OracleJob: [0xe717Ec34b2707fc8c226b34be5eae8482d06ED03](https://etherscan.io/address/0xe717Ec34b2707fc8c226b34be5eae8482d06ED03#code) 22 | FlapJob [maxGasPrice=138 gwei]: [0xc32506E9bB590971671b649d9B8e18CB6260559F](https://etherscan.io/address/0xc32506E9bB590971671b649d9B8e18CB6260559F#code) 23 | 24 | ## Network Payment Adapters 25 | 26 | NetworkPaymentAdapter (Gelato): [0x0B5a34D084b6A5ae4361de033d1e6255623b41eD](https://etherscan.io/address/0x0B5a34D084b6A5ae4361de033d1e6255623b41eD#code) 27 | NetworkPaymentAdapter (Keep3r Network): [0xaeFed819b6657B3960A8515863abe0529Dfc444A](https://etherscan.io/address/0xaeFed819b6657B3960A8515863abe0529Dfc444A#code) 28 | NetworkPaymentAdapter (Chainlink): [0xfB5e1D841BDA584Af789bDFABe3c6419140EC065](https://etherscan.io/address/0xfB5e1D841BDA584Af789bDFABe3c6419140EC065#code) 29 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | script = 'script' 5 | libs = ["lib"] 6 | solc = '0.8.13' 7 | # Enabling optimizations to improve gas usage. 8 | optimizer = true 9 | 10 | fs_permissions = [ 11 | { access = "read", path = "./out/" }, 12 | { access = "read", path = "./script/input/" }, 13 | { access = "read-write", path = "./script/output/" } 14 | ] 15 | 16 | [rpc_endpoints] 17 | mainnet = "${ETH_RPC_URL}" 18 | 19 | [etherscan] 20 | unknown_chain = { key = "${TENDERLY_ACCESS_KEY}", chain = 314311, url = "${ETH_RPC_URL}/verify/etherscan" } 21 | -------------------------------------------------------------------------------- /script/LitePsmJobDeploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2023 Dai Foundation 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity ^0.8.13; 17 | 18 | import {Script} from "forge-std/Script.sol"; 19 | import {stdJson} from "forge-std/StdJson.sol"; 20 | import {MCD, DssInstance} from "dss-test/MCD.sol"; 21 | import {ScriptTools} from "dss-test/ScriptTools.sol"; 22 | import {LitePsmJobDeploy, LitePsmJobDeployParams} from "src/deployment/LitePsmJob/LitePsmJobDeploy.sol"; 23 | import {LitePsmJobInstance} from "src/deployment/LitePsmJob/LitePsmJobInstance.sol"; 24 | 25 | contract LitePsmJobDeployScript is Script { 26 | using stdJson for string; 27 | using ScriptTools for string; 28 | 29 | string constant NAME = "lite-psm-job-deploy"; 30 | string config; 31 | 32 | address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; 33 | DssInstance dss = MCD.loadFromChainlog(CHAINLOG); 34 | address sequencer = dss.chainlog.getAddress("CRON_SEQUENCER"); 35 | LitePsmJobInstance inst; 36 | address litePsm; 37 | uint256 rushThreshold; 38 | uint256 gushThreshold; 39 | uint256 cutThreshold; 40 | 41 | function run() external { 42 | config = ScriptTools.loadConfig(); 43 | 44 | litePsm = config.readAddress(".litePsm", "FOUNDRY_LITE_PSM"); 45 | rushThreshold = config.readUint(".rushThreshold", "FOUNDRY_RUSH_THRESHOLD"); 46 | gushThreshold = config.readUint(".gushThreshold", "FOUNDRY_GUSH_THRESHOLD"); 47 | cutThreshold = config.readUint(".cutThreshold", "FOUNDRY_CUT_THRESHOLD"); 48 | 49 | vm.startBroadcast(); 50 | 51 | inst = LitePsmJobDeploy.deploy( 52 | LitePsmJobDeployParams({ 53 | sequencer: sequencer, 54 | litePsm: litePsm, 55 | rushThreshold: rushThreshold, 56 | gushThreshold: gushThreshold, 57 | cutThreshold: cutThreshold 58 | }) 59 | ); 60 | 61 | vm.stopBroadcast(); 62 | 63 | ScriptTools.exportContract(NAME, "litePsmJob", inst.job); 64 | ScriptTools.exportContract(NAME, "sequencer", sequencer); 65 | ScriptTools.exportContract(NAME, "litePsm", litePsm); 66 | ScriptTools.exportValue(NAME, "rushThreshold", rushThreshold); 67 | ScriptTools.exportValue(NAME, "gushThreshold", gushThreshold); 68 | ScriptTools.exportValue(NAME, "cutThreshold", cutThreshold); 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /script/VestedRewardsDistributionJobDeploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2023 Dai Foundation 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity ^0.8.13; 17 | 18 | import {Script} from "forge-std/Script.sol"; 19 | import {stdJson} from "forge-std/StdJson.sol"; 20 | import {MCD, DssInstance} from "dss-test/MCD.sol"; 21 | import {ScriptTools} from "dss-test/ScriptTools.sol"; 22 | import { 23 | VestedRewardsDistributionJobDeploy, 24 | VestedRewardsDistributionJobDeployConfig 25 | } from "src/deployment/VestedRewardsDistributionJob/VestedRewardsDistributionJobDeploy.sol"; 26 | 27 | contract VestedRewardsDistributionJobDeployScript is Script { 28 | using stdJson for string; 29 | using ScriptTools for string; 30 | 31 | string constant NAME = "vested-rewards-distribution-deploy"; 32 | 33 | address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; 34 | DssInstance dss = MCD.loadFromChainlog(CHAINLOG); 35 | address sequencer = dss.chainlog.getAddress("CRON_SEQUENCER"); 36 | address pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); 37 | 38 | function run() external { 39 | vm.startBroadcast(); 40 | 41 | address job = 42 | VestedRewardsDistributionJobDeploy.deploy(VestedRewardsDistributionJobDeployConfig({ 43 | deployer: msg.sender, 44 | owner: pauseProxy, 45 | sequencer: sequencer 46 | })); 47 | 48 | vm.stopBroadcast(); 49 | 50 | ScriptTools.exportContract(NAME, "vestedRewardsDistributionJob", job); 51 | ScriptTools.exportContract(NAME, "sequencer", sequencer); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /script/VestedRewardsDistributionJobInit.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2023 Dai Foundation 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity ^0.8.13; 17 | 18 | import {Script} from "forge-std/Script.sol"; 19 | import {stdJson} from "forge-std/StdJson.sol"; 20 | import {MCD, DssInstance} from "dss-test/MCD.sol"; 21 | import {ScriptTools} from "dss-test/ScriptTools.sol"; 22 | import { 23 | VestedRewardsDistributionJobInit, 24 | VestedRewardsDistributionJobInitConfig 25 | } from "src/deployment/VestedRewardsDistributionJob/VestedRewardsDistributionJobInit.sol"; 26 | 27 | contract VestedRewardsDistributionJobInitScript is Script { 28 | using stdJson for string; 29 | using ScriptTools for string; 30 | 31 | string constant NAME = "vested-rewards-distribution-init"; 32 | string config; 33 | 34 | address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; 35 | DssInstance dss = MCD.loadFromChainlog(CHAINLOG); 36 | address pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); 37 | 38 | function run() external { 39 | config = ScriptTools.loadConfig(); 40 | address job = config.readAddress(".job", "FOUNDRY_JOB"); 41 | string memory jobKeyStr = config.readString(".jobKey", "FOUNDRY_JOB_KEY"); 42 | bytes32 jobKey = ScriptTools.stringToBytes32(jobKeyStr); 43 | 44 | vm.startBroadcast(); 45 | 46 | VestedRewardsDistributionInitSpell spell = new VestedRewardsDistributionInitSpell(); 47 | ProxyLike(pauseProxy).exec( 48 | address(spell), abi.encodeCall(spell.cast, (job, VestedRewardsDistributionJobInitConfig({jobKey: jobKey}))) 49 | ); 50 | 51 | vm.stopBroadcast(); 52 | 53 | ScriptTools.exportContract(NAME, "vestedRewardsDistributionJob", job); 54 | ScriptTools.exportValue(NAME, "vestedRewardsDistributionJobKey", jobKeyStr); 55 | } 56 | } 57 | 58 | contract VestedRewardsDistributionInitSpell { 59 | function cast(address job, VestedRewardsDistributionJobInitConfig memory cfg) public { 60 | VestedRewardsDistributionJobInit.init(job, cfg); 61 | } 62 | } 63 | 64 | interface ProxyLike { 65 | function exec(address usr, bytes memory fax) external returns (bytes memory out); 66 | } 67 | -------------------------------------------------------------------------------- /script/VestedRewardsDistributionJobSetDist.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2023 Dai Foundation 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity ^0.8.13; 17 | 18 | import {Script} from "forge-std/Script.sol"; 19 | import {stdJson} from "forge-std/StdJson.sol"; 20 | import {MCD, DssInstance} from "dss-test/MCD.sol"; 21 | import {ScriptTools} from "dss-test/ScriptTools.sol"; 22 | import { 23 | VestedRewardsDistributionJobInit, 24 | VestedRewardsDistributionJobSetDistConfig 25 | } from "src/deployment/VestedRewardsDistributionJob/VestedRewardsDistributionJobInit.sol"; 26 | 27 | contract VestedRewardsDistributionJobSetDistScript is Script { 28 | using stdJson for string; 29 | using ScriptTools for string; 30 | 31 | string constant NAME = "vested-rewards-distribution-set-dist"; 32 | string config; 33 | 34 | address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; 35 | DssInstance dss = MCD.loadFromChainlog(CHAINLOG); 36 | address pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); 37 | 38 | function run() external { 39 | config = ScriptTools.loadConfig(); 40 | address job = config.readAddress(".job", "FOUNDRY_JOB"); 41 | address dist = config.readAddress(".dist", "FOUNDRY_DIST"); 42 | uint256 interval = config.readUint(".interval", "FOUNDRY_INTERVAL"); 43 | 44 | vm.startBroadcast(); 45 | 46 | VestedRewardsDistributionSetDistSpell spell = new VestedRewardsDistributionSetDistSpell(); 47 | ProxyLike(pauseProxy).exec( 48 | address(spell), 49 | abi.encodeCall( 50 | spell.cast, (job, VestedRewardsDistributionJobSetDistConfig({dist: dist, interval: interval})) 51 | ) 52 | ); 53 | 54 | vm.stopBroadcast(); 55 | 56 | ScriptTools.exportContract(NAME, "vestedRewardsDistributionJob", job); 57 | ScriptTools.exportContract(NAME, "vestedRewardsDistribution", dist); 58 | ScriptTools.exportValue(NAME, "vestedRewardsDistributionInterval", interval); 59 | } 60 | } 61 | 62 | contract VestedRewardsDistributionSetDistSpell { 63 | function cast(address job, VestedRewardsDistributionJobSetDistConfig memory cfg) public { 64 | VestedRewardsDistributionJobInit.setDist(job, cfg); 65 | } 66 | } 67 | 68 | interface ProxyLike { 69 | function exec(address usr, bytes memory fax) external returns (bytes memory out); 70 | } 71 | -------------------------------------------------------------------------------- /script/input/1/README.md: -------------------------------------------------------------------------------- 1 | Inputs for Mainnet scripts. 2 | -------------------------------------------------------------------------------- /script/input/1/template-lite-psm-job-deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "litePsm": "address: the LitePSM instance address", 3 | "rushThreshold": "uint256: the threhsold for rush", 4 | "gushThreshold": "uint256: the threhsold for gush", 5 | "cutThreshold": "uint256: the threhsold for cut" 6 | } 7 | -------------------------------------------------------------------------------- /script/input/1/template-vested-rewards-distribution-job-init.json: -------------------------------------------------------------------------------- 1 | { 2 | "job": "address: the address of the job", 3 | "jobKey": "bytes32: the chainlog key for the job contract" 4 | } 5 | -------------------------------------------------------------------------------- /script/input/1/template-vested-rewards-distribution-job-set-dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "job": "address: the address of the job", 3 | "dist": "address: the address of the dist contract", 4 | "interval": "number: the interval for distribution [seconds]" 5 | } 6 | -------------------------------------------------------------------------------- /script/input/314311/README.md: -------------------------------------------------------------------------------- 1 | Inputs for Tenderly Testnets scripts. 2 | -------------------------------------------------------------------------------- /script/input/314311/template-vested-rewards-distribution-job-init.json: -------------------------------------------------------------------------------- 1 | { 2 | "job": "address: the address of the job", 3 | "jobKey": "bytes32: the chainlog key for the job contract" 4 | } 5 | -------------------------------------------------------------------------------- /script/input/314311/template-vested-rewards-distribution-job-set-dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "job": "address: the address of the job", 3 | "dist": "address: the address of the dist contract", 4 | "interval": "number: the interval for distribution [seconds]" 5 | } 6 | -------------------------------------------------------------------------------- /script/output/1/README.md: -------------------------------------------------------------------------------- 1 | Outputs for Mainnet scripts. 2 | -------------------------------------------------------------------------------- /script/output/314311/README.md: -------------------------------------------------------------------------------- 1 | Outputs for Tenderly Testnets scripts. 2 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { url 2 | , dappPkgs ? ( 3 | import (fetchTarball "https://github.com/makerdao/makerpkgs/tarball/master") {} 4 | ).dappPkgsVersions.hevm-0_43_1 5 | }: with dappPkgs; 6 | 7 | mkShell { 8 | DAPP_SOLC = solc-static-versions.solc_0_8_9 + "/bin/solc-0.8.9"; 9 | # No optimizations 10 | SOLC_FLAGS = ""; 11 | buildInputs = [ 12 | dapp 13 | ]; 14 | 15 | shellHook = '' 16 | export NIX_SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt 17 | unset SSL_CERT_FILE 18 | 19 | export ETH_RPC_URL="''${ETH_RPC_URL:-${url}}" 20 | ''; 21 | } 22 | -------------------------------------------------------------------------------- /src/AutoLineJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {IJob} from "./interfaces/IJob.sol"; 19 | 20 | interface SequencerLike { 21 | function isMaster(bytes32 network) external view returns (bool); 22 | } 23 | 24 | interface IlkRegistryLike { 25 | function list() external view returns (bytes32[] memory); 26 | } 27 | 28 | interface AutoLineLike { 29 | function vat() external view returns (address); 30 | function ilks(bytes32) external view returns (uint256, uint256, uint48, uint48, uint48); 31 | function exec(bytes32) external returns (uint256); 32 | } 33 | 34 | interface VatLike { 35 | function ilks(bytes32) external view returns (uint256, uint256, uint256, uint256, uint256); 36 | } 37 | 38 | /// @title Trigger autoline updates based on thresholds 39 | contract AutoLineJob is IJob { 40 | 41 | uint256 constant internal BPS = 10 ** 4; 42 | 43 | SequencerLike public immutable sequencer; 44 | IlkRegistryLike public immutable ilkRegistry; 45 | AutoLineLike public immutable autoline; 46 | VatLike public immutable vat; 47 | uint256 public immutable thi; // % above the previously exec'ed debt level 48 | uint256 public immutable tlo; // % below the previously exec'ed debt level 49 | 50 | // --- Errors --- 51 | error NotMaster(bytes32 network); 52 | error OutsideThreshold(uint256 line, uint256 nextLine); 53 | 54 | // --- Events --- 55 | event Work(bytes32 indexed network, bytes32 indexed ilk); 56 | 57 | constructor(address _sequencer, address _ilkRegistry, address _autoline, uint256 _thi, uint256 _tlo) { 58 | sequencer = SequencerLike(_sequencer); 59 | ilkRegistry = IlkRegistryLike(_ilkRegistry); 60 | autoline = AutoLineLike(_autoline); 61 | vat = VatLike(autoline.vat()); 62 | thi = _thi; 63 | tlo = _tlo; 64 | } 65 | 66 | function work(bytes32 network, bytes calldata args) external override { 67 | if (!sequencer.isMaster(network)) revert NotMaster(network); 68 | 69 | bytes32 ilk = abi.decode(args, (bytes32)); 70 | 71 | (,,, uint256 line,) = vat.ilks(ilk); 72 | uint256 nextLine = autoline.exec(ilk); 73 | 74 | // Execution is not enough 75 | // We need to be over the threshold amounts 76 | (uint256 maxLine, uint256 gap,,,) = autoline.ilks(ilk); 77 | if ( 78 | nextLine != maxLine && 79 | nextLine < line + gap * thi / BPS && 80 | nextLine + gap * tlo / BPS > line 81 | ) revert OutsideThreshold(line, nextLine); 82 | 83 | emit Work(network, ilk); 84 | } 85 | 86 | function workable(bytes32 network) external view override returns (bool, bytes memory) { 87 | if (!sequencer.isMaster(network)) return (false, bytes("Network is not master")); 88 | 89 | bytes32[] memory ilks = ilkRegistry.list(); 90 | for (uint256 i = 0; i < ilks.length; i++) { 91 | bytes32 ilk = ilks[i]; 92 | 93 | (uint256 Art, uint256 rate,, uint256 line,) = vat.ilks(ilk); 94 | uint256 debt = Art * rate; 95 | (uint256 maxLine, uint256 gap, uint48 ttl, uint48 last, uint48 lastInc) = autoline.ilks(ilk); 96 | uint256 nextLine = debt + gap; 97 | if (nextLine > maxLine) nextLine = maxLine; 98 | 99 | // Check autoline rules 100 | if (maxLine == 0) continue; // Ilk is not enabled 101 | if (last == block.number) continue; // Already triggered this block 102 | if (line == nextLine || // No change in line 103 | nextLine > line && // Increase in line 104 | block.timestamp < lastInc + ttl) continue; // TTL hasn't expired 105 | 106 | // Check if current debt level is inside our do-nothing range 107 | // Re-arranged to remove any subtraction (and thus underflow) 108 | // Exception if we are at the maxLine 109 | if ( 110 | nextLine != maxLine && 111 | nextLine < line + gap * thi / BPS && 112 | nextLine + gap * tlo / BPS > line 113 | ) continue; 114 | 115 | // Good to adjust! 116 | return (true, abi.encode(ilk)); 117 | } 118 | 119 | return (false, bytes("No ilks ready")); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/ClipperMomJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2022 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {IJob} from "./interfaces/IJob.sol"; 19 | 20 | interface SequencerLike { 21 | function isMaster(bytes32 network) external view returns (bool); 22 | } 23 | 24 | interface IlkRegistryLike { 25 | function list() external view returns (bytes32[] memory); 26 | function xlip(bytes32 ilk) external view returns (address); 27 | } 28 | 29 | interface ClipperMomLike { 30 | function tripBreaker(address clip) external; 31 | } 32 | 33 | /// @title Will trigger a clipper to shutdown if oracle price drops too quickly 34 | contract ClipperMomJob is IJob { 35 | 36 | SequencerLike public immutable sequencer; 37 | IlkRegistryLike public immutable ilkRegistry; 38 | ClipperMomLike public immutable clipperMom; 39 | 40 | // --- Errors --- 41 | error NotMaster(bytes32 network); 42 | 43 | // --- Events --- 44 | event Work(bytes32 indexed network, address indexed clip); 45 | 46 | constructor(address _sequencer, address _ilkRegistry, address _clipperMom) { 47 | sequencer = SequencerLike(_sequencer); 48 | ilkRegistry = IlkRegistryLike(_ilkRegistry); 49 | clipperMom = ClipperMomLike(_clipperMom); 50 | } 51 | 52 | function work(bytes32 network, bytes calldata args) external override { 53 | if (!sequencer.isMaster(network)) revert NotMaster(network); 54 | 55 | address clip = abi.decode(args, (address)); 56 | clipperMom.tripBreaker(clip); 57 | 58 | emit Work(network, clip); 59 | } 60 | 61 | function workable(bytes32 network) external override returns (bool, bytes memory) { 62 | if (!sequencer.isMaster(network)) return (false, bytes("Network is not master")); 63 | 64 | bytes32[] memory ilks = ilkRegistry.list(); 65 | for (uint256 i = 0; i < ilks.length; i++) { 66 | address clip = ilkRegistry.xlip(ilks[i]); 67 | if (clip == address(0)) continue; 68 | 69 | // We cannot retrieve oracle prices (whitelist-only), so we have to just try and run the trip breaker 70 | try clipperMom.tripBreaker(clip) { 71 | // Found a valid trip 72 | return (true, abi.encode(clip)); 73 | } catch { 74 | // No valid trip -- carry on 75 | } 76 | } 77 | 78 | return (false, bytes("No ilks ready")); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/D3MJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {IJob} from "./interfaces/IJob.sol"; 19 | 20 | interface SequencerLike { 21 | function isMaster(bytes32 network) external view returns (bool); 22 | } 23 | 24 | interface IlkRegistryLike { 25 | function list() external view returns (bytes32[] memory); 26 | } 27 | 28 | interface VatLike { 29 | function urns(bytes32, address) external view returns (uint256, uint256); 30 | } 31 | 32 | interface D3MHubLike { 33 | function vat() external view returns (VatLike); 34 | function pool(bytes32) external view returns (address); 35 | function exec(bytes32) external; 36 | } 37 | 38 | /// @title Trigger D3M updates based on threshold 39 | contract D3MJob is IJob { 40 | 41 | uint256 constant internal BPS = 10 ** 4; 42 | 43 | SequencerLike public immutable sequencer; 44 | IlkRegistryLike public immutable ilkRegistry; 45 | D3MHubLike public immutable hub; 46 | VatLike public immutable vat; 47 | uint256 public immutable threshold; // Threshold deviation to kick off exec [BPS] 48 | uint256 public immutable ttl; // Cooldown before you can call exec again [seconds] 49 | 50 | mapping (bytes32 => uint256) public expiry; // Timestamp of when exec is allowed again 51 | 52 | // --- Errors --- 53 | error NotMaster(bytes32 network); 54 | error Cooldown(bytes32 ilk, uint256 expiry); 55 | error ShouldNotTrigger(bytes32 ilk, uint256 part, uint256 nart); 56 | 57 | // --- Events --- 58 | event Work(bytes32 indexed network); 59 | 60 | constructor(address _sequencer, address _ilkRegistry, address _hub, uint256 _threshold, uint256 _ttl) { 61 | sequencer = SequencerLike(_sequencer); 62 | ilkRegistry = IlkRegistryLike(_ilkRegistry); 63 | hub = D3MHubLike(_hub); 64 | vat = hub.vat(); 65 | threshold = _threshold; 66 | ttl = _ttl; 67 | } 68 | 69 | function shouldTrigger(uint256 part, uint256 nart) internal view returns (bool) { 70 | if (part == 0 && nart != 0) return true; // From zero to non-zero 71 | if (part != 0 && nart == 0) return true; // From non-zero to zero 72 | if (part == 0 && nart == 0) return false; // No change at zero 73 | 74 | // Check if the delta is above the threshold 75 | uint256 delta = nart * BPS / part; 76 | if (delta < BPS) delta = BPS * BPS / delta; // Flip decreases to increase 77 | 78 | return delta >= (BPS + threshold); 79 | } 80 | 81 | function work(bytes32 network, bytes calldata args) external override { 82 | if (!sequencer.isMaster(network)) revert NotMaster(network); 83 | 84 | bytes32 ilk = abi.decode(args, (bytes32)); 85 | uint256 _expiry = expiry[ilk]; 86 | if (block.timestamp < _expiry) revert Cooldown(ilk, _expiry); 87 | address pool = hub.pool(ilk); 88 | (, uint256 part) = vat.urns(ilk, pool); 89 | 90 | hub.exec(ilk); 91 | 92 | (, uint256 nart) = vat.urns(ilk, pool); 93 | if (!shouldTrigger(part, nart)) revert ShouldNotTrigger(ilk, part, nart); 94 | 95 | expiry[ilk] = block.timestamp + ttl; 96 | 97 | emit Work(network); 98 | } 99 | 100 | function workable(bytes32 network) external override returns (bool, bytes memory) { 101 | if (!sequencer.isMaster(network)) return (false, bytes("Network is not master")); 102 | 103 | bytes32[] memory ilks = ilkRegistry.list(); 104 | for (uint256 i = 0; i < ilks.length; i++) { 105 | bytes memory args = abi.encode(ilks[i]); 106 | try this.work(network, args) { 107 | // Found a valid execution 108 | return (true, args); 109 | } catch { 110 | // For some reason this errored -- carry on 111 | } 112 | } 113 | 114 | return (false, bytes("No ilks ready")); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/FlapJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {IJob} from "./interfaces/IJob.sol"; 19 | 20 | interface SequencerLike { 21 | function isMaster(bytes32 network) external view returns (bool); 22 | } 23 | 24 | interface VatLike { 25 | function sin(address) external view returns (uint256); 26 | } 27 | 28 | interface VowLike { 29 | function Sin() external view returns (uint256); 30 | function Ash() external view returns (uint256); 31 | function heal(uint256) external; 32 | function flap() external; 33 | } 34 | 35 | /// @title Call flap when possible 36 | contract FlapJob is IJob { 37 | 38 | SequencerLike public immutable sequencer; 39 | VatLike public immutable vat; 40 | VowLike public immutable vow; 41 | uint256 public immutable maxGasPrice; 42 | 43 | // --- Errors --- 44 | error NotMaster(bytes32 network); 45 | error GasPriceTooHigh(uint256 gasPrice, uint256 maxGasPrice); 46 | 47 | // --- Events --- 48 | event Work(bytes32 indexed network); 49 | 50 | constructor(address _sequencer, address _vat, address _vow, uint256 _maxGasPrice) { 51 | sequencer = SequencerLike(_sequencer); 52 | vat = VatLike(_vat); 53 | vow = VowLike(_vow); 54 | maxGasPrice = _maxGasPrice; 55 | } 56 | 57 | function work(bytes32 network, bytes calldata args) public { 58 | if (!sequencer.isMaster(network)) revert NotMaster(network); 59 | if (tx.gasprice > maxGasPrice) revert GasPriceTooHigh(tx.gasprice, maxGasPrice); 60 | 61 | uint256 toHeal = abi.decode(args, (uint256)); 62 | if (toHeal > 0) vow.heal(toHeal); 63 | vow.flap(); 64 | 65 | emit Work(network); 66 | } 67 | 68 | function workable(bytes32 network) external override returns (bool, bytes memory) { 69 | if (!sequencer.isMaster(network)) return (false, bytes("Network is not master")); 70 | 71 | bytes memory args; 72 | uint256 unbackedTotal = vat.sin(address(vow)); 73 | uint256 unbackedVow = vow.Sin() + vow.Ash(); 74 | 75 | // Check if need to cancel out free unbacked debt with system surplus 76 | uint256 toHeal = unbackedTotal > unbackedVow ? unbackedTotal - unbackedVow : 0; 77 | args = abi.encode(toHeal); 78 | 79 | try this.work(network, args) { 80 | // Flap succeeds 81 | return (true, args); 82 | } catch { 83 | // Can not flap -- carry on 84 | } 85 | return (false, bytes("Flap not possible")); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/LerpJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {TimedJob} from "./base/TimedJob.sol"; 19 | 20 | interface LerpFactoryLike { 21 | function count() external view returns (uint256); 22 | function tall() external; 23 | } 24 | 25 | /// @title Tick all lerps 26 | contract LerpJob is TimedJob { 27 | 28 | LerpFactoryLike public immutable lerpFactory; 29 | 30 | constructor(address _sequencer, address _lerpFactory, uint256 _duration) TimedJob(_sequencer, _duration) { 31 | lerpFactory = LerpFactoryLike(_lerpFactory); 32 | } 33 | 34 | function shouldUpdate() internal override view returns (bool) { 35 | return lerpFactory.count() > 0; 36 | } 37 | 38 | function update() internal override { 39 | lerpFactory.tall(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/LiquidatorJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {IJob} from "./interfaces/IJob.sol"; 19 | 20 | interface SequencerLike { 21 | function isMaster(bytes32 network) external view returns (bool); 22 | } 23 | 24 | interface VatLike { 25 | function can(address, address) external view returns (uint256); 26 | function hope(address) external; 27 | function dai(address) external view returns (uint256); 28 | function move(address, address, uint256) external; 29 | function wards(address) external view returns (uint256); 30 | } 31 | 32 | interface DaiJoinLike { 33 | function vat() external view returns (address); 34 | function dai() external view returns (address); 35 | function join(address, uint256) external; 36 | } 37 | 38 | interface DaiLike { 39 | function approve(address, uint256) external returns (bool); 40 | function balanceOf(address) external view returns (uint256); 41 | } 42 | 43 | interface IlkRegistryLike { 44 | function list() external view returns (bytes32[] memory); 45 | function xlip() external view returns (address); 46 | function info(bytes32 ilk) external view returns ( 47 | string memory name, 48 | string memory symbol, 49 | uint256 class, 50 | uint256 dec, 51 | address gem, 52 | address pip, 53 | address join, 54 | address xlip 55 | ); 56 | } 57 | 58 | interface ClipLike { 59 | function list() external view returns (uint256[] memory); 60 | function take( 61 | uint256 id, // Auction id 62 | uint256 amt, // Upper limit on amount of collateral to buy [wad] 63 | uint256 max, // Maximum acceptable price (DAI / collateral) [ray] 64 | address who, // Receiver of collateral and external call address 65 | bytes calldata data // Data to pass in external call; if length 0, no call is done 66 | ) external; 67 | function sales(uint256) external view returns (uint256,uint256,uint256,address,uint96,uint256); 68 | } 69 | 70 | /// @title Provide backstop liquidations across all supported DEXs 71 | contract LiquidatorJob is IJob { 72 | 73 | uint256 constant internal BPS = 10 ** 4; 74 | uint256 constant internal RAY = 10 ** 27; 75 | 76 | SequencerLike public immutable sequencer; 77 | VatLike public immutable vat; 78 | DaiJoinLike public immutable daiJoin; 79 | DaiLike public immutable dai; 80 | IlkRegistryLike public immutable ilkRegistry; 81 | address public immutable profitTarget; 82 | 83 | address public immutable uniswapV3Callee; 84 | uint256 public immutable minProfitBPS; // Profit as % of debt owed 85 | 86 | // --- Errors --- 87 | error NotMaster(bytes32 network); 88 | error InvalidClipper(address clip); 89 | 90 | // --- Events --- 91 | event Work(bytes32 indexed network); 92 | 93 | constructor(address _sequencer, address _daiJoin, address _ilkRegistry, address _profitTarget, address _uniswapV3Callee, uint256 _minProfitBPS) { 94 | sequencer = SequencerLike(_sequencer); 95 | daiJoin = DaiJoinLike(_daiJoin); 96 | vat = VatLike(daiJoin.vat()); 97 | dai = DaiLike(daiJoin.dai()); 98 | ilkRegistry = IlkRegistryLike(_ilkRegistry); 99 | profitTarget = _profitTarget; 100 | uniswapV3Callee = _uniswapV3Callee; 101 | minProfitBPS = _minProfitBPS; 102 | 103 | dai.approve(_daiJoin, type(uint256).max); 104 | } 105 | 106 | function work(bytes32 network, bytes calldata args) public { 107 | if (!sequencer.isMaster(network)) revert NotMaster(network); 108 | 109 | (address clip, uint256 auction, bytes memory calleePayload) = abi.decode(args, (address, uint256, bytes)); 110 | 111 | // Verify clipper is a valid contract 112 | // Easiest way to do this is check it's authed on the vat 113 | if (vat.wards(clip) != 1) revert InvalidClipper(clip); 114 | 115 | if (vat.can(address(this), clip) != 1) { 116 | vat.hope(clip); 117 | } 118 | ClipLike(clip).take( 119 | auction, 120 | type(uint256).max, 121 | type(uint256).max, 122 | uniswapV3Callee, 123 | calleePayload 124 | ); 125 | 126 | // Dump all extra DAI into the profit target 127 | daiJoin.join(address(this), dai.balanceOf(address(this))); 128 | vat.move(address(this), profitTarget, vat.dai(address(this))); 129 | 130 | emit Work(network); 131 | } 132 | 133 | function workable(bytes32 network) external override returns (bool, bytes memory) { 134 | if (!sequencer.isMaster(network)) return (false, bytes("Network is not master")); 135 | 136 | bytes32[] memory ilks = ilkRegistry.list(); 137 | for (uint256 i = 0; i < ilks.length; i++) { 138 | (,, uint256 class,, address gem,, address join, address clip) = ilkRegistry.info(ilks[i]); 139 | if (class != 1) continue; 140 | if (clip == address(0)) continue; 141 | 142 | uint256[] memory auctions = ClipLike(clip).list(); 143 | for (uint256 o = 0; o < auctions.length; o++) { 144 | // Attempt to run this through Uniswap V3 liquidator 145 | uint24[2] memory fees = [uint24(500), uint24(3000)]; 146 | for (uint256 p = 0; p < fees.length; p++) { 147 | bytes memory args; 148 | { 149 | // Stack too deep 150 | (, uint256 tab,,,,) = ClipLike(clip).sales(auctions[o]); 151 | bytes memory calleePayload = abi.encode( 152 | address(this), 153 | join, 154 | tab * minProfitBPS / BPS / RAY, 155 | abi.encodePacked(gem, fees[p], dai), 156 | address(0) 157 | ); 158 | args = abi.encode( 159 | clip, 160 | auctions[o], 161 | calleePayload 162 | ); 163 | } 164 | 165 | try this.work(network, args) { 166 | // Found an auction! 167 | return (true, args); 168 | } catch { 169 | // No valid auction -- carry on 170 | } 171 | } 172 | } 173 | } 174 | 175 | return (false, bytes("No auctions")); 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /src/LitePsmJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {IJob} from "./interfaces/IJob.sol"; 19 | 20 | interface SequencerLike { 21 | function isMaster(bytes32 network) external view returns (bool); 22 | } 23 | 24 | interface LitePsmLike { 25 | function chug() external returns (uint256 wad); 26 | function cut() external view returns (uint256 wad); 27 | function fill() external returns (uint256 wad); 28 | function gush() external view returns (uint256 wad); 29 | function rush() external view returns (uint256 wad); 30 | function trim() external returns (uint256 wad); 31 | } 32 | 33 | /// @title Call flap when possible 34 | contract LitePsmJob is IJob { 35 | SequencerLike public immutable sequencer; 36 | LitePsmLike public immutable litePsm; 37 | 38 | uint256 public immutable rushThreshold; 39 | uint256 public immutable cutThreshold; 40 | uint256 public immutable gushThreshold; 41 | 42 | // --- Errors --- 43 | error NotMaster(bytes32 network); 44 | error ThresholdNotReached(bytes4 fn); 45 | error UnsupportedFunction(bytes4 fn); 46 | 47 | // --- Events --- 48 | event Work(bytes32 indexed network, bytes4 indexed action); 49 | 50 | constructor( 51 | address _sequencer, 52 | address _litePsm, 53 | uint256 _rushThreshold, 54 | uint256 _cutThreshold, 55 | uint256 _gushThreshold 56 | ) { 57 | sequencer = SequencerLike(_sequencer); 58 | litePsm = LitePsmLike(_litePsm); 59 | rushThreshold = _rushThreshold; 60 | cutThreshold = _cutThreshold; 61 | gushThreshold = _gushThreshold; 62 | } 63 | 64 | function work(bytes32 network, bytes calldata args) public { 65 | if (!sequencer.isMaster(network)) revert NotMaster(network); 66 | 67 | (bytes4 fn) = abi.decode(args, (bytes4)); 68 | 69 | if (fn == litePsm.fill.selector) { 70 | if (litePsm.rush() >= rushThreshold) litePsm.fill(); 71 | else revert ThresholdNotReached(fn); 72 | } else if (fn == litePsm.chug.selector) { 73 | if(litePsm.cut() >= cutThreshold) litePsm.chug(); 74 | else revert ThresholdNotReached(fn); 75 | } else if (fn == litePsm.trim.selector) { 76 | if (litePsm.gush() >= gushThreshold) litePsm.trim(); 77 | else revert ThresholdNotReached(fn); 78 | } else { 79 | revert UnsupportedFunction(fn); 80 | } 81 | 82 | emit Work(network, fn); 83 | } 84 | 85 | function workable(bytes32 network) external view override returns (bool, bytes memory) { 86 | if (!sequencer.isMaster(network)) return (false, bytes("Network is not master")); 87 | 88 | if (litePsm.rush() >= rushThreshold) { 89 | return (true, abi.encode(litePsm.fill.selector)); 90 | } else if (litePsm.cut() >= cutThreshold) { 91 | return (true, abi.encode(litePsm.chug.selector)); 92 | } else if (litePsm.gush() >= gushThreshold) { 93 | return (true, abi.encode(litePsm.trim.selector)); 94 | } else { 95 | return (false, bytes("No work to do")); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/NetworkPaymentAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2022 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {INetworkTreasury} from "./interfaces/INetworkTreasury.sol"; 19 | 20 | interface VestLike { 21 | function vest(uint256) external; 22 | function unpaid(uint256) external view returns (uint256); 23 | } 24 | 25 | interface DaiJoinLike { 26 | function dai() external view returns (address); 27 | function join(address, uint256) external; 28 | } 29 | 30 | interface DaiLike { 31 | function balanceOf(address) external view returns (uint256); 32 | function approve(address, uint256) external returns (bool); 33 | function transfer(address, uint256) external returns (bool); 34 | } 35 | 36 | /// @title Payment adapter to the keeper network treasury 37 | /// @dev Sits between dss-vest and the keeper network treasury contract 38 | contract NetworkPaymentAdapter { 39 | 40 | // --- Auth --- 41 | mapping (address => uint256) public wards; 42 | function rely(address usr) external auth { 43 | wards[usr] = 1; 44 | 45 | emit Rely(usr); 46 | } 47 | function deny(address usr) external auth { 48 | wards[usr] = 0; 49 | 50 | emit Deny(usr); 51 | } 52 | modifier auth { 53 | require(wards[msg.sender] == 1, "NetworkPaymentAdapter/not-authorized"); 54 | _; 55 | } 56 | 57 | // --- Data --- 58 | VestLike public immutable vest; 59 | DaiJoinLike public immutable daiJoin; 60 | DaiLike public immutable dai; 61 | address public immutable vow; 62 | 63 | // --- Parameters --- 64 | uint256 public vestId; 65 | uint256 public bufferMax; 66 | uint256 public minimumPayment; 67 | INetworkTreasury public treasury; 68 | 69 | // --- Tracking --- 70 | uint256 public totalSent; 71 | 72 | // --- Events --- 73 | event Rely(address indexed usr); 74 | event Deny(address indexed usr); 75 | event File(bytes32 indexed what, address data); 76 | event File(bytes32 indexed what, uint256 data); 77 | event TopUp(uint256 bufferSize, uint256 daiBalance, uint256 daiSent); 78 | 79 | // --- Errors --- 80 | error InvalidFileParam(bytes32 what); 81 | error UnauthorizedSender(address sender); 82 | error BufferFull(uint256 bufferSize, uint256 minimumPayment, uint256 bufferMax); 83 | error PendingDaiTooSmall(uint256 pendingDai, uint256 minimumPayment); 84 | 85 | constructor(address _vest, address _daiJoin, address _vow) { 86 | wards[msg.sender] = 1; 87 | emit Rely(msg.sender); 88 | 89 | vest = VestLike(_vest); 90 | daiJoin = DaiJoinLike(_daiJoin); 91 | dai = DaiLike(daiJoin.dai()); 92 | vow = _vow; 93 | 94 | dai.approve(address(daiJoin), type(uint256).max); 95 | } 96 | 97 | // --- Administration --- 98 | function file(bytes32 what, address data) external auth { 99 | if (what == "treasury") { 100 | treasury = INetworkTreasury(data); 101 | } else revert("NetworkPaymentAdapter/file-unrecognized-param"); 102 | 103 | emit File(what, data); 104 | } 105 | 106 | function file(bytes32 what, uint256 data) external auth { 107 | if (what == "vestId") { 108 | vestId = data; 109 | } else if (what == "bufferMax") { 110 | bufferMax = data; 111 | } else if (what == "minimumPayment") { 112 | minimumPayment = data; 113 | } else revert("NetworkPaymentAdapter/file-unrecognized-param"); 114 | 115 | emit File(what, data); 116 | } 117 | 118 | /** 119 | * @notice Top up the treasury with any outstanding DAI 120 | * @dev Only callable from treasury. Call canTopUp() to see if this will pass. 121 | */ 122 | function topUp() external returns (uint256 daiSent) { 123 | if (msg.sender != address(treasury)) revert UnauthorizedSender(msg.sender); 124 | 125 | uint256 bufferSize = treasury.getBufferSize(); 126 | uint256 pendingDai = vest.unpaid(vestId); 127 | uint256 _bufferMax = bufferMax; 128 | uint256 _minimumPayment = minimumPayment; 129 | 130 | if (bufferSize + _minimumPayment > _bufferMax) revert BufferFull(bufferSize, _minimumPayment, _bufferMax); 131 | else if (pendingDai >= _minimumPayment) { 132 | vest.vest(vestId); 133 | 134 | // Send DAI up to the maximum and the rest should go back into the surplus buffer 135 | // Use the balance in case someone sends DAI directly to this contract (can be used in emergency) 136 | uint256 daiBalance = dai.balanceOf(address(this)); 137 | if (daiBalance + bufferSize > _bufferMax) { 138 | daiSent = _bufferMax - bufferSize; 139 | 140 | // Send the rest back to the surplus buffer 141 | daiJoin.join(vow, daiBalance - daiSent); 142 | } else { 143 | daiSent = daiBalance; 144 | } 145 | dai.transfer(address(treasury), daiSent); 146 | totalSent += daiSent; 147 | 148 | emit TopUp(bufferSize, daiBalance, daiSent); 149 | } else revert PendingDaiTooSmall(pendingDai, _minimumPayment); 150 | } 151 | 152 | /** 153 | * @notice Check if we can call the topUp() function. 154 | */ 155 | function canTopUp() external view returns (bool) { 156 | uint256 bufferSize = treasury.getBufferSize(); 157 | uint256 pendingDai = vest.unpaid(vestId); 158 | uint256 _minimumPayment = minimumPayment; 159 | 160 | return (bufferSize + _minimumPayment < bufferMax) && (pendingDai >= _minimumPayment); 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/OracleJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2022 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {IJob} from "./interfaces/IJob.sol"; 19 | 20 | interface SequencerLike { 21 | function isMaster(bytes32 network) external view returns (bool); 22 | } 23 | 24 | interface IlkRegistryLike { 25 | function list() external view returns (bytes32[] memory); 26 | function pip(bytes32 ilk) external view returns (address); 27 | } 28 | 29 | interface VatLike { 30 | function ilks(bytes32 ilk) external view returns ( 31 | uint256 Art, 32 | uint256 rate, 33 | uint256 spot, 34 | uint256 line, 35 | uint256 dust 36 | ); 37 | } 38 | 39 | interface PokeLike { 40 | function poke() external; 41 | } 42 | 43 | interface SpotterLike { 44 | function vat() external view returns (address); 45 | function poke(bytes32 ilk) external; 46 | } 47 | 48 | /// @title Triggers osm / oracle updates for all ilks 49 | contract OracleJob is IJob { 50 | 51 | SequencerLike public immutable sequencer; 52 | IlkRegistryLike public immutable ilkRegistry; 53 | VatLike public immutable vat; 54 | SpotterLike public immutable spotter; 55 | 56 | // Don't actually store anything 57 | bytes32[] private toPoke; 58 | bytes32[] private spotterIlksToPoke; 59 | 60 | // --- Errors --- 61 | error NotMaster(bytes32 network); 62 | error NotSuccessful(); 63 | 64 | // --- Events --- 65 | event Work(bytes32 indexed network, bytes32[] toPoke, bytes32[] spotterIlksToPoke, uint256 numSuccessful); 66 | 67 | constructor(address _sequencer, address _ilkRegistry, address _spotter) { 68 | sequencer = SequencerLike(_sequencer); 69 | ilkRegistry = IlkRegistryLike(_ilkRegistry); 70 | spotter = SpotterLike(_spotter); 71 | vat = VatLike(spotter.vat()); 72 | } 73 | 74 | function work(bytes32 network, bytes calldata args) external override { 75 | if (!sequencer.isMaster(network)) revert NotMaster(network); 76 | 77 | (bytes32[] memory _toPoke, bytes32[] memory _spotterIlksToPoke) = abi.decode(args, (bytes32[], bytes32[])); 78 | uint256 numSuccessful = 0; 79 | for (uint256 i = 0; i < _toPoke.length; i++) { 80 | bytes32 ilk = _toPoke[i]; 81 | (uint256 Art,,, uint256 line,) = vat.ilks(ilk); 82 | if (Art == 0 && line == 0) continue; 83 | PokeLike pip = PokeLike(ilkRegistry.pip(ilk)); 84 | try pip.poke() { 85 | numSuccessful++; 86 | } catch { 87 | } 88 | } 89 | for (uint256 i = 0; i < _spotterIlksToPoke.length; i++) { 90 | bytes32 ilk = _spotterIlksToPoke[i]; 91 | (uint256 Art,, uint256 beforeSpot, uint256 line,) = vat.ilks(ilk); 92 | if (Art == 0 && line == 0) continue; 93 | spotter.poke(ilk); 94 | (,, uint256 afterSpot,,) = vat.ilks(ilk); 95 | if (beforeSpot != afterSpot) { 96 | numSuccessful++; 97 | } 98 | } 99 | 100 | if (numSuccessful == 0) revert NotSuccessful(); 101 | 102 | emit Work(network, _toPoke, _spotterIlksToPoke, numSuccessful); 103 | } 104 | 105 | function workable(bytes32 network) external override returns (bool, bytes memory) { 106 | if (!sequencer.isMaster(network)) return (false, bytes("Network is not master")); 107 | 108 | delete toPoke; 109 | delete spotterIlksToPoke; 110 | 111 | bytes32[] memory ilks = ilkRegistry.list(); 112 | for (uint256 i = 0; i < ilks.length; i++) { 113 | bytes32 ilk = ilks[i]; 114 | PokeLike pip = PokeLike(ilkRegistry.pip(ilk)); 115 | 116 | if (address(pip) == address(0)) continue; 117 | (uint256 Art,, uint256 beforeSpot, uint256 line,) = vat.ilks(ilk); 118 | if (Art == 0 && line == 0) continue; // Skip if no debt / line 119 | 120 | // Just try to poke the oracle and add to the list if it works 121 | // This won't add an OSM twice 122 | try pip.poke() { 123 | toPoke.push(ilk); 124 | } catch { 125 | } 126 | 127 | // See if the spot price changes 128 | spotter.poke(ilk); 129 | (,, uint256 afterSpot,,) = vat.ilks(ilk); 130 | if (beforeSpot != afterSpot) { 131 | spotterIlksToPoke.push(ilk); 132 | } 133 | } 134 | 135 | if (toPoke.length > 0 || spotterIlksToPoke.length > 0) { 136 | return (true, abi.encode(toPoke, spotterIlksToPoke)); 137 | } else { 138 | return (false, bytes("No ilks ready")); 139 | } 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/Sequencer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "./utils/EnumerableSet.sol"; 19 | 20 | interface JobLike { 21 | function workable(bytes32 network) external returns (bool canWork, bytes memory args); 22 | } 23 | 24 | /// @title Coordination between Keeper Networks 25 | /// @dev Only one should be active at a time 26 | /// 27 | /// Use the block number to switch between networks 28 | contract Sequencer { 29 | 30 | using EnumerableSet for EnumerableSet.AddressSet; 31 | using EnumerableSet for EnumerableSet.Bytes32Set; 32 | 33 | struct Window { 34 | uint256 start; 35 | uint256 length; 36 | } 37 | 38 | struct WorkableJob { 39 | address job; 40 | bool canWork; 41 | bytes args; 42 | } 43 | 44 | // --- Auth --- 45 | mapping (address => uint256) public wards; 46 | function rely(address usr) external auth { 47 | wards[usr] = 1; 48 | 49 | emit Rely(usr); 50 | } 51 | function deny(address usr) external auth { 52 | wards[usr] = 0; 53 | 54 | emit Deny(usr); 55 | } 56 | modifier auth { 57 | require(wards[msg.sender] == 1, "Sequencer/not-authorized"); 58 | _; 59 | } 60 | 61 | EnumerableSet.Bytes32Set private networks; 62 | EnumerableSet.AddressSet private jobs; 63 | mapping(bytes32 => Window) public windows; 64 | uint256 public totalWindowSize; 65 | 66 | // --- Events --- 67 | event Rely(address indexed usr); 68 | event Deny(address indexed usr); 69 | event AddNetwork(bytes32 indexed network, uint256 windowSize); 70 | event RemoveNetwork(bytes32 indexed network); 71 | event AddJob(address indexed job); 72 | event RemoveJob(address indexed job); 73 | 74 | // --- Errors --- 75 | error WindowZero(bytes32 network); 76 | error NetworkExists(bytes32 network); 77 | error NetworkDoesNotExist(bytes32 network); 78 | error JobExists(address job); 79 | error JobDoesNotExist(address network); 80 | error IndexTooHigh(uint256 index, uint256 length); 81 | error BadIndicies(uint256 startIndex, uint256 exclEndIndex); 82 | 83 | constructor() { 84 | wards[msg.sender] = 1; 85 | emit Rely(msg.sender); 86 | } 87 | 88 | // --- Network Admin --- 89 | function refreshStarts() internal { 90 | uint256 start = 0; 91 | uint256 netLen = networks.length(); 92 | for (uint256 i = 0; i < netLen; i++) { 93 | bytes32 network = networks.at(i); 94 | windows[network].start = start; 95 | start += windows[network].length; 96 | } 97 | } 98 | function addNetwork(bytes32 network, uint256 windowSize) external auth { 99 | if (!networks.add(network)) revert NetworkExists(network); 100 | if (windowSize == 0) revert WindowZero(network); 101 | windows[network] = Window({ 102 | start: 0, // start will be set in refreshStarts 103 | length: windowSize 104 | }); 105 | totalWindowSize += windowSize; 106 | refreshStarts(); 107 | emit AddNetwork(network, windowSize); 108 | } 109 | function removeNetwork(bytes32 network) external auth { 110 | if (!networks.remove(network)) revert NetworkDoesNotExist(network); 111 | uint256 windowSize = windows[network].length; 112 | delete windows[network]; 113 | totalWindowSize -= windowSize; 114 | refreshStarts(); 115 | emit RemoveNetwork(network); 116 | } 117 | 118 | // --- Job Admin --- 119 | function addJob(address job) external auth { 120 | if (!jobs.add(job)) revert JobExists(job); 121 | emit AddJob(job); 122 | } 123 | function removeJob(address job) external auth { 124 | if (!jobs.remove(job)) revert JobDoesNotExist(job); 125 | emit RemoveJob(job); 126 | } 127 | 128 | // --- Views --- 129 | function isMaster(bytes32 network) external view returns (bool) { 130 | if (networks.length() == 0) return false; 131 | 132 | Window memory window = windows[network]; 133 | uint256 pos = block.number % totalWindowSize; 134 | return window.start <= pos && pos < window.start + window.length; 135 | } 136 | function getMaster() external view returns (bytes32) { 137 | uint256 netLen = networks.length(); 138 | if (netLen == 0) return bytes32(0); 139 | 140 | uint256 pos = block.number % totalWindowSize; 141 | for (uint256 i = 0; i < netLen; i++) { 142 | bytes32 network = networks.at(i); 143 | Window memory window = windows[network]; 144 | if (window.start <= pos && pos < window.start + window.length) { 145 | return network; 146 | } 147 | } 148 | return bytes32(0); 149 | } 150 | 151 | function numNetworks() external view returns (uint256) { 152 | return networks.length(); 153 | } 154 | function hasNetwork(bytes32 network) public view returns (bool) { 155 | return networks.contains(network); 156 | } 157 | function networkAt(uint256 index) public view returns (bytes32) { 158 | return networks.at(index); 159 | } 160 | 161 | function numJobs() external view returns (uint256) { 162 | return jobs.length(); 163 | } 164 | function hasJob(address job) public view returns (bool) { 165 | return jobs.contains(job); 166 | } 167 | function jobAt(uint256 index) public view returns (address) { 168 | return jobs.at(index); 169 | } 170 | 171 | // --- Job helper functions --- 172 | function getNextJobs(bytes32 network, uint256 startIndex, uint256 endIndexExcl) public returns (WorkableJob[] memory) { 173 | if (endIndexExcl < startIndex) revert BadIndicies(startIndex, endIndexExcl); 174 | uint256 length = jobs.length(); 175 | if (endIndexExcl > length) revert IndexTooHigh(endIndexExcl, length); 176 | 177 | WorkableJob[] memory _jobs = new WorkableJob[](endIndexExcl - startIndex); 178 | for (uint256 i = startIndex; i < endIndexExcl; i++) { 179 | JobLike job = JobLike(jobs.at(i)); 180 | (bool canWork, bytes memory args) = job.workable(network); 181 | _jobs[i - startIndex] = WorkableJob(address(job), canWork, args); 182 | } 183 | return _jobs; 184 | } 185 | function getNextJobs(bytes32 network) external returns (WorkableJob[] memory) { 186 | return getNextJobs(network, 0, jobs.length()); 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /src/VestedRewardsDistributionJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {IJob} from "./interfaces/IJob.sol"; 19 | import "./utils/EnumerableSet.sol"; 20 | 21 | interface SequencerLike { 22 | function isMaster(bytes32 network) external view returns (bool); 23 | } 24 | 25 | interface DssVestWithGemLike { 26 | function unpaid(uint256 _id) external view returns (uint256); 27 | } 28 | 29 | interface VestedRewardsDistributionLike { 30 | function distribute() external returns (uint256 amount); 31 | function dssVest() external view returns (DssVestWithGemLike); 32 | function lastDistributedAt() external view returns (uint256); 33 | function vestId() external view returns (uint256); 34 | } 35 | 36 | /// @title Call distribute() when possible 37 | contract VestedRewardsDistributionJob is IJob { 38 | using EnumerableSet for EnumerableSet.AddressSet; 39 | 40 | /// @notice Keeper Network sequencer. 41 | SequencerLike public immutable sequencer; 42 | 43 | /// @notice Address with admin access to this contract. wards[usr]. 44 | mapping(address => uint256) public wards; 45 | /// @notice Minimum intervals between distributions for each distribution contract. intervals[dist]. 46 | mapping(address => uint256) public intervals; 47 | /// @notice Iterable set of distribution contracts added to the job. 48 | EnumerableSet.AddressSet private distributions; 49 | 50 | // --- Errors --- 51 | 52 | /** 53 | * @notice The keeper trying to execute `work` is not the current master. 54 | * @param network The keeper identifier. 55 | */ 56 | error NotMaster(bytes32 network); 57 | /// @notice No args were provided to `work`. 58 | error NoArgs(); 59 | /// @notice Trying to set the distribution contract interval to zero. 60 | error InvalidInterval(); 61 | /// @notice `wark` was called too early or no vested amount is available. 62 | error NotDue(address dist); 63 | /// @notice The distribution contract was was not added to the job. 64 | error NotFound(address dist); 65 | 66 | // --- Events --- 67 | 68 | /** 69 | * @notice `usr` was granted admin access. 70 | * @param usr The user address. 71 | */ 72 | event Rely(address indexed usr); 73 | /** 74 | * @notice `usr` admin access was revoked. 75 | * @param usr The user address. 76 | */ 77 | event Deny(address indexed usr); 78 | /** 79 | * @notice A `VestedRewardsDistribution` contract was added to or modified in the job. 80 | * @param dist The distribution contract. 81 | * @param interval The minimum interval between distributions. 82 | */ 83 | event Set(address indexed dist, uint256 interval); 84 | /** 85 | * @notice A distribution contract was removed from the job. 86 | * @param dist The removed distribution contract. 87 | */ 88 | event Rem(address indexed dist); 89 | /** 90 | * @notice Work os executed for a distribution contract. 91 | * @param network The keeper who executed the job. 92 | * @param dist The distribution contract where the distribution was made. 93 | * @param amount The amount distributed. 94 | */ 95 | event Work(bytes32 indexed network, address indexed dist, uint256 amount); 96 | 97 | /** 98 | * @param _sequencer The keeper network sequencer. 99 | */ 100 | constructor(address _sequencer) { 101 | sequencer = SequencerLike(_sequencer); 102 | 103 | wards[msg.sender] = 1; 104 | emit Rely(msg.sender); 105 | } 106 | 107 | // --- Auth --- 108 | 109 | modifier auth() { 110 | require(wards[msg.sender] == 1, "VestedRewardsDistributionJob/not-authorized"); 111 | _; 112 | } 113 | 114 | /** 115 | * @notice Grants `usr` admin access to this contract. 116 | * @param usr The user address. 117 | */ 118 | function rely(address usr) external auth { 119 | wards[usr] = 1; 120 | emit Rely(usr); 121 | } 122 | 123 | /** 124 | * @notice Revokes `usr` admin access from this contract. 125 | * @param usr The user address. 126 | */ 127 | function deny(address usr) external auth { 128 | wards[usr] = 0; 129 | emit Deny(usr); 130 | } 131 | 132 | // --- Rewards Distribution Admin --- 133 | 134 | /** 135 | * @notice Sets the interval for the distribution contract in the job. 136 | * @dev `interval` MUST be greater than zero. 137 | * @param dist The distribution contract. 138 | * @param interval The interval for distribution. 139 | */ 140 | function set(address dist, uint256 interval) external auth { 141 | if (interval == 0) revert InvalidInterval(); 142 | 143 | if (!distributions.contains(dist)) distributions.add(dist); 144 | intervals[dist] = interval; 145 | emit Set(dist, interval); 146 | } 147 | 148 | /** 149 | * @notice Removes the distribution contract from the job. 150 | * @param dist The distribution contract. 151 | */ 152 | function rem(address dist) external auth { 153 | if (!distributions.remove(dist)) revert NotFound(dist); 154 | 155 | delete intervals[dist]; 156 | emit Rem(dist); 157 | } 158 | 159 | /** 160 | * @notice Checks if the job has the specified distribution contract. 161 | * @param dist The distribution contract. 162 | * @return Whether the distribution contract is set in the job or not. 163 | */ 164 | function has(address dist) public view returns (bool) { 165 | return distributions.contains(dist); 166 | } 167 | 168 | /** 169 | * @notice Checks if the distribution is due for the specified contract. 170 | * @param dist The distribution contract. 171 | * @return Whether the distribution is due or not. 172 | */ 173 | function due(address dist) public view returns (bool) { 174 | // Gets the last time distribute() was called 175 | uint256 last = VestedRewardsDistributionLike(dist).lastDistributedAt(); 176 | // If `last == 0` (no distribution so far), we allow it to be distributed immediately, 177 | // otherwise, we can only distribute if enough time has elapsed since the last one. 178 | if (last != 0 && block.timestamp < last + intervals[dist]) return false; 179 | 180 | uint256 vestId = VestedRewardsDistributionLike(dist).vestId(); 181 | DssVestWithGemLike vest = VestedRewardsDistributionLike(dist).dssVest(); 182 | // Distribution is only due if there are unpaid tokens. 183 | return vest.unpaid(vestId) > 0; 184 | } 185 | 186 | // --- Keeper Network Interface --- 187 | 188 | /** 189 | * @notice Executes the job though the keeper network. 190 | * @param network The keeper identifier. 191 | * @param args The arguments for execution. 192 | */ 193 | function work(bytes32 network, bytes calldata args) external { 194 | if (!sequencer.isMaster(network)) revert NotMaster(network); 195 | if (args.length == 0) revert NoArgs(); 196 | 197 | (address dist) = abi.decode(args, (address)); 198 | // Prevents keeper from calling random contracts with a `distribute` method. 199 | if (!has(dist)) revert NotFound(dist); 200 | // Ensures that enough time has passed. 201 | if (!due(dist)) revert NotDue(dist); 202 | 203 | uint256 amount = VestedRewardsDistributionLike(dist).distribute(); 204 | emit Work(network, dist, amount); 205 | } 206 | 207 | /** 208 | * @notice Checks if there is work to be done in the job. 209 | * @dev Most providers define a gas limit for `eth_call` requests to prevent DoS. 210 | * Notice that hitting that limit is higly unlikely, as it would require hundreds or thousands of active 211 | * contracts in this job. 212 | * Keepers are expected to take that into consideration, especially if they are using self-hosted 213 | * infrastructure, which might have arbitrary values configured. 214 | * @param network The keeper identifier. 215 | * @return ok Whether it should execute or not. 216 | * @return args The args for execution. 217 | */ 218 | function workable(bytes32 network) external override returns (bool ok, bytes memory args) { 219 | if (!sequencer.isMaster(network)) return (false, bytes("Network is not master")); 220 | 221 | uint256 len = distributions.length(); 222 | for (uint256 i = 0; i < len; i++) { 223 | address dist = distributions.at(i); 224 | if (!due(dist)) continue; 225 | 226 | try this.work(network, abi.encode(dist)) { 227 | return (true, abi.encode(dist)); 228 | } catch { 229 | continue; 230 | } 231 | } 232 | return (false, bytes("No distribution")); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/base/TimedJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {IJob} from "../interfaces/IJob.sol"; 19 | 20 | interface SequencerLike { 21 | function isMaster(bytes32 network) external view returns (bool); 22 | } 23 | 24 | /// @title A job that executes at a fixed interval 25 | /// @dev Extend this contract to easily execute some action at a fixed interval 26 | abstract contract TimedJob is IJob { 27 | 28 | SequencerLike public immutable sequencer; 29 | uint256 public immutable maxDuration; // The max duration between ticks 30 | uint256 public last; 31 | 32 | // --- Errors --- 33 | error NotMaster(bytes32 network); 34 | error TimerNotElapsed(uint256 currentTime, uint256 expiry); 35 | error ShouldUpdateIsFalse(); 36 | 37 | // --- Events --- 38 | event Work(bytes32 indexed network); 39 | 40 | constructor(address _sequencer, uint256 _maxDuration) { 41 | sequencer = SequencerLike(_sequencer); 42 | maxDuration = _maxDuration; 43 | } 44 | 45 | function work(bytes32 network, bytes calldata) external { 46 | if (!sequencer.isMaster(network)) revert NotMaster(network); 47 | uint256 expiry = last + maxDuration; 48 | if (block.timestamp < expiry) revert TimerNotElapsed(block.timestamp, expiry); 49 | if (!shouldUpdate()) revert ShouldUpdateIsFalse(); 50 | 51 | last = block.timestamp; 52 | update(); 53 | 54 | emit Work(network); 55 | } 56 | 57 | function workable(bytes32 network) external view override returns (bool, bytes memory) { 58 | if (!sequencer.isMaster(network)) return (false, bytes("Network is not master")); 59 | if (block.timestamp < last + maxDuration) return (false, bytes("Timer hasn't elapsed")); 60 | if (!shouldUpdate()) return (false, bytes("shouldUpdate is false")); 61 | 62 | return (true, ""); 63 | } 64 | 65 | function shouldUpdate() virtual internal view returns (bool); 66 | function update() virtual internal; 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/deployment/LitePsmJob/LitePsmJobDeploy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2023 Dai Foundation 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {ScriptTools} from "dss-test/ScriptTools.sol"; 19 | import {LitePsmJob} from "src/LitePsmJob.sol"; 20 | import {LitePsmJobInstance} from "./LitePsmJobInstance.sol"; 21 | 22 | struct LitePsmJobDeployParams { 23 | address sequencer; 24 | address litePsm; 25 | uint256 rushThreshold; 26 | uint256 gushThreshold; 27 | uint256 cutThreshold; 28 | } 29 | 30 | library LitePsmJobDeploy { 31 | function deploy(LitePsmJobDeployParams memory p) internal returns (LitePsmJobInstance memory r) { 32 | r.job = address(new LitePsmJob({ 33 | _sequencer: p.sequencer, 34 | _litePsm: p.litePsm, 35 | _rushThreshold: p.rushThreshold, 36 | _gushThreshold: p.gushThreshold, 37 | _cutThreshold: p.cutThreshold 38 | })); 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/deployment/LitePsmJob/LitePsmJobInstance.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2023 Dai Foundation 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | struct LitePsmJobInstance { 19 | address job; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/deployment/VestedRewardsDistributionJob/VestedRewardsDistributionJobDeploy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2023 Dai Foundation 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import {ScriptTools} from "dss-test/ScriptTools.sol"; 19 | import {VestedRewardsDistributionJob} from "src/VestedRewardsDistributionJob.sol"; 20 | 21 | struct VestedRewardsDistributionJobDeployConfig { 22 | address deployer; 23 | address owner; 24 | address sequencer; 25 | } 26 | 27 | library VestedRewardsDistributionJobDeploy { 28 | function deploy(VestedRewardsDistributionJobDeployConfig memory cfg) 29 | internal 30 | returns (address job) 31 | { 32 | job = address(new VestedRewardsDistributionJob({_sequencer: cfg.sequencer})); 33 | ScriptTools.switchOwner(job, cfg.deployer, cfg.owner); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/deployment/VestedRewardsDistributionJob/VestedRewardsDistributionJobDeploy.t.integration.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2023 Dai Foundation 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "dss-test/DssTest.sol"; 19 | import { 20 | VestedRewardsDistributionJobDeploy, 21 | VestedRewardsDistributionJobDeployConfig 22 | } from "./VestedRewardsDistributionJobDeploy.sol"; 23 | 24 | contract VestedRewardsDistributionJobDeployTest is DssTest { 25 | address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; 26 | 27 | DssInstance dss; 28 | address pauseProxy; 29 | address sequencer; 30 | DeployCaller caller; 31 | VestedRewardsDistributionJobDeployConfig cfg; 32 | 33 | function setUp() public { 34 | vm.createSelectFork("mainnet"); 35 | 36 | dss = MCD.loadFromChainlog(CHAINLOG); 37 | pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY"); 38 | sequencer = dss.chainlog.getAddress("CRON_SEQUENCER"); 39 | caller = new DeployCaller(); 40 | cfg = 41 | VestedRewardsDistributionJobDeployConfig({deployer: address(caller), owner: pauseProxy, sequencer: sequencer}); 42 | } 43 | 44 | function testDeploy() public { 45 | VestedRewardsDistributionJobLike job = VestedRewardsDistributionJobLike(caller.deploy(cfg)); 46 | 47 | assertEq(job.sequencer(), sequencer, "invalid sequencer"); 48 | assertEq(job.wards(pauseProxy), 1, "pauseProxy not ward"); 49 | assertEq(job.wards(address(caller)), 0, "deployer still ward"); 50 | } 51 | } 52 | 53 | interface VestedRewardsDistributionJobLike { 54 | function sequencer() external view returns (address); 55 | function wards(address who) external view returns (uint256); 56 | } 57 | 58 | contract DeployCaller { 59 | function deploy(VestedRewardsDistributionJobDeployConfig memory cfg) external returns (address) { 60 | return VestedRewardsDistributionJobDeploy.deploy(cfg); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/deployment/VestedRewardsDistributionJob/VestedRewardsDistributionJobInit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2023 Dai Foundation 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | struct VestedRewardsDistributionJobInitConfig { 19 | bytes32 jobKey; // Chainlog key 20 | } 21 | 22 | struct VestedRewardsDistributionJobDeinitConfig { 23 | bytes32 jobKey; // Chainlog key 24 | } 25 | 26 | struct VestedRewardsDistributionJobSetDistConfig { 27 | address dist; 28 | uint256 interval; 29 | } 30 | 31 | struct VestedRewardsDistributionJobRemDistConfig { 32 | address dist; 33 | } 34 | 35 | library VestedRewardsDistributionJobInit { 36 | ChainlogLike internal constant chainlog = ChainlogLike(0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F); 37 | 38 | function init(address job, VestedRewardsDistributionJobInitConfig memory cfg) internal { 39 | SequencerLike sequencer = SequencerLike(chainlog.getAddress("CRON_SEQUENCER")); 40 | require( 41 | VestedRewardsDistributionJobLike(job).sequencer() == address(sequencer), 42 | "VestedRewardsDistributionJobInit/invalid-sequencer" 43 | ); 44 | sequencer.addJob(job); 45 | chainlog.setAddress(cfg.jobKey, job); 46 | } 47 | 48 | function deinit(address job, VestedRewardsDistributionJobDeinitConfig memory cfg) internal { 49 | SequencerLike sequencer = SequencerLike(chainlog.getAddress("CRON_SEQUENCER")); 50 | require( 51 | VestedRewardsDistributionJobLike(job).sequencer() == address(sequencer), 52 | "VestedRewardsDistributionJobInit/invalid-sequencer" 53 | ); 54 | sequencer.removeJob(job); 55 | chainlog.removeAddress(cfg.jobKey); 56 | } 57 | 58 | function setDist(address job, VestedRewardsDistributionJobSetDistConfig memory cfg) internal { 59 | VestedRewardsDistributionJobLike(job).set(cfg.dist, cfg.interval); 60 | } 61 | 62 | function remDist(address job, VestedRewardsDistributionJobRemDistConfig memory cfg) internal { 63 | VestedRewardsDistributionJobLike(job).rem(cfg.dist); 64 | } 65 | } 66 | 67 | interface VestedRewardsDistributionJobLike { 68 | function sequencer() external view returns (address); 69 | function set(address dist, uint256 interval) external; 70 | function rem(address dist) external; 71 | } 72 | 73 | interface ChainlogLike { 74 | function getAddress(bytes32 key) external view returns (address); 75 | function removeAddress(bytes32 key) external; 76 | function setAddress(bytes32 key, address val) external; 77 | } 78 | 79 | interface SequencerLike { 80 | function addJob(address job) external; 81 | function removeJob(address job) external; 82 | } 83 | -------------------------------------------------------------------------------- /src/deployment/VestedRewardsDistributionJob/VestedRewardsDistributionJobInit.t.integration.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2023 Dai Foundation 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "dss-test/DssTest.sol"; 19 | import { 20 | VestedRewardsDistributionJobDeploy, 21 | VestedRewardsDistributionJobDeployConfig 22 | } from "./VestedRewardsDistributionJobDeploy.sol"; 23 | import { 24 | VestedRewardsDistributionJobInit, 25 | VestedRewardsDistributionJobInitConfig, 26 | VestedRewardsDistributionJobDeinitConfig, 27 | VestedRewardsDistributionJobSetDistConfig, 28 | VestedRewardsDistributionJobRemDistConfig 29 | } from "./VestedRewardsDistributionJobInit.sol"; 30 | 31 | contract VestedRewardsDistributionJobInitTest is DssTest { 32 | address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; 33 | 34 | DssInstance dss; 35 | address pause; 36 | ProxyLike pauseProxy; 37 | SequencerLike sequencer; 38 | Caller caller; 39 | VestedRewardsDistributionJobLike job; 40 | bytes32 constant JOB_KEY = "CRON_VESTED_REWARDS_DIST_JOB"; 41 | 42 | function setUp() public { 43 | vm.createSelectFork("mainnet"); 44 | 45 | dss = MCD.loadFromChainlog(CHAINLOG); 46 | pause = dss.chainlog.getAddress("MCD_PAUSE"); 47 | pauseProxy = ProxyLike(dss.chainlog.getAddress("MCD_PAUSE_PROXY")); 48 | sequencer = SequencerLike(dss.chainlog.getAddress("CRON_SEQUENCER")); 49 | caller = new Caller(); 50 | job = VestedRewardsDistributionJobLike( 51 | caller.deploy( 52 | VestedRewardsDistributionJobDeployConfig({ 53 | deployer: address(caller), 54 | owner: address(pauseProxy), 55 | sequencer: address(sequencer) 56 | }) 57 | ) 58 | ); 59 | } 60 | 61 | function testInit() public { 62 | assertFalse(sequencer.hasJob(address(job)), "job already added to sequencer"); 63 | try dss.chainlog.getAddress(JOB_KEY) { 64 | revert("job already in chainlog"); 65 | } catch {} 66 | 67 | // Simulate a spell casting 68 | vm.prank(pause); 69 | pauseProxy.exec( 70 | address(caller), 71 | abi.encodeCall(caller.init, (address(job), VestedRewardsDistributionJobInitConfig({jobKey: JOB_KEY}))) 72 | ); 73 | 74 | assertTrue(sequencer.hasJob(address(job)), "job not added to sequencer"); 75 | assertEq(dss.chainlog.getAddress(JOB_KEY), address(job), "job not added to chainlog"); 76 | } 77 | 78 | function testDeinit() public { 79 | // Simulate a spell casting 80 | vm.prank(pause); 81 | pauseProxy.exec( 82 | address(caller), 83 | abi.encodeCall(caller.init, (address(job), VestedRewardsDistributionJobInitConfig({jobKey: JOB_KEY}))) 84 | ); 85 | 86 | assertTrue(sequencer.hasJob(address(job)), "job not in sequencer"); 87 | assertEq(dss.chainlog.getAddress(JOB_KEY), address(job), "job not in chainlog"); 88 | 89 | // Simulate a spell casting 90 | vm.prank(pause); 91 | pauseProxy.exec( 92 | address(caller), 93 | abi.encodeCall(caller.deinit, (address(job), VestedRewardsDistributionJobDeinitConfig({jobKey: JOB_KEY}))) 94 | ); 95 | 96 | assertFalse(sequencer.hasJob(address(job)), "job not removed from sequencer"); 97 | try dss.chainlog.getAddress(JOB_KEY) { 98 | revert("job not removed from chainlog"); 99 | } catch {} 100 | } 101 | 102 | function testSetDist() public { 103 | // Simulate a spell casting 104 | vm.prank(pause); 105 | pauseProxy.exec( 106 | address(caller), 107 | abi.encodeCall(caller.init, (address(job), VestedRewardsDistributionJobInitConfig({jobKey: JOB_KEY}))) 108 | ); 109 | 110 | address dist = address(0x1337); 111 | uint256 interval = 7 days; 112 | 113 | assertFalse(job.has(dist), "dist already in job"); 114 | assertEq(job.intervals(dist), 0, "dist interval already configured in job"); 115 | 116 | // Simulate a spell casting 117 | vm.prank(pause); 118 | pauseProxy.exec( 119 | address(caller), 120 | abi.encodeCall( 121 | caller.setDist, 122 | (address(job), VestedRewardsDistributionJobSetDistConfig({dist: dist, interval: interval})) 123 | ) 124 | ); 125 | 126 | assertTrue(job.has(dist), "dist not added to job"); 127 | assertEq(job.intervals(dist), interval, "dist interval not set in job"); 128 | } 129 | 130 | function testRemDist() public { 131 | // Simulate a spell casting 132 | vm.prank(pause); 133 | pauseProxy.exec( 134 | address(caller), 135 | abi.encodeCall(caller.init, (address(job), VestedRewardsDistributionJobInitConfig({jobKey: JOB_KEY}))) 136 | ); 137 | 138 | address dist = address(0x1337); 139 | uint256 interval = 7 days; 140 | // Simulate a spell casting 141 | vm.prank(pause); 142 | pauseProxy.exec( 143 | address(caller), 144 | abi.encodeCall( 145 | caller.setDist, 146 | (address(job), VestedRewardsDistributionJobSetDistConfig({dist: dist, interval: interval})) 147 | ) 148 | ); 149 | 150 | assertTrue(job.has(dist), "dist not in job"); 151 | assertEq(job.intervals(dist), interval, "dist interval not configured in job"); 152 | 153 | // Simulate a spell casting 154 | vm.prank(pause); 155 | pauseProxy.exec( 156 | address(caller), 157 | abi.encodeCall(caller.remDist, (address(job), VestedRewardsDistributionJobRemDistConfig({dist: dist}))) 158 | ); 159 | 160 | assertFalse(job.has(dist), "dist not removed from job"); 161 | assertEq(job.intervals(dist), 0, "dist interval not removed from job"); 162 | } 163 | } 164 | 165 | interface ProxyLike { 166 | function exec(address usr, bytes memory fax) external returns (bytes memory out); 167 | } 168 | 169 | interface VestedRewardsDistributionJobLike { 170 | function has(address job) external view returns (bool); 171 | function intervals(address job) external view returns (uint256); 172 | function sequencer() external view returns (address); 173 | function wards(address who) external view returns (uint256); 174 | } 175 | 176 | interface SequencerLike { 177 | function hasJob(address job) external view returns (bool); 178 | } 179 | 180 | contract Caller { 181 | function deploy(VestedRewardsDistributionJobDeployConfig memory cfg) external returns (address) { 182 | return VestedRewardsDistributionJobDeploy.deploy(cfg); 183 | } 184 | 185 | function init(address job, VestedRewardsDistributionJobInitConfig memory cfg) external { 186 | VestedRewardsDistributionJobInit.init(job, cfg); 187 | } 188 | 189 | function deinit(address job, VestedRewardsDistributionJobDeinitConfig memory cfg) external { 190 | VestedRewardsDistributionJobInit.deinit(job, cfg); 191 | } 192 | 193 | function setDist(address job, VestedRewardsDistributionJobSetDistConfig memory cfg) external { 194 | VestedRewardsDistributionJobInit.setDist(job, cfg); 195 | } 196 | 197 | function remDist(address job, VestedRewardsDistributionJobRemDistConfig memory cfg) external { 198 | VestedRewardsDistributionJobInit.remDist(job, cfg); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/interfaces/IJob.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity >=0.8.0; 17 | 18 | /// @title Maker Keeper Network Job 19 | /// @notice A job represents an independant unit of work that can be done by a keeper 20 | interface IJob { 21 | 22 | /// @notice Executes this unit of work 23 | /// @dev Should revert iff workable() returns canWork of false 24 | /// @param network The name of the external keeper network 25 | /// @param args Custom arguments supplied to the job, should be copied from workable response 26 | function work(bytes32 network, bytes calldata args) external; 27 | 28 | /// @notice Ask this job if it has a unit of work available 29 | /// @dev This should never revert, only return false if nothing is available 30 | /// @dev This should normally be a view, but sometimes that's not possible 31 | /// @param network The name of the external keeper network 32 | /// @return canWork Returns true if a unit of work is available 33 | /// @return args The custom arguments to be provided to work() or an error string if canWork is false 34 | function workable(bytes32 network) external returns (bool canWork, bytes memory args); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/interfaces/INetworkTreasury.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2022 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | interface INetworkTreasury { 19 | 20 | /** 21 | * @dev This should return an estimate of the total value of the buffer in DAI. 22 | * Keeper Networks should convert non-DAI assets to DAI value via an oracle. 23 | * 24 | * Ex) If the network bulk trades DAI for ETH then the value of the ETH sitting 25 | * in the treasury should count towards this buffer size. 26 | */ 27 | function getBufferSize() external view returns (uint256); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/tests/AutoLineJob-integration.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "./DssCronBase.t.sol"; 19 | import {DssAutoLineAbstract} from "dss-interfaces/Interfaces.sol"; 20 | 21 | import {AutoLineJob} from "../AutoLineJob.sol"; 22 | 23 | contract AutoLineJobIntegrationTest is DssCronBaseTest { 24 | 25 | using GodMode for *; 26 | 27 | DssAutoLineAbstract autoline; 28 | 29 | AutoLineJob autoLineJob; 30 | 31 | function setUpSub() virtual override internal { 32 | autoline = DssAutoLineAbstract(dss.chainlog.getAddress("MCD_IAM_AUTO_LINE")); 33 | 34 | // Setup with 10% / 50% bands 35 | autoLineJob = new AutoLineJob(address(sequencer), address(ilkRegistry), address(autoline), 1000, 5000); 36 | 37 | dss.vat.setWard(address(this), 1); 38 | address(autoline).setWard(address(this), 1); 39 | 40 | // Setup a dummy ilk in the vat 41 | dss.vat.init(ILK); 42 | dss.vat.file(ILK, "spot", RAY); 43 | dss.vat.file(ILK, "line", 1_000 * RAD); 44 | autoline.setIlk(ILK, 100_000 * RAD, 1_000 * RAD, 8 hours); 45 | 46 | // Add to ilk regitry as well (only care about the ilk ids array) 47 | bytes32 pos = bytes32(uint256(5)); 48 | uint256 size = uint256(GodMode.vm().load(address(ilkRegistry), pos)); 49 | GodMode.vm().store( 50 | address(ilkRegistry), 51 | bytes32(uint256(keccak256(abi.encode(pos))) + size), 52 | ILK 53 | ); // Append new ilk 54 | GodMode.vm().store( 55 | address(ilkRegistry), 56 | pos, 57 | bytes32(size + 1) 58 | ); // Increase size of array 59 | 60 | // Clear out any autolines that need to be triggered 61 | clear_other_ilks(NET_A); 62 | } 63 | 64 | function clear_other_ilks(bytes32 network) internal { 65 | while(true) { 66 | (bool canWork, bytes memory args) = autoLineJob.workable(network); 67 | if (!canWork) break; 68 | bytes32 ilk = abi.decode(args, (bytes32)); 69 | if (ilk == ILK) break; 70 | (,,, uint256 line,) = dss.vat.ilks(ilk); 71 | autoLineJob.work(network, args); 72 | (,,, uint256 newLine,) = dss.vat.ilks(ilk); 73 | assertTrue(line != newLine, "Line should have changed."); 74 | } 75 | } 76 | 77 | function mint(bytes32 ilk, uint256 wad) internal { 78 | (uint256 Art,,,,) = dss.vat.ilks(ilk); 79 | dss.vat.slip(ilk, address(this), int256(wad)); 80 | dss.vat.frob(ilk, address(this), address(this), address(this), int256(wad), int256(wad)); 81 | (uint256 nextArt,,,,) = dss.vat.ilks(ilk); 82 | assertEq(nextArt, Art + wad); 83 | } 84 | function repay(bytes32 ilk, uint256 wad) internal { 85 | (uint256 Art,,,,) = dss.vat.ilks(ilk); 86 | dss.vat.frob(ilk, address(this), address(this), address(this), -int256(wad), -int256(wad)); 87 | dss.vat.slip(ilk, address(this), -int256(wad)); 88 | (uint256 nextArt,,,,) = dss.vat.ilks(ilk); 89 | assertEq(nextArt, Art - wad); 90 | } 91 | 92 | function trigger_next_autoline_job(bytes32 network, bytes32 ilk) internal { 93 | (bool canWork, bytes memory args) = autoLineJob.workable(network); 94 | assertTrue(canWork, "Expecting to be able to execute."); 95 | bytes memory expectedArgs = abi.encode(ilk); 96 | for (uint256 i = 0; i < expectedArgs.length; i++) { 97 | assertEq(args[i], expectedArgs[i]); 98 | } 99 | (,,, uint256 line,) = dss.vat.ilks(ilk); 100 | autoLineJob.work(network, args); 101 | (,,, uint256 newLine,) = dss.vat.ilks(ilk); 102 | assertTrue(line != newLine, "Line should have changed."); 103 | } 104 | 105 | function verify_no_autoline_job(bytes32 network) internal { 106 | (bool canWork, bytes memory args) = autoLineJob.workable(network); 107 | assertTrue(!canWork, "Expecting NOT to be able to execute."); 108 | bytes memory expectedArgs = "No ilks ready"; 109 | for (uint256 i = 0; i < expectedArgs.length; i++) { 110 | assertEq(args[i], expectedArgs[i]); 111 | } 112 | } 113 | 114 | function test_raise_line() public { 115 | verify_no_autoline_job(NET_A); 116 | 117 | mint(ILK, 110 * WAD); // Over the threshold to raise the DC (10%) 118 | 119 | trigger_next_autoline_job(NET_A, ILK); 120 | 121 | verify_no_autoline_job(NET_A); 122 | } 123 | 124 | function test_disabled() public { 125 | verify_no_autoline_job(NET_A); 126 | 127 | mint(ILK, 110 * WAD); 128 | 129 | // Disable the autoline 130 | autoline.remIlk(ILK); 131 | 132 | verify_no_autoline_job(NET_A); 133 | } 134 | 135 | function test_same_block() public { 136 | verify_no_autoline_job(NET_A); 137 | 138 | mint(ILK, 200 * WAD); 139 | trigger_next_autoline_job(NET_A, ILK); 140 | mint(ILK, 200 * WAD); 141 | verify_no_autoline_job(NET_A); 142 | } 143 | 144 | function test_under_ttl() public { 145 | verify_no_autoline_job(NET_A); 146 | 147 | mint(ILK, 200 * WAD); 148 | trigger_next_autoline_job(NET_A, ILK); 149 | 150 | GodMode.vm().roll(block.number + 1); 151 | 152 | // It's possible some other ilks are valid now 153 | clear_other_ilks(NET_A); 154 | 155 | mint(ILK, 200 * WAD); 156 | verify_no_autoline_job(NET_A); 157 | } 158 | 159 | function test_diff_block_ttl() public { 160 | verify_no_autoline_job(NET_A); 161 | 162 | mint(ILK, 200 * WAD); 163 | trigger_next_autoline_job(NET_A, ILK); 164 | 165 | GodMode.vm().roll(block.number + 1); 166 | GodMode.vm().warp(block.timestamp + 8 hours); 167 | 168 | // It's possible some other ilks are valid now 169 | clear_other_ilks(NET_A); 170 | 171 | mint(ILK, 200 * WAD); 172 | trigger_next_autoline_job(NET_A, ILK); 173 | } 174 | 175 | function test_lower_line() public { 176 | verify_no_autoline_job(NET_A); 177 | 178 | mint(ILK, 1000 * WAD); 179 | trigger_next_autoline_job(NET_A, ILK); 180 | GodMode.vm().roll(block.number + 1); 181 | GodMode.vm().warp(block.timestamp + 8 hours); 182 | clear_other_ilks(NET_A); 183 | mint(ILK, 1000 * WAD); 184 | trigger_next_autoline_job(NET_A, ILK); 185 | GodMode.vm().roll(block.number + 1); 186 | repay(ILK, 500 * WAD); // 50% threshold of gap 187 | trigger_next_autoline_job(NET_A, ILK); 188 | verify_no_autoline_job(NET_A); 189 | } 190 | 191 | function test_autoline_param_change() public { 192 | // Adjust max line / gap 193 | autoline.setIlk(ILK, 6_000 * RAD, 5_000 * RAD, 8 hours); 194 | 195 | // Should be triggerable now as we are 1000 away from 196 | // the line which is 80% above the line - gap 197 | trigger_next_autoline_job(NET_A, ILK); 198 | } 199 | 200 | function test_max_line_within_do_nothing_range() public { 201 | // Set the new gap / maxLine to be slightly less 202 | autoline.setIlk(ILK, 999 * RAD, 999 * RAD, 8 hours); 203 | 204 | // This should be within the do-nothing range, but should still 205 | // trigger due to the next adjustment being set to maxLine 206 | trigger_next_autoline_job(NET_A, ILK); 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /src/tests/ClipperMomJob-integration.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "./DssCronBase.t.sol"; 19 | import {ClipperMomAbstract,ClipAbstract} from "dss-interfaces/Interfaces.sol"; 20 | 21 | import {ClipperMomJob} from "../ClipperMomJob.sol"; 22 | 23 | contract ClipperMomJobIntegrationTest is DssCronBaseTest { 24 | 25 | using GodMode for *; 26 | using MCD for DssInstance; 27 | 28 | ClipperMomAbstract clipperMom; 29 | DssIlkInstance ilk; 30 | 31 | ClipperMomJob clipperMomJob; 32 | 33 | function setUpSub() virtual override internal { 34 | clipperMom = ClipperMomAbstract(dss.chainlog.getAddress("CLIPPER_MOM")); 35 | ilk = dss.getIlk("ETH", "A"); 36 | 37 | // Execute all lerps once a day 38 | clipperMomJob = new ClipperMomJob(address(sequencer), address(ilkRegistry), address(clipperMom)); 39 | } 40 | 41 | function set_bad_price(address clip, address pip) internal { 42 | uint256 tolerance = clipperMom.tolerance(clip); 43 | bytes32 _cur = GodMode.vm().load( 44 | address(pip), 45 | bytes32(uint256(3)) 46 | ); 47 | uint256 cur = uint256(_cur) & type(uint128).max; 48 | uint256 nxt = cur * tolerance / RAY - 1; 49 | GodMode.vm().store( 50 | address(pip), 51 | bytes32(uint256(4)), 52 | bytes32((1 << 128) | nxt) 53 | ); 54 | } 55 | 56 | function test_no_break() public { 57 | // By default there should be no clipper that is triggerable except in the very rare circumstance of oracle attack 58 | (bool canWork,) = clipperMomJob.workable(NET_A); 59 | assertTrue(!canWork); 60 | } 61 | 62 | function test_no_break_invalid_xlip() public { 63 | bytes32 ilk_ = ilk.clip.ilk(); 64 | vm.prank(dss.chainlog.getAddress("MCD_PAUSE_PROXY")); ilkRegistry.file(ilk_, "xlip", address(123)); 65 | assertEq(ilkRegistry.xlip(ilk_), address(123)); 66 | 67 | // Make sure that workable() still finishes even when the xlip is an empty code address which is not zero 68 | (bool canWork,) = clipperMomJob.workable(NET_A); 69 | assertTrue(!canWork); 70 | } 71 | 72 | function test_break() public { 73 | // Place a bad oracle price in the OSM 74 | set_bad_price(address(ilk.clip), address(ilk.pip)); 75 | 76 | // Should be able to work and target the ETH-A clipper 77 | // Workable triggers the actual clipperMom.tripBreaker() 78 | assertEq(ilk.clip.stopped(), 0); 79 | (bool canWork, bytes memory args) = clipperMomJob.workable(NET_A); 80 | assertTrue(canWork); 81 | assertEq(abi.decode(args, (address)), address(ilk.clip)); 82 | assertEq(ilk.clip.stopped(), 2); 83 | } 84 | 85 | function test_break_work() public { 86 | // Place a bad oracle price in the OSM 87 | set_bad_price(address(ilk.clip), address(ilk.pip)); 88 | 89 | // Test the actual work function 90 | assertEq(ilk.clip.stopped(), 0); 91 | clipperMomJob.work(NET_A, abi.encode(address(ilk.clip))); 92 | assertEq(ilk.clip.stopped(), 2); 93 | } 94 | 95 | function test_break_multiple() public { 96 | // Place a bad oracle price in the OSM 97 | set_bad_price(address(ilk.clip), address(ilk.pip)); 98 | 99 | // Should be able to trigger 3 clips 100 | ClipAbstract wethBClip = ClipAbstract(dss.chainlog.getAddress("MCD_CLIP_ETH_B")); 101 | ClipAbstract wethCClip = ClipAbstract(dss.chainlog.getAddress("MCD_CLIP_ETH_C")); 102 | 103 | // ETH-A 104 | assertEq(ilk.clip.stopped(), 0); 105 | (bool canWork, bytes memory args) = clipperMomJob.workable(NET_A); 106 | assertTrue(canWork); 107 | assertEq(abi.decode(args, (address)), address(ilk.clip)); 108 | assertEq(ilk.clip.stopped(), 2); 109 | 110 | // ETH-B 111 | assertEq(wethBClip.stopped(), 0); 112 | (canWork, args) = clipperMomJob.workable(NET_A); 113 | assertTrue(canWork); 114 | assertEq(abi.decode(args, (address)), address(wethBClip)); 115 | assertEq(wethBClip.stopped(), 2); 116 | 117 | // ETH-C 118 | assertEq(wethCClip.stopped(), 0); 119 | (canWork, args) = clipperMomJob.workable(NET_A); 120 | assertTrue(canWork); 121 | assertEq(abi.decode(args, (address)), address(wethCClip)); 122 | assertEq(wethCClip.stopped(), 2); 123 | } 124 | 125 | // An old ClipperMomJob implementation supported only class 1, check it now supports other classes 126 | function test_break_other_class() public { 127 | // Place a bad oracle price in the OSM 128 | set_bad_price(address(ilk.clip), address(ilk.pip)); 129 | 130 | bytes32 ilk_ = ilk.clip.ilk(); 131 | assertEq(ilkRegistry.class(ilk_), 1); 132 | 133 | vm.prank(dss.chainlog.getAddress("MCD_PAUSE_PROXY")); ilkRegistry.file(ilk_, "class", 7); 134 | assertEq(ilkRegistry.class(ilk_), 7); 135 | 136 | assertEq(ilk.clip.stopped(), 0); 137 | (bool canWork, bytes memory args) = clipperMomJob.workable(NET_A); 138 | assertTrue(canWork); 139 | assertEq(abi.decode(args, (address)), address(ilk.clip)); 140 | assertEq(ilk.clip.stopped(), 2); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/tests/D3MJob.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "./DssCronBase.t.sol"; 19 | import {D3MJob} from "../D3MJob.sol"; 20 | 21 | contract VatMock { 22 | 23 | uint256 public art; 24 | 25 | function urns(bytes32, address owner) external view returns (uint256, uint256) { 26 | return owner == address(123) ? (art, art) : (0, 0); 27 | } 28 | 29 | function setUrn(bytes32, uint256 _art) external { 30 | art = _art; 31 | } 32 | 33 | } 34 | 35 | contract D3MHubMock { 36 | 37 | VatMock public vat; 38 | uint256 public target; 39 | 40 | constructor(address _vat) { 41 | vat = VatMock(_vat); 42 | } 43 | 44 | function setTarget(uint256 _target) external { 45 | target = _target; 46 | } 47 | 48 | function pool(bytes32) external pure returns (address) { 49 | return address(123); 50 | } 51 | 52 | function exec(bytes32 ilk) external { 53 | vat.setUrn(ilk, target); 54 | } 55 | 56 | } 57 | 58 | contract IlkRegistryMock { 59 | 60 | function list() external pure returns (bytes32[] memory ) { 61 | bytes32[] memory result = new bytes32[](1); 62 | result[0] = ""; 63 | return result; 64 | } 65 | 66 | } 67 | 68 | contract DontExecute { 69 | 70 | function tryWorkable(D3MJob job, bytes32 network) external { 71 | try job.workable(network) returns (bool success, bytes memory args) { 72 | revert(string(abi.encode(success, args))); 73 | } catch { 74 | } 75 | } 76 | 77 | } 78 | 79 | contract D3MJobTest is DssCronBaseTest { 80 | 81 | using GodMode for *; 82 | 83 | VatMock vat; 84 | IlkRegistryMock ilkRegistryMock; 85 | D3MHubMock hub; 86 | DontExecute dontExecute; 87 | 88 | D3MJob d3mJob; 89 | 90 | function setUpSub() virtual override internal { 91 | vat = new VatMock(); 92 | hub = new D3MHubMock(address(vat)); 93 | ilkRegistryMock = new IlkRegistryMock(); 94 | dontExecute = new DontExecute(); 95 | 96 | // Kick off D3M update when things deviate outside 500bps and 10 minutes expiry 97 | d3mJob = new D3MJob(address(sequencer), address(ilkRegistryMock), address(hub), 500, 10 minutes); 98 | } 99 | 100 | function getDebt() internal view returns (uint256 art) { 101 | (, art) = vat.urns("", address(123)); 102 | } 103 | 104 | function isWorkable() internal returns (bool success) { 105 | try dontExecute.tryWorkable(d3mJob, NET_A) { 106 | // Should never succeed 107 | } catch Error(string memory result) { 108 | (success,) = abi.decode(bytes(result), (bool, bytes)); 109 | } 110 | } 111 | 112 | function test_zero_to_non_zero() public { 113 | assertEq(getDebt(), 0); 114 | assertTrue(!isWorkable()); 115 | 116 | hub.setTarget(100 ether); 117 | 118 | assertEq(getDebt(), 0); 119 | assertTrue(isWorkable()); 120 | 121 | d3mJob.work(NET_A, abi.encode(bytes32(""))); 122 | 123 | assertEq(getDebt(), 100 ether); 124 | assertTrue(!isWorkable()); 125 | } 126 | 127 | function test_non_zero_to_zero() public { 128 | hub.setTarget(100 ether); 129 | d3mJob.work(NET_A, abi.encode(bytes32(""))); 130 | hub.setTarget(0); 131 | GodMode.vm().warp(block.timestamp + 10 minutes); 132 | 133 | assertEq(getDebt(), 100 ether); 134 | assertTrue(isWorkable()); 135 | 136 | d3mJob.work(NET_A, abi.encode(bytes32(""))); 137 | 138 | assertEq(getDebt(), 0); 139 | assertTrue(!isWorkable()); 140 | } 141 | 142 | function test_inside_threshold() public { 143 | hub.setTarget(100 ether); 144 | d3mJob.work(NET_A, abi.encode(bytes32(""))); 145 | hub.setTarget(99 ether); // 1% inside threshold 146 | 147 | assertEq(getDebt(), 100 ether); 148 | assertTrue(!isWorkable()); 149 | } 150 | 151 | function test_outside_threshold() public { 152 | hub.setTarget(100 ether); 153 | d3mJob.work(NET_A, abi.encode(bytes32(""))); 154 | hub.setTarget(105 ether); // 5% outside threshold 155 | GodMode.vm().warp(block.timestamp + 10 minutes); 156 | 157 | assertEq(getDebt(), 100 ether); 158 | assertTrue(isWorkable()); 159 | 160 | d3mJob.work(NET_A, abi.encode(bytes32(""))); 161 | 162 | assertEq(getDebt(), 105 ether); 163 | assertTrue(!isWorkable()); 164 | } 165 | 166 | function test_inside_timeout() public { 167 | hub.setTarget(100 ether); 168 | d3mJob.work(NET_A, abi.encode(bytes32(""))); 169 | hub.setTarget(105 ether); // 5% outside threshold 170 | GodMode.vm().warp(block.timestamp + 8 minutes); 171 | 172 | assertEq(getDebt(), 100 ether); 173 | assertTrue(!isWorkable()); 174 | GodMode.vm().warp(block.timestamp + 2 minutes); 175 | assertTrue(isWorkable()); 176 | 177 | d3mJob.work(NET_A, abi.encode(bytes32(""))); 178 | 179 | assertEq(getDebt(), 105 ether); 180 | assertTrue(!isWorkable()); 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /src/tests/DssCronBase.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "dss-test/DssTest.sol"; 19 | import {IlkRegistryAbstract} from "dss-interfaces/Interfaces.sol"; 20 | 21 | import {Sequencer} from "../Sequencer.sol"; 22 | import {LiquidatorJob} from "../LiquidatorJob.sol"; 23 | import {LerpJob} from "../LerpJob.sol"; 24 | 25 | // Integration tests against live MCD 26 | abstract contract DssCronBaseTest is DssTest { 27 | 28 | using MCD for DssInstance; 29 | 30 | bytes32 constant NET_A = "NTWK-A"; 31 | bytes32 constant NET_B = "NTWK-B"; 32 | bytes32 constant NET_C = "NTWK-C"; 33 | bytes32 constant ILK = "TEST-ILK"; 34 | 35 | IlkRegistryAbstract ilkRegistry; 36 | Sequencer sequencer; 37 | 38 | DssInstance dss; 39 | 40 | MCDUser user; 41 | 42 | function setUp() public { 43 | dss = MCD.loadFromChainlog(0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F); 44 | 45 | sequencer = new Sequencer(); 46 | 47 | ilkRegistry = IlkRegistryAbstract(dss.chainlog.getAddress("ILK_REGISTRY")); 48 | 49 | // Add a default network 50 | sequencer.addNetwork(NET_A, 13); 51 | assertEq(sequencer.totalWindowSize(), 13); 52 | (uint256 start, uint256 length) = sequencer.windows(NET_A); 53 | assertEq(start, 0); 54 | assertEq(length, 13); 55 | 56 | // Add a default user 57 | user = dss.newUser(); 58 | 59 | setUpSub(); 60 | } 61 | 62 | function setUpSub() virtual internal; 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/tests/FlapJob-integration.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "./DssCronBase.t.sol"; 19 | 20 | import {FlapJob} from "../FlapJob.sol"; 21 | 22 | contract FlapJobIntegrationTest is DssCronBaseTest { 23 | using stdStorage for StdStorage; 24 | using GodMode for *; 25 | 26 | FlapJob flapJob; 27 | 28 | event LogNote( 29 | bytes4 indexed sig, 30 | address indexed usr, 31 | bytes32 indexed arg1, 32 | bytes32 indexed arg2, 33 | bytes data 34 | ) anonymous; 35 | 36 | function setUpSub() virtual override internal { 37 | flapJob = new FlapJob(address(sequencer), address(dss.vat), address(dss.vow), tx.gasprice); 38 | 39 | // Make sure that if a flapper has a cooldown period it already passed 40 | GodMode.vm().warp(block.timestamp + 10 days); 41 | 42 | // Set default values that assure flap will succeed without a need to heal 43 | stdstore.target(address(dss.vat)).sig("dai(address)").with_key(address(dss.vow)).depth(0).checked_write(70 * MILLION * RAD); 44 | stdstore.target(address(dss.vat)).sig("sin(address)").with_key(address(dss.vow)).depth(0).checked_write(uint256(0)); 45 | stdstore.target(address(dss.vow)).sig("hump()").checked_write(50 * MILLION * RAD); 46 | stdstore.target(address(dss.vow)).sig("bump()").checked_write(10 * THOUSAND * RAD); 47 | stdstore.target(address(dss.vow)).sig("Sin()").checked_write(uint256(0)); 48 | stdstore.target(address(dss.vow)).sig("Ash()").checked_write(uint256(0)); 49 | 50 | } 51 | 52 | function test_flap_no_need_to_heal() public { 53 | uint256 snapshot = vm.snapshot(); 54 | (bool canWork, bytes memory args) = flapJob.workable(NET_A); 55 | assertTrue(canWork, "Should be able to work"); 56 | vm.revertTo(snapshot); 57 | 58 | vm.expectEmit(false, false, false, false); 59 | emit LogNote(dss.vow.flap.selector, address(0), 0, 0, bytes("")); 60 | flapJob.work(NET_A, args); 61 | } 62 | 63 | function test_flap_need_to_heal() public { 64 | // force free bad debt of 1 65 | uint256 newVatSin = dss.vow.Sin() + dss.vow.Ash() + 1; 66 | stdstore.target(address(dss.vat)).sig("sin(address)").with_key(address(dss.vow)).depth(0).checked_write(newVatSin); 67 | 68 | uint256 snapshot = vm.snapshot(); 69 | (bool canWork, bytes memory args) = flapJob.workable(NET_A); 70 | assertTrue(canWork, "Should be able to work"); 71 | vm.revertTo(snapshot); 72 | 73 | vm.expectEmit(false, false, false, false); 74 | emit LogNote(dss.vow.heal.selector, address(0), 0, 0, bytes("")); 75 | vm.expectEmit(false, false, false, false); 76 | emit LogNote(dss.vow.flap.selector, address(0), 0, 0, bytes("")); 77 | flapJob.work(NET_A, args); 78 | } 79 | 80 | function test_flap_heal_fails() public { 81 | // force system surplus to be negative 82 | uint256 newVatSin = dss.vat.dai(address(dss.vow)) + 1; 83 | stdstore.target(address(dss.vat)).sig("sin(address)").with_key(address(dss.vow)).depth(0).checked_write(newVatSin); 84 | 85 | (bool canWork,) = flapJob.workable(NET_A); 86 | assertTrue(!canWork, "Should not be able to work"); 87 | } 88 | 89 | function test_flap_fails() public { 90 | // force hump to be higher than the SB 91 | uint256 newVowHump = dss.vat.dai(address(dss.vow)) + 1; 92 | stdstore.target(address(dss.vow)).sig("hump()").checked_write(newVowHump); 93 | 94 | (bool canWork,) = flapJob.workable(NET_A); 95 | assertTrue(!canWork, "Should not be able to work"); 96 | } 97 | 98 | function test_flap_gasPriceTooHigh() public { 99 | flapJob = new FlapJob(address(sequencer), address(dss.vat), address(dss.vow), tx.gasprice - 1); 100 | 101 | (bool canWork,) = flapJob.workable(NET_A); 102 | assertTrue(!canWork, "Should not be able to work"); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/tests/LerpJob-integration.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "./DssCronBase.t.sol"; 19 | import {LerpFactoryAbstract} from "dss-interfaces/Interfaces.sol"; 20 | 21 | import {LerpJob} from "../LerpJob.sol"; 22 | 23 | contract LerpJobIntegrationTest is DssCronBaseTest { 24 | 25 | using GodMode for *; 26 | 27 | LerpFactoryAbstract lerpFactory; 28 | 29 | LerpJob lerpJob; 30 | 31 | function setUpSub() virtual override internal { 32 | lerpFactory = LerpFactoryAbstract(dss.chainlog.getAddress("LERP_FAB")); 33 | 34 | // Execute all lerps once a day 35 | lerpJob = new LerpJob(address(sequencer), address(lerpFactory), 1 days); 36 | 37 | // Give admin to this contract 38 | address(lerpFactory).setWard(address(this), 1); 39 | 40 | // Clear out all existing lerps by moving ahead 50 years 41 | GodMode.vm().warp(block.timestamp + 365 days * 50); 42 | lerpFactory.tall(); 43 | } 44 | 45 | function test_lerp() public { 46 | // Setup a dummy lerp to track the timestamps 47 | uint256 start = block.timestamp; 48 | uint256 end = start + 10 days; 49 | address lerp = lerpFactory.newLerp("A TEST", address(dss.vat), "Line", start, start, end, end - start); 50 | dss.vat.setWard(lerp, 1); 51 | 52 | assertTrue(dss.vat.Line() != block.timestamp); // Randomly this could be false, but seems practically impossible 53 | 54 | // Initially should be able to work as the expiry is way in the past 55 | (bool canWork, bytes memory args) = lerpJob.workable(NET_A); 56 | assertTrue(canWork, "Should be able to work"); 57 | lerpJob.work(NET_A, args); 58 | assertEq(dss.vat.Line(), block.timestamp); 59 | 60 | // Cannot call again 61 | (canWork, args) = lerpJob.workable(NET_A); 62 | assertTrue(!canWork, "Should not be able to work"); 63 | 64 | // Fast forward by 23 hours -- still can't call 65 | GodMode.vm().warp(block.timestamp + 23 hours); 66 | (canWork, args) = lerpJob.workable(NET_A); 67 | assertTrue(!canWork, "Should not be able to work"); 68 | 69 | // Fast forward by 1 hours -- we can call again 70 | GodMode.vm().warp(block.timestamp + 1 hours); 71 | (canWork, args) = lerpJob.workable(NET_A); 72 | assertTrue(canWork, "Should be able to work"); 73 | lerpJob.work(NET_A, args); 74 | assertEq(dss.vat.Line(), block.timestamp); 75 | } 76 | 77 | function test_no_lerp() public { 78 | // Should not trigger when there is no lerp 79 | (bool canWork,) = lerpJob.workable(NET_A); 80 | assertTrue(!canWork, "should not be able to work"); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/tests/LiquidatorJob-integration.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "./DssCronBase.t.sol"; 19 | import {LiquidatorJob} from "../LiquidatorJob.sol"; 20 | 21 | abstract contract LiquidatorIntegrationTest is DssCronBaseTest { 22 | 23 | using MCD for DssInstance; 24 | 25 | address uniswapV3Callee; 26 | DssIlkInstance ilk; 27 | 28 | LiquidatorJob liquidatorJob; 29 | LiquidatorJob liquidatorJob500; 30 | 31 | function setUpSub() virtual override internal { 32 | uniswapV3Callee = 0xdB9C76109d102d2A1E645dCa3a7E671EBfd8e11A; 33 | ilk = dss.getIlk("ETH", "A"); 34 | 35 | // 0% profit expectation 36 | liquidatorJob = new LiquidatorJob(address(sequencer), address(dss.daiJoin), address(ilkRegistry), address(dss.vow), uniswapV3Callee, 0); 37 | 38 | // 5% profit expectation 39 | liquidatorJob500 = new LiquidatorJob(address(sequencer), address(dss.daiJoin), address(ilkRegistry), address(dss.vow), uniswapV3Callee, 500); 40 | 41 | // TODO clear out any existing auctions 42 | 43 | // Create an auction on ETH-A 44 | user.createAuction(ilk.join, 100 ether); 45 | } 46 | 47 | function trigger_next_liquidation_job(bytes32 network, LiquidatorJob liquidator) internal { 48 | // TODO dont actually trigger liquidation here 49 | (bool canWork,) = liquidator.workable(network); 50 | assertTrue(canWork, "Expecting to be able to execute."); 51 | // No need to actually execute as the detection of a successful job will execute 52 | //(bool success,) = target.call(args); 53 | //assertTrue(success, "Execution should have succeeded."); 54 | } 55 | 56 | function verify_no_liquidation_job(bytes32 network, LiquidatorJob liquidator) internal { 57 | (bool canWork, bytes memory args) = liquidator.workable(network); 58 | assertTrue(!canWork, "Expecting NOT to be able to execute."); 59 | bytes memory expectedArgs = "No auctions"; 60 | for (uint256 i = 0; i < expectedArgs.length; i++) { 61 | assertEq(args[i], expectedArgs[i]); 62 | } 63 | } 64 | 65 | function test_eth_a() public { 66 | // Setup auction 67 | uint256 auctionId = ilk.clip.kicks(); 68 | (,uint256 tab,,,,) = ilk.clip.sales(auctionId); 69 | assertTrue(tab != 0, "auction didn't kick off"); 70 | 71 | // Liquidation should not be available because the price is too high 72 | verify_no_liquidation_job(NET_A, liquidatorJob500); 73 | verify_no_liquidation_job(NET_A, liquidatorJob); 74 | 75 | // This will put it just below market price -- should trigger with only the no profit one 76 | // TODO - this can fail with market volatility -- should make this more robust by comparing Oracle to Uniswap price 77 | GodMode.vm().warp(block.timestamp + 33 minutes); 78 | 79 | verify_no_liquidation_job(NET_A, liquidatorJob500); 80 | uint256 vowDai = dss.vat.dai(address(dss.vow)); 81 | trigger_next_liquidation_job(NET_A, liquidatorJob); 82 | 83 | // Auction should be cleared 84 | (,tab,,,,) = ilk.clip.sales(auctionId); 85 | assertEq(tab, 0); 86 | 87 | // Profit should go to vow 88 | assertGt(dss.vat.dai(address(dss.vow)), vowDai); 89 | } 90 | 91 | function test_eth_a_profit() public { 92 | // Setup auction 93 | uint256 auctionId = ilk.clip.kicks(); 94 | (,uint256 tab,,,,) = ilk.clip.sales(auctionId); 95 | assertTrue(tab != 0, "auction didn't kick off"); 96 | 97 | // Liquidation should not be available because the price is too high 98 | verify_no_liquidation_job(NET_A, liquidatorJob500); 99 | 100 | // This will put it just below market price -- should still not trigger 101 | GodMode.vm().warp(block.timestamp + 33 minutes); 102 | verify_no_liquidation_job(NET_A, liquidatorJob500); 103 | 104 | // A little bit further 105 | GodMode.vm().warp(block.timestamp + 8 minutes); 106 | 107 | uint256 vowDai = dss.vat.dai(address(dss.vow)); 108 | trigger_next_liquidation_job(NET_A, liquidatorJob500); 109 | 110 | // Auction should be cleared 111 | (,tab,,,,) = ilk.clip.sales(auctionId); 112 | assertEq(tab, 0); 113 | 114 | // Profit should go to vow 115 | assertGt(dss.vat.dai(address(dss.vow)), vowDai); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/tests/LitePsmJob-integration.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "forge-std/Test.sol"; 19 | import "./DssCronBase.t.sol"; 20 | 21 | import {LitePsmJob} from "../LitePsmJob.sol"; 22 | 23 | interface LitePsmLike { 24 | function chug() external returns (uint256 wad); 25 | function cut() external view returns (uint256 wad); 26 | function file(bytes32 what, uint256 data) external; 27 | function fill() external returns (uint256 wad); 28 | function gem() external returns (address); 29 | function gush() external view returns (uint256 wad); 30 | function ilk() external returns (bytes32); 31 | function rush() external view returns (uint256 wad); 32 | function trim() external returns (uint256 wad); 33 | function vat() external returns (address); 34 | } 35 | 36 | interface GemLike { 37 | function decimals() external view returns (uint8); 38 | } 39 | 40 | interface VatLike { 41 | function ilks(bytes32) external view returns (uint256, uint256, uint256, uint256, uint256); 42 | function debt() external view returns (uint256); 43 | function Line() external view returns (uint256); 44 | function urns(bytes32, address) external view returns (uint256, uint256); 45 | function file(bytes32 ilk, bytes32 what, uint256 data) external; 46 | function file(bytes32 what, uint256 data) external; 47 | } 48 | 49 | contract LitePsmJobIntegrationTest is DssCronBaseTest { 50 | using GodMode for *; 51 | 52 | uint256 constant MILLION_WAD = MILLION * WAD; 53 | 54 | LitePsmLike public litePsm; 55 | LitePsmJob public litePsmJob; 56 | address public gem; 57 | address public dai; 58 | address public pocket; 59 | address vat; 60 | bytes32 ilk; 61 | 62 | // --- Events --- 63 | event Chug(uint256 wad); 64 | event Fill(uint256 wad); 65 | event Trim(uint256 wad); 66 | event Work(bytes32 indexed network, bytes4 indexed action); 67 | 68 | function setUpSub() internal virtual override { 69 | litePsm = LitePsmLike(dss.chainlog.getAddress("MCD_LITE_PSM_USDC_A")); 70 | pocket = dss.chainlog.getAddress("MCD_LITE_PSM_USDC_A_POCKET"); 71 | dai = dss.chainlog.getAddress("MCD_DAI"); 72 | litePsmJob = 73 | new LitePsmJob(address(sequencer), address(litePsm), MILLION_WAD, MILLION_WAD, MILLION_WAD); 74 | gem = litePsm.gem(); 75 | ilk = litePsm.ilk(); 76 | vat = litePsm.vat(); 77 | // give auth access to this contract (caller) to vat for manipulating params 78 | GodMode.setWard(vat, address(this), 1); 79 | } 80 | 81 | function test_fill() public { 82 | (uint256 Art,,, uint256 line,) = VatLike(vat).ilks(ilk); 83 | 84 | // tArt must be greater than Art 85 | // tArt = GemLike(gem).balanceOf(pocket) * gemConversionFactor + buf; 86 | uint256 gemConversionFactor = 10 ** (18 - GemLike(gem).decimals()); 87 | deal(gem, pocket, Art * 2 / gemConversionFactor); 88 | 89 | // ilk line must be greater than Art 90 | uint256 newLine = Art * 2 * RAY; 91 | VatLike(vat).file(ilk, "line", newLine); 92 | (Art,,, line,) = VatLike(vat).ilks(ilk); 93 | 94 | // vat.Line() must be greater than vat.debt() 95 | uint256 vatLine = VatLike(vat).Line(); 96 | uint256 vatDebt = VatLike(vat).debt(); 97 | VatLike(vat).file("Line", vatLine + vatDebt); 98 | 99 | uint256 wad = litePsm.rush(); 100 | assertTrue(wad != 0, "rush() returns 0"); 101 | (bool canWork, bytes memory args) = litePsmJob.workable(NET_A); 102 | assertTrue(canWork, "workable returns false"); 103 | (bytes4 fn) = abi.decode(args, (bytes4)); 104 | assertEq(fn, litePsm.fill.selector, "fill() selector mismatch"); 105 | vm.expectEmit(false, false, false, true); 106 | emit Fill(wad); 107 | vm.expectEmit(true, false, false, false); 108 | emit Work(NET_A, fn); 109 | litePsmJob.work(NET_A, args); 110 | wad = litePsm.rush(); 111 | assertEq(wad, 0, "rush() does not return 0"); 112 | } 113 | 114 | function test_chug() public { 115 | // the dai balance of LitePsm must be greater than the urn's art for this ilk 116 | (, uint256 art) = VatLike(vat).urns(ilk, address(litePsm)); 117 | deal(dai, address(litePsm), art + 1); //must be greater than art so we dont have underflow 118 | uint256 wad = litePsm.cut(); 119 | assertTrue(wad != 0, "cut() returns 0"); 120 | (bool canWork, bytes memory args) = litePsmJob.workable(NET_A); 121 | assertTrue(canWork, "workable returns false"); 122 | (bytes4 fn) = abi.decode(args, (bytes4)); 123 | assertEq(fn, litePsm.chug.selector, "chug() selector mismatch"); 124 | vm.expectEmit(false, false, false, true); 125 | emit Chug(wad); 126 | vm.expectEmit(true, false, false, false); 127 | emit Work(NET_A, fn); 128 | litePsmJob.work(NET_A, args); 129 | wad = litePsm.cut(); 130 | assertEq(wad, 0, "cut() does not return 0"); 131 | } 132 | 133 | function test_trim_only_after_chug() public { 134 | (uint256 Art,,, uint256 line,) = VatLike(vat).ilks(ilk); 135 | // Art must be greater than ilk line 136 | uint256 newLine = (Art / 2) * RAY; 137 | VatLike(vat).file(ilk, "line", newLine); 138 | (Art,,, line,) = VatLike(vat).ilks(ilk); 139 | // dai balance of LitePsm must be non-zero 140 | deal(dai, address(litePsm), Art); 141 | // workable() will return chug() because it has precedence! 142 | (bool canWork, bytes memory args) = litePsmJob.workable(NET_A); 143 | assertTrue(canWork, "workable returns false"); 144 | (bytes4 fn) = abi.decode(args, (bytes4)); 145 | assertEq(fn, litePsm.chug.selector, "chug() selector mismatch"); 146 | // call chug() first! 147 | litePsmJob.work(NET_A, args); 148 | // gush() should return a non zero value so trim() meaning that trim can be called 149 | uint256 wad = litePsm.gush(); 150 | assertTrue(wad != 0, "gush() returns 0"); 151 | // we call workable again, it should return trim() now! 152 | (canWork, args) = litePsmJob.workable(NET_A); 153 | assertTrue(canWork, "workable returns false"); 154 | (fn) = abi.decode(args, (bytes4)); 155 | assertEq(fn, litePsm.trim.selector, "trim() selector mismatch"); 156 | vm.expectEmit(false, false, false, true); 157 | emit Trim(wad); 158 | vm.expectEmit(true, false, false, false); 159 | emit Work(NET_A, fn); 160 | litePsmJob.work(NET_A, args); 161 | wad = litePsm.gush(); 162 | assertEq(wad, 0, "gush() does not return 0"); 163 | } 164 | 165 | /** 166 | * Revert Test Cases ** 167 | */ 168 | function test_no_work() public { 169 | (bool canWork, bytes memory args) = litePsmJob.workable(NET_A); 170 | assertTrue(canWork == false, "workable() returns true"); 171 | assertEq(args, bytes("No work to do"), "Wrong No work message"); 172 | } 173 | 174 | function test_unsupported_function() public { 175 | bytes4 fn = 0x00000000; 176 | bytes memory args = abi.encode(fn); 177 | vm.expectRevert(abi.encodeWithSelector(LitePsmJob.UnsupportedFunction.selector, fn)); 178 | litePsmJob.work(NET_A, args); 179 | } 180 | 181 | function test_unreached_threshold_fill() public { 182 | bytes4 fn = litePsm.fill.selector; 183 | bytes memory args = abi.encode(fn); 184 | vm.expectRevert(abi.encodeWithSelector(LitePsmJob.ThresholdNotReached.selector, fn)); 185 | litePsmJob.work(NET_A, args); 186 | } 187 | 188 | function test_unreached_threshold_chug() public { 189 | bytes4 fn = litePsm.chug.selector; 190 | bytes memory args = abi.encode(fn); 191 | vm.expectRevert(abi.encodeWithSelector(LitePsmJob.ThresholdNotReached.selector, fn)); 192 | litePsmJob.work(NET_A, args); 193 | } 194 | 195 | function test_unreached_threshold_trim() public { 196 | bytes4 fn = litePsm.trim.selector; 197 | bytes memory args = abi.encode(fn); 198 | vm.expectRevert(abi.encodeWithSelector(LitePsmJob.ThresholdNotReached.selector, fn)); 199 | litePsmJob.work(NET_A, args); 200 | } 201 | 202 | function test_non_master_network() public { 203 | bytes32 network = "ERROR"; 204 | bytes memory args = abi.encode("0"); 205 | vm.expectRevert(abi.encodeWithSelector(LitePsmJob.NotMaster.selector, network)); 206 | litePsmJob.work(network, args); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/tests/NetworkPaymentAdapter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "dss-test/DssTest.sol"; 19 | 20 | import {DaiMock} from "./mocks/DaiMock.sol"; 21 | import {DaiJoinMock} from "./mocks/DaiJoinMock.sol"; 22 | import {VatMock} from "./mocks/VatMock.sol"; 23 | import {NetworkPaymentAdapter} from "../NetworkPaymentAdapter.sol"; 24 | 25 | contract VestMock { 26 | 27 | mapping (uint256 => uint256) public vests; 28 | DaiMock public dai; 29 | 30 | constructor(DaiMock _dai) { 31 | dai = _dai; 32 | } 33 | 34 | function setVest(uint256 id, uint256 amt) external { 35 | vests[id] = amt; 36 | } 37 | 38 | function vest(uint256 id) external { 39 | dai.transfer(msg.sender, vests[id]); 40 | vests[id] = 0; 41 | } 42 | 43 | function unpaid(uint256 id) external view returns (uint256) { 44 | return vests[id]; 45 | } 46 | 47 | } 48 | 49 | contract TreasuryMock { 50 | 51 | DaiMock public dai; 52 | 53 | constructor(DaiMock _dai) { 54 | dai = _dai; 55 | } 56 | 57 | function topUp(NetworkPaymentAdapter adapter) external returns (uint256) { 58 | return adapter.topUp(); 59 | } 60 | 61 | function getBufferSize() external view returns (uint256) { 62 | return dai.balanceOf(address(this)); 63 | } 64 | 65 | } 66 | 67 | contract NetworkPaymentAdapterTest is DssTest { 68 | 69 | uint256 constant VEST_ID = 123; 70 | 71 | VestMock vest; 72 | TreasuryMock treasury; 73 | DaiMock dai; 74 | DaiJoinMock daiJoin; 75 | VatMock vat; 76 | address vow; 77 | 78 | NetworkPaymentAdapter adapter; 79 | 80 | function setUp() public { 81 | dai = new DaiMock(); 82 | vat = new VatMock(); 83 | daiJoin = new DaiJoinMock(address(vat), address(dai)); 84 | vest = new VestMock(dai); 85 | treasury = new TreasuryMock(dai); 86 | vow = TEST_ADDRESS; 87 | 88 | dai.rely(address(daiJoin)); 89 | 90 | vat.suck(address(this), address(this), 10_000 * RAD); 91 | vat.hope(address(daiJoin)); 92 | daiJoin.exit(address(vest), 10_000 ether); 93 | 94 | adapter = new NetworkPaymentAdapter( 95 | address(vest), 96 | address(daiJoin), 97 | vow 98 | ); 99 | adapter.file("vestId", VEST_ID); 100 | adapter.file("treasury", address(treasury)); 101 | } 102 | 103 | function test_auth() public { 104 | checkAuth(address(adapter), "NetworkPaymentAdapter"); 105 | } 106 | 107 | function test_file() public { 108 | checkFileAddress(address(adapter), "NetworkPaymentAdapter", ["treasury"]); 109 | checkFileUint(address(adapter), "NetworkPaymentAdapter", ["vestId", "bufferMax", "minimumPayment"]); 110 | } 111 | 112 | function test_topUp() public { 113 | uint256 vestAmount = 100 ether; 114 | vest.setVest(VEST_ID, vestAmount); 115 | adapter.file("bufferMax", 1000 ether); 116 | adapter.file("minimumPayment", 100 ether); 117 | 118 | assertTrue(adapter.canTopUp()); 119 | assertEq(dai.balanceOf(address(treasury)), 0); 120 | assertEq(treasury.getBufferSize(), 0); 121 | 122 | uint256 daiSent = treasury.topUp(adapter); 123 | 124 | assertEq(daiSent, vestAmount); 125 | assertTrue(!adapter.canTopUp()); 126 | assertEq(dai.balanceOf(address(treasury)), vestAmount); 127 | assertEq(treasury.getBufferSize(), vestAmount); 128 | } 129 | 130 | function test_topUpMultiple() public { 131 | uint256 vestAmount = 100 ether; 132 | vest.setVest(VEST_ID, vestAmount); 133 | adapter.file("bufferMax", 1000 ether); 134 | adapter.file("minimumPayment", 100 ether); 135 | 136 | treasury.topUp(adapter); 137 | vest.setVest(VEST_ID, vestAmount); 138 | 139 | assertTrue(adapter.canTopUp()); 140 | assertEq(dai.balanceOf(address(treasury)), vestAmount); 141 | assertEq(treasury.getBufferSize(), vestAmount); 142 | 143 | uint256 daiSent = treasury.topUp(adapter); 144 | 145 | assertEq(daiSent, vestAmount); 146 | assertEq(dai.balanceOf(address(treasury)), 2 * vestAmount); 147 | assertEq(treasury.getBufferSize(), 2 * vestAmount); 148 | } 149 | 150 | function test_topUpOverMax() public { 151 | uint256 vestAmount = 100 ether; 152 | vest.setVest(VEST_ID, vestAmount); 153 | adapter.file("bufferMax", 60 ether); 154 | adapter.file("minimumPayment", 10 ether); 155 | 156 | assertTrue(adapter.canTopUp()); 157 | assertEq(dai.balanceOf(address(treasury)), 0); 158 | assertEq(treasury.getBufferSize(), 0); 159 | assertEq(vat.dai(vow), 0); 160 | 161 | uint256 daiSent = treasury.topUp(adapter); 162 | 163 | assertEq(daiSent, 60 ether); 164 | assertEq(dai.balanceOf(address(treasury)), 60 ether); 165 | assertEq(treasury.getBufferSize(), 60 ether); 166 | assertEq(vat.dai(vow), 40 * RAD); 167 | } 168 | 169 | function test_topUpBufferFull() public { 170 | vest.setVest(VEST_ID, 90 ether + 1); 171 | adapter.file("bufferMax", 100 ether); 172 | adapter.file("minimumPayment", 10 ether); 173 | treasury.topUp(adapter); 174 | vest.setVest(VEST_ID, 90 ether); 175 | assertTrue(!adapter.canTopUp()); 176 | 177 | vm.expectRevert(abi.encodeWithSignature("BufferFull(uint256,uint256,uint256)", 90 ether + 1, 10 ether, 100 ether)); 178 | treasury.topUp(adapter); 179 | } 180 | 181 | function test_topUpPendingDaiTooSmall() public { 182 | vest.setVest(VEST_ID, 5 ether); 183 | adapter.file("bufferMax", 100 ether); 184 | adapter.file("minimumPayment", 10 ether); 185 | assertTrue(!adapter.canTopUp()); 186 | 187 | vm.expectRevert(abi.encodeWithSignature("PendingDaiTooSmall(uint256,uint256)", 5 ether, 10 ether)); 188 | treasury.topUp(adapter); 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /src/tests/OracleJob-integration.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "./DssCronBase.t.sol"; 19 | import "dss-interfaces/Interfaces.sol"; 20 | 21 | import { 22 | OracleJob, 23 | PokeLike 24 | } from "../OracleJob.sol"; 25 | 26 | contract OracleJobIntegrationTest is DssCronBaseTest { 27 | 28 | using GodMode for *; 29 | using MCD for DssInstance; 30 | 31 | DssIlkInstance ethA; 32 | 33 | OracleJob oracleJob; 34 | 35 | function setUpSub() virtual override internal { 36 | ethA = dss.getIlk("ETH", "A"); 37 | ethA.pip.src().setWard(address(this), 1); 38 | MedianAbstract(ethA.pip.src()).kiss(address(this)); 39 | 40 | oracleJob = new OracleJob(address(sequencer), address(ilkRegistry), address(dss.spotter)); 41 | 42 | // Update all spotters and osms to make sure we are up to date 43 | vm.warp(block.timestamp + 1 hours); 44 | bytes32[] memory ilks = ilkRegistry.list(); 45 | for (uint256 i = 0; i < ilks.length; i++) { 46 | bytes32 ilk = ilks[i]; 47 | address pip = ilkRegistry.pip(ilk); 48 | if (pip == address(0)) continue; 49 | try PokeLike(pip).poke() { 50 | } catch { 51 | } 52 | } 53 | vm.warp(block.timestamp + 1 hours); 54 | for (uint256 i = 0; i < ilks.length; i++) { 55 | bytes32 ilk = ilks[i]; 56 | address pip = ilkRegistry.pip(ilk); 57 | if (pip == address(0)) continue; 58 | try PokeLike(pip).poke() { 59 | } catch { 60 | } 61 | dss.spotter.poke(ilk); 62 | } 63 | } 64 | 65 | function setPrice(address medianizer, uint256 price) internal { 66 | vm.store( 67 | address(medianizer), 68 | bytes32(uint256(1)), 69 | bytes32(price) 70 | ); 71 | assertEq(MedianAbstract(medianizer).read(), price, "failed to set price"); 72 | } 73 | 74 | function test_nothing_workable() public { 75 | (bool canWork,) = oracleJob.workable(NET_A); 76 | assertEq(canWork, false); 77 | } 78 | 79 | function test_osm_passed() public { 80 | vm.warp(block.timestamp + 1 hours); 81 | uint256 zzz = ethA.pip.zzz(); 82 | (bool canWork, bytes memory args) = oracleJob.workable(NET_A); 83 | assertEq(canWork, true); 84 | (bytes32[] memory _toPoke, bytes32[] memory _spotterIlksToPoke) = abi.decode(args, (bytes32[], bytes32[])); 85 | assertGt(_toPoke.length, 0, "should update all osms"); 86 | assertEq(_spotterIlksToPoke.length, 0, "should not have any spotters to update"); 87 | assertGt(ethA.pip.zzz(), zzz, "should have updated osm"); 88 | (canWork,) = oracleJob.workable(NET_A); 89 | assertEq(canWork, false); 90 | } 91 | 92 | function test_price_update() public { 93 | setPrice(ethA.pip.src(), 123 ether); // $123 94 | vm.warp(block.timestamp + 1 hours); 95 | (bool canWork, bytes memory args) = oracleJob.workable(NET_A); 96 | assertEq(canWork, true); 97 | (bytes32[] memory _toPoke, bytes32[] memory _spotterIlksToPoke) = abi.decode(args, (bytes32[], bytes32[])); 98 | assertGt(_toPoke.length, 0); 99 | assertEq(_spotterIlksToPoke.length, 0); 100 | vm.warp(block.timestamp + 1 hours); 101 | (canWork, args) = oracleJob.workable(NET_A); 102 | assertEq(canWork, true); 103 | (_toPoke, _spotterIlksToPoke) = abi.decode(args, (bytes32[], bytes32[])); 104 | assertGt(_toPoke.length, 0); 105 | assertEq(_spotterIlksToPoke.length, 5); // ETH-A, ETH-B, ETH-C, UNIV2DAIETH-A, UNIV2USDCETH-A, CRVV1ETHSTETH-A 106 | (,, uint256 spot,,) = dss.vat.ilks(ethA.join.ilk()); 107 | (, uint256 mat) = dss.spotter.ilks(ethA.join.ilk()); 108 | assertEq(spot, 123 * RAD * 10 ** 9 / mat); 109 | (canWork,) = oracleJob.workable(NET_A); 110 | assertEq(canWork, false); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/tests/Sequencer.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "./DssCronBase.t.sol"; 19 | 20 | contract SequencerTest is DssCronBaseTest { 21 | 22 | address constant ADDR0 = address(123); 23 | address constant ADDR1 = address(456); 24 | address constant ADDR2 = address(789); 25 | 26 | event AddNetwork(bytes32 indexed network, uint256 windowSize); 27 | event RemoveNetwork(bytes32 indexed network); 28 | event AddJob(address indexed job); 29 | event RemoveJob(address indexed job); 30 | 31 | function setUpSub() virtual override internal { 32 | // Remove the default network 33 | sequencer.removeNetwork(NET_A); 34 | } 35 | 36 | function test_auth() public { 37 | checkAuth(address(sequencer), "Sequencer"); 38 | } 39 | 40 | function checkWindow(bytes32 network, uint256 start, uint256 length) internal { 41 | (uint256 _start, uint256 _length) = sequencer.windows(network); 42 | assertEq(_start, start); 43 | assertEq(_length, length); 44 | } 45 | 46 | function test_add_network() public { 47 | vm.expectEmit(true, true, true, true); 48 | emit AddNetwork(NET_A, 123); 49 | sequencer.addNetwork(NET_A, 123); 50 | 51 | assertEq(sequencer.networkAt(0), NET_A); 52 | assertTrue(sequencer.hasNetwork(NET_A)); 53 | assertEq(sequencer.numNetworks(), 1); 54 | assertEq(sequencer.totalWindowSize(), 123); 55 | checkWindow(NET_A, 0, 123); 56 | } 57 | 58 | function test_add_dupe_network() public { 59 | sequencer.addNetwork(NET_A, 123); 60 | vm.expectRevert(abi.encodeWithSignature("NetworkExists(bytes32)", NET_A)); 61 | sequencer.addNetwork(NET_A, 123); 62 | } 63 | 64 | function test_add_network_zero_window() public { 65 | vm.expectRevert(abi.encodeWithSignature("WindowZero(bytes32)", NET_A)); 66 | sequencer.addNetwork(NET_A, 0); 67 | } 68 | 69 | function test_add_remove_network() public { 70 | sequencer.addNetwork(NET_A, 123); 71 | vm.expectEmit(true, true, true, true); 72 | emit RemoveNetwork(NET_A); 73 | sequencer.removeNetwork(NET_A); 74 | 75 | assertTrue(!sequencer.hasNetwork(NET_A)); 76 | assertEq(sequencer.numNetworks(), 0); 77 | assertEq(sequencer.totalWindowSize(), 0); 78 | checkWindow(NET_A, 0, 0); 79 | } 80 | 81 | function test_remove_non_existent_network() public { 82 | sequencer.addNetwork(NET_A, 123); 83 | vm.expectRevert(abi.encodeWithSignature("NetworkDoesNotExist(bytes32)", NET_B)); 84 | sequencer.removeNetwork(NET_B); 85 | } 86 | 87 | function test_add_remove_networks() public { 88 | sequencer.addNetwork(NET_A, 10); 89 | sequencer.addNetwork(NET_B, 20); 90 | sequencer.addNetwork(NET_C, 30); 91 | 92 | assertEq(sequencer.numNetworks(), 3); 93 | assertEq(sequencer.networkAt(0), NET_A); 94 | assertEq(sequencer.networkAt(1), NET_B); 95 | assertEq(sequencer.networkAt(2), NET_C); 96 | assertEq(sequencer.totalWindowSize(), 60); 97 | checkWindow(NET_A, 0, 10); 98 | checkWindow(NET_B, 10, 20); 99 | checkWindow(NET_C, 30, 30); 100 | 101 | // Should move NET_C (last element) to slot 0 102 | sequencer.removeNetwork(NET_A); 103 | 104 | assertEq(sequencer.numNetworks(), 2); 105 | assertEq(sequencer.networkAt(0), NET_C); 106 | assertEq(sequencer.networkAt(1), NET_B); 107 | assertEq(sequencer.totalWindowSize(), 50); 108 | checkWindow(NET_C, 0, 30); 109 | checkWindow(NET_B, 30, 20); 110 | } 111 | 112 | function test_add_remove_networks_last() public { 113 | sequencer.addNetwork(NET_A, 10); 114 | sequencer.addNetwork(NET_B, 20); 115 | sequencer.addNetwork(NET_C, 10); 116 | 117 | assertEq(sequencer.numNetworks(), 3); 118 | assertEq(sequencer.networkAt(0), NET_A); 119 | assertEq(sequencer.networkAt(1), NET_B); 120 | assertEq(sequencer.networkAt(2), NET_C); 121 | assertEq(sequencer.totalWindowSize(), 40); 122 | checkWindow(NET_A, 0, 10); 123 | checkWindow(NET_B, 10, 20); 124 | checkWindow(NET_C, 30, 10); 125 | 126 | // Should remove the last element and not re-arrange 127 | sequencer.removeNetwork(NET_C); 128 | 129 | assertEq(sequencer.numNetworks(), 2); 130 | assertEq(sequencer.networkAt(0), NET_A); 131 | assertEq(sequencer.networkAt(1), NET_B); 132 | assertEq(sequencer.totalWindowSize(), 30); 133 | checkWindow(NET_A, 0, 10); 134 | checkWindow(NET_B, 10, 20); 135 | } 136 | 137 | function test_rotation() public { 138 | sequencer.addNetwork(NET_A, 3); 139 | sequencer.addNetwork(NET_B, 7); 140 | sequencer.addNetwork(NET_C, 25); 141 | 142 | bytes32[3] memory networks = [NET_A, NET_B, NET_C]; 143 | 144 | for (uint256 i = 0; i < sequencer.totalWindowSize() * 10; i++) { 145 | bytes32 master = sequencer.getMaster(); 146 | uint256 pos = block.number % sequencer.totalWindowSize(); 147 | assertTrue(sequencer.isMaster(master)); 148 | assertTrue(sequencer.isMaster(networks[0]) == (pos >= 0 && pos < 3)); 149 | assertTrue(sequencer.isMaster(networks[1]) == (pos >= 3 && pos < 10)); 150 | assertTrue(sequencer.isMaster(networks[2]) == (pos >= 10 && pos < 35)); 151 | assertEq( 152 | (sequencer.isMaster(networks[0]) ? 1 : 0) + 153 | (sequencer.isMaster(networks[1]) ? 1 : 0) + 154 | (sequencer.isMaster(networks[2]) ? 1 : 0) 155 | , 1); // Only one active at a time 156 | 157 | vm.roll(block.number + 1); 158 | } 159 | } 160 | 161 | function test_add_job() public { 162 | vm.expectEmit(true, true, true, true); 163 | emit AddJob(ADDR0); 164 | sequencer.addJob(ADDR0); 165 | 166 | assertEq(sequencer.jobAt(0), ADDR0); 167 | assertTrue(sequencer.hasJob(ADDR0)); 168 | assertEq(sequencer.numJobs(), 1); 169 | } 170 | 171 | function test_add_dupe_job() public { 172 | sequencer.addJob(ADDR0); 173 | vm.expectRevert(abi.encodeWithSignature("JobExists(address)", ADDR0)); 174 | sequencer.addJob(ADDR0); 175 | } 176 | 177 | function test_add_remove_job() public { 178 | sequencer.addJob(ADDR0); 179 | vm.expectEmit(true, true, true, true); 180 | emit RemoveJob(ADDR0); 181 | sequencer.removeJob(ADDR0); 182 | 183 | assertTrue(!sequencer.hasJob(ADDR0)); 184 | assertEq(sequencer.numJobs(), 0); 185 | } 186 | 187 | function test_remove_non_existent_job() public { 188 | sequencer.addJob(ADDR0); 189 | vm.expectRevert(abi.encodeWithSignature("JobDoesNotExist(address)", ADDR1)); 190 | sequencer.removeJob(ADDR1); 191 | } 192 | 193 | function test_add_remove_jobs() public { 194 | sequencer.addJob(ADDR0); 195 | sequencer.addJob(ADDR1); 196 | sequencer.addJob(ADDR2); 197 | 198 | assertEq(sequencer.numJobs(), 3); 199 | assertEq(sequencer.jobAt(0), ADDR0); 200 | assertEq(sequencer.jobAt(1), ADDR1); 201 | assertEq(sequencer.jobAt(2), ADDR2); 202 | 203 | // Should move liquidatorJob500 (last element) to slot 0 204 | sequencer.removeJob(ADDR0); 205 | 206 | assertEq(sequencer.numJobs(), 2); 207 | assertEq(sequencer.jobAt(0), ADDR2); 208 | assertEq(sequencer.jobAt(1), ADDR1); 209 | } 210 | 211 | function test_add_remove_jobs_last() public { 212 | sequencer.addJob(ADDR0); 213 | sequencer.addJob(ADDR1); 214 | sequencer.addJob(ADDR2); 215 | 216 | assertEq(sequencer.numJobs(), 3); 217 | assertEq(sequencer.jobAt(0), ADDR0); 218 | assertEq(sequencer.jobAt(1), ADDR1); 219 | assertEq(sequencer.jobAt(2), ADDR2); 220 | 221 | // Should remove the last element and not re-arrange anything 222 | sequencer.removeJob(ADDR2); 223 | 224 | assertEq(sequencer.numJobs(), 2); 225 | assertEq(sequencer.jobAt(0), ADDR0); 226 | assertEq(sequencer.jobAt(1), ADDR1); 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /src/tests/VestedRewardsDistribution-integration.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | // Copyright (C) 2021 Dai Foundation 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | pragma solidity 0.8.13; 17 | 18 | import "forge-std/Test.sol"; 19 | import "./DssCronBase.t.sol"; 20 | 21 | import {VestedRewardsDistributionJob} from "../VestedRewardsDistributionJob.sol"; 22 | 23 | interface VestedRewardsDistributionLike { 24 | function distribute() external; 25 | function dssVest() external view returns (address); 26 | function file(bytes32 what, uint256 data) external; 27 | function lastDistributedAt() external view returns (uint256); 28 | function vestId() external view returns (uint256); 29 | } 30 | 31 | interface DssVestLike { 32 | function awards(uint256 _id) 33 | external 34 | view 35 | returns (address usr, uint48 bgn, uint48 clf, uint48 fin, address mgr, uint8 res, uint128 tot, uint128 rxd); 36 | function create(address _usr, uint256 _tot, uint256 _bgn, uint256 _tau, uint256 _eta, address _mgr) 37 | external 38 | returns (uint256 id); 39 | function restrict(uint256 _id) external; 40 | function unpaid(uint256 _id) external view returns (uint256 amt); 41 | } 42 | 43 | // Note: these tests run only in fork mode on a Tenderly virtual testnet 44 | // RPC URL: https://virtual.mainnet.rpc.tenderly.co/470dbf59-a384-4e77-974c-9430acb2fccb 45 | contract VestedRewardsDistributionJobIntegrationTest is DssCronBaseTest { 46 | using GodMode for *; 47 | using stdStorage for StdStorage; 48 | 49 | uint256 RANDOM_INTERVAL = 15; 50 | VestedRewardsDistributionLike public constant vestedRewardsDist1 = 51 | VestedRewardsDistributionLike(0x69cA348Bd928A158ADe7aa193C133f315803b06e); 52 | VestedRewardsDistributionLike public constant vestedRewardsDist2 = 53 | VestedRewardsDistributionLike(0x53E15917309385Ec8235a5d025A8BeDa2fd0BE3E); 54 | 55 | VestedRewardsDistributionJob public job; 56 | 57 | function setUpSub() internal virtual override { 58 | job = new VestedRewardsDistributionJob(address(sequencer)); 59 | // add exisitng distros 60 | job.set(address(vestedRewardsDist1), RANDOM_INTERVAL); 61 | job.set(address(vestedRewardsDist2), RANDOM_INTERVAL); 62 | 63 | // Give admin access the test contract 64 | GodMode.setWard(address(vestedRewardsDist1), address(this), 1); 65 | GodMode.setWard(address(vestedRewardsDist2), address(this), 1); 66 | 67 | GodMode.setWard(vestedRewardsDist1.dssVest(), address(this), 1); 68 | GodMode.setWard(vestedRewardsDist2.dssVest(), address(this), 1); 69 | } 70 | 71 | function test_add_rewards_distribution() public { 72 | address rewardsDist = address(0); //test address 73 | vm.expectEmit(true, false, false, true); 74 | emit Set(rewardsDist, RANDOM_INTERVAL); 75 | job.set(rewardsDist, RANDOM_INTERVAL); 76 | assertTrue(job.has(rewardsDist)); 77 | assertEq(job.intervals(rewardsDist), RANDOM_INTERVAL); 78 | } 79 | 80 | function test_add_rewards_distribution_revert_auth() public { 81 | vm.prank(address(1)); 82 | vm.expectRevert("VestedRewardsDistributionJob/not-authorized"); 83 | job.set(address(0), RANDOM_INTERVAL); 84 | } 85 | 86 | function test_add_rewards_distribution_overwrite_duplicate() public { 87 | address rewardsDist = address(0); //test address 88 | job.set(rewardsDist, RANDOM_INTERVAL); 89 | job.set(rewardsDist, RANDOM_INTERVAL + 1); 90 | 91 | assertEq(job.intervals(rewardsDist), RANDOM_INTERVAL + 1); 92 | } 93 | 94 | function test_remove_rewards_distribution() public { 95 | address rewardsDist = address(0); //test address 96 | job.set(rewardsDist, RANDOM_INTERVAL); 97 | vm.expectEmit(true, false, false, false); 98 | emit Rem(rewardsDist); 99 | job.rem(rewardsDist); 100 | assertFalse(job.has(rewardsDist)); 101 | assertEq(job.intervals(rewardsDist), 0); 102 | } 103 | 104 | function test_remove_rewards_distribution_revert_auth() public { 105 | vm.prank(address(1)); 106 | vm.expectRevert("VestedRewardsDistributionJob/not-authorized"); 107 | job.rem(address(0)); 108 | } 109 | 110 | function test_remove_rewards_distribution_revert_not_found() public { 111 | address rewardsDist = address(0); //test address 112 | vm.expectRevert(abi.encodeWithSelector(VestedRewardsDistributionJob.NotFound.selector, rewardsDist)); 113 | job.rem(rewardsDist); 114 | } 115 | 116 | function test_modify_distribution_interval() public { 117 | uint256 newInterval = RANDOM_INTERVAL + 1; 118 | vm.expectEmit(true, false, false, true); 119 | emit Set(address(vestedRewardsDist1), newInterval); 120 | job.set(address(vestedRewardsDist1), newInterval); 121 | assertEq(job.intervals(address(vestedRewardsDist1)), newInterval); 122 | } 123 | 124 | function test_modify_distribution_interval_revert_auth() public { 125 | vm.prank(address(1)); 126 | vm.expectRevert("VestedRewardsDistributionJob/not-authorized"); 127 | job.set(address(vestedRewardsDist1), RANDOM_INTERVAL); 128 | } 129 | 130 | function test_modify_distribution_interval_revert_invalid_arg() public { 131 | vm.expectRevert(abi.encodeWithSelector(VestedRewardsDistributionJob.InvalidInterval.selector)); 132 | job.set(address(vestedRewardsDist1), 0); 133 | } 134 | 135 | function test_work() public { 136 | uint256 duration = 360 days; 137 | uint256 total = 100 ether; 138 | uint256 interval = 7 days; 139 | 140 | job.set(address(vestedRewardsDist1), interval); 141 | job.rem(address(vestedRewardsDist2)); 142 | DssVestLike vest = DssVestLike(vestedRewardsDist1.dssVest()); 143 | uint256 vestId = _replaceVestingStream( 144 | address(vestedRewardsDist1), VestParams({bgn: block.timestamp, eta: 0, tau: duration, tot: total}) 145 | ); 146 | 147 | // Workable should return false because vest.unpaid(vestId) == 0 148 | { 149 | (bool canWork,) = job.workable(NET_A); 150 | assertFalse(canWork, "initial: workable() should return false"); 151 | } 152 | 153 | // Since this will be the first distribution, the interval cannot be easily enforced 154 | { 155 | skip(2 days); 156 | // Workable now modifies state, so we need this hack to make the test pass. 157 | uint256 beforeWorkable = vm.snapshot(); 158 | (bool canWork, bytes memory args) = job.workable(NET_A); 159 | vm.revertTo(beforeWorkable); 160 | 161 | assertTrue(canWork, "1st distribution before interval has passed: workable() should return true"); 162 | (address rewDist) = abi.decode(args, (address)); 163 | 164 | vm.expectEmit(true, false, false, true); 165 | emit Work(NET_A, rewDist, vest.unpaid(vestId)); 166 | job.work(NET_A, args); 167 | 168 | // Checks that there is no vesting amount to be paid 169 | assertEq(vest.unpaid(vestId), 0, "1st distribution before interval has passed: unexpected unpaid amount"); 170 | } 171 | 172 | // Next not enough time has passed, so the job is not be workable 173 | { 174 | skip(3 days); 175 | assertGt(vest.unpaid(vestId), 0, "before 2nd distribution: unexpected unpaid amount"); 176 | 177 | // Workable now modifies state, so we need this hack to make the test pass. 178 | uint256 beforeWorkable = vm.snapshot(); 179 | (bool canWork, bytes memory args) = job.workable(NET_A); 180 | vm.revertTo(beforeWorkable); 181 | 182 | assertFalse(canWork, "before 2nd distribution: workable() should return false"); 183 | assertEq(args, bytes("No distribution")); 184 | 185 | vm.expectRevert( 186 | abi.encodeWithSelector(VestedRewardsDistributionJob.NotDue.selector, address(vestedRewardsDist1)) 187 | ); 188 | job.work(NET_A, abi.encode(address(vestedRewardsDist1))); 189 | } 190 | 191 | // Now enough time has passed, so the distribution can be made 192 | { 193 | skip(4 days); 194 | 195 | // Workable now modifies state, so we need this hack to make the test pass. 196 | uint256 beforeWorkable = vm.snapshot(); 197 | (bool canWork, bytes memory args) = job.workable(NET_A); 198 | vm.revertTo(beforeWorkable); 199 | 200 | assertTrue(canWork, "2nd distribution: workable() should return true"); 201 | (address rewDist) = abi.decode(args, (address)); 202 | 203 | vm.expectEmit(true, false, false, true); 204 | emit Work(NET_A, rewDist, vest.unpaid(vestId)); 205 | job.work(NET_A, args); 206 | 207 | // Checks that there is no vesting amount to be paid 208 | assertEq(vest.unpaid(vestId), 0, "2nd distribution: unexpected unpaid amount"); 209 | } 210 | 211 | // After the vest is expired, the job is no longer executable 212 | { 213 | skip(duration); 214 | // Distribute manually, so there is no remaining 215 | vestedRewardsDist1.distribute(); 216 | assertEq(vest.unpaid(vestId), 0, "after stream expiration: unpaid amount should be zero"); 217 | 218 | // Workable now modifies state, so we need this hack to make the test pass. 219 | uint256 beforeWorkable = vm.snapshot(); 220 | (bool canWork, bytes memory args) = job.workable(NET_A); 221 | vm.revertTo(beforeWorkable); 222 | 223 | assertFalse(canWork, "after stream expiration: workable() should return false"); 224 | assertEq(args, bytes("No distribution")); 225 | } 226 | } 227 | 228 | function test_work_two_farms() public { 229 | VestedRewardsDistributionLike[2] memory rewDistributions = [vestedRewardsDist1, vestedRewardsDist2]; 230 | uint256[] memory vestAmounts = new uint256[](2); 231 | uint256 duration = 360 days; 232 | uint256 total = 100 ether; 233 | 234 | for (uint256 i = 0; i < 2; i++) { 235 | VestedRewardsDistributionLike dist = rewDistributions[i]; 236 | DssVestLike vest = DssVestLike(dist.dssVest()); 237 | 238 | uint256 vestId = _replaceVestingStream( 239 | address(dist), VestParams({bgn: block.timestamp - duration / 2, eta: 0, tau: duration, tot: total}) 240 | ); 241 | 242 | vestAmounts[i] = vest.unpaid(vestId); 243 | assertEq(vestAmounts[i], 50 ether, "1st: invalid vest amount"); 244 | assertEq(dist.lastDistributedAt(), 0, "1st: invalid lastDistributedAt"); 245 | 246 | // Workable now modifies state, so we need this hack to make the test pass. 247 | uint256 beforeWorkable = vm.snapshot(); 248 | (, bytes memory args) = job.workable(NET_A); 249 | vm.revertTo(beforeWorkable); 250 | 251 | (address rewDist) = abi.decode(args, (address)); 252 | vm.expectEmit(true, false, false, true); 253 | emit Work(NET_A, rewDist, vestAmounts[i]); 254 | job.work(NET_A, args); 255 | 256 | // check that there is no vesting amount to be paid 257 | uint256 vestAmount = vest.unpaid(vestId); 258 | assertEq(vestAmount, 0); 259 | } 260 | // now workable should return false 261 | (bool canWork,) = job.workable(NET_A); 262 | assertFalse(canWork, "after 1st: workable() returns true"); 263 | 264 | // Advances time and try to execute the job once again for both 265 | uint256 prevTimestamp = block.timestamp; 266 | job.set(address(vestedRewardsDist1), 7 days); 267 | job.set(address(vestedRewardsDist2), 7 days); 268 | 269 | // workable should return false because not enough time has elapsed 270 | skip(2 days); 271 | 272 | for (uint256 i = 0; i < 2; i++) { 273 | VestedRewardsDistributionLike dist = rewDistributions[i]; 274 | DssVestLike vest = DssVestLike(dist.dssVest()); 275 | 276 | vestAmounts[i] = vest.unpaid(dist.vestId()); 277 | assertGe(vestAmounts[i], 0, "after 1st: invalid vest amount"); 278 | assertEq(dist.lastDistributedAt(), prevTimestamp, "2nd: invalid lastDistributedAt"); 279 | 280 | // Workable now modifies state, so we need this hack to make the test pass. 281 | uint256 beforeWorkable = vm.snapshot(); 282 | (canWork,) = job.workable(NET_A); 283 | vm.revertTo(beforeWorkable); 284 | 285 | assertFalse(canWork, "after 1st: workable() returns true"); 286 | } 287 | 288 | // finally enough time passes, then the job must be workable again 289 | skip(5 days); 290 | 291 | for (uint256 i = 0; i < 2; i++) { 292 | VestedRewardsDistributionLike dist = rewDistributions[i]; 293 | DssVestLike vest = DssVestLike(dist.dssVest()); 294 | 295 | vestAmounts[i] = vest.unpaid(dist.vestId()); 296 | assertGe(vestAmounts[i], 0, "2nd: invalid vest amount"); 297 | assertEq(dist.lastDistributedAt(), prevTimestamp, "2nd: invalid lastDistributedAt"); 298 | 299 | // Workable now modifies state, so we need this hack to make the test pass. 300 | uint256 beforeWorkable = vm.snapshot(); 301 | (, bytes memory args) = job.workable(NET_A); 302 | vm.revertTo(beforeWorkable); 303 | 304 | (address rewDist) = abi.decode(args, (address)); 305 | vm.expectEmit(true, false, false, true); 306 | emit Work(NET_A, rewDist, vestAmounts[i]); 307 | job.work(NET_A, args); 308 | 309 | // check that there is no vesting amount to be paid 310 | uint256 vestAmount = vest.unpaid(dist.vestId()); 311 | assertEq(vestAmount, 0); 312 | } 313 | 314 | // now workable should return false 315 | (canWork,) = job.workable(NET_A); 316 | assertFalse(canWork, "after 2nd: workable() returns true"); 317 | } 318 | 319 | function test_cannot_work_if_distribute_reverts() public { 320 | uint256 duration = 360 days; 321 | uint256 total = 100 ether; 322 | 323 | job.rem(address(vestedRewardsDist1)); 324 | job.rem(address(vestedRewardsDist2)); 325 | 326 | // Ensures the vesting stream is valid 327 | uint256 vestId = _replaceVestingStream( 328 | address(vestedRewardsDist1), VestParams({bgn: block.timestamp, eta: 0, tau: duration, tot: total}) 329 | ); 330 | DssVestLike vest = DssVestLike(vestedRewardsDist1.dssVest()); 331 | 332 | RevertOnDistributeWrapper dist = new RevertOnDistributeWrapper(address(vestedRewardsDist1)); 333 | job.set(address(dist), RANDOM_INTERVAL); 334 | 335 | // Since this would be the first distribution, the interval cannot be easily enforced, 336 | // so the job would be workable if distribute did not revert 337 | { 338 | skip(2 days); 339 | assertGt(vest.unpaid(vestId), 0, "unpaid amount should not be zero"); 340 | 341 | // Workable now modifies state, so we need this hack to make the test pass. 342 | uint256 beforeWorkable = vm.snapshot(); 343 | (bool canWork, bytes memory args) = job.workable(NET_A); 344 | vm.revertTo(beforeWorkable); 345 | 346 | assertFalse(canWork, "workable() should return false"); 347 | assertEq(args, bytes("No distribution")); 348 | 349 | vm.expectRevert("Cannot distribute"); 350 | job.work(NET_A, abi.encode(address(dist))); 351 | } 352 | } 353 | 354 | function test_workable_no_distribution() public { 355 | // call work for both contracts 356 | bytes memory args = abi.encode(address(vestedRewardsDist1)); 357 | job.work(NET_A, args); 358 | args = abi.encode(vestedRewardsDist2); 359 | job.work(NET_A, args); 360 | bool canWork; 361 | (canWork, args) = job.workable(NET_A); 362 | assertFalse(canWork, "workable() returns true"); 363 | assertEq(args, bytes("No distribution"), "Wrong message"); 364 | } 365 | 366 | function test_work_revert_non_master_network() public { 367 | bytes32 network = "ERROR"; 368 | bytes memory args = abi.encode("0"); 369 | vm.expectRevert(abi.encodeWithSelector(VestedRewardsDistributionJob.NotMaster.selector, network)); 370 | job.work(network, args); 371 | } 372 | 373 | function test_work_revert_random_distribution() public { 374 | address rewDist = address(42); 375 | bytes memory args = abi.encode(rewDist); 376 | vm.expectRevert(abi.encodeWithSelector(VestedRewardsDistributionJob.NotFound.selector, rewDist)); 377 | job.work(NET_A, args); 378 | } 379 | 380 | function test_work_revert_garbage_args() public { 381 | bytes memory args = abi.encode(0x74389); 382 | (address rewDist) = abi.decode(args, (address)); 383 | vm.expectRevert(abi.encodeWithSelector(VestedRewardsDistributionJob.NotFound.selector, rewDist)); 384 | job.work(NET_A, args); 385 | } 386 | 387 | function test_work_revert_no_args() public { 388 | bytes memory emptyArray; 389 | // empty array, work() should revert 390 | vm.expectRevert(abi.encodeWithSelector(VestedRewardsDistributionJob.NoArgs.selector)); 391 | job.work(NET_A, emptyArray); 392 | } 393 | 394 | struct VestParams { 395 | uint256 bgn; 396 | uint256 tau; 397 | uint256 eta; 398 | uint256 tot; 399 | } 400 | 401 | function _replaceVestingStream(address dist, VestParams memory p) internal returns (uint256 newVestId) { 402 | VestedRewardsDistributionLike _dist = VestedRewardsDistributionLike(dist); 403 | uint256 currentVestId = _dist.vestId(); 404 | DssVestLike vest = DssVestLike(_dist.dssVest()); 405 | (address usr,,,, address mgr, uint8 res,,) = vest.awards(currentVestId); 406 | 407 | newVestId = vest.create(usr, p.tot, p.bgn, p.tau, p.eta, mgr); 408 | if (res == 1) { 409 | vest.restrict(newVestId); 410 | } 411 | 412 | _dist.file("vestId", newVestId); 413 | } 414 | 415 | // --- Events --- 416 | event Work(bytes32 indexed network, address indexed rewDist, uint256 amount); 417 | event Set(address indexed rewdist, uint256 interval); 418 | event Rem(address indexed rewDist); 419 | } 420 | 421 | contract RevertOnDistributeWrapper { 422 | address internal immutable dist; 423 | 424 | constructor(address _dist) { 425 | dist = _dist; 426 | } 427 | 428 | function distribute() public pure { 429 | revert("Cannot distribute"); 430 | } 431 | 432 | /** 433 | * @dev Fallback method to forward every other call to the underlying VestedRewardsDistribution contract. 434 | */ 435 | fallback(bytes calldata _in) external returns (bytes memory) { 436 | (bool ok, bytes memory out) = dist.call(_in); 437 | require(ok, string(out)); 438 | return out; 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /src/tests/mocks/DaiJoinMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | /// DaiJoin.sol -- Dai adapter 4 | 5 | // Copyright (C) 2018 Rain 6 | // Copyright (C) 2022 Dai Foundation 7 | // 8 | // This program is free software: you can redistribute it and/or modify 9 | // it under the terms of the GNU Affero General Public License as published by 10 | // the Free Software Foundation, either version 3 of the License, or 11 | // (at your option) any later version. 12 | // 13 | // This program is distributed in the hope that it will be useful, 14 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | // GNU Affero General Public License for more details. 17 | // 18 | // You should have received a copy of the GNU Affero General Public License 19 | // along with this program. If not, see . 20 | 21 | pragma solidity ^0.8.13; 22 | 23 | interface DaiLike { 24 | function burn(address,uint256) external; 25 | function mint(address,uint256) external; 26 | } 27 | 28 | interface VatLike { 29 | function move(address,address,uint256) external; 30 | } 31 | 32 | contract DaiJoinMock { 33 | VatLike public immutable vat; // CDP Engine 34 | DaiLike public immutable dai; // Stablecoin Token 35 | uint256 constant RAY = 10 ** 27; 36 | 37 | // --- Events --- 38 | event Join(address indexed usr, uint256 wad); 39 | event Exit(address indexed usr, uint256 wad); 40 | 41 | constructor(address vat_, address dai_) { 42 | vat = VatLike(vat_); 43 | dai = DaiLike(dai_); 44 | } 45 | 46 | // --- User's functions --- 47 | function join(address usr, uint256 wad) external { 48 | vat.move(address(this), usr, RAY * wad); 49 | dai.burn(msg.sender, wad); 50 | emit Join(usr, wad); 51 | } 52 | 53 | function exit(address usr, uint256 wad) external { 54 | vat.move(msg.sender, address(this), RAY * wad); 55 | dai.mint(usr, wad); 56 | emit Exit(usr, wad); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/tests/mocks/DaiMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | /// Dai.sol -- Dai token 4 | 5 | // Copyright (C) 2017, 2018, 2019 dbrock, rain, mrchico 6 | // Copyright (C) 2021-2022 Dai Foundation 7 | // 8 | // This program is free software: you can redistribute it and/or modify 9 | // it under the terms of the GNU Affero General Public License as published by 10 | // the Free Software Foundation, either version 3 of the License, or 11 | // (at your option) any later version. 12 | // 13 | // This program is distributed in the hope that it will be useful, 14 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | // GNU Affero General Public License for more details. 17 | // 18 | // You should have received a copy of the GNU Affero General Public License 19 | // along with this program. If not, see . 20 | 21 | pragma solidity ^0.8.13; 22 | 23 | interface IERC1271 { 24 | function isValidSignature( 25 | bytes32, 26 | bytes memory 27 | ) external view returns (bytes4); 28 | } 29 | 30 | contract DaiMock { 31 | mapping (address => uint256) public wards; 32 | 33 | // --- ERC20 Data --- 34 | string public constant name = "Dai Stablecoin"; 35 | string public constant symbol = "DAI"; 36 | string public constant version = "3"; 37 | uint8 public constant decimals = 18; 38 | uint256 public totalSupply; 39 | 40 | mapping (address => uint256) public balanceOf; 41 | mapping (address => mapping (address => uint256)) public allowance; 42 | mapping (address => uint256) public nonces; 43 | 44 | // --- Events --- 45 | event Rely(address indexed usr); 46 | event Deny(address indexed usr); 47 | event Approval(address indexed owner, address indexed spender, uint256 value); 48 | event Transfer(address indexed from, address indexed to, uint256 value); 49 | 50 | // --- EIP712 niceties --- 51 | uint256 public immutable deploymentChainId; 52 | bytes32 private immutable _DOMAIN_SEPARATOR; 53 | bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 54 | 55 | modifier auth { 56 | require(wards[msg.sender] == 1, "Dai/not-authorized"); 57 | _; 58 | } 59 | 60 | constructor() { 61 | wards[msg.sender] = 1; 62 | emit Rely(msg.sender); 63 | 64 | deploymentChainId = block.chainid; 65 | _DOMAIN_SEPARATOR = _calculateDomainSeparator(block.chainid); 66 | } 67 | 68 | function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) { 69 | return keccak256( 70 | abi.encode( 71 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 72 | keccak256(bytes(name)), 73 | keccak256(bytes(version)), 74 | chainId, 75 | address(this) 76 | ) 77 | ); 78 | } 79 | 80 | function DOMAIN_SEPARATOR() external view returns (bytes32) { 81 | return block.chainid == deploymentChainId ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(block.chainid); 82 | } 83 | 84 | // --- Administration --- 85 | function rely(address usr) external auth { 86 | wards[usr] = 1; 87 | emit Rely(usr); 88 | } 89 | 90 | function deny(address usr) external auth { 91 | wards[usr] = 0; 92 | emit Deny(usr); 93 | } 94 | 95 | // --- ERC20 Mutations --- 96 | function transfer(address to, uint256 value) external returns (bool) { 97 | require(to != address(0) && to != address(this), "Dai/invalid-address"); 98 | uint256 balance = balanceOf[msg.sender]; 99 | require(balance >= value, "Dai/insufficient-balance"); 100 | 101 | unchecked { 102 | balanceOf[msg.sender] = balance - value; 103 | balanceOf[to] += value; 104 | } 105 | 106 | emit Transfer(msg.sender, to, value); 107 | 108 | return true; 109 | } 110 | 111 | function transferFrom(address from, address to, uint256 value) external returns (bool) { 112 | require(to != address(0) && to != address(this), "Dai/invalid-address"); 113 | uint256 balance = balanceOf[from]; 114 | require(balance >= value, "Dai/insufficient-balance"); 115 | 116 | if (from != msg.sender) { 117 | uint256 allowed = allowance[from][msg.sender]; 118 | if (allowed != type(uint256).max) { 119 | require(allowed >= value, "Dai/insufficient-allowance"); 120 | 121 | unchecked { 122 | allowance[from][msg.sender] = allowed - value; 123 | } 124 | } 125 | } 126 | 127 | unchecked { 128 | balanceOf[from] = balance - value; 129 | balanceOf[to] += value; 130 | } 131 | 132 | emit Transfer(from, to, value); 133 | 134 | return true; 135 | } 136 | 137 | function approve(address spender, uint256 value) external returns (bool) { 138 | allowance[msg.sender][spender] = value; 139 | 140 | emit Approval(msg.sender, spender, value); 141 | 142 | return true; 143 | } 144 | 145 | function increaseAllowance(address spender, uint256 addedValue) external returns (bool) { 146 | uint256 newValue = allowance[msg.sender][spender] + addedValue; 147 | allowance[msg.sender][spender] = newValue; 148 | 149 | emit Approval(msg.sender, spender, newValue); 150 | 151 | return true; 152 | } 153 | 154 | function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) { 155 | uint256 allowed = allowance[msg.sender][spender]; 156 | require(allowed >= subtractedValue, "Dai/insufficient-allowance"); 157 | unchecked{ 158 | allowed = allowed - subtractedValue; 159 | } 160 | allowance[msg.sender][spender] = allowed; 161 | 162 | emit Approval(msg.sender, spender, allowed); 163 | 164 | return true; 165 | } 166 | 167 | // --- Mint/Burn --- 168 | function mint(address to, uint256 value) external auth { 169 | require(to != address(0) && to != address(this), "Dai/invalid-address"); 170 | unchecked { 171 | balanceOf[to] = balanceOf[to] + value; // note: we don't need an overflow check here b/c balanceOf[to] <= totalSupply and there is an overflow check below 172 | } 173 | totalSupply = totalSupply + value; 174 | 175 | emit Transfer(address(0), to, value); 176 | } 177 | 178 | function burn(address from, uint256 value) external { 179 | uint256 balance = balanceOf[from]; 180 | require(balance >= value, "Dai/insufficient-balance"); 181 | 182 | if (from != msg.sender) { 183 | uint256 allowed = allowance[from][msg.sender]; 184 | if (allowed != type(uint256).max) { 185 | require(allowed >= value, "Dai/insufficient-allowance"); 186 | 187 | unchecked { 188 | allowance[from][msg.sender] = allowed - value; 189 | } 190 | } 191 | } 192 | 193 | unchecked { 194 | balanceOf[from] = balance - value; // note: we don't need overflow checks b/c require(balance >= value) and balance <= totalSupply 195 | totalSupply = totalSupply - value; 196 | } 197 | 198 | emit Transfer(from, address(0), value); 199 | } 200 | 201 | // --- Approve by signature --- 202 | 203 | function _isValidSignature( 204 | address signer, 205 | bytes32 digest, 206 | bytes memory signature 207 | ) internal view returns (bool) { 208 | if (signature.length == 65) { 209 | bytes32 r; 210 | bytes32 s; 211 | uint8 v; 212 | assembly { 213 | r := mload(add(signature, 0x20)) 214 | s := mload(add(signature, 0x40)) 215 | v := byte(0, mload(add(signature, 0x60))) 216 | } 217 | if (signer == ecrecover(digest, v, r, s)) { 218 | return true; 219 | } 220 | } 221 | 222 | (bool success, bytes memory result) = signer.staticcall( 223 | abi.encodeWithSelector(IERC1271.isValidSignature.selector, digest, signature) 224 | ); 225 | return (success && 226 | result.length == 32 && 227 | abi.decode(result, (bytes4)) == IERC1271.isValidSignature.selector); 228 | } 229 | 230 | function permit( 231 | address owner, 232 | address spender, 233 | uint256 value, 234 | uint256 deadline, 235 | bytes memory signature 236 | ) public { 237 | require(block.timestamp <= deadline, "Dai/permit-expired"); 238 | require(owner != address(0), "Dai/invalid-owner"); 239 | 240 | uint256 nonce; 241 | unchecked { nonce = nonces[owner]++; } 242 | 243 | bytes32 digest = 244 | keccak256(abi.encodePacked( 245 | "\x19\x01", 246 | block.chainid == deploymentChainId ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(block.chainid), 247 | keccak256(abi.encode( 248 | PERMIT_TYPEHASH, 249 | owner, 250 | spender, 251 | value, 252 | nonce, 253 | deadline 254 | )) 255 | )); 256 | 257 | require(_isValidSignature(owner, digest, signature), "Dai/invalid-permit"); 258 | 259 | allowance[owner][spender] = value; 260 | emit Approval(owner, spender, value); 261 | } 262 | 263 | function permit( 264 | address owner, 265 | address spender, 266 | uint256 value, 267 | uint256 deadline, 268 | uint8 v, 269 | bytes32 r, 270 | bytes32 s 271 | ) external { 272 | permit(owner, spender, value, deadline, abi.encodePacked(r, s, v)); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/tests/mocks/VatMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | /// vat.sol -- Dai CDP database 4 | 5 | // Copyright (C) 2018 Rain 6 | // 7 | // This program is free software: you can redistribute it and/or modify 8 | // it under the terms of the GNU Affero General Public License as published by 9 | // the Free Software Foundation, either version 3 of the License, or 10 | // (at your option) any later version. 11 | // 12 | // This program is distributed in the hope that it will be useful, 13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | // GNU Affero General Public License for more details. 16 | // 17 | // You should have received a copy of the GNU Affero General Public License 18 | // along with this program. If not, see . 19 | 20 | pragma solidity ^0.8.13; 21 | 22 | contract VatMock { 23 | // --- Data --- 24 | mapping (address => uint256) public wards; 25 | 26 | mapping(address => mapping (address => uint256)) public can; 27 | 28 | struct Ilk { 29 | uint256 Art; // Total Normalised Debt [wad] 30 | uint256 rate; // Accumulated Rates [ray] 31 | uint256 spot; // Price with Safety Margin [ray] 32 | uint256 line; // Debt Ceiling [rad] 33 | uint256 dust; // Urn Debt Floor [rad] 34 | } 35 | struct Urn { 36 | uint256 ink; // Locked Collateral [wad] 37 | uint256 art; // Normalised Debt [wad] 38 | } 39 | 40 | mapping (bytes32 => Ilk) public ilks; 41 | mapping (bytes32 => mapping (address => Urn)) public urns; 42 | mapping (bytes32 => mapping (address => uint256)) public gem; // [wad] 43 | mapping (address => uint256) public dai; // [rad] 44 | mapping (address => uint256) public sin; // [rad] 45 | 46 | uint256 public debt; // Total Dai Issued [rad] 47 | uint256 public vice; // Total Unbacked Dai [rad] 48 | uint256 public Line; // Total Debt Ceiling [rad] 49 | uint256 public live; // Active Flag 50 | 51 | // --- Events --- 52 | event Rely(address indexed usr); 53 | event Deny(address indexed usr); 54 | event Init(bytes32 indexed ilk); 55 | event File(bytes32 indexed what, uint256 data); 56 | event File(bytes32 indexed ilk, bytes32 indexed what, uint256 data); 57 | event Cage(); 58 | event Hope(address indexed from, address indexed to); 59 | event Nope(address indexed from, address indexed to); 60 | event Slip(bytes32 indexed ilk, address indexed usr, int256 wad); 61 | event Flux(bytes32 indexed ilk, address indexed src, address indexed dst, uint256 wad); 62 | event Move(address indexed src, address indexed dst, uint256 rad); 63 | event Frob(bytes32 indexed i, address indexed u, address v, address w, int256 dink, int256 dart); 64 | event Fork(bytes32 indexed ilk, address indexed src, address indexed dst, int256 dink, int256 dart); 65 | event Grab(bytes32 indexed i, address indexed u, address v, address w, int256 dink, int256 dart); 66 | event Heal(address indexed u, uint256 rad); 67 | event Suck(address indexed u, address indexed v, uint256 rad); 68 | event Fold(bytes32 indexed i, address indexed u, int256 rate); 69 | 70 | modifier auth { 71 | require(wards[msg.sender] == 1, "Vat/not-authorized"); 72 | _; 73 | } 74 | 75 | function wish(address bit, address usr) internal view returns (bool) { 76 | return either(bit == usr, can[bit][usr] == 1); 77 | } 78 | 79 | // --- Init --- 80 | constructor() { 81 | wards[msg.sender] = 1; 82 | live = 1; 83 | emit Rely(msg.sender); 84 | } 85 | 86 | // --- Math --- 87 | string private constant ARITHMETIC_ERROR = string(abi.encodeWithSignature("Panic(uint256)", 0x11)); 88 | function _add(uint256 x, int256 y) internal pure returns (uint256 z) { 89 | unchecked { 90 | z = x + uint256(y); 91 | } 92 | require(y >= 0 || z <= x, ARITHMETIC_ERROR); 93 | require(y <= 0 || z >= x, ARITHMETIC_ERROR); 94 | } 95 | function _sub(uint256 x, int256 y) internal pure returns (uint256 z) { 96 | unchecked { 97 | z = x - uint256(y); 98 | } 99 | require(y <= 0 || z <= x, ARITHMETIC_ERROR); 100 | require(y >= 0 || z >= x, ARITHMETIC_ERROR); 101 | } 102 | function _int256(uint256 x) internal pure returns (int256 y) { 103 | require((y = int256(x)) >= 0, ARITHMETIC_ERROR); 104 | } 105 | 106 | // --- Administration --- 107 | function rely(address usr) external auth { 108 | require(live == 1, "Vat/not-live"); 109 | wards[usr] = 1; 110 | emit Rely(usr); 111 | } 112 | 113 | function deny(address usr) external auth { 114 | require(live == 1, "Vat/not-live"); 115 | wards[usr] = 0; 116 | emit Deny(usr); 117 | } 118 | 119 | function init(bytes32 ilk) external auth { 120 | require(ilks[ilk].rate == 0, "Vat/ilk-already-init"); 121 | ilks[ilk].rate = 10 ** 27; 122 | emit Init(ilk); 123 | } 124 | 125 | function file(bytes32 what, uint256 data) external auth { 126 | require(live == 1, "Vat/not-live"); 127 | if (what == "Line") Line = data; 128 | else revert("Vat/file-unrecognized-param"); 129 | emit File(what, data); 130 | } 131 | 132 | function file(bytes32 ilk, bytes32 what, uint256 data) external auth { 133 | require(live == 1, "Vat/not-live"); 134 | if (what == "spot") ilks[ilk].spot = data; 135 | else if (what == "line") ilks[ilk].line = data; 136 | else if (what == "dust") ilks[ilk].dust = data; 137 | else revert("Vat/file-unrecognized-param"); 138 | emit File(ilk, what, data); 139 | } 140 | 141 | function cage() external auth { 142 | live = 0; 143 | emit Cage(); 144 | } 145 | 146 | // --- Structs getters --- 147 | function Art(bytes32 ilk) external view returns (uint256 Art_) { 148 | Art_ = ilks[ilk].Art; 149 | } 150 | 151 | function rate(bytes32 ilk) external view returns (uint256 rate_) { 152 | rate_ = ilks[ilk].rate; 153 | } 154 | 155 | function spot(bytes32 ilk) external view returns (uint256 spot_) { 156 | spot_ = ilks[ilk].spot; 157 | } 158 | 159 | function line(bytes32 ilk) external view returns (uint256 line_) { 160 | line_ = ilks[ilk].line; 161 | } 162 | 163 | function dust(bytes32 ilk) external view returns (uint256 dust_) { 164 | dust_ = ilks[ilk].dust; 165 | } 166 | 167 | function ink(bytes32 ilk, address urn) external view returns (uint256 ink_) { 168 | ink_ = urns[ilk][urn].ink; 169 | } 170 | 171 | function art(bytes32 ilk, address urn) external view returns (uint256 art_) { 172 | art_ = urns[ilk][urn].art; 173 | } 174 | 175 | // --- Allowance --- 176 | function hope(address usr) external { 177 | can[msg.sender][usr] = 1; 178 | emit Hope(msg.sender, usr); 179 | } 180 | 181 | function nope(address usr) external { 182 | can[msg.sender][usr] = 0; 183 | emit Nope(msg.sender, usr); 184 | } 185 | 186 | // --- Fungibility --- 187 | function slip(bytes32 ilk, address usr, int256 wad) external auth { 188 | gem[ilk][usr] = _add(gem[ilk][usr], wad); 189 | emit Slip(ilk, usr, wad); 190 | } 191 | 192 | function flux(bytes32 ilk, address src, address dst, uint256 wad) external { 193 | require(wish(src, msg.sender), "Vat/not-allowed"); 194 | gem[ilk][src] = gem[ilk][src] - wad; 195 | gem[ilk][dst] = gem[ilk][dst] + wad; 196 | emit Flux(ilk, src, dst, wad); 197 | } 198 | 199 | function move(address src, address dst, uint256 rad) external { 200 | require(wish(src, msg.sender), "Vat/not-allowed"); 201 | dai[src] = dai[src] - rad; 202 | dai[dst] = dai[dst] + rad; 203 | emit Move(src, dst, rad); 204 | } 205 | 206 | function either(bool x, bool y) internal pure returns (bool z) { 207 | assembly{ z := or(x, y)} 208 | } 209 | 210 | function both(bool x, bool y) internal pure returns (bool z) { 211 | assembly{ z := and(x, y)} 212 | } 213 | 214 | // --- CDP Manipulation --- 215 | function frob(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external { 216 | // system is live 217 | require(live == 1, "Vat/not-live"); 218 | 219 | uint256 rate_ = ilks[i].rate; 220 | // ilk has been initialised 221 | require(rate_ != 0, "Vat/ilk-not-init"); 222 | 223 | Urn memory urn = urns[i][u]; 224 | urn.ink = _add(urn.ink, dink); 225 | urn.art = _add(urn.art, dart); 226 | 227 | uint256 Art_ = _add(ilks[i].Art, dart); 228 | int256 dtab = _int256(rate_) * dart; 229 | uint256 debt_ = _add(debt, dtab); 230 | 231 | // either debt has decreased, or debt ceilings are not exceeded 232 | require(either(dart <= 0, both(Art_ * rate_ <= ilks[i].line, debt_ <= Line)), "Vat/ceiling-exceeded"); 233 | uint256 tab = rate_ * urn.art; 234 | // urn is either less risky than before, or it is safe 235 | require(either(both(dart <= 0, dink >= 0), tab <= urn.ink * ilks[i].spot), "Vat/not-safe"); 236 | 237 | // urn is either more safe, or the owner consents 238 | require(either(both(dart <= 0, dink >= 0), wish(u, msg.sender)), "Vat/not-allowed-u"); 239 | // collateral src consents 240 | require(either(dink <= 0, wish(v, msg.sender)), "Vat/not-allowed-v"); 241 | // debt dst consents 242 | require(either(dart >= 0, wish(w, msg.sender)), "Vat/not-allowed-w"); 243 | 244 | // urn has no debt, or a non-dusty amount 245 | require(either(urn.art == 0, tab >= ilks[i].dust), "Vat/dust"); 246 | 247 | // update storage values 248 | gem[i][v] = _sub(gem[i][v], dink); 249 | dai[w] = _add(dai[w], dtab); 250 | urns[i][u] = urn; 251 | ilks[i].Art = Art_; 252 | debt = debt_; 253 | 254 | emit Frob(i, u, v, w, dink, dart); 255 | } 256 | 257 | // --- CDP Fungibility --- 258 | function fork(bytes32 ilk, address src, address dst, int256 dink, int256 dart) external { 259 | Urn storage u = urns[ilk][src]; 260 | Urn storage v = urns[ilk][dst]; 261 | Ilk storage i = ilks[ilk]; 262 | 263 | u.ink = _sub(u.ink, dink); 264 | u.art = _sub(u.art, dart); 265 | v.ink = _add(v.ink, dink); 266 | v.art = _add(v.art, dart); 267 | 268 | uint256 utab = u.art * i.rate; 269 | uint256 vtab = v.art * i.rate; 270 | 271 | // both sides consent 272 | require(both(wish(src, msg.sender), wish(dst, msg.sender)), "Vat/not-allowed"); 273 | 274 | // both sides safe 275 | require(utab <= u.ink * i.spot, "Vat/not-safe-src"); 276 | require(vtab <= v.ink * i.spot, "Vat/not-safe-dst"); 277 | 278 | // both sides non-dusty 279 | require(either(utab >= i.dust, u.art == 0), "Vat/dust-src"); 280 | require(either(vtab >= i.dust, v.art == 0), "Vat/dust-dst"); 281 | 282 | emit Fork(ilk, src, dst, dink, dart); 283 | } 284 | 285 | // --- CDP Confiscation --- 286 | function grab(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external auth { 287 | Urn storage urn = urns[i][u]; 288 | Ilk storage ilk = ilks[i]; 289 | 290 | urn.ink = _add(urn.ink, dink); 291 | urn.art = _add(urn.art, dart); 292 | ilk.Art = _add(ilk.Art, dart); 293 | 294 | int256 dtab = _int256(ilk.rate) * dart; 295 | 296 | gem[i][v] = _sub(gem[i][v], dink); 297 | sin[w] = _sub(sin[w], dtab); 298 | vice = _sub(vice, dtab); 299 | 300 | emit Grab(i, u, v, w, dink, dart); 301 | } 302 | 303 | // --- Settlement --- 304 | function heal(uint256 rad) external { 305 | address u = msg.sender; 306 | sin[u] = sin[u] - rad; 307 | dai[u] = dai[u] - rad; 308 | vice = vice - rad; 309 | debt = debt - rad; 310 | 311 | emit Heal(msg.sender, rad); 312 | } 313 | 314 | function suck(address u, address v, uint256 rad) external auth { 315 | sin[u] = sin[u] + rad; 316 | dai[v] = dai[v] + rad; 317 | vice = vice + rad; 318 | debt = debt + rad; 319 | 320 | emit Suck(u, v, rad); 321 | } 322 | 323 | // --- Rates --- 324 | function fold(bytes32 i, address u, int256 rate_) external auth { 325 | require(live == 1, "Vat/not-live"); 326 | Ilk storage ilk = ilks[i]; 327 | ilk.rate = _add(ilk.rate, rate_); 328 | int256 rad = _int256(ilk.Art) * rate_; 329 | dai[u] = _add(dai[u], rad); 330 | debt = _add(debt, rad); 331 | 332 | emit Fold(i, u, rate_); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/utils/EnumerableSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (utils/structs/EnumerableSet.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Library for managing 8 | * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive 9 | * types. 10 | * 11 | * Sets have the following properties: 12 | * 13 | * - Elements are added, removed, and checked for existence in constant time 14 | * (O(1)). 15 | * - Elements are enumerated in O(n). No guarantees are made on the ordering. 16 | * 17 | * ``` 18 | * contract Example { 19 | * // Add the library methods 20 | * using EnumerableSet for EnumerableSet.AddressSet; 21 | * 22 | * // Declare a set state variable 23 | * EnumerableSet.AddressSet private mySet; 24 | * } 25 | * ``` 26 | * 27 | * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) 28 | * and `uint256` (`UintSet`) are supported. 29 | */ 30 | library EnumerableSet { 31 | // To implement this library for multiple types with as little code 32 | // repetition as possible, we write it in terms of a generic Set type with 33 | // bytes32 values. 34 | // The Set implementation uses private functions, and user-facing 35 | // implementations (such as AddressSet) are just wrappers around the 36 | // underlying Set. 37 | // This means that we can only create new EnumerableSets for types that fit 38 | // in bytes32. 39 | 40 | struct Set { 41 | // Storage of set values 42 | bytes32[] _values; 43 | // Position of the value in the `values` array, plus 1 because index 0 44 | // means a value is not in the set. 45 | mapping(bytes32 => uint256) _indexes; 46 | } 47 | 48 | /** 49 | * @dev Add a value to a set. O(1). 50 | * 51 | * Returns true if the value was added to the set, that is if it was not 52 | * already present. 53 | */ 54 | function _add(Set storage set, bytes32 value) private returns (bool) { 55 | if (!_contains(set, value)) { 56 | set._values.push(value); 57 | // The value is stored at length-1, but we add 1 to all indexes 58 | // and use 0 as a sentinel value 59 | set._indexes[value] = set._values.length; 60 | return true; 61 | } else { 62 | return false; 63 | } 64 | } 65 | 66 | /** 67 | * @dev Removes a value from a set. O(1). 68 | * 69 | * Returns true if the value was removed from the set, that is if it was 70 | * present. 71 | */ 72 | function _remove(Set storage set, bytes32 value) private returns (bool) { 73 | // We read and store the value's index to prevent multiple reads from the same storage slot 74 | uint256 valueIndex = set._indexes[value]; 75 | 76 | if (valueIndex != 0) { 77 | // Equivalent to contains(set, value) 78 | // To delete an element from the _values array in O(1), we swap the element to delete with the last one in 79 | // the array, and then remove the last element (sometimes called as 'swap and pop'). 80 | // This modifies the order of the array, as noted in {at}. 81 | 82 | uint256 toDeleteIndex = valueIndex - 1; 83 | uint256 lastIndex = set._values.length - 1; 84 | 85 | if (lastIndex != toDeleteIndex) { 86 | bytes32 lastvalue = set._values[lastIndex]; 87 | 88 | // Move the last value to the index where the value to delete is 89 | set._values[toDeleteIndex] = lastvalue; 90 | // Update the index for the moved value 91 | set._indexes[lastvalue] = valueIndex; // Replace lastvalue's index to valueIndex 92 | } 93 | 94 | // Delete the slot where the moved value was stored 95 | set._values.pop(); 96 | 97 | // Delete the index for the deleted slot 98 | delete set._indexes[value]; 99 | 100 | return true; 101 | } else { 102 | return false; 103 | } 104 | } 105 | 106 | /** 107 | * @dev Returns true if the value is in the set. O(1). 108 | */ 109 | function _contains(Set storage set, bytes32 value) private view returns (bool) { 110 | return set._indexes[value] != 0; 111 | } 112 | 113 | /** 114 | * @dev Returns the number of values on the set. O(1). 115 | */ 116 | function _length(Set storage set) private view returns (uint256) { 117 | return set._values.length; 118 | } 119 | 120 | /** 121 | * @dev Returns the value stored at position `index` in the set. O(1). 122 | * 123 | * Note that there are no guarantees on the ordering of values inside the 124 | * array, and it may change when more values are added or removed. 125 | * 126 | * Requirements: 127 | * 128 | * - `index` must be strictly less than {length}. 129 | */ 130 | function _at(Set storage set, uint256 index) private view returns (bytes32) { 131 | return set._values[index]; 132 | } 133 | 134 | /** 135 | * @dev Return the entire set in an array 136 | * 137 | * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed 138 | * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that 139 | * this function has an unbounded cost, and using it as part of a state-changing function may render the function 140 | * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. 141 | */ 142 | function _values(Set storage set) private view returns (bytes32[] memory) { 143 | return set._values; 144 | } 145 | 146 | // Bytes32Set 147 | 148 | struct Bytes32Set { 149 | Set _inner; 150 | } 151 | 152 | /** 153 | * @dev Add a value to a set. O(1). 154 | * 155 | * Returns true if the value was added to the set, that is if it was not 156 | * already present. 157 | */ 158 | function add(Bytes32Set storage set, bytes32 value) internal returns (bool) { 159 | return _add(set._inner, value); 160 | } 161 | 162 | /** 163 | * @dev Removes a value from a set. O(1). 164 | * 165 | * Returns true if the value was removed from the set, that is if it was 166 | * present. 167 | */ 168 | function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) { 169 | return _remove(set._inner, value); 170 | } 171 | 172 | /** 173 | * @dev Returns true if the value is in the set. O(1). 174 | */ 175 | function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { 176 | return _contains(set._inner, value); 177 | } 178 | 179 | /** 180 | * @dev Returns the number of values in the set. O(1). 181 | */ 182 | function length(Bytes32Set storage set) internal view returns (uint256) { 183 | return _length(set._inner); 184 | } 185 | 186 | /** 187 | * @dev Returns the value stored at position `index` in the set. O(1). 188 | * 189 | * Note that there are no guarantees on the ordering of values inside the 190 | * array, and it may change when more values are added or removed. 191 | * 192 | * Requirements: 193 | * 194 | * - `index` must be strictly less than {length}. 195 | */ 196 | function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { 197 | return _at(set._inner, index); 198 | } 199 | 200 | /** 201 | * @dev Return the entire set in an array 202 | * 203 | * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed 204 | * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that 205 | * this function has an unbounded cost, and using it as part of a state-changing function may render the function 206 | * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. 207 | */ 208 | function values(Bytes32Set storage set) internal view returns (bytes32[] memory) { 209 | return _values(set._inner); 210 | } 211 | 212 | // AddressSet 213 | 214 | struct AddressSet { 215 | Set _inner; 216 | } 217 | 218 | /** 219 | * @dev Add a value to a set. O(1). 220 | * 221 | * Returns true if the value was added to the set, that is if it was not 222 | * already present. 223 | */ 224 | function add(AddressSet storage set, address value) internal returns (bool) { 225 | return _add(set._inner, bytes32(uint256(uint160(value)))); 226 | } 227 | 228 | /** 229 | * @dev Removes a value from a set. O(1). 230 | * 231 | * Returns true if the value was removed from the set, that is if it was 232 | * present. 233 | */ 234 | function remove(AddressSet storage set, address value) internal returns (bool) { 235 | return _remove(set._inner, bytes32(uint256(uint160(value)))); 236 | } 237 | 238 | /** 239 | * @dev Returns true if the value is in the set. O(1). 240 | */ 241 | function contains(AddressSet storage set, address value) internal view returns (bool) { 242 | return _contains(set._inner, bytes32(uint256(uint160(value)))); 243 | } 244 | 245 | /** 246 | * @dev Returns the number of values in the set. O(1). 247 | */ 248 | function length(AddressSet storage set) internal view returns (uint256) { 249 | return _length(set._inner); 250 | } 251 | 252 | /** 253 | * @dev Returns the value stored at position `index` in the set. O(1). 254 | * 255 | * Note that there are no guarantees on the ordering of values inside the 256 | * array, and it may change when more values are added or removed. 257 | * 258 | * Requirements: 259 | * 260 | * - `index` must be strictly less than {length}. 261 | */ 262 | function at(AddressSet storage set, uint256 index) internal view returns (address) { 263 | return address(uint160(uint256(_at(set._inner, index)))); 264 | } 265 | 266 | /** 267 | * @dev Return the entire set in an array 268 | * 269 | * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed 270 | * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that 271 | * this function has an unbounded cost, and using it as part of a state-changing function may render the function 272 | * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. 273 | */ 274 | function values(AddressSet storage set) internal view returns (address[] memory) { 275 | bytes32[] memory store = _values(set._inner); 276 | address[] memory result; 277 | 278 | assembly { 279 | result := store 280 | } 281 | 282 | return result; 283 | } 284 | 285 | // UintSet 286 | 287 | struct UintSet { 288 | Set _inner; 289 | } 290 | 291 | /** 292 | * @dev Add a value to a set. O(1). 293 | * 294 | * Returns true if the value was added to the set, that is if it was not 295 | * already present. 296 | */ 297 | function add(UintSet storage set, uint256 value) internal returns (bool) { 298 | return _add(set._inner, bytes32(value)); 299 | } 300 | 301 | /** 302 | * @dev Removes a value from a set. O(1). 303 | * 304 | * Returns true if the value was removed from the set, that is if it was 305 | * present. 306 | */ 307 | function remove(UintSet storage set, uint256 value) internal returns (bool) { 308 | return _remove(set._inner, bytes32(value)); 309 | } 310 | 311 | /** 312 | * @dev Returns true if the value is in the set. O(1). 313 | */ 314 | function contains(UintSet storage set, uint256 value) internal view returns (bool) { 315 | return _contains(set._inner, bytes32(value)); 316 | } 317 | 318 | /** 319 | * @dev Returns the number of values on the set. O(1). 320 | */ 321 | function length(UintSet storage set) internal view returns (uint256) { 322 | return _length(set._inner); 323 | } 324 | 325 | /** 326 | * @dev Returns the value stored at position `index` in the set. O(1). 327 | * 328 | * Note that there are no guarantees on the ordering of values inside the 329 | * array, and it may change when more values are added or removed. 330 | * 331 | * Requirements: 332 | * 333 | * - `index` must be strictly less than {length}. 334 | */ 335 | function at(UintSet storage set, uint256 index) internal view returns (uint256) { 336 | return uint256(_at(set._inner, index)); 337 | } 338 | 339 | /** 340 | * @dev Return the entire set in an array 341 | * 342 | * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed 343 | * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that 344 | * this function has an unbounded cost, and using it as part of a state-changing function may render the function 345 | * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. 346 | */ 347 | function values(UintSet storage set) internal view returns (uint256[] memory) { 348 | bytes32[] memory store = _values(set._inner); 349 | uint256[] memory result; 350 | 351 | assembly { 352 | result := store 353 | } 354 | 355 | return result; 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | [[ "$ETH_RPC_URL" && "$(seth chain)" == "ethlive" ]] || { echo "Please set a mainnet ETH_RPC_URL"; exit 1; } 5 | 6 | if [[ -z "$1" ]]; then 7 | forge test --rpc-url="$ETH_RPC_URL" 8 | else 9 | forge test --rpc-url="$ETH_RPC_URL" --match "$1" -vvvv 10 | fi 11 | --------------------------------------------------------------------------------