diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml
new file mode 100644
index 00000000..105bdf0e
--- /dev/null
+++ b/.github/workflows/certora.yml
@@ -0,0 +1,54 @@
+name: Certora
+
+on: [push, pull_request]
+
+jobs:
+ certora:
+ name: Certora
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ lockstake:
+ - urn
+ - lsmkr
+ - engine
+ - engine-multicall
+ - clipper
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ submodules: recursive
+
+ - uses: actions/setup-java@v2
+ with:
+ distribution: 'zulu'
+ java-version: '11'
+ java-package: jre
+
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v3
+ with:
+ python-version: 3.8
+
+ - name: Install solc-select
+ run: pip3 install solc-select
+
+ - name: Solc Select 0.8.21
+ run: solc-select install 0.8.21
+
+ - name: Solc Select 0.6.12
+ run: solc-select install 0.6.12
+
+ - name: Solc Select 0.5.12
+ run: solc-select install 0.5.12
+
+ - name: Install Certora
+ run: pip3 install certora-cli-beta
+
+ - name: Verify ${{ matrix.lockstake }}
+ run: make certora-${{ matrix.lockstake }} results=1
+ env:
+ CERTORAKEY: ${{ secrets.CERTORAKEY }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 09880b1d..5f538bb1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,6 +1,6 @@
name: test
-on: workflow_dispatch
+on: [push, pull_request]
env:
FOUNDRY_PROFILE: ci
@@ -32,3 +32,5 @@ jobs:
run: |
forge test -vvv
id: test
+ env:
+ ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}
diff --git a/.gitignore b/.gitignore
index 85198aaa..f665798a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,9 @@ docs/
# Dotenv file
.env
+
+# Certora
+.certora_internal
+
+# Vim
+.*.swp
diff --git a/.gitmodules b/.gitmodules
index a2df3f1f..33cefb58 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
-[submodule "lib/dss-test"]
- path = lib/dss-test
- url = https://github.com/makerdao/dss-test
+[submodule "lib/token-tests"]
+ path = lib/token-tests
+ url = https://github.com/makerdao/token-tests/
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..b2f4c0b8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,6 @@
+PATH := ~/.solc-select/artifacts/:~/.solc-select/artifacts/solc-0.5.12:~/.solc-select/artifacts/solc-0.6.12:~/.solc-select/artifacts/solc-0.8.21:$(PATH)
+certora-urn :; PATH=${PATH} certoraRun certora/LockstakeUrn.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
+certora-lsmkr :; PATH=${PATH} certoraRun certora/LockstakeMkr.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
+certora-engine :; PATH=${PATH} certoraRun certora/LockstakeEngine.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
+certora-engine-multicall :; PATH=${PATH} certoraRun certora/LockstakeEngineMulticall.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
+certora-clipper :; PATH=${PATH} certoraRun certora/LockstakeClipper.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..2159c136
--- /dev/null
+++ b/README.md
@@ -0,0 +1,225 @@
+# Lockstake Engine
+
+A technical description of the components of the LockStake Engine (LSE).
+
+## 1. LockstakeEngine
+
+The LockstakeEngine is the main contract in the set of contracts that implement and support the LSE. On a high level, it supports locking MKR in the contract, and using it to:
+* Vote through a delegate contract.
+* Farm USDS or SDAO tokens.
+* Borrow USDS through a vault.
+
+When withdrawing back the MKR the user has to pay an exit fee.
+
+There is also support for locking and freeing SKY instead of MKR.
+
+**System Attributes:**
+
+* A single user address can open multiple positions (each denoted as `urn`).
+* Each `urn` relates to zero or one chosen delegate contract, zero or one chosen farm, and one vault.
+* MKR cannot be moved outside of an `urn` or between `urn`s without paying the exit fee.
+* At any time the `urn`'s entire locked MKR amount is either staked or not, and is either delegated or not.
+* Staking rewards are not part of the collateral, and are still claimable after freeing from the engine, changing a farm or being liquidated.
+* The entire locked MKR amount is also credited as collateral for the user. However, the user itself decides if and how much USDS to borrow, and should be aware of liquidation risk.
+* A user can delegate control of an `urn` that it controls to another EOA/contract. This is helpful for supporting manager-type contracts that can be built on top of the engine.
+* Once a vault goes into liquidation, its MKR is undelegated and unstaked. It and can only be re-delegated or re-staked once there are no more auctions for it.
+
+**User Functions:**
+
+* `open(uint256 index)` - Create a new `urn` for the sender. The `index` parameter specifies how many `urn`s have been created so far by the user (should be 0 for the first call). It is used to avoid race conditions.
+* `hope(address owner, uint256 index, address usr)` - Allow `usr` to also manage the `owner-index` `urn`.
+* `nope(address owner, uint256 index, address usr)` - Disallow `usr` from managing the `owner-index` `urn`.
+* `lock(address owner, uint256 index, uint256 wad, uint16 ref)` - Deposit `wad` amount of MKR into the `owner-index` `urn`. This also delegates the MKR to the chosen delegate (if such exists) and stakes it to the chosen farm (if such exists) using the `ref` code.
+* `lockSky(address owner, uint256 index, uint256 skyWad, uint16 ref)` - Deposit `skyWad` amount of SKY. The SKY is first converted to MKR, which then gets deposited into the `owner-index` `urn`. This also delegates the MKR to the chosen delegate (if such exists) and stakes it to the chosen farm (if such exists) using the `ref` code.
+* `free(address owner, uint256 index, address to, uint256 wad)` - Withdraw `wad` amount of MKR from the `owner-index` `urn` to the `to` address (which will receive it minus the exit fee). This will undelegate the requested amount of MKR (if a delegate was chosen) and unstake it (if a farm was chosen). It will require the user to pay down debt beforehand if needed.
+* `freeSky(address owner, uint256 index, address to, uint256 skyWad)` - Withdraw `skyWad - skyWad % mkrSkyRate` amount of SKY to the `to` address. In practice, a proportional amount of MKR is first freed from the `owner-index` `urn` (minus the exit fee), then gets converted to SKY and sent out. This will undelegate the MKR (if a delegate was chosen) and unstake it (if a farm was chosen). It will require the user to pay down debt beforehand if needed. Note that freeing SKY is possible even if the position was previously entered via regular locking (using MKR), and vice-vera.
+* `freeNoFee(address owner, uint256 index, address to, uint256 wad)` - Withdraw `wad` amount of MKR from the `owner-index` `urn` to the `to` address without paying any fee. This will undelegate the requested amount of MKR (if a delegate was chosen) and unstake it (if a farm was chosen). It will require the user to pay down debt beforehand if needed. This function can only be called by an address which was both authorized on the contract by governance and for which the urn owner has called `hope`. It is useful for implementing a migration contract that will move the funds to another engine contract (if ever needed).
+* `selectVoteDelegate(address owner, uint256 index, address voteDelegate)` - Choose which delegate contract to delegate the `owner-index` `urn`'s entire MKR amount to. In case it is `address(0)` the MKR will stay (or become) undelegated.
+* `selectFarm(address owner, uint256 index, address farm, uint16 ref)` - Select which farm (from the whitelisted ones) to stake the `owner-index` `urn`'s MKR to (along with the `ref` code). In case it is `address(0)` the MKR will stay (or become) unstaked.
+* `draw(address owner, uint256 index, address to, uint256 wad)` - Generate `wad` amount of USDS using the `owner-index` `urn`’s MKR as collateral and send it to the `to` address.
+* `wipe(address owner, uint256 index, uint256 wad)` - Repay `wad` amount of USDS backed by the `owner-index` `urn`’s MKR.
+* `wipeAll(address owner, uint256 index)` - Repay the amount of USDS that is needed to wipe the `owner-index` `urn`’s entire debt.
+* `getReward(address owner, uint256 index, address farm, address to)` - Claim the reward generated from a farm on behalf of the `owner-index` `urn` and send it to the specified `to` address.
+* `multicall(bytes[] calldata data)` - Batch multiple methods in a single call to the contract.
+
+**Sequence Diagram:**
+
+Below is a diagram of a typical user sequence for winding up an LSE position.
+
+For simplicity it does not include all external messages, internal operations or token interactions.
+
+```mermaid
+sequenceDiagram
+ Actor user
+ participant engine
+ participant urn0
+ participant delegate0
+ participant farm0
+ participant vat
+
+ user->>engine: open(0)
+ engine-->>urn0: (creation)
+ engine-->>user: return `urn0` address
+
+ user->>engine: lock(`user`, 0, 10, 0)
+ engine-->>vat: vat.frob(ilk, `urn0`, `urn0`, address(0), 10, 0) // lock collateral
+
+ user->>engine: selectVoteDelegate(`user`, 0, `delegate0`)
+ engine-->>delegate0: lock(10)
+
+ user->>engine: selectFarm(`user`, 0, `farm0`, `ref`)
+ engine-->>urn0: stake(`farm0`, 10, `ref`)
+ urn0-->>farm0: stake(10, `ref`);
+
+
+ user->>engine: draw(`user`, 0, `user`, 1000)
+ engine-->>vat: vat.frob(ilk, `urn0`, address(0), address(this), 0, 1000) // borrow
+```
+
+**Multicall:**
+
+LockstakeEngine implements a function, which allows batching several function calls.
+
+For example, a typical flow for a user (or an app/front-end) would be to first query `index=ownerUrnsCount(usr)` off-chain to retrieve the expected `index`, then use it to perform a multicall sequence that includes `open`, `selectFarm`, `lock` and `stake`.
+
+This way, locking and farm-staking can be achieved in only 2 transactions (including the token approval).
+
+Note that since the `index` is first fetched off-chain and there is no support for passing return values between batched calls, there could be race conditions for calling `open`. For example, `open` can be called twice by the user (e.g. in two different contexts) with the second `ownerUrnsCount` query happening before the first `open` call has been confirmed. This would lead to both calls using the same `urn` for `selectFarm`, `lock` and `stake`.
+
+To mitigate this, the `index` parameter for `open` is used to make sure the multicall transaction creates the intended `urn`.
+
+**Minimal Proxies:**
+
+Upon calling `open`, an `urn` contract is deployed for each position. The `urn` contracts are controlled by the engine and represent each user position for farming, delegation and borrowing. This deployment process uses the [ERC-1167 minimal proxy pattern](https://eips.ethereum.org/EIPS/eip-1167), which helps reduce the `open` gas consumption by around 70%.
+
+**Liquidation Callbacks:**
+
+The following functions are called from the LockstakeClipper (see below) throughout the liquidation process.
+
+* `onKick(address urn, uint256 wad)` - Undelegate and unstake the entire `urn`'s MKR amount. Users need to manually delegate and stake again if there are leftovers after liquidation finishes.
+* `onTake(address urn, address who, uint256 wad)` - Transfer MKR to the liquidation auction buyer.
+* `onRemove(address urn, uint256 sold, uint256 left)` - Burn a proportional amount of the MKR which was bought in the auction and return the rest to the `urn`.
+
+**Configurable Parameters:**
+
+* `farms` - Whitelisted set of farms to choose from.
+* `jug` - The Dai lending rate calculation module.
+* `fee` - Exit fee.
+
+
+## 2. LockstakeClipper
+
+A modified version of the Liquidations 2.0 Clipper contract, which uses specific callbacks to the LockstakeEngine on certain events. This follows the same paradigm which was introduced in [proxy-manager-clipper](https://github.com/makerdao/proxy-manager-clipper/blob/67b7b5661c01bb09d771803a2be48f0455cd3bd3/src/ProxyManagerClipper.sol) (used for [dss-crop-join](https://github.com/makerdao/dss-crop-join)).
+
+Specifically, the LockstakeEngine is called upon a beginning of an auction (`onKick`), a sell of collateral (`onTake`), and when the auction is concluded (`onRemove`).
+
+The LSE liquidation process differs from the usual liquidations by the fact that it sends the taker callee the collateral (MKR) in the form of ERC20 tokens and not `vat.gem`.
+
+**Exit Fee on Liquidation**
+
+For a liquidated position the relative exit fee is burned from the MKR (collateral) leftovers upon completion of the auction. To ensure enough MKR is left, and also prevent incentives for self-liquidation, the ilk's liquidation ratio (`mat`) must be set high enough. We calculate below the minimal `mat` (while ignoring parameters resolution for simplicity):
+
+To be able to liquidate we need the vault to be liquidate-able. The point where that happens is:
+`① ink * price / mat = debt`
+
+The debt to be auctioned is enlarged (by the penalty) to `debt * chop` (where typically `chop` is 113%). If we assume the auction selling is at market price and that the market price didn't move since the auction trigger, then the amount of collateral sold is:
+`debt * chop / price`
+
+Since we need to make sure that only up to `(1-fee)` of the total collateral is sold (where `fee` will typically be 15%), we require:
+`② debt * chop / price < (1-fee) * ink`
+
+From ① and ② we get the requirement on `mat`:
+`mat > chop / (1 - fee)`
+
+For the mentioned examples of `chop` and `fee` we get:
+`mat > 1.13 / 0.85 ~= 133%`
+
+Note that in practice the `mat` value is expected to be significantly larger and have buffers over this rough calculation.
+It should take into account market fluctuations and protocol safety, especially considering that the governance token is used as collateral.
+
+**Trusted Farms and Reward Tokens**
+
+It is assumed that the farm owner is trusted, the reward token implementation is non-malicious, and that the reward token minter/s are not malicious. Therefore, theoretic attacks, in which for example the reward rate is inflated to a point where the farm mechanics block liquidations, are assumed non-feasible.
+
+**Liquidation Bark Gas Benchmarks**
+
+Delegate: N, Staking: N - 483456 gas
+Delegate: Y, Staking: Y, Yays: 1 - 614201 gas
+Delegate: Y, Staking: Y, Yays: 5 - 646481 gas
+Measured on: https://github.com/makerdao/lockstake/commit/a9c7a3e16f1655bdb60f75253d986a9e70a61e51
+
+For reference, a regular collateral bark cost is around 450K.
+Source: https://docs.google.com/spreadsheets/d/1ifb9ePno6KHNNGQA8s6u8KG7BRWa7fhUYH3Z5JGOxag/edit#gid=0
+
+Note that the increased gas cost should be taken into consideration when determining liquidation incentives, along with the dust amount.
+
+**Configurable Parameters (similar to a regular Clipper):**
+
+* `dog` - Liquidation module.
+* `vow` - Recipient of DAI raised in auctions.
+* `spotter` - Collateral price module.
+* `calc` - Current price calculator.
+* `buf` - Multiplicative factor to increase starting price.
+* `tail` - Time elapsed before auction reset.
+* `cusp` - Percentage drop before auction reset.
+* `chip` - Percentage of tab to suck from vow to incentivize keepers.
+* `tip` - Flat fee to suck from vow to incentivize keepers.
+* `stopped` - Level used to disable various types of functionality.
+* `chost` - Cached value of the ilk dust times the ilk chop. Set through `upchost()`.
+
+## 3. Vote Delegation
+### 3.a. VoteDelegate
+
+The LSE integrates with the current [VoteDelegate](https://github.com/makerdao/vote-delegate/blob/c2345b78376d5b0bb24749a97f82fe9171b53394/src/VoteDelegate.sol) contracts almost as is. However, there are three changes done:
+* In order to support long-term locking, the delegate's expiration functionality needs to be removed.
+* In order to simplify the logic, the IOU tokens generated by DSChief are kept in the new VoteDelegate contract.
+* In order to protect against an attack vector of delaying liquidations or blocking freeing of MKR, an on-demand window where locking MKR is blocked is introduced. The need for this stems from the Chief's flash loan protection, which doesn't allow to free MKR from a delegate in case MKR locking was already done in the same block.
+
+### 3.b. VoteDelegateFactory
+
+Since the VoteDelegate code is being modified (as described above), the factory also needs to be re-deployed.
+
+Note that it is important for the LSE to only allow using VoteDelegate contracts from the factory, so it can be made sure that liquidations can not be blocked.
+
+## 4. Keepers Support
+
+In general participating in MKR liquidations should be pretty straightforward using the existing on-chain liquidity. However there is a small caveat:
+
+Current Makerdao ecosystem keepers expect receiving collateral in the form of `vat.gem` (usually to a keeper arbitrage callee contract), which they then need to `exit` to ERC20 from. However the LSE liquidation mechanism sends the MKR directly in the form of ERC20, which requires a slight change in the keepers mode of operation.
+
+For example, keepers using the Maker supplied [exchange-callee for Uniswap V2](https://github.com/makerdao/exchange-callees/blob/3b080ecd4169fe09a59be51e2f85ddcea3242461/src/UniswapV2Callee.sol#L109) would need to use a version that gets the `gem` instead of the `gemJoin` and does not call `gemJoin.exit`.
+Additionaly, the callee might need to convert the MKR to SKY, in case it interacts with the USDS/SKY Uniswap pool.
+
+## 5. Splitter
+
+The Splitter contract is in charge of distributing the Surplus Buffer funds on each `vow.flap` to the Smart Burn Engine (SBE) and the LSE's USDS farm. The total amount sent each time is `vow.bump`.
+
+To accomplish this, it exposes a `kick` operation to be triggered periodically. Its logic withdraws DAI from the `vow` and splits it in two parts. The first part (`burn`) is sent to the underlying `flapper` contract to be processed by the SBE. The second part (`WAD - burn`) is distributed as reward to a `farm` contract. Note that `burn == 1 WAD` indicates funneling 100% of the DAI to the SBE without sending any rewards to the farm.
+
+When sending DAI to the farm, the splitter also calls `farm.notifyRewardAmount` to update the farm contract on the new rewards distribution. This resets the farming distribution period to the governance configured duration and sets the rewards rate according to the sent reward amount and rewards leftovers from the previous distribution (in case there are any).
+
+The Splitter implements rate-limiting using a `hop` parameter.
+
+**Configurable Parameters:**
+* `flapper` - The underlying burner strategy (e.g. the address of `FlapperUniV2SwapOnly`).
+* `burn` - The percentage of the `vow.bump` to be moved to the underlying `flapper`. For example, a value of 0.70 \* `WAD` corresponds to a funneling 70% of the DAI to the burn engine.
+* `hop` - Minimal time between kicks.
+
+## 6. StakingRewards
+
+The LSE uses a Maker modified [version](https://github.com/makerdao/endgame-toolkit/blob/master/README.md#stakingrewards) of the Synthetix Staking Reward as the farm for distributing USDS to stakers.
+
+For compatibility with the SBE, the assumption is that the duration of each farming distribution (`farm.rewardsDuration`) is similar to the flapper's cooldown period (`flap.hop`). This in practice divides the overall farming reward distribution to a set of smaller non overlapping distributions. It also allows for periods where there is no distribution at all.
+
+The StakingRewards contract `setRewardsDuration` function was modified to enable governance to change the farming distribution duration even if the previous distribution has not finished. This now supports changing it simultaneously with the SBE cooldown period (through a governance spell).
+
+**Configurable Parameters:**
+* `rewardsDistribution` - The address which is allowed to start a rewards distribution. Will be set to the splitter.
+* `rewardsDuration` - The amount of seconds each distribution should take.
+
+## General Notes
+* In many of the modules, such as the splitter and the flappers, USDS can replace DAI. This will usually require a deployment of the contract with UsdsJoin as a replacement of the DaiJoin address.
+* The LSE assumes that the ESM threshold is set large enough prior to its deployment, so Emergency Shutdown can never be called.
+* Freeing very small amounts could bypass the exit fees (due to the rounding down) but since the LSE is meant to only be deployed on Ethereum, this is assumed to not be economically viable.
+* As opposed to other collateral types, if a user notices an upcoming governance action that can hurt their position (or that they just don't like), they can not exit their position without losing the exit fee.
+* It is assumed that MKR to/from SKY conversions are not blocked.
diff --git a/audit/20240626-cantina-report-maker-LSE.pdf b/audit/20240626-cantina-report-maker-LSE.pdf
new file mode 100644
index 00000000..574d1a87
Binary files /dev/null and b/audit/20240626-cantina-report-maker-LSE.pdf differ
diff --git a/audit/20240909-ChainSecurity_MakerDAO_Lockstake_audit.pdf b/audit/20240909-ChainSecurity_MakerDAO_Lockstake_audit.pdf
new file mode 100644
index 00000000..f42eed58
Binary files /dev/null and b/audit/20240909-ChainSecurity_MakerDAO_Lockstake_audit.pdf differ
diff --git a/audit/20240909-cantina-report-maker-LSE-updates.pdf b/audit/20240909-cantina-report-maker-LSE-updates.pdf
new file mode 100644
index 00000000..ade4cae4
Binary files /dev/null and b/audit/20240909-cantina-report-maker-LSE-updates.pdf differ
diff --git a/audit/20240917-Sherlock_MakerDAO_Endgame_Audit_Report.pdf b/audit/20240917-Sherlock_MakerDAO_Endgame_Audit_Report.pdf
new file mode 100644
index 00000000..a1c7bf95
Binary files /dev/null and b/audit/20240917-Sherlock_MakerDAO_Endgame_Audit_Report.pdf differ
diff --git a/certora/LockstakeClipper.conf b/certora/LockstakeClipper.conf
new file mode 100644
index 00000000..94c4b77f
--- /dev/null
+++ b/certora/LockstakeClipper.conf
@@ -0,0 +1,88 @@
+{
+ "files": [
+ "src/LockstakeClipper.sol",
+ "src/LockstakeEngine.sol",
+ "src/LockstakeUrn.sol",
+ "src/LockstakeMkr.sol",
+ "certora/harness/dss/Vat.sol",
+ "certora/harness/dss/Spotter.sol",
+ "certora/harness/dss/Dog.sol",
+ "certora/harness/dss/ClipperCallee.sol:BadGuy",
+ "certora/harness/dss/ClipperCallee.sol:RedoGuy",
+ "certora/harness/dss/ClipperCallee.sol:KickGuy",
+ "certora/harness/dss/ClipperCallee.sol:FileUintGuy",
+ "certora/harness/dss/ClipperCallee.sol:FileAddrGuy",
+ "certora/harness/dss/ClipperCallee.sol:YankGuy",
+ "test/mocks/VoteDelegateMock.sol",
+ "certora/harness/tokens/MkrMock.sol",
+ "test/mocks/StakingRewardsMock.sol"
+ ],
+ "solc_map": {
+ "LockstakeClipper": "solc-0.8.21",
+ "LockstakeEngine": "solc-0.8.21",
+ "LockstakeUrn": "solc-0.8.21",
+ "LockstakeMkr": "solc-0.8.21",
+ "Vat": "solc-0.5.12",
+ "Spotter": "solc-0.5.12",
+ "Dog": "solc-0.6.12",
+ "BadGuy": "solc-0.8.21",
+ "RedoGuy": "solc-0.8.21",
+ "KickGuy": "solc-0.8.21",
+ "FileUintGuy": "solc-0.8.21",
+ "FileAddrGuy": "solc-0.8.21",
+ "YankGuy": "solc-0.8.21",
+ "VoteDelegateMock": "solc-0.8.21",
+ "MkrMock": "solc-0.8.21",
+ "StakingRewardsMock": "solc-0.8.21"
+ },
+ "solc_optimize_map": {
+ "LockstakeClipper": "200",
+ "LockstakeEngine": "200",
+ "LockstakeUrn": "200",
+ "LockstakeMkr": "200",
+ "Vat": "0",
+ "Spotter": "0",
+ "Dog": "0",
+ "BadGuy": "0",
+ "RedoGuy": "0",
+ "KickGuy": "0",
+ "FileUintGuy": "0",
+ "FileAddrGuy": "0",
+ "YankGuy": "0",
+ "VoteDelegateMock": "0",
+ "MkrMock": "0",
+ "StakingRewardsMock": "0",
+ },
+ "link": [
+ "LockstakeClipper:vat=Vat",
+ "LockstakeClipper:engine=LockstakeEngine",
+ "LockstakeClipper:dog=Dog",
+ "LockstakeClipper:spotter=Spotter",
+ "LockstakeEngine:vat=Vat",
+ "LockstakeEngine:mkr=MkrMock",
+ "LockstakeEngine:lsmkr=LockstakeMkr",
+ "LockstakeUrn:engine=LockstakeEngine",
+ "VoteDelegateMock:gov=MkrMock",
+ "StakingRewardsMock:stakingToken=LockstakeMkr",
+ "Dog:vat=Vat",
+ "BadGuy:clip=LockstakeClipper",
+ "RedoGuy:clip=LockstakeClipper",
+ "KickGuy:clip=LockstakeClipper",
+ "FileUintGuy:clip=LockstakeClipper",
+ "FileAddrGuy:clip=LockstakeClipper",
+ "YankGuy:clip=LockstakeClipper"
+ ],
+ "verify": "LockstakeClipper:certora/LockstakeClipper.spec",
+ "prover_args": [
+ "-rewriteMSizeAllocations true"
+ ],
+ "smt_timeout": "7000",
+ "rule_sanity": "basic",
+ "optimistic_loop": true,
+ "optimistic_contract_recursion": true,
+ "contract_recursion_limit": "2",
+ "multi_assert_check": true,
+ "parametric_contracts": ["LockstakeClipper"],
+ "build_cache": true,
+ "msg": "LockstakeClipper"
+}
diff --git a/certora/LockstakeClipper.spec b/certora/LockstakeClipper.spec
new file mode 100644
index 00000000..09d61168
--- /dev/null
+++ b/certora/LockstakeClipper.spec
@@ -0,0 +1,1033 @@
+// LockstakeClipper.spec
+
+using LockstakeEngine as lockstakeEngine;
+using LockstakeUrn as lockstakeUrn;
+using LockstakeMkr as lsmkr;
+using MkrMock as mkr;
+using Vat as vat;
+using Spotter as spotter;
+using Dog as dog;
+using VoteDelegateMock as voteDelegate;
+using StakingRewardsMock as stakingRewards;
+using BadGuy as badGuy;
+using RedoGuy as redoGuy;
+using KickGuy as kickGuy;
+using FileUintGuy as fileUintGuy;
+using FileAddrGuy as fileAddrGuy;
+using YankGuy as yankGuy;
+
+methods {
+ // storage variables
+ function wards(address) external returns (uint256) envfree;
+ function dog() external returns (address) envfree;
+ function vow() external returns (address) envfree;
+ function spotter() external returns (address) envfree;
+ function calc() external returns (address) envfree;
+ function buf() external returns (uint256) envfree;
+ function tail() external returns (uint256) envfree;
+ function cusp() external returns (uint256) envfree;
+ function chip() external returns (uint64) envfree;
+ function tip() external returns (uint192) envfree;
+ function chost() external returns (uint256) envfree;
+ function kicks() external returns (uint256) envfree;
+ function active(uint256) external returns (uint256) envfree;
+ function sales(uint256) external returns (uint256,uint256,uint256,uint256,address,uint96,uint256) envfree;
+ function stopped() external returns (uint256) envfree;
+ function count() external returns (uint256) envfree;
+ function active(uint256) external returns (uint256) envfree;
+ // immutables
+ function ilk() external returns (bytes32) envfree;
+ //
+ function lockstakeEngine.wards(address) external returns (uint256) envfree;
+ function lockstakeEngine.urnAuctions(address) external returns (uint256) envfree;
+ function lockstakeEngine.urnVoteDelegates(address) external returns (address) envfree;
+ function lockstakeEngine.urnFarms(address) external returns (address) envfree;
+ function lockstakeEngine.ilk() external returns (bytes32) envfree;
+ function lockstakeEngine.fee() external returns (uint256) envfree;
+ function mkr.totalSupply() external returns (uint256) envfree;
+ function mkr.balanceOf(address) external returns (uint256) envfree;
+ function lsmkr.wards(address) external returns (uint256) envfree;
+ function lsmkr.totalSupply() external returns (uint256) envfree;
+ function lsmkr.allowance(address,address) external returns (uint256) envfree;
+ function lsmkr.balanceOf(address) external returns (uint256) envfree;
+ function stakingRewards.balanceOf(address) external returns (uint256) envfree;
+ function stakingRewards.totalSupply() external returns (uint256) envfree;
+ function voteDelegate.stake(address) external returns (uint256) envfree;
+ function vat.wards(address) external returns (uint256) envfree;
+ function vat.live() external returns (uint256) envfree;
+ function vat.can(address, address) external returns (uint256) envfree;
+ function vat.debt() external returns (uint256) envfree;
+ function vat.vice() external returns (uint256) envfree;
+ function vat.dai(address) external returns (uint256) envfree;
+ function vat.sin(address) external returns (uint256) envfree;
+ function vat.gem(bytes32,address) external returns (uint256) envfree;
+ function vat.ilks(bytes32) external returns (uint256,uint256,uint256,uint256,uint256) envfree;
+ function vat.urns(bytes32, address) external returns (uint256,uint256) envfree;
+ function spotter.ilks(bytes32) external returns (address,uint256) envfree;
+ function spotter.par() external returns (uint256) envfree;
+ function dog.wards(address) external returns (uint256) envfree;
+ function dog.chop(bytes32) external returns (uint256) envfree;
+ function dog.Dirt() external returns (uint256) envfree;
+ function dog.ilks(bytes32) external returns (address,uint256,uint256,uint256) envfree;
+ //
+ function _.peek() external => peekSummary() expect (uint256, bool);
+ function _.price(uint256,uint256) external => calcPriceSummary() expect (uint256);
+ function _.free(uint256) external => DISPATCHER(true);
+ function _.withdraw(uint256) external => DISPATCHER(true);
+ function _.withdraw(address,uint256) external => DISPATCHER(true);
+ function _.transfer(address,uint256) external => DISPATCHER(true);
+ // `ClipperCallee`
+ // NOTE: this might result in recursion, since we linked all the `ClipperCallee`
+ // to the `LockstakeClipper`.
+ function _.clipperCall(
+ address, uint256, uint256, bytes
+ ) external => DISPATCHER(true);
+}
+
+definition max_int256() returns mathint = 2^255 - 1;
+definition WAD() returns mathint = 10^18;
+definition RAY() returns mathint = 10^27;
+definition _min(mathint x, mathint y) returns mathint = x < y ? x : y;
+
+ghost uint256 pipVal;
+ghost bool pipOk;
+function peekSummary() returns (uint256, bool) {
+ return (pipVal, pipOk);
+}
+
+ghost uint256 calcPrice;
+function calcPriceSummary() returns uint256 {
+ return calcPrice;
+}
+
+ghost lockedGhost() returns uint256;
+
+hook Sstore locked uint256 n_locked {
+ havoc lockedGhost assuming lockedGhost@new() == n_locked;
+}
+
+hook Sload uint256 value locked {
+ require lockedGhost() == value;
+}
+
+// Verify that each storage layout is only modified in the corresponding functions
+rule storageAffected(method f) {
+ env e;
+
+ address anyAddr;
+ uint256 anyUint256;
+
+ mathint wardsBefore = wards(anyAddr);
+ address dogBefore = dog();
+ address vowBefore = vow();
+ address spotterBefore = spotter();
+ address calcBefore = calc();
+ mathint bufBefore = buf();
+ mathint tailBefore = tail();
+ mathint cuspBefore = cusp();
+ mathint chipBefore = chip();
+ mathint tipBefore = tip();
+ mathint chostBefore = chost();
+ mathint kicksBefore = kicks();
+ mathint activeBefore = active(anyUint256);
+ mathint countBefore = count();
+ mathint salesAnyPosBefore; mathint salesAnyTabBefore; mathint salesAnyLotBefore; mathint salesAnyTotBefore; address salesAnyUsrBefore; mathint salesAnyTicBefore; mathint salesAnyTopBefore;
+ salesAnyPosBefore, salesAnyTabBefore, salesAnyLotBefore, salesAnyTotBefore, salesAnyUsrBefore, salesAnyTicBefore, salesAnyTopBefore = sales(anyUint256);
+ mathint stoppedBefore = stopped();
+
+ calldataarg args;
+ f(e, args);
+
+ mathint wardsAfter = wards(anyAddr);
+ address dogAfter = dog();
+ address vowAfter = vow();
+ address spotterAfter = spotter();
+ address calcAfter = calc();
+ mathint bufAfter = buf();
+ mathint tailAfter = tail();
+ mathint cuspAfter = cusp();
+ mathint chipAfter = chip();
+ mathint tipAfter = tip();
+ mathint chostAfter = chost();
+ mathint kicksAfter = kicks();
+ mathint activeAfter = active(anyUint256);
+ mathint countAfter = count();
+ mathint salesAnyPosAfter; mathint salesAnyTabAfter; mathint salesAnyLotAfter; mathint salesAnyTotAfter; address salesAnyUsrAfter; mathint salesAnyTicAfter; mathint salesAnyTopAfter;
+ salesAnyPosAfter, salesAnyTabAfter, salesAnyLotAfter, salesAnyTotAfter, salesAnyUsrAfter, salesAnyTicAfter, salesAnyTopAfter = sales(anyUint256);
+ mathint stoppedAfter = stopped();
+
+ assert wardsAfter != wardsBefore => f.selector == sig:rely(address).selector || f.selector == sig:deny(address).selector, "Assert 1";
+ assert dogAfter != dogBefore => f.selector == sig:file(bytes32,address).selector, "Assert 2";
+ assert vowAfter != vowBefore => f.selector == sig:file(bytes32,address).selector, "Assert 3";
+ assert spotterAfter != spotterBefore => f.selector == sig:file(bytes32,address).selector, "Assert 4";
+ assert calcAfter != calcBefore => f.selector == sig:file(bytes32,address).selector, "Assert 5";
+ assert bufAfter != bufBefore => f.selector == sig:file(bytes32,uint256).selector, "Assert 6";
+ assert tailAfter != tailBefore => f.selector == sig:file(bytes32,uint256).selector, "Assert 7";
+ assert cuspAfter != cuspBefore => f.selector == sig:file(bytes32,uint256).selector, "Assert 8";
+ assert chipAfter != chipBefore => f.selector == sig:file(bytes32,uint256).selector, "Assert 9";
+ assert tipAfter != tipBefore => f.selector == sig:file(bytes32,uint256).selector, "Assert 10";
+ assert chostAfter != chostBefore => f.selector == sig:upchost().selector, "Assert 11";
+ assert kicksAfter != kicksBefore => f.selector == sig:kick(uint256,uint256,address,address).selector, "Assert 12";
+ assert countAfter != countBefore => f.selector == sig:kick(uint256,uint256,address,address).selector || f.selector == sig:take(uint256,uint256,uint256,address,bytes).selector || f.selector == sig:yank(uint256).selector, "Assert 13";
+ assert activeAfter != activeBefore => f.selector == sig:kick(uint256,uint256,address,address).selector || f.selector == sig:take(uint256,uint256,uint256,address,bytes).selector || f.selector == sig:yank(uint256).selector, "Assert 14";
+ assert salesAnyPosAfter != salesAnyPosBefore => f.selector == sig:kick(uint256,uint256,address,address).selector || f.selector == sig:take(uint256,uint256,uint256,address,bytes).selector || f.selector == sig:yank(uint256).selector, "Assert 15";
+ assert salesAnyTabAfter != salesAnyTabBefore => f.selector == sig:kick(uint256,uint256,address,address).selector || f.selector == sig:take(uint256,uint256,uint256,address,bytes).selector || f.selector == sig:yank(uint256).selector, "Assert 16";
+ assert salesAnyLotAfter != salesAnyLotBefore => f.selector == sig:kick(uint256,uint256,address,address).selector || f.selector == sig:take(uint256,uint256,uint256,address,bytes).selector || f.selector == sig:yank(uint256).selector, "Assert 17";
+ assert salesAnyTotAfter != salesAnyTotBefore => f.selector == sig:kick(uint256,uint256,address,address).selector || f.selector == sig:take(uint256,uint256,uint256,address,bytes).selector || f.selector == sig:yank(uint256).selector, "Assert 18";
+ assert salesAnyUsrAfter != salesAnyUsrBefore => f.selector == sig:kick(uint256,uint256,address,address).selector || f.selector == sig:take(uint256,uint256,uint256,address,bytes).selector || f.selector == sig:yank(uint256).selector, "Assert 19";
+ assert salesAnyTicAfter != salesAnyTicBefore => f.selector == sig:kick(uint256,uint256,address,address).selector || f.selector == sig:redo(uint256,address).selector || f.selector == sig:take(uint256,uint256,uint256,address,bytes).selector || f.selector == sig:yank(uint256).selector, "Assert 20";
+ assert salesAnyTopAfter != salesAnyTopBefore => f.selector == sig:kick(uint256,uint256,address,address).selector || f.selector == sig:redo(uint256,address).selector || f.selector == sig:take(uint256,uint256,uint256,address,bytes).selector || f.selector == sig:yank(uint256).selector, "Assert 21";
+ assert stoppedAfter != stoppedBefore => f.selector == sig:file(bytes32,uint256).selector, "Assert 22";
+}
+
+// Verify correct storage changes for non reverting rely
+rule rely(address usr) {
+ env e;
+
+ address other;
+ require other != usr;
+
+ mathint wardsOtherBefore = wards(other);
+
+ rely(e, usr);
+
+ mathint wardsUsrAfter = wards(usr);
+ mathint wardsOtherAfter = wards(other);
+
+ assert wardsUsrAfter == 1, "Assert 1";
+ assert wardsOtherAfter == wardsOtherBefore, "Assert 2";
+}
+
+// Verify revert rules on rely
+rule rely_revert(address usr) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+
+ rely@withrevert(e, usr);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting deny
+rule deny(address usr) {
+ env e;
+
+ address other;
+ require other != usr;
+
+ mathint wardsOtherBefore = wards(other);
+
+ deny(e, usr);
+
+ mathint wardsUsrAfter = wards(usr);
+ mathint wardsOtherAfter = wards(other);
+
+ assert wardsUsrAfter == 0, "Assert 1";
+ assert wardsOtherAfter == wardsOtherBefore, "Assert 2";
+}
+
+// Verify revert rules on deny
+rule deny_revert(address usr) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+
+ deny@withrevert(e, usr);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting file
+rule file_uint256(bytes32 what, uint256 data) {
+ env e;
+
+ mathint bufBefore = buf();
+ mathint tailBefore = tail();
+ mathint cuspBefore = cusp();
+ mathint chipBefore = chip();
+ mathint tipBefore = tip();
+ mathint stoppedBefore = stopped();
+
+ file(e, what, data);
+
+ mathint bufAfter = buf();
+ mathint tailAfter = tail();
+ mathint cuspAfter = cusp();
+ mathint chipAfter = chip();
+ mathint tipAfter = tip();
+ mathint stoppedAfter = stopped();
+
+ assert what == to_bytes32(0x6275660000000000000000000000000000000000000000000000000000000000) => bufAfter == to_mathint(data), "Assert 1";
+ assert what != to_bytes32(0x6275660000000000000000000000000000000000000000000000000000000000) => bufAfter == bufBefore, "Assert 2";
+ assert what == to_bytes32(0x7461696c00000000000000000000000000000000000000000000000000000000) => tailAfter == to_mathint(data), "Assert 3";
+ assert what != to_bytes32(0x7461696c00000000000000000000000000000000000000000000000000000000) => tailAfter == tailBefore, "Assert 4";
+ assert what == to_bytes32(0x6375737000000000000000000000000000000000000000000000000000000000) => cuspAfter == to_mathint(data), "Assert 5";
+ assert what != to_bytes32(0x6375737000000000000000000000000000000000000000000000000000000000) => cuspAfter == cuspBefore, "Assert 6";
+ assert what == to_bytes32(0x6368697000000000000000000000000000000000000000000000000000000000) => chipAfter == data % (max_uint64 + 1), "Assert 7";
+ assert what != to_bytes32(0x6368697000000000000000000000000000000000000000000000000000000000) => chipAfter == chipBefore, "Assert 8";
+ assert what == to_bytes32(0x7469700000000000000000000000000000000000000000000000000000000000) => tipAfter == data % (max_uint192 + 1), "Assert 9";
+ assert what != to_bytes32(0x7469700000000000000000000000000000000000000000000000000000000000) => tipAfter == tipBefore, "Assert 10";
+ assert what == to_bytes32(0x73746f7070656400000000000000000000000000000000000000000000000000) => stoppedAfter == to_mathint(data), "Assert 11";
+ assert what != to_bytes32(0x73746f7070656400000000000000000000000000000000000000000000000000) => stoppedAfter == stoppedBefore, "Assert 12";
+}
+
+// Verify revert rules on file
+rule file_uint256_revert(bytes32 what, uint256 data) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+ mathint locked = lockedGhost();
+
+ file@withrevert(e, what, data);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+ bool revert3 = locked != 0;
+ bool revert4 = what != to_bytes32(0x6275660000000000000000000000000000000000000000000000000000000000) &&
+ what != to_bytes32(0x7461696c00000000000000000000000000000000000000000000000000000000) &&
+ what != to_bytes32(0x6375737000000000000000000000000000000000000000000000000000000000) &&
+ what != to_bytes32(0x6368697000000000000000000000000000000000000000000000000000000000) &&
+ what != to_bytes32(0x7469700000000000000000000000000000000000000000000000000000000000) &&
+ what != to_bytes32(0x73746f7070656400000000000000000000000000000000000000000000000000);
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting file
+rule file_address(bytes32 what, address data) {
+ env e;
+
+ address spotterBefore = spotter();
+ address dogBefore = dog();
+ address vowBefore = vow();
+ address calcBefore = calc();
+
+ file(e, what, data);
+
+ address spotterAfter = spotter();
+ address dogAfter = dog();
+ address vowAfter = vow();
+ address calcAfter = calc();
+
+ assert what == to_bytes32(0x73706f7474657200000000000000000000000000000000000000000000000000) => spotterAfter == data, "Assert 1";
+ assert what != to_bytes32(0x73706f7474657200000000000000000000000000000000000000000000000000) => spotterAfter == spotterBefore, "Assert 2";
+ assert what == to_bytes32(0x646f670000000000000000000000000000000000000000000000000000000000) => dogAfter == data, "Assert 3";
+ assert what != to_bytes32(0x646f670000000000000000000000000000000000000000000000000000000000) => dogAfter == dogBefore, "Assert 4";
+ assert what == to_bytes32(0x766f770000000000000000000000000000000000000000000000000000000000) => vowAfter == data, "Assert 5";
+ assert what != to_bytes32(0x766f770000000000000000000000000000000000000000000000000000000000) => vowAfter == vowBefore, "Assert 6";
+ assert what == to_bytes32(0x63616c6300000000000000000000000000000000000000000000000000000000) => calcAfter == data, "Assert 7";
+ assert what != to_bytes32(0x63616c6300000000000000000000000000000000000000000000000000000000) => calcAfter == calcBefore, "Assert 8";
+}
+
+// Verify revert rules on file
+rule file_address_revert(bytes32 what, address data) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+ mathint locked = lockedGhost();
+
+ file@withrevert(e, what, data);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+ bool revert3 = locked != 0;
+ bool revert4 = what != to_bytes32(0x73706f7474657200000000000000000000000000000000000000000000000000) &&
+ what != to_bytes32(0x646f670000000000000000000000000000000000000000000000000000000000) &&
+ what != to_bytes32(0x766f770000000000000000000000000000000000000000000000000000000000) &&
+ what != to_bytes32(0x63616c6300000000000000000000000000000000000000000000000000000000);
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting kick
+rule kick(uint256 tab, uint256 lot, address usr, address kpr) {
+ env e;
+
+ mathint kicksBefore = kicks();
+ mathint countBefore = count();
+ mathint id = kicksBefore + 1;
+ uint256 otherUint256;
+ require to_mathint(otherUint256) != id;
+ mathint salesOtherPosBefore; mathint salesOtherTabBefore; mathint salesOtherLotBefore; mathint salesOtherTotBefore; address salesOtherUsrBefore; mathint salesOtherTicBefore; mathint salesOtherTopBefore;
+ salesOtherPosBefore, salesOtherTabBefore, salesOtherLotBefore, salesOtherTotBefore, salesOtherUsrBefore, salesOtherTicBefore, salesOtherTopBefore = sales(otherUint256);
+ mathint vatDaiKprBefore = vat.dai(kpr);
+ address vow = vow();
+ mathint vatSinVowBefore = vat.sin(vow);
+ bytes32 ilk = ilk();
+ mathint engineUrnAuctionsUsrBefore = lockstakeEngine.urnAuctions(usr);
+
+ mathint par = spotter.par();
+ // Avoid division by zero
+ require par > 0;
+ mathint val; bool b;
+ val, b = peekSummary();
+ mathint feedPrice = val * 10^9 * RAY() / par;
+ mathint buf = buf();
+ mathint coin = tip() + tab * chip() / WAD();
+
+ kick(e, tab, lot, usr, kpr);
+
+ mathint kicksAfter = kicks();
+ mathint countAfter = count();
+ mathint activeCountAfter = active(require_uint256(countAfter - 1));
+ mathint salesIdPosAfter; mathint salesIdTabAfter; mathint salesIdLotAfter; mathint salesIdTotAfter; address salesIdUsrAfter; mathint salesIdTicAfter; mathint salesIdTopAfter;
+ salesIdPosAfter, salesIdTabAfter, salesIdLotAfter, salesIdTotAfter, salesIdUsrAfter, salesIdTicAfter, salesIdTopAfter = sales(require_uint256(id));
+ mathint salesOtherPosAfter; mathint salesOtherTabAfter; mathint salesOtherLotAfter; mathint salesOtherTotAfter; address salesOtherUsrAfter; mathint salesOtherTicAfter; mathint salesOtherTopAfter;
+ salesOtherPosAfter, salesOtherTabAfter, salesOtherLotAfter, salesOtherTotAfter, salesOtherUsrAfter, salesOtherTicAfter, salesOtherTopAfter = sales(otherUint256);
+ mathint vatDaiKprAfter= vat.dai(kpr);
+ mathint vatSinVowAfter= vat.sin(vow);
+ mathint engineUrnAuctionsUsrAfter = lockstakeEngine.urnAuctions(usr);
+
+ assert kicksAfter == kicksBefore + 1, "Assert 1";
+ assert countAfter == countBefore + 1, "Assert 2";
+ assert activeCountAfter == id, "Assert 3";
+ assert salesIdPosAfter == countAfter - 1, "Assert 4";
+ assert salesIdTabAfter == to_mathint(tab), "Assert 5";
+ assert salesIdLotAfter == to_mathint(lot), "Assert 6";
+ assert salesIdTotAfter == to_mathint(lot), "Assert 7";
+ assert salesIdUsrAfter == usr, "Assert 8";
+ assert salesIdTicAfter == e.block.timestamp % (max_uint96 + 1), "Assert 9";
+ assert salesIdTopAfter == feedPrice * buf / RAY(), "Assert 10";
+ assert salesOtherPosAfter == salesOtherPosBefore, "Assert 11";
+ assert salesOtherTabAfter == salesOtherTabBefore, "Assert 12";
+ assert salesOtherLotAfter == salesOtherLotBefore, "Assert 13";
+ assert salesOtherTotAfter == salesOtherTotBefore, "Assert 14";
+ assert salesOtherUsrAfter == salesOtherUsrBefore, "Assert 15";
+ assert salesOtherTicAfter == salesOtherTicBefore, "Assert 16";
+ assert salesOtherTopAfter == salesOtherTopBefore, "Assert 17";
+ assert vatDaiKprAfter == vatDaiKprBefore + coin, "Assert 18";
+ assert vatSinVowAfter == vatSinVowBefore + coin, "Assert 19";
+ assert engineUrnAuctionsUsrAfter == engineUrnAuctionsUsrBefore + 1, "Assert 20";
+}
+
+// Verify revert rules on kick
+rule kick_revert(uint256 tab, uint256 lot, address usr, address kpr) {
+ env e;
+
+ require usr == lockstakeUrn;
+ address prevVoteDelegate = lockstakeEngine.urnVoteDelegates(usr);
+ require prevVoteDelegate == 0 || prevVoteDelegate == voteDelegate;
+ address prevFarm = lockstakeEngine.urnFarms(usr);
+ require prevFarm == 0 || prevFarm == stakingRewards;
+
+ mathint wardsSender = wards(e.msg.sender);
+ mathint locked = lockedGhost();
+ mathint stopped = stopped();
+ mathint kicks = kicks();
+ mathint count = count();
+ mathint buf = buf();
+ mathint par = spotter.par();
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUsrInk; mathint a;
+ vatUrnsIlkUsrInk, a = vat.urns(ilk, usr);
+ // Avoid division by zero
+ require par > 0;
+ mathint val; bool has;
+ val, has = peekSummary();
+ mathint feedPrice = val * 10^9 * RAY() / par;
+ mathint chip = chip();
+ mathint coin = tip() + tab * chip / WAD();
+ // Happening in deploy scripts
+ require vat.wards(currentContract) == 1;
+ require lockstakeEngine.wards(currentContract) == 1;
+ // Happening in urn (usr) init
+ require lsmkr.allowance(usr, lockstakeEngine) == max_uint256;
+ // Tokens invariants
+ require to_mathint(lsmkr.totalSupply()) >= lsmkr.balanceOf(prevFarm) + lsmkr.balanceOf(usr) + lsmkr.balanceOf(lockstakeEngine);
+ require stakingRewards.totalSupply() >= stakingRewards.balanceOf(usr);
+ // VoteDelegate assumptions
+ require prevVoteDelegate == 0 || to_mathint(voteDelegate.stake(lockstakeEngine)) >= vatUrnsIlkUsrInk + lot;
+ require prevVoteDelegate == 0 || mkr.balanceOf(voteDelegate) >= voteDelegate.stake(lockstakeEngine);
+ // StakingRewards assumptions
+ require prevFarm == 0 && lsmkr.balanceOf(usr) >= lot ||
+ prevFarm != 0 && to_mathint(stakingRewards.balanceOf(usr)) >= vatUrnsIlkUsrInk + lot && to_mathint(lsmkr.balanceOf(prevFarm)) >= vatUrnsIlkUsrInk + lot;
+ // Practical Vat assumptions
+ require vat.sin(vow()) + coin <= max_uint256;
+ require vat.dai(kpr) + coin <= max_uint256;
+ require vat.vice() + coin <= max_uint256;
+ require vat.debt() + coin <= max_uint256;
+ // Practical assumption (vatUrnsIlkUsrInk + lot should be the same than the vatUrnsIlkUsrInk prev to the kick call)
+ require vatUrnsIlkUsrInk + lot <= max_uint256;
+ // LockstakeEngine assumption
+ require lockstakeEngine.urnAuctions(usr) < max_uint256;
+ require lockstakeEngine.ilk() == ilk;
+
+ kick@withrevert(e, tab, lot, usr, kpr);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+ bool revert3 = locked != 0;
+ bool revert4 = stopped >= 1;
+ bool revert5 = tab == 0;
+ bool revert6 = lot == 0;
+ bool revert7 = to_mathint(lot) > max_int256();
+ bool revert8 = usr == 0;
+ bool revert9 = kicks == max_uint256;
+ bool revert10 = count == max_uint256;
+ bool revert11 = !has;
+ bool revert12 = val * 10^9 * RAY() > max_uint256;
+ bool revert13 = feedPrice * buf > max_uint256;
+ bool revert14 = feedPrice * buf / RAY() == 0;
+ bool revert15 = tab * chip > max_uint256;
+ bool revert16 = coin > max_uint256;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5 || revert6 ||
+ revert7 || revert8 || revert9 ||
+ revert10 || revert11 || revert12 ||
+ revert13 || revert14 || revert15 ||
+ revert16, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting redo
+rule redo(uint256 id, address kpr) {
+ env e;
+
+ uint256 otherUint256;
+ require otherUint256 != id;
+
+ mathint chost = chost();
+ mathint a; address b;
+ mathint salesIdTab; mathint salesIdLot; mathint salesIdTicBefore; mathint salesIdTopBefore;
+ a, salesIdTab, salesIdLot, a, b, salesIdTicBefore, salesIdTopBefore = sales(id);
+ mathint salesOtherTicBefore; mathint salesOtherTopBefore;
+ a, a, a, a, b, salesOtherTicBefore, salesOtherTopBefore = sales(otherUint256);
+ mathint vatDaiKprBefore = vat.dai(kpr);
+ address vow = vow();
+ mathint vatSinVowBefore = vat.sin(vow);
+
+ mathint par = spotter.par();
+ // Avoid division by zero
+ require par > 0;
+ mathint val; bool c;
+ val, c = peekSummary();
+ mathint feedPrice = val * 10^9 * RAY() / par;
+ mathint buf = buf();
+ mathint coin = tip() + salesIdTab * chip() / WAD();
+ bool paysKpr = salesIdTab >= chost && salesIdLot * feedPrice >= chost;
+
+ redo(e, id, kpr);
+
+ mathint salesIdTicAfter; mathint salesIdTopAfter;
+ a, a, a, a, b, salesIdTicAfter, salesIdTopAfter = sales(id);
+ mathint salesOtherTicAfter; mathint salesOtherTopAfter;
+ a, a, a, a, b, salesOtherTicAfter, salesOtherTopAfter = sales(otherUint256);
+ mathint vatDaiKprAfter = vat.dai(kpr);
+ mathint vatSinVowAfter = vat.sin(vow);
+
+ assert salesIdTicAfter == e.block.timestamp % (max_uint96 + 1), "Assert 1";
+ assert salesIdTopAfter == feedPrice * buf / RAY(), "Assert 2";
+ assert salesOtherTicAfter == salesOtherTicBefore, "Assert 3";
+ assert salesOtherTopAfter == salesOtherTopBefore, "Assert 4";
+ assert paysKpr => vatDaiKprAfter == vatDaiKprBefore + coin, "Assert 5";
+ assert !paysKpr => vatDaiKprAfter == vatDaiKprBefore, "Assert 6";
+ assert paysKpr => vatSinVowAfter == vatSinVowBefore + coin, "Assert 7";
+ assert !paysKpr => vatSinVowAfter == vatSinVowBefore, "Assert 8";
+}
+
+// Verify revert rules on redo
+rule redo_revert(uint256 id, address kpr) {
+ env e;
+
+ mathint locked = lockedGhost();
+ mathint stopped = stopped();
+ mathint tail = tail();
+ mathint cusp = cusp();
+ mathint chost = chost();
+
+ mathint a;
+ mathint salesIdTab; mathint salesIdLot; address salesIdUsr; mathint salesIdTic; mathint salesIdTop;
+ a, salesIdTab, salesIdLot, a, salesIdUsr, salesIdTic, salesIdTop = sales(id);
+
+ require to_mathint(e.block.timestamp) >= salesIdTic;
+ mathint price = calcPriceSummary();
+ // Avoid division by zero
+ require salesIdTop > 0;
+ bool done = e.block.timestamp - salesIdTic > tail || price * RAY() / salesIdTop < cusp;
+
+ mathint par = spotter.par();
+ // Avoid division by zero
+ require par > 0;
+ mathint val; bool has;
+ val, has = peekSummary();
+ mathint feedPrice = val * 10^9 * RAY() / par;
+ mathint buf = buf();
+ mathint tip = tip();
+ mathint chip = chip();
+ mathint coin = tip + salesIdTab * chip() / WAD();
+ bool paysKpr = salesIdTab >= chost && salesIdLot * feedPrice >= chost;
+
+ // Happening in deploy scripts
+ require vat.wards(currentContract) == 1;
+ // Practical Vat assumptions
+ require vat.sin(vow()) + coin <= max_uint256;
+ require vat.dai(kpr) + coin <= max_uint256;
+ require vat.vice() + coin <= max_uint256;
+ require vat.debt() + coin <= max_uint256;
+
+ redo@withrevert(e, id, kpr);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = locked != 0;
+ bool revert3 = stopped >= 2;
+ bool revert4 = salesIdUsr == 0;
+ bool revert5 = to_mathint(e.block.timestamp) < salesIdTic;
+ bool revert6 = e.block.timestamp - salesIdTic <= tail && price * RAY() > max_uint256;
+ bool revert7 = !done;
+ bool revert8 = !has;
+ bool revert9 = val * 10^9 * RAY() > max_uint256;
+ bool revert10 = feedPrice * buf > max_uint256;
+ bool revert11 = feedPrice * buf / RAY() == 0;
+ bool revert12 = (tip > 0 || chip > 0) && salesIdTab >= chost && salesIdLot * feedPrice > max_uint256;
+ bool revert13 = paysKpr && salesIdTab * chip > max_uint256;
+ bool revert14 = paysKpr && coin > max_uint256;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5 || revert6 ||
+ revert7 || revert8 || revert9 ||
+ revert10 || revert11 || revert12 ||
+ revert13 || revert14, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting take
+rule take(uint256 id, uint256 amt, uint256 max, address who, bytes data) {
+ env e;
+
+ bytes32 ilk = ilk();
+ address vow = vow();
+
+ mathint countBefore = count();
+ uint256 otherUint256;
+ require otherUint256 != id;
+ mathint activeLastBefore;
+ if (countBefore > 0) {
+ activeLastBefore = active(assert_uint256(countBefore - 1));
+ } else {
+ activeLastBefore = 0;
+ }
+
+ mathint salesIdPosBefore; mathint salesIdTabBefore; mathint salesIdLotBefore; mathint salesIdTotBefore; address salesIdUsrBefore; mathint salesIdTicBefore; mathint salesIdTopBefore;
+ salesIdPosBefore, salesIdTabBefore, salesIdLotBefore, salesIdTotBefore, salesIdUsrBefore, salesIdTicBefore, salesIdTopBefore = sales(id);
+ require salesIdUsrBefore == lockstakeUrn;
+ mathint salesOtherPosBefore; mathint salesOtherTabBefore; mathint salesOtherLotBefore; mathint salesOtherTotBefore; address salesOtherUsrBefore; mathint salesOtherTicBefore; mathint salesOtherTopBefore;
+ salesOtherPosBefore, salesOtherTabBefore, salesOtherLotBefore, salesOtherTotBefore, salesOtherUsrBefore, salesOtherTicBefore, salesOtherTopBefore = sales(otherUint256);
+ mathint vatGemIlkClipperBefore = vat.gem(ilk, currentContract);
+ mathint mkrTotalSupplyBefore = mkr.totalSupply();
+ mathint mkrBalanceOfEngineBefore = mkr.balanceOf(lockstakeEngine);
+ mathint mkrBalanceOfWhoBefore = mkr.balanceOf(who);
+ mathint vatDaiSenderBefore = vat.dai(e.msg.sender);
+ mathint vatDaiVowBefore = vat.dai(vow);
+ mathint dogDirtBefore = dog.Dirt();
+ address a; mathint b;
+ mathint dogIlkDirtBefore;
+ a, b, b, dogIlkDirtBefore = dog.ilks(ilk);
+ mathint vatUrnsIlkUsrInkBefore;
+ vatUrnsIlkUsrInkBefore, b = vat.urns(ilk, salesIdUsrBefore);
+ mathint lsmkrTotalSupplyBefore = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUsrBefore = lsmkr.balanceOf(salesIdUsrBefore);
+ mathint engineUrnAuctionsUsrBefore = lockstakeEngine.urnAuctions(salesIdUsrBefore);
+
+ mathint price = calcPriceSummary();
+ // Avoid division by zero
+ require price > 0;
+ // Token invariants
+ require mkrTotalSupplyBefore >= mkrBalanceOfEngineBefore + mkrBalanceOfWhoBefore;
+ require lsmkrTotalSupplyBefore >= lsmkrBalanceOfUsrBefore;
+ // LockstakeEngine assumption
+ require lockstakeEngine.ilk() == ilk;
+ // Governance setting assumption
+ require vow != e.msg.sender;
+
+ mathint sliceAux = _min(salesIdLotBefore, amt);
+ mathint oweAux = sliceAux * price;
+ mathint chost = chost();
+ mathint slice; mathint owe;
+ if (oweAux > salesIdTabBefore) {
+ owe = salesIdTabBefore;
+ slice = owe / price;
+ } else {
+ if (oweAux < salesIdTabBefore && sliceAux < salesIdLotBefore) {
+ if (salesIdTabBefore - oweAux < chost) {
+ owe = salesIdTabBefore - chost;
+ slice = owe / price;
+ } else {
+ owe = oweAux;
+ slice = sliceAux;
+ }
+ } else {
+ owe = oweAux;
+ slice = sliceAux;
+ }
+ }
+ mathint calcTabAfter = salesIdTabBefore - owe;
+ mathint calcLotAfter = salesIdLotBefore - slice;
+ bool isRemoved = calcLotAfter == 0 || calcTabAfter == 0;
+ mathint fee = lockstakeEngine.fee();
+ // Happening in kick
+ require salesIdLotBefore <= max_int256();
+ require salesIdTotBefore >= salesIdLotBefore;
+ // Happening in Engine constructor
+ require fee < WAD();
+ mathint sold = calcLotAfter == 0 ? salesIdTotBefore : (calcTabAfter == 0 ? salesIdTotBefore - calcLotAfter : 0);
+ mathint left = calcTabAfter == 0 ? calcLotAfter : 0;
+ mathint burn = _min(sold * fee / (WAD() - fee), left);
+ mathint refund = left - burn;
+
+ take(e, id, amt, max, who, data);
+
+ mathint kicksAfter = kicks();
+ mathint countAfter = count();
+ mathint activeCountAfter = active(require_uint256(countAfter - 1));
+ mathint salesIdPosAfter; mathint salesIdTabAfter; mathint salesIdLotAfter; mathint salesIdTotAfter; address salesIdUsrAfter; mathint salesIdTicAfter; mathint salesIdTopAfter;
+ salesIdPosAfter, salesIdTabAfter, salesIdLotAfter, salesIdTotAfter, salesIdUsrAfter, salesIdTicAfter, salesIdTopAfter = sales(id);
+ mathint salesOtherPosAfter; mathint salesOtherTabAfter; mathint salesOtherLotAfter; mathint salesOtherTotAfter; address salesOtherUsrAfter; mathint salesOtherTicAfter; mathint salesOtherTopAfter;
+ salesOtherPosAfter, salesOtherTabAfter, salesOtherLotAfter, salesOtherTotAfter, salesOtherUsrAfter, salesOtherTicAfter, salesOtherTopAfter = sales(otherUint256);
+ mathint vatGemIlkClipperAfter = vat.gem(ilk, currentContract);
+ mathint mkrTotalSupplyAfter = mkr.totalSupply();
+ mathint mkrBalanceOfEngineAfter = mkr.balanceOf(lockstakeEngine);
+ mathint mkrBalanceOfWhoAfter = mkr.balanceOf(who);
+ mathint vatDaiSenderAfter = vat.dai(e.msg.sender);
+ mathint vatDaiVowAfter = vat.dai(vow);
+ mathint dogDirtAfter = dog.Dirt();
+ mathint dogIlkDirtAfter;
+ a, b, b, dogIlkDirtAfter = dog.ilks(ilk);
+ mathint vatUrnsIlkUsrInkAfter;
+ vatUrnsIlkUsrInkAfter, b = vat.urns(ilk, salesIdUsrBefore);
+ mathint lsmkrTotalSupplyAfter = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUsrAfter = lsmkr.balanceOf(salesIdUsrBefore);
+ mathint engineUrnAuctionsUsrAfter = lockstakeEngine.urnAuctions(salesIdUsrBefore);
+
+ assert countAfter == (isRemoved ? countBefore - 1 : countBefore), "Assert 1";
+ assert salesIdPosAfter == (isRemoved ? 0 : salesIdPosBefore), "Assert 2";
+ assert salesIdTabAfter == (isRemoved ? 0 : calcTabAfter), "Assert 3";
+ assert salesIdLotAfter == (isRemoved ? 0 : calcLotAfter), "Assert 4";
+ assert salesIdTotAfter == (isRemoved ? 0 : salesIdTotBefore), "Assert 5";
+ assert salesIdUsrAfter == (isRemoved ? 0 : salesIdUsrBefore), "Assert 6";
+ assert salesIdTicAfter == (isRemoved ? 0 : salesIdTicBefore), "Assert 7";
+ assert salesIdTopAfter == (isRemoved ? 0 : salesIdTopBefore), "Assert 8";
+ assert salesOtherPosAfter == (to_mathint(otherUint256) == activeLastBefore && isRemoved ? salesIdPosBefore : salesOtherPosBefore), "Assert 9";
+ assert salesOtherTabAfter == salesOtherTabBefore, "Assert 10";
+ assert salesOtherLotAfter == salesOtherLotBefore, "Assert 11";
+ assert salesOtherTotAfter == salesOtherTotBefore, "Assert 12";
+ assert salesOtherUsrAfter == salesOtherUsrBefore, "Assert 13";
+ assert salesOtherTicAfter == salesOtherTicBefore, "Assert 14";
+ assert salesOtherTopAfter == salesOtherTopBefore, "Assert 15";
+ assert vatGemIlkClipperAfter == vatGemIlkClipperBefore - (calcLotAfter > 0 && calcTabAfter == 0 ? salesIdLotBefore : slice), "Assert 16";
+ assert mkrTotalSupplyAfter == mkrTotalSupplyBefore - burn, "Assert 17";
+ assert who == lockstakeEngine => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore - burn, "Assert 18";
+ assert who != lockstakeEngine => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore - slice - burn, "Assert 19";
+ assert who != lockstakeEngine && who != salesIdUsrBefore => mkrBalanceOfWhoAfter == mkrBalanceOfWhoBefore + slice, "Assert 20";
+ assert vatDaiSenderAfter == vatDaiSenderBefore - owe, "Assert 21";
+ assert vatDaiVowAfter == vatDaiVowBefore + owe, "Assert 22";
+ assert dogDirtAfter == dogDirtBefore - (calcLotAfter == 0 ? salesIdTabBefore : owe), "Assert 23";
+ assert dogIlkDirtAfter == dogIlkDirtBefore - (calcLotAfter == 0 ? salesIdTabBefore : owe), "Assert 24";
+ assert vatUrnsIlkUsrInkAfter == vatUrnsIlkUsrInkBefore + refund, "Assert 25";
+ assert lsmkrTotalSupplyAfter == lsmkrTotalSupplyBefore + refund, "Assert 26";
+ assert lsmkrBalanceOfUsrAfter == lsmkrBalanceOfUsrBefore + refund, "Assert 27";
+ assert engineUrnAuctionsUsrAfter == engineUrnAuctionsUsrBefore - (isRemoved ? 1 : 0), "Assert 28";
+}
+
+// Verify revert rules on take
+rule take_revert(uint256 id, uint256 amt, uint256 max, address who, bytes data) {
+ env e;
+
+ require e.msg.sender != currentContract;
+
+ bytes32 ilk = ilk();
+ address vow = vow();
+ mathint locked = lockedGhost();
+ mathint stopped = stopped();
+ mathint tail = tail();
+ mathint cusp = cusp();
+ mathint chost = chost();
+ mathint count = count();
+ mathint activeLast;
+ if (count > 0) {
+ activeLast = active(assert_uint256(count - 1));
+ } else {
+ activeLast = 0;
+ }
+
+ mathint salesIdPos; mathint salesIdTab; mathint salesIdLot; mathint salesIdTot; address salesIdUsr; mathint salesIdTic; mathint salesIdTop;
+ salesIdPos, salesIdTab, salesIdLot, salesIdTot, salesIdUsr, salesIdTic, salesIdTop = sales(id);
+
+ mathint vatGemIlkClipper = vat.gem(ilk, currentContract);
+ mathint vatCanSenderClipper = vat.can(e.msg.sender, currentContract);
+ mathint vatDaiSender = vat.dai(e.msg.sender);
+
+ mathint vatIlksIlkArt; mathint vatIlksIlkRate; mathint vatIlksIlkSpot; mathint vatIlksIlkDust; mathint a;
+ vatIlksIlkArt, vatIlksIlkRate, vatIlksIlkSpot, a, vatIlksIlkDust = vat.ilks(ilk);
+ mathint vatUrnsIlkUsrInk; mathint vatUrnsIlkUsrArt;
+ vatUrnsIlkUsrInk, vatUrnsIlkUsrArt = vat.urns(ilk, salesIdUsr);
+
+ mathint dogDirt = dog.Dirt();
+ address b; mathint dogIlkDirt;
+ b, a, a, dogIlkDirt = dog.ilks(ilk);
+
+ require to_mathint(e.block.timestamp) >= salesIdTic;
+ mathint price = calcPriceSummary();
+ // Avoid division by zero
+ require salesIdTop > 0;
+ bool done = e.block.timestamp - salesIdTic > tail || price * RAY() / salesIdTop < cusp;
+
+ mathint sliceAux = _min(salesIdLot, amt);
+ mathint oweAux = sliceAux * price;
+ mathint slice; mathint owe;
+ if (oweAux > salesIdTab) {
+ owe = salesIdTab;
+ slice = owe / price;
+ } else {
+ if (oweAux < salesIdTab && sliceAux < salesIdLot) {
+ if (salesIdTab - oweAux < chost) {
+ owe = salesIdTab - chost;
+ slice = price > 0 ? owe / price : max_uint256; // Just a placeholder if price == 0
+ } else {
+ owe = oweAux;
+ slice = sliceAux;
+ }
+ } else {
+ owe = oweAux;
+ slice = sliceAux;
+ }
+ }
+ mathint calcTabAfter = salesIdTab - owe;
+ mathint calcLotAfter = salesIdLot - slice;
+ mathint digAmt = calcLotAfter == 0 ? salesIdTab : owe;
+ bool isRemoved = calcLotAfter == 0 || calcTabAfter == 0;
+ mathint fee = lockstakeEngine.fee();
+
+ // Happening in kick
+ require salesIdLot <= max_int256();
+ require salesIdTot >= salesIdLot;
+ // Happening in Engine constructor
+ require fee < WAD();
+ require lsmkr.wards(lockstakeEngine) == 1;
+ mathint sold = calcLotAfter == 0 ? salesIdTot : (calcTabAfter == 0 ? salesIdTot - calcLotAfter : 0);
+ mathint left = calcTabAfter == 0 ? calcLotAfter : 0;
+ mathint burn = _min(sold * fee / (WAD() - fee), left);
+ mathint refund = left - burn;
+ // Happening in urn init
+ require vat.can(salesIdUsr, lockstakeEngine) == 1;
+ // Tokens invariants
+ require to_mathint(mkr.totalSupply()) >= mkr.balanceOf(lockstakeEngine) + mkr.balanceOf(who);
+ require lsmkr.totalSupply() >= mkr.balanceOf(salesIdUsr);
+ // Happening in deploy scripts
+ require vat.wards(currentContract) == 1;
+ require vat.wards(lockstakeEngine) == 1;
+ require dog.wards(currentContract) == 1;
+ require lockstakeEngine.wards(currentContract) == 1;
+ // LockstakeEngine assumtions
+ require lockstakeEngine.ilk() == ilk;
+ require to_mathint(mkr.balanceOf(lockstakeEngine)) >= slice + burn;
+ require lockstakeEngine.urnAuctions(salesIdUsr) > 0;
+ require sold * fee <= max_uint256;
+ require refund <= max_int256();
+ require vat.gem(ilk, salesIdUsr) + refund <= max_uint256;
+ require salesIdUsr != 0 && salesIdUsr != lsmkr;
+ require lsmkr.totalSupply() + refund <= max_uint256;
+ // Dog assumptions
+ require dogDirt >= digAmt;
+ require dogIlkDirt >= digAmt;
+ // Practical Vat assumptions
+ require vat.live() == 1;
+ require vat.dai(vow) + owe <= max_uint256;
+ require vatIlksIlkRate >= RAY() && vatIlksIlkRate <= max_int256();
+ require vatUrnsIlkUsrInk + refund <= max_uint256;
+ require (vatUrnsIlkUsrInk + refund) * vatIlksIlkSpot <= max_uint256;
+ require vatIlksIlkRate * vatIlksIlkArt <= max_uint256;
+ require vatIlksIlkArt >= vatUrnsIlkUsrArt;
+ require vatUrnsIlkUsrArt == 0 || vatIlksIlkRate * vatUrnsIlkUsrArt >= vatIlksIlkDust;
+
+ take@withrevert(e, id, amt, max, who, data);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = locked != 0;
+ bool revert3 = stopped >= 3;
+ bool revert4 = salesIdUsr == 0;
+ bool revert5 = price * RAY() > max_uint256;
+ bool revert6 = done;
+ bool revert7 = to_mathint(max) < price;
+ bool revert8 = sliceAux * price > max_uint256;
+ bool revert9 = oweAux < salesIdTab && sliceAux < salesIdLot && salesIdTab - oweAux < chost && salesIdTab <= chost;
+ bool revert10 = oweAux < salesIdTab && sliceAux < salesIdLot && salesIdTab - oweAux < chost && price == 0;
+ bool revert11 = vatGemIlkClipper < slice;
+ bool revert12 = data.length > 0 && (who == badGuy || who == redoGuy || who == kickGuy || who == fileUintGuy || who == fileAddrGuy || who == yankGuy);
+ bool revert13 = vatCanSenderClipper != 1;
+ bool revert14 = vatDaiSender < owe;
+ bool revert15 = (calcLotAfter == 0 || calcTabAfter == 0) && count == 0;
+ bool revert16 = (calcLotAfter == 0 || calcTabAfter == 0) && to_mathint(id) != activeLast && salesIdPos > count - 1;
+ bool revert17 = calcLotAfter > 0 && calcTabAfter == 0 && vatGemIlkClipper < salesIdLot;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5 || revert6 ||
+ revert7 || revert8 || revert9 ||
+ revert10 || revert11 || revert12 ||
+ revert13 || revert14 || revert15 ||
+ revert16 || revert17, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting upchost
+rule upchost() {
+ env e;
+
+ bytes32 ilk = ilk();
+
+ mathint vatIlksIlkDust; mathint a;
+ a, a, a, a, vatIlksIlkDust = vat.ilks(ilk);
+
+ mathint dogChopIlk = dog.chop(ilk);
+
+ upchost(e);
+
+ mathint chostAfter = chost();
+
+ assert chostAfter == vatIlksIlkDust * dogChopIlk / WAD(), "Assert 1";
+}
+
+// Verify revert rules on upchost
+rule upchost_revert() {
+ env e;
+
+ bytes32 ilk = ilk();
+
+ mathint vatIlksIlkDust; mathint a;
+ a, a, a, a, vatIlksIlkDust = vat.ilks(ilk);
+
+ mathint dogChopIlk = dog.chop(ilk);
+
+ upchost@withrevert(e);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = vatIlksIlkDust * dogChopIlk > max_uint256;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting yank
+rule yank(uint256 id) {
+ env e;
+
+ require e.msg.sender != currentContract;
+
+ bytes32 ilk = ilk();
+ // address vow = vow();
+
+ mathint countBefore = count();
+ uint256 otherUint256;
+ require otherUint256 != id;
+ mathint activeLastBefore;
+ if (countBefore > 0) {
+ activeLastBefore = active(assert_uint256(countBefore - 1));
+ } else {
+ activeLastBefore = 0;
+ }
+ mathint a; address b;
+ mathint salesIdPosBefore; mathint salesIdTabBefore; mathint salesIdLotBefore; address salesIdUsrBefore;
+ salesIdPosBefore, salesIdTabBefore, salesIdLotBefore, a, salesIdUsrBefore, a, a = sales(id);
+ mathint salesOtherPosBefore; mathint salesOtherTabBefore; mathint salesOtherLotBefore; mathint salesOtherTotBefore; address salesOtherUsrBefore; mathint salesOtherTicBefore; mathint salesOtherTopBefore;
+ salesOtherPosBefore, salesOtherTabBefore, salesOtherLotBefore, salesOtherTotBefore, salesOtherUsrBefore, salesOtherTicBefore, salesOtherTopBefore = sales(otherUint256);
+ mathint dogDirtBefore = dog.Dirt();
+ mathint dogIlkDirtBefore;
+ b, a, a, dogIlkDirtBefore = dog.ilks(ilk);
+ mathint vatGemIlkClipperBefore = vat.gem(ilk, currentContract);
+ mathint vatGemIlkSenderBefore = vat.gem(ilk, e.msg.sender);
+ mathint engineUrnAuctionsUsrBefore = lockstakeEngine.urnAuctions(salesIdUsrBefore);
+
+ yank(e, id);
+
+ mathint countAfter = count();
+ mathint salesIdPosAfter; mathint salesIdTabAfter; mathint salesIdLotAfter; mathint salesIdTotAfter; address salesIdUsrAfter; mathint salesIdTicAfter; mathint salesIdTopAfter;
+ salesIdPosAfter, salesIdTabAfter, salesIdLotAfter, salesIdTotAfter, salesIdUsrAfter, salesIdTicAfter, salesIdTopAfter = sales(id);
+ mathint salesOtherPosAfter; mathint salesOtherTabAfter; mathint salesOtherLotAfter; mathint salesOtherTotAfter; address salesOtherUsrAfter; mathint salesOtherTicAfter; mathint salesOtherTopAfter;
+ salesOtherPosAfter, salesOtherTabAfter, salesOtherLotAfter, salesOtherTotAfter, salesOtherUsrAfter, salesOtherTicAfter, salesOtherTopAfter = sales(otherUint256);
+ mathint dogDirtAfter = dog.Dirt();
+ mathint dogIlkDirtAfter;
+ b, a, a, dogIlkDirtAfter = dog.ilks(ilk);
+ mathint vatGemIlkClipperAfter = vat.gem(ilk, currentContract);
+ mathint vatGemIlkSenderAfter = vat.gem(ilk, e.msg.sender);
+ mathint engineUrnAuctionsUsrAfter = lockstakeEngine.urnAuctions(salesIdUsrBefore);
+
+ assert countAfter == countBefore - 1, "Assert 1";
+ assert salesIdPosAfter == 0, "Assert 2";
+ assert salesIdTabAfter == 0, "Assert 3";
+ assert salesIdLotAfter == 0, "Assert 4";
+ assert salesIdTotAfter == 0, "Assert 5";
+ assert salesIdUsrAfter == 0, "Assert 6";
+ assert salesIdTicAfter == 0, "Assert 7";
+ assert salesIdTopAfter == 0, "Assert 8";
+ assert salesOtherPosAfter == (to_mathint(otherUint256) == activeLastBefore ? salesIdPosBefore : salesOtherPosBefore), "Assert 9";
+ assert salesOtherTabAfter == salesOtherTabBefore, "Assert 10";
+ assert salesOtherLotAfter == salesOtherLotBefore, "Assert 11";
+ assert salesOtherTotAfter == salesOtherTotBefore, "Assert 12";
+ assert salesOtherUsrAfter == salesOtherUsrBefore, "Assert 13";
+ assert salesOtherTicAfter == salesOtherTicBefore, "Assert 14";
+ assert salesOtherTopAfter == salesOtherTopBefore, "Assert 15";
+ assert dogDirtAfter == dogDirtBefore - salesIdTabBefore, "Assert 16";
+ assert dogIlkDirtAfter == dogIlkDirtBefore - salesIdTabBefore, "Assert 17";
+ assert vatGemIlkClipperAfter == vatGemIlkClipperBefore - salesIdLotBefore, "Assert 18";
+ assert vatGemIlkSenderAfter == vatGemIlkSenderBefore + salesIdLotBefore, "Assert 19";
+ assert engineUrnAuctionsUsrAfter == engineUrnAuctionsUsrBefore - 1, "Assert 20";
+}
+
+// Verify revert rules on yank
+rule yank_revert(uint256 id) {
+ env e;
+
+ require e.msg.sender != currentContract;
+
+ mathint wardsSender = wards(e.msg.sender);
+ mathint locked = lockedGhost();
+ bytes32 ilk = ilk();
+
+ mathint count = count();
+ mathint activeLast;
+ if (count > 0) {
+ activeLast = active(assert_uint256(count - 1));
+ } else {
+ activeLast = 0;
+ }
+
+ mathint salesIdPos; mathint salesIdTab; mathint salesIdLot; address salesIdUsr; mathint a;
+ salesIdPos, salesIdTab, salesIdLot, a, salesIdUsr, a, a = sales(id);
+
+ mathint engineWardsClipper = lockstakeEngine.wards(currentContract);
+
+ mathint dogWardsClipper = dog.wards(currentContract);
+ mathint dogDirt = dog.Dirt();
+ address b; mathint dogIlkDirt;
+ b, a, a, dogIlkDirt = dog.ilks(ilk);
+
+ mathint vatGemIlkClipper = vat.gem(ilk, currentContract);
+ mathint vatGemIlkSender = vat.gem(ilk, e.msg.sender);
+
+ // LockstakeEngine assumptions
+ require engineWardsClipper == 1;
+ require lockstakeEngine.urnAuctions(salesIdUsr) > 0;
+ // Dog assumptions
+ require dogWardsClipper == 1;
+ require dogDirt >= salesIdTab;
+ require dogIlkDirt >= salesIdTab;
+ // Vat assumption
+ require vatGemIlkSender + salesIdLot <= max_uint256;
+
+ yank@withrevert(e, id);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+ bool revert3 = locked != 0;
+ bool revert4 = salesIdUsr == 0;
+ bool revert5 = vatGemIlkClipper < salesIdLot;
+ bool revert6 = count == 0 || to_mathint(id) != activeLast && salesIdPos > count - 1;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5 || revert6, "Revert rules failed";
+}
diff --git a/certora/LockstakeEngine.conf b/certora/LockstakeEngine.conf
new file mode 100644
index 00000000..9a05a537
--- /dev/null
+++ b/certora/LockstakeEngine.conf
@@ -0,0 +1,101 @@
+{
+ "files": [
+ "src/LockstakeEngine.sol",
+ "src/LockstakeUrn.sol",
+ "src/LockstakeMkr.sol",
+ "certora/harness/dss/Jug.sol",
+ "certora/harness/dss/Vat.sol",
+ "test/mocks/VoteDelegateMock.sol",
+ "certora/harness/VoteDelegate2Mock.sol",
+ "test/mocks/VoteDelegateMock.sol:VoteDelegateFactoryMock",
+ "test/mocks/UsdsJoinMock.sol",
+ "test/mocks/UsdsMock.sol",
+ "certora/harness/tokens/MkrMock.sol",
+ "test/mocks/MkrSkyMock.sol",
+ "certora/harness/tokens/SkyMock.sol",
+ "certora/harness/tokens/RewardsMock.sol",
+ "test/mocks/StakingRewardsMock.sol",
+ "certora/harness/StakingRewards2Mock.sol"
+ ],
+ "solc_map": {
+ "LockstakeEngine": "solc-0.8.21",
+ "LockstakeUrn": "solc-0.8.21",
+ "LockstakeMkr": "solc-0.8.21",
+ "Jug": "solc-0.5.12",
+ "Vat": "solc-0.5.12",
+ "VoteDelegateMock": "solc-0.8.21",
+ "VoteDelegate2Mock": "solc-0.8.21",
+ "VoteDelegateFactoryMock": "solc-0.8.21",
+ "UsdsJoinMock": "solc-0.8.21",
+ "UsdsMock": "solc-0.8.21",
+ "MkrMock": "solc-0.8.21",
+ "MkrSkyMock": "solc-0.8.21",
+ "SkyMock": "solc-0.8.21",
+ "StakingRewardsMock": "solc-0.8.21",
+ "StakingRewards2Mock": "solc-0.8.21",
+ "RewardsMock": "solc-0.8.21"
+ },
+ "solc_optimize_map": {
+ "LockstakeEngine": "200",
+ "LockstakeUrn": "200",
+ "LockstakeMkr": "200",
+ "Jug": "0",
+ "Vat": "0",
+ "UsdsJoinMock": "0",
+ "UsdsMock": "0",
+ "MkrMock": "0",
+ "MkrSkyMock": "0",
+ "SkyMock": "0",
+ "VoteDelegateMock": "0",
+ "VoteDelegate2Mock": "0",
+ "VoteDelegateFactoryMock": "0",
+ "StakingRewardsMock": "0",
+ "StakingRewards2Mock": "0",
+ "RewardsMock": "0"
+ },
+ "link": [
+ "LockstakeEngine:jug=Jug",
+ "LockstakeEngine:voteDelegateFactory=VoteDelegateFactoryMock",
+ "LockstakeEngine:vat=Vat",
+ "LockstakeEngine:usdsJoin=UsdsJoinMock",
+ "LockstakeEngine:usds=UsdsMock",
+ "LockstakeEngine:mkr=MkrMock",
+ "LockstakeEngine:lsmkr=LockstakeMkr",
+ "LockstakeEngine:mkrSky=MkrSkyMock",
+ "LockstakeEngine:sky=SkyMock",
+ "LockstakeEngine:urnImplementation=LockstakeUrn",
+ "LockstakeUrn:engine=LockstakeEngine",
+ "LockstakeUrn:lsmkr=LockstakeMkr",
+ "LockstakeUrn:vat=Vat",
+ "Jug:vat=Vat",
+ "UsdsJoinMock:vat=Vat",
+ "UsdsJoinMock:usds=UsdsMock",
+ "MkrSkyMock:mkr=MkrMock",
+ "MkrSkyMock:sky=SkyMock",
+ "VoteDelegateMock:gov=MkrMock",
+ "VoteDelegate2Mock:gov=MkrMock",
+ "VoteDelegateFactoryMock:gov=MkrMock",
+ "StakingRewardsMock:rewardsToken=RewardsMock",
+ "StakingRewardsMock:stakingToken=LockstakeMkr",
+ "StakingRewards2Mock:rewardsToken=RewardsMock",
+ "StakingRewards2Mock:stakingToken=LockstakeMkr"
+ ],
+ "verify": "LockstakeEngine:certora/LockstakeEngine.spec",
+ "prover_args": [
+ "-smt_nonLinearArithmetic true",
+ "-adaptiveSolverConfig false",
+ "-depth 0",
+ "-s [z3:def{randomSeed=1},z3:def{randomSeed=2},z3:def{randomSeed=3},z3:def{randomSeed=4},z3:def{randomSeed=5},z3:def{randomSeed=6},z3:def{randomSeed=7},z3:def{randomSeed=8},z3:def{randomSeed=9},z3:def{randomSeed=10}]"
+ ],
+ "smt_timeout": "7000",
+ "rule_sanity": "basic",
+ "optimistic_loop": true,
+ "multi_assert_check": true,
+ "parametric_contracts": ["LockstakeEngine"],
+ "build_cache": true,
+ "dynamic_bound": "1",
+ "prototype": [
+ "3d602d80600a3d3981f3363d3d373d3d3d363d73=LockstakeUrn"
+ ],
+ "msg": "LockstakeEngine"
+}
diff --git a/certora/LockstakeEngine.spec b/certora/LockstakeEngine.spec
new file mode 100644
index 00000000..4d0a9bea
--- /dev/null
+++ b/certora/LockstakeEngine.spec
@@ -0,0 +1,2196 @@
+// LockstakeEngine.spec
+
+using LockstakeUrn as lockstakeUrn;
+using Vat as vat;
+using MkrMock as mkr;
+using LockstakeMkr as lsmkr;
+using VoteDelegateMock as voteDelegate;
+using VoteDelegate2Mock as voteDelegate2;
+using VoteDelegateFactoryMock as voteDelegateFactory;
+using StakingRewardsMock as stakingRewards;
+using StakingRewards2Mock as stakingRewards2;
+using MkrSkyMock as mkrSky;
+using SkyMock as sky;
+using UsdsMock as usds;
+using UsdsJoinMock as usdsJoin;
+using Jug as jug;
+using RewardsMock as rewardsToken;
+
+methods {
+ // storage variables
+ function wards(address) external returns (uint256) envfree;
+ function farms(address) external returns (LockstakeEngine.FarmStatus) envfree;
+ function ownerUrnsCount(address) external returns (uint256) envfree;
+ function ownerUrns(address,uint256) external returns (address) envfree;
+ function urnOwners(address) external returns (address) envfree;
+ function urnCan(address,address) external returns (uint256) envfree;
+ function urnVoteDelegates(address) external returns (address) envfree;
+ function urnFarms(address) external returns (address) envfree;
+ function urnAuctions(address) external returns (uint256) envfree;
+ function jug() external returns (address) envfree;
+ function fee() external returns (uint256) envfree;
+ // immutables
+ function voteDelegateFactory() external returns (address) envfree;
+ function vat() external returns (address) envfree;
+ function usdsJoin() external returns (address) envfree;
+ function usds() external returns (address) envfree;
+ function ilk() external returns (bytes32) envfree;
+ function mkr() external returns (address) envfree;
+ function lsmkr() external returns (address) envfree;
+ function usds() external returns (address) envfree;
+ function sky() external returns (address) envfree;
+ function mkrSkyRate() external returns (uint256) envfree;
+ function urnImplementation() external returns (address) envfree;
+ //
+ function lockstakeUrn.engine() external returns (address) envfree;
+ function vat.live() external returns (uint256) envfree;
+ function vat.Line() external returns (uint256) envfree;
+ function vat.debt() external returns (uint256) envfree;
+ function vat.ilks(bytes32) external returns (uint256,uint256,uint256,uint256,uint256) envfree;
+ function vat.dai(address) external returns (uint256) envfree;
+ function vat.gem(bytes32,address) external returns (uint256) envfree;
+ function vat.urns(bytes32,address) external returns (uint256,uint256) envfree;
+ function vat.can(address,address) external returns (uint256) envfree;
+ function vat.wards(address) external returns (uint256) envfree;
+ function mkr.allowance(address,address) external returns (uint256) envfree;
+ function mkr.balanceOf(address) external returns (uint256) envfree;
+ function mkr.totalSupply() external returns (uint256) envfree;
+ function sky.allowance(address,address) external returns (uint256) envfree;
+ function sky.balanceOf(address) external returns (uint256) envfree;
+ function sky.totalSupply() external returns (uint256) envfree;
+ function lsmkr.allowance(address,address) external returns (uint256) envfree;
+ function lsmkr.balanceOf(address) external returns (uint256) envfree;
+ function lsmkr.totalSupply() external returns (uint256) envfree;
+ function lsmkr.wards(address) external returns (uint256) envfree;
+ function stakingRewards.balanceOf(address) external returns (uint256) envfree;
+ function stakingRewards.totalSupply() external returns (uint256) envfree;
+ function stakingRewards.rewardsToken() external returns (address) envfree;
+ function stakingRewards.rewards(address) external returns (uint256) envfree;
+ function stakingRewards2.balanceOf(address) external returns (uint256) envfree;
+ function stakingRewards2.totalSupply() external returns (uint256) envfree;
+ function mkrSky.rate() external returns (uint256) envfree;
+ function usds.allowance(address,address) external returns (uint256) envfree;
+ function usds.balanceOf(address) external returns (uint256) envfree;
+ function usds.totalSupply() external returns (uint256) envfree;
+ function jug.vow() external returns (address) envfree;
+ function rewardsToken.balanceOf(address) external returns (uint256) envfree;
+ function rewardsToken.totalSupply() external returns (uint256) envfree;
+ function voteDelegate.stake(address) external returns (uint256) envfree;
+ function voteDelegate2.stake(address) external returns (uint256) envfree;
+ function voteDelegateFactory.created(address) external returns (uint256) envfree;
+ //
+ function jug.drip(bytes32 ilk) external returns (uint256) => dripSummary(ilk);
+ function _.mul(uint256 x,int256 y) internal => mulISummary(x,y) expect int256;
+ function _.mul(uint256 x,uint256 y) internal => mulSummary(x,y) expect uint256;
+ function _.hope(address) external => DISPATCHER(true);
+ function _.approve(address,uint256) external => DISPATCHER(true);
+ function _.init() external => DISPATCHER(true);
+ function _.lock(uint256) external => DISPATCHER(true);
+ function _.free(uint256) external => DISPATCHER(true);
+ function _.stake(address,uint256,uint16) external => DISPATCHER(true);
+ function _.withdraw(address,uint256) external => DISPATCHER(true);
+ function _.stake(uint256,uint16) external => DISPATCHER(true);
+ function _.withdraw(uint256) external => DISPATCHER(true);
+ function _.getReward(address,address) external => DISPATCHER(true);
+ function _.getReward() external => DISPATCHER(true);
+ function _.rewardsToken() external => DISPATCHER(true);
+ function _.balanceOf(address) external => DISPATCHER(true);
+ function _.transfer(address,uint256) external => DISPATCHER(true);
+ function _.transferFrom(address,address,uint256) external => DISPATCHER(true);
+}
+
+definition max_int256() returns mathint = 2^255 - 1;
+definition min_int256() returns mathint = -2^255;
+definition WAD() returns mathint = 10^18;
+definition RAY() returns mathint = 10^27;
+definition _divup(mathint x, mathint y) returns mathint = x != 0 ? ((x - 1) / y) + 1 : 0;
+definition _min(mathint x, mathint y) returns mathint = x < y ? x : y;
+
+function mulISummary(uint256 x, int256 y) returns int256 {
+ require x <= max_int256();
+ mathint z = x * y;
+ return require_int256(z);
+}
+
+function mulSummary(uint256 x, uint256 y) returns uint256 {
+ mathint z = x * y;
+ return require_uint256(z);
+}
+
+persistent ghost address createdUrn;
+hook CREATE1(uint value, uint offset, uint length) address v {
+ createdUrn = v;
+}
+
+persistent ghost address queriedUrn;
+hook Sload address v ownerUrns[KEY address owner][KEY uint256 index] {
+ queriedUrn = v;
+}
+
+persistent ghost address passedUrn;
+hook Sload uint256 v urnAuctions[KEY address urn] {
+ passedUrn = urn;
+}
+
+ghost mathint duty;
+ghost mathint timeDiff;
+function dripSummary(bytes32 ilk) returns uint256 {
+ env e;
+ require duty >= RAY();
+ uint256 prev; uint256 a;
+ a, prev, a, a, a = vat.ilks(ilk);
+ uint256 rate = timeDiff == 0 ? prev : require_uint256(duty * timeDiff * prev / RAY());
+ timeDiff = 0;
+ vat.fold(e, ilk, jug.vow(), require_int256(rate - prev));
+ return rate;
+}
+
+// Verify that each storage layout is only modified in the corresponding functions
+rule storageAffected(method f) filtered { f -> f.selector != sig:multicall(bytes[]).selector } {
+ env e;
+
+ address anyAddr;
+ address anyAddr2;
+ uint256 anyUint256;
+
+ bytes32 ilk = ilk();
+
+ mathint wardsBefore = wards(anyAddr);
+ LockstakeEngine.FarmStatus farmsBefore = farms(anyAddr);
+ mathint ownerUrnsCountBefore = ownerUrnsCount(anyAddr);
+ address ownerUrnsBefore = ownerUrns(anyAddr, anyUint256);
+ address urnOwnersBefore = urnOwners(anyAddr);
+ mathint urnCanBefore = urnCan(anyAddr, anyAddr2);
+ address urnVoteDelegatesBefore = urnVoteDelegates(anyAddr);
+ address urnFarmsBefore = urnFarms(anyAddr);
+ mathint urnAuctionsBefore = urnAuctions(anyAddr);
+ address jugBefore = jug();
+
+ calldataarg args;
+ f(e, args);
+
+ mathint wardsAfter = wards(anyAddr);
+ LockstakeEngine.FarmStatus farmsAfter = farms(anyAddr);
+ mathint ownerUrnsCountAfter = ownerUrnsCount(anyAddr);
+ address ownerUrnsAfter = ownerUrns(anyAddr, anyUint256);
+ address urnOwnersAfter = urnOwners(anyAddr);
+ mathint urnCanAfter = urnCan(anyAddr, anyAddr2);
+ address urnVoteDelegatesAfter = urnVoteDelegates(anyAddr);
+ address urnFarmsAfter = urnFarms(anyAddr);
+ mathint urnAuctionsAfter = urnAuctions(anyAddr);
+ address jugAfter = jug();
+
+ assert wardsAfter != wardsBefore => f.selector == sig:rely(address).selector || f.selector == sig:deny(address).selector, "Assert 1";
+ assert farmsAfter != farmsBefore => f.selector == sig:addFarm(address).selector || f.selector == sig:delFarm(address).selector, "Assert 2";
+ assert ownerUrnsCountAfter != ownerUrnsCountBefore => f.selector == sig:open(uint256).selector, "Assert 3";
+ assert ownerUrnsAfter != ownerUrnsBefore => f.selector == sig:open(uint256).selector, "Assert 4";
+ assert urnOwnersAfter != urnOwnersBefore => f.selector == sig:open(uint256).selector, "Assert 5";
+ assert urnCanAfter != urnCanBefore => f.selector == sig:hope(address,uint256,address).selector || f.selector == sig:nope(address,uint256,address).selector, "Assert 6";
+ assert urnVoteDelegatesAfter != urnVoteDelegatesBefore => f.selector == sig:selectVoteDelegate(address,uint256,address).selector || f.selector == sig:onKick(address,uint256).selector, "Assert 7";
+ assert urnFarmsAfter != urnFarmsBefore => f.selector == sig:selectFarm(address,uint256,address,uint16).selector || f.selector == sig:onKick(address,uint256).selector, "Assert 8";
+ assert urnAuctionsAfter != urnAuctionsBefore => f.selector == sig:onKick(address,uint256).selector || f.selector == sig:onRemove(address,uint256,uint256).selector, "Assert 9";
+ assert jugAfter != jugBefore => f.selector == sig:file(bytes32,address).selector, "Assert 10";
+}
+
+rule vatGemKeepsUnchanged(method f) filtered { f -> f.selector != sig:multicall(bytes[]).selector } {
+ env e;
+
+ address anyAddr;
+
+ bytes32 ilk = ilk();
+
+ mathint vatGemIlkAnyBefore = vat.gem(ilk, anyAddr);
+
+ calldataarg args;
+ f(e, args);
+
+ mathint vatGemIlkAnyAfter = vat.gem(ilk, anyAddr);
+
+ assert vatGemIlkAnyAfter == vatGemIlkAnyBefore, "Assert 1";
+}
+
+rule inkChangeMatchesMkrChange(method f) filtered { f -> f.selector != sig:multicall(bytes[]).selector } {
+ env e;
+
+ createdUrn = 0;
+ queriedUrn = 0;
+ passedUrn = 0;
+ storage init = lastStorage;
+ address onTakeWho;
+ uint256 onTakeWad;
+ if (f.selector == sig:free(address,uint256,address,uint256).selector ||
+ f.selector == sig:freeNoFee(address,uint256,address,uint256).selector) {
+ address owner;
+ uint256 index;
+ address to;
+ uint256 wad;
+ require to != currentContract && to != voteDelegate && to != voteDelegate2;
+ if (f.selector == sig:free(address,uint256,address,uint256).selector) {
+ free(e, owner, index, to, wad);
+ } else {
+ freeNoFee(e, owner, index, to, wad);
+ }
+ } else {
+ calldataarg args;
+ f(e, args);
+ }
+ storage final = lastStorage;
+
+ address urn = createdUrn != 0 ? createdUrn : (queriedUrn != 0 ? queriedUrn : (passedUrn != 0 ? passedUrn : 0));
+ require urn != currentContract && urn != voteDelegate && urn != voteDelegate2;
+
+ address voteDelegateAfter = urnVoteDelegates(urn);
+ require voteDelegateAfter == 0 || voteDelegateAfter == voteDelegate || voteDelegateAfter == voteDelegate2;
+
+ ilk() at init;
+
+ require e.msg.sender != currentContract && e.msg.sender != voteDelegate && e.msg.sender != voteDelegate2;
+ require mkrSkyRate() == mkrSky.rate();
+
+ bytes32 ilk = ilk();
+
+ address voteDelegateBefore = urnVoteDelegates(urn);
+ require voteDelegateBefore == 0 || voteDelegateBefore == voteDelegate;
+ mathint vatUrnsIlkUrnInkBefore; mathint a;
+ vatUrnsIlkUrnInkBefore, a = vat.urns(ilk, urn);
+ mathint mkrTotalSupplyBefore = mkr.totalSupply();
+ mathint mkrBalanceOfEngineBefore = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfVoteDelegateBeforeBefore = voteDelegateBefore == 0 ? 0 : mkr.balanceOf(voteDelegateBefore);
+ mathint mkrBalanceOfVoteDelegateAfterBefore = voteDelegateAfter == 0 ? 0 : mkr.balanceOf(voteDelegateAfter);
+ require mkr.balanceOf(e.msg.sender) + mkrBalanceOfEngineBefore + mkrBalanceOfVoteDelegateBeforeBefore + mkrBalanceOfVoteDelegateAfterBefore <= mkr.totalSupply();
+ require mkrBalanceOfEngineBefore + mkrBalanceOfVoteDelegateBeforeBefore >= vatUrnsIlkUrnInkBefore;
+
+ ilk() at final;
+
+ mathint vatUrnsIlkUrnInkAfter;
+ vatUrnsIlkUrnInkAfter, a = vat.urns(ilk, urn);
+ mathint mkrTotalSupplyAfter = mkr.totalSupply();
+ mathint mkrBalanceOfEngineAfter = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfVoteDelegateBeforeAfter = voteDelegateBefore == 0 ? 0 : mkr.balanceOf(voteDelegateBefore);
+ mathint mkrBalanceOfVoteDelegateAfterAfter = voteDelegateAfter == 0 ? 0 : mkr.balanceOf(voteDelegateAfter);
+ require f.selector == sig:onRemove(address,uint256,uint256).selector => voteDelegateBefore == 0;
+ mathint burntOnRemove = f.selector == sig:onRemove(address,uint256,uint256).selector ? mkrTotalSupplyBefore - mkrTotalSupplyAfter + vatUrnsIlkUrnInkAfter - vatUrnsIlkUrnInkBefore : 0;
+ mathint transferredOnTake = f.selector == sig:onTake(address,address,uint256).selector ? mkrBalanceOfEngineBefore - mkrBalanceOfEngineAfter : 0;
+ mathint receivedOnTake = f.selector == sig:onTake(address,address,uint256).selector ? mkrBalanceOfVoteDelegateBeforeAfter - mkrBalanceOfVoteDelegateBeforeBefore : 0;
+
+ // It checks that the ink change matches the MKR balance change + that is all or nothing delegated
+ assert voteDelegateAfter == voteDelegateBefore && voteDelegateBefore == 0 =>
+ vatUrnsIlkUrnInkAfter - vatUrnsIlkUrnInkBefore == mkrBalanceOfEngineAfter - mkrBalanceOfEngineBefore + burntOnRemove + transferredOnTake, "Assert 1";
+ assert voteDelegateAfter == voteDelegateBefore && voteDelegateBefore != 0 =>
+ vatUrnsIlkUrnInkAfter - vatUrnsIlkUrnInkBefore == mkrBalanceOfVoteDelegateBeforeAfter - mkrBalanceOfVoteDelegateBeforeBefore - receivedOnTake &&
+ mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore - transferredOnTake, "Assert 2";
+ assert voteDelegateAfter != voteDelegateBefore && voteDelegateBefore != 0 && voteDelegateAfter != 0 =>
+ mkrBalanceOfVoteDelegateBeforeAfter - mkrBalanceOfVoteDelegateBeforeBefore == mkrBalanceOfVoteDelegateAfterBefore - mkrBalanceOfVoteDelegateAfterAfter, "Assert3";
+}
+
+rule inkChangeMatchesLsmkrChange(method f) filtered { f -> f.selector != sig:multicall(bytes[]).selector } {
+ env e;
+
+ createdUrn = 0;
+ queriedUrn = 0;
+ passedUrn = 0;
+ storage init = lastStorage;
+ calldataarg args;
+ f(e, args);
+ storage final = lastStorage;
+
+ address urn = createdUrn != 0 ? createdUrn : (queriedUrn != 0 ? queriedUrn : (passedUrn != 0 ? passedUrn : 0));
+
+ address farmAfter = urnFarms(urn);
+ require farmAfter == 0 || farmAfter == stakingRewards || farmAfter == stakingRewards2;
+
+ ilk() at init;
+
+ bytes32 ilk = ilk();
+
+ address farmBefore = urnFarms(urn);
+ require farmBefore == 0 || farmBefore == stakingRewards;
+ mathint vatUrnsIlkUrnInkBefore; mathint a;
+ vatUrnsIlkUrnInkBefore, a = vat.urns(ilk, urn);
+ mathint lsmkrTotalSupplyBefore = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUrnBefore = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfFarmBeforeBefore = farmBefore == 0 ? 0 : lsmkr.balanceOf(farmBefore);
+ mathint lsmkrBalanceOfFarmAfterBefore = farmAfter == 0 ? 0 : lsmkr.balanceOf(farmAfter);
+ require lsmkrBalanceOfUrnBefore + lsmkrBalanceOfFarmBeforeBefore + lsmkrBalanceOfFarmAfterBefore <= lsmkrTotalSupplyBefore;
+ require vatUrnsIlkUrnInkBefore <= lsmkrTotalSupplyBefore;
+ mathint farmBeforeBalanceOfUrnBefore = 0;
+ if (farmBefore != 0) {
+ farmBeforeBalanceOfUrnBefore = farmBefore.balanceOf(e, urn);
+ require farmBeforeBalanceOfUrnBefore <= to_mathint(farmBefore.totalSupply(e));
+ }
+ mathint farmAfterBalanceOfUrnBefore = 0;
+ if (farmAfter != 0) {
+ farmAfterBalanceOfUrnBefore = farmAfter.balanceOf(e, urn);
+ require farmAfterBalanceOfUrnBefore <= to_mathint(farmAfter.totalSupply(e));
+ }
+ mathint stakingRewardsBalanceOfUrnBefore = stakingRewards.balanceOf(urn);
+ mathint stakingRewards2BalanceOfUrnBefore = stakingRewards2.balanceOf(urn);
+
+ ilk() at final;
+
+ mathint vatUrnsIlkUrnInkAfter;
+ vatUrnsIlkUrnInkAfter, a = vat.urns(ilk, urn);
+ mathint lsmkrTotalSupplyAfter = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUrnAfter = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfFarmBeforAfter = farmBefore == 0 ? 0 : lsmkr.balanceOf(farmBefore);
+ mathint lsmkrBalanceOfFarmAfterAfter = farmAfter == 0 ? 0 : lsmkr.balanceOf(farmAfter);
+ mathint farmBeforeBalanceOfUrnAfter = 0;
+ if (farmBefore != 0) {
+ farmBeforeBalanceOfUrnAfter = farmBefore.balanceOf(e, urn);
+ require farmBeforeBalanceOfUrnAfter <= to_mathint(farmBefore.totalSupply(e));
+ }
+ mathint farmAfterBalanceOfUrnAfter = 0;
+ if (farmAfter != 0) {
+ farmAfterBalanceOfUrnAfter = farmAfter.balanceOf(e, urn);
+ require farmAfterBalanceOfUrnAfter <= to_mathint(farmAfter.totalSupply(e));
+ }
+ mathint stakingRewardsBalanceOfUrnAfter = stakingRewards.balanceOf(urn);
+ mathint stakingRewards2BalanceOfUrnAfter = stakingRewards2.balanceOf(urn);
+
+ require farmBefore != 0 => lsmkrBalanceOfUrnBefore == 0;
+ require farmBefore == stakingRewards => stakingRewards2BalanceOfUrnBefore == 0;
+ require farmBefore == stakingRewards2 => stakingRewardsBalanceOfUrnBefore == 0;
+ require farmBefore == 0 => stakingRewardsBalanceOfUrnBefore == 0 && stakingRewards2BalanceOfUrnBefore == 0;
+ mathint burntOnKick = f.selector == sig:onKick(address,uint256).selector ? lsmkrTotalSupplyBefore - lsmkrTotalSupplyAfter : 0;
+ require vatUrnsIlkUrnInkBefore == lsmkrBalanceOfUrnBefore + stakingRewardsBalanceOfUrnBefore + stakingRewards2BalanceOfUrnBefore - burntOnKick;
+ require f.selector == sig:onRemove(address,uint256,uint256).selector => farmBefore == 0;
+
+ assert vatUrnsIlkUrnInkAfter - vatUrnsIlkUrnInkBefore == lsmkrTotalSupplyAfter - lsmkrTotalSupplyBefore + burntOnKick, "Assert 1";
+ assert farmAfter == farmBefore && farmBefore == 0 =>
+ vatUrnsIlkUrnInkAfter - vatUrnsIlkUrnInkBefore == lsmkrBalanceOfUrnAfter - lsmkrBalanceOfUrnBefore + burntOnKick, "Assert 2";
+ assert farmAfter == farmBefore && farmBefore != 0 =>
+ vatUrnsIlkUrnInkAfter - vatUrnsIlkUrnInkBefore == lsmkrBalanceOfFarmBeforAfter - lsmkrBalanceOfFarmBeforeBefore, "Assert 3";
+ assert farmAfter != farmBefore && farmBefore == 0 =>
+ vatUrnsIlkUrnInkAfter == lsmkrBalanceOfFarmAfterAfter - lsmkrBalanceOfFarmAfterBefore &&
+ vatUrnsIlkUrnInkBefore == lsmkrBalanceOfUrnBefore - lsmkrBalanceOfUrnAfter, "Assert 4";
+ assert farmAfter != farmBefore && farmAfter == 0 =>
+ vatUrnsIlkUrnInkAfter == lsmkrBalanceOfUrnAfter - lsmkrBalanceOfUrnBefore &&
+ vatUrnsIlkUrnInkBefore == lsmkrBalanceOfFarmBeforeBefore - lsmkrBalanceOfFarmBeforAfter - burntOnKick, "Assert 5";
+ assert farmAfter != farmBefore && farmBefore != 0 && farmAfter != 0 =>
+ vatUrnsIlkUrnInkAfter == lsmkrBalanceOfFarmAfterAfter - lsmkrBalanceOfFarmAfterBefore &&
+ vatUrnsIlkUrnInkBefore == lsmkrBalanceOfFarmBeforeBefore - lsmkrBalanceOfFarmBeforAfter, "Assert 6";
+ assert farmAfter == 0 =>
+ lsmkrBalanceOfUrnAfter == vatUrnsIlkUrnInkAfter && stakingRewardsBalanceOfUrnAfter == 0 && stakingRewards2BalanceOfUrnAfter == 0, "Assert 7";
+ assert farmAfter == stakingRewards =>
+ stakingRewardsBalanceOfUrnAfter == vatUrnsIlkUrnInkAfter && lsmkrBalanceOfUrnAfter == 0 && stakingRewards2BalanceOfUrnAfter == 0, "Assert 8";
+ assert farmAfter == stakingRewards2 =>
+ stakingRewards2BalanceOfUrnAfter == vatUrnsIlkUrnInkAfter && lsmkrBalanceOfUrnAfter == 0 && stakingRewardsBalanceOfUrnAfter == 0, "Assert 9";
+}
+
+rule inkMatchesLsmkrFarmOnKick(address urn, uint256 wad) {
+ env e;
+
+ address anyUrn;
+ require anyUrn != stakingRewards && anyUrn != stakingRewards2;
+
+ bytes32 ilk = ilk();
+
+ address farmBefore = urnFarms(anyUrn);
+ require farmBefore == 0 || farmBefore == stakingRewards;
+
+ mathint vatUrnsIlkAnyUrnInkBefore; mathint a;
+ vatUrnsIlkAnyUrnInkBefore, a = vat.urns(ilk, anyUrn);
+
+ mathint lsmkrBalanceOfAnyUrnBefore = lsmkr.balanceOf(anyUrn);
+ mathint farmBalanceOfAnyUrnBefore = farmBefore == 0 ? 0 : stakingRewards.balanceOf(anyUrn);
+
+ require stakingRewards2.balanceOf(anyUrn) == 0;
+ require lsmkrBalanceOfAnyUrnBefore == 0 || farmBalanceOfAnyUrnBefore == 0;
+ require lsmkrBalanceOfAnyUrnBefore > 0 => farmBefore == 0;
+ require farmBalanceOfAnyUrnBefore > 0 => farmBefore != 0;
+ require vatUrnsIlkAnyUrnInkBefore == lsmkrBalanceOfAnyUrnBefore + farmBalanceOfAnyUrnBefore;
+
+ onKick(e, urn, wad);
+
+ address farmAfter = urnFarms(anyUrn);
+ require farmAfter == 0 || farmAfter == farmBefore || farmAfter != farmBefore && farmAfter == stakingRewards2;
+
+ mathint vatUrnsIlkAnyUrnInkAfter;
+ vatUrnsIlkAnyUrnInkAfter, a = vat.urns(ilk, anyUrn);
+
+ mathint lsmkrBalanceOfAnyUrnAfter = lsmkr.balanceOf(anyUrn);
+ mathint farmBalanceOfAnyUrnAfter = farmAfter == 0 ? 0 : (farmAfter == farmBefore ? stakingRewards.balanceOf(anyUrn) : stakingRewards2.balanceOf(anyUrn));
+
+ assert urn != anyUrn => vatUrnsIlkAnyUrnInkAfter == lsmkrBalanceOfAnyUrnAfter + farmBalanceOfAnyUrnAfter, "Assert 1";
+ assert urn == anyUrn => vatUrnsIlkAnyUrnInkAfter == lsmkrBalanceOfAnyUrnAfter + farmBalanceOfAnyUrnAfter + wad, "Assert 2";
+}
+
+// Verify correct storage changes for non reverting rely
+rule rely(address usr) {
+ env e;
+
+ address other;
+ require other != usr;
+
+ mathint wardsOtherBefore = wards(other);
+
+ rely(e, usr);
+
+ mathint wardsUsrAfter = wards(usr);
+ mathint wardsOtherAfter = wards(other);
+
+ assert wardsUsrAfter == 1, "Assert 1";
+ assert wardsOtherAfter == wardsOtherBefore, "Assert 2";
+}
+
+// Verify revert rules on rely
+rule rely_revert(address usr) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+
+ rely@withrevert(e, usr);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting deny
+rule deny(address usr) {
+ env e;
+
+ address other;
+ require other != usr;
+
+ mathint wardsOtherBefore = wards(other);
+
+ deny(e, usr);
+
+ mathint wardsUsrAfter = wards(usr);
+ mathint wardsOtherAfter = wards(other);
+
+ assert wardsUsrAfter == 0, "Assert 1";
+ assert wardsOtherAfter == wardsOtherBefore, "Assert 2";
+}
+
+// Verify revert rules on deny
+rule deny_revert(address usr) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+
+ deny@withrevert(e, usr);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting file
+rule file(bytes32 what, address data) {
+ env e;
+
+ file(e, what, data);
+
+ address jugAfter = jug();
+
+ assert jugAfter == data, "Assert 1";
+}
+
+// Verify revert rules on file
+rule file_revert(bytes32 what, address data) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+
+ file@withrevert(e, what, data);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+ bool revert3 = what != to_bytes32(0x6a75670000000000000000000000000000000000000000000000000000000000);
+
+ assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting addFarm
+rule addFarm(address farm) {
+ env e;
+
+ address other;
+ require other != farm;
+
+ LockstakeEngine.FarmStatus farmsOtherBefore = farms(other);
+
+ addFarm(e, farm);
+
+ LockstakeEngine.FarmStatus farmsFarmAfter = farms(farm);
+ LockstakeEngine.FarmStatus farmsOtherAfter = farms(other);
+
+ assert farmsFarmAfter == LockstakeEngine.FarmStatus.ACTIVE, "Assert 1";
+ assert farmsOtherAfter == farmsOtherBefore, "Assert 2";
+}
+
+// Verify revert rules on addFarm
+rule addFarm_revert(address farm) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+
+ addFarm@withrevert(e, farm);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting delFarm
+rule delFarm(address farm) {
+ env e;
+
+ address other;
+ require other != farm;
+
+ LockstakeEngine.FarmStatus farmsOtherBefore = farms(other);
+
+ delFarm(e, farm);
+
+ LockstakeEngine.FarmStatus farmsFarmAfter = farms(farm);
+ LockstakeEngine.FarmStatus farmsOtherAfter = farms(other);
+
+ assert farmsFarmAfter == LockstakeEngine.FarmStatus.DELETED, "Assert 1";
+ assert farmsOtherAfter == farmsOtherBefore, "Assert 2";
+}
+
+// Verify revert rules on delFarm
+rule delFarm_revert(address farm) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+
+ delFarm@withrevert(e, farm);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting open
+rule open(uint256 index) {
+ env e;
+
+ address other;
+ require other != e.msg.sender;
+ address anyAddr; uint256 anyUint256;
+ require anyAddr != e.msg.sender || anyUint256 != index;
+
+ mathint ownerUrnsCountSenderBefore = ownerUrnsCount(e.msg.sender);
+ mathint ownerUrnsCountOtherBefore = ownerUrnsCount(other);
+ address ownerUrnsOtherBefore = ownerUrns(anyAddr, anyUint256);
+
+ address urn = open(e, index);
+ require urn.lsmkr(e) == lsmkr;
+
+ mathint ownerUrnsCountSenderAfter = ownerUrnsCount(e.msg.sender);
+ mathint ownerUrnsCountOtherAfter = ownerUrnsCount(other);
+ address ownerUrnsSenderIndexAfter = ownerUrns(e.msg.sender, index);
+ address ownerUrnsOtherAfter = ownerUrns(anyAddr, anyUint256);
+ address urnOwnersUrnAfter = urnOwners(urn);
+ mathint vatCanUrnEngineAfter = vat.can(urn, currentContract);
+ mathint lsmkrAllowanceUrnEngine = lsmkr.allowance(urn, currentContract);
+
+ assert ownerUrnsCountSenderAfter == ownerUrnsCountSenderBefore + 1, "Assert 1";
+ assert ownerUrnsCountOtherAfter == ownerUrnsCountOtherBefore, "Assert 2";
+ assert ownerUrnsSenderIndexAfter == urn, "Assert 3";
+ assert ownerUrnsOtherAfter == ownerUrnsOtherBefore, "Assert 4";
+ assert urnOwnersUrnAfter == e.msg.sender, "Assert 5";
+ assert vatCanUrnEngineAfter == 1, "Assert 6";
+ assert lsmkrAllowanceUrnEngine == max_uint256, "Assert 7";
+}
+
+// Verify revert rules on open
+rule open_revert(uint256 index) {
+ env e;
+
+ createdUrn = 0; // Now we can identify if the urn was created
+
+ mathint ownerUrnsCountSender = ownerUrnsCount(e.msg.sender);
+
+ open@withrevert(e, index);
+ bool reverted = lastReverted; // `lastReverted` will be modified by `createdUrn.engine(e)`
+ if (createdUrn != 0) {
+ require createdUrn.engine(e) == currentContract;
+ }
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = to_mathint(index) != ownerUrnsCountSender;
+ bool revert3 = ownerUrnsCountSender == max_uint256;
+
+ assert reverted <=> revert1 || revert2 || revert3, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting hope
+rule hope(address owner, uint256 index, address usr) {
+ env e;
+
+ address other;
+ address other2;
+ address urn = ownerUrns(owner, index);
+ require other != urn || other2 != usr;
+
+ mathint urnCanOtherBefore = urnCan(other, other2);
+
+ hope(e, owner, index, usr);
+
+ mathint urnCanUrnUsrAfter = urnCan(urn, usr);
+ mathint urnCanOtherAfter = urnCan(other, other2);
+
+ assert urnCanUrnUsrAfter == 1, "Assert 1";
+ assert urnCanOtherAfter == urnCanOtherBefore, "Assert 2";
+}
+
+// Verify revert rules on hope
+rule hope_revert(address owner, uint256 index, address usr) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ mathint urnCanUrnSender = urnCan(urn, e.msg.sender);
+
+ hope@withrevert(e, owner, index, usr);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = owner != e.msg.sender && urnCanUrnSender != 1;
+
+ assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting nope
+rule nope(address owner, uint256 index, address usr) {
+ env e;
+
+ address other;
+ address other2;
+ address urn = ownerUrns(owner, index);
+ require other != urn || other2 != usr;
+
+ mathint urnCanOtherBefore = urnCan(other, other2);
+
+ nope(e, owner, index, usr);
+
+ mathint urnCanUrnUsrAfter = urnCan(urn, usr);
+ mathint urnCanOtherAfter = urnCan(other, other2);
+
+ assert urnCanUrnUsrAfter == 0, "Assert 1";
+ assert urnCanOtherAfter == urnCanOtherBefore, "Assert 2";
+}
+
+// Verify revert rules on nope
+rule nope_revert(address owner, uint256 index, address usr) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ mathint urnCanUrnSender = urnCan(urn, e.msg.sender);
+
+ nope@withrevert(e, owner, index, usr);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = owner != e.msg.sender && urnCanUrnSender != 1;
+
+ assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting selectVoteDelegate
+rule selectVoteDelegate(address owner, uint256 index, address voteDelegate_) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address prevVoteDelegate = urnVoteDelegates(urn);
+ require prevVoteDelegate == 0 || prevVoteDelegate == voteDelegate2;
+
+ address other;
+ require other != urn;
+ address other2;
+ require other2 != voteDelegate_ && other2 != prevVoteDelegate && other2 != currentContract;
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInk; mathint a;
+ vatUrnsIlkUrnInk, a = vat.urns(ilk, urn);
+
+ address urnVoteDelegatesOtherBefore = urnVoteDelegates(other);
+ mathint mkrBalanceOfPrevVoteDelegateBefore = mkr.balanceOf(prevVoteDelegate);
+ mathint mkrBalanceOfNewVoteDelegateBefore = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfEngineBefore = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfOtherBefore = mkr.balanceOf(other2);
+
+ // Tokens invariants
+ require to_mathint(mkr.totalSupply()) >= mkrBalanceOfPrevVoteDelegateBefore + mkrBalanceOfNewVoteDelegateBefore + mkrBalanceOfEngineBefore + mkrBalanceOfOtherBefore;
+
+ selectVoteDelegate(e, owner, index, voteDelegate_);
+
+ address urnVoteDelegatesUrnAfter = urnVoteDelegates(urn);
+ address urnVoteDelegatesOtherAfter = urnVoteDelegates(other);
+ mathint mkrBalanceOfPrevVoteDelegateAfter = mkr.balanceOf(prevVoteDelegate);
+ mathint mkrBalanceOfNewVoteDelegateAfter = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfEngineAfter = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfOtherAfter = mkr.balanceOf(other2);
+
+ assert urnVoteDelegatesUrnAfter == voteDelegate_, "Assert 1";
+ assert urnVoteDelegatesOtherAfter == urnVoteDelegatesOtherBefore, "Assert 2";
+ assert prevVoteDelegate == 0 => mkrBalanceOfPrevVoteDelegateAfter == mkrBalanceOfPrevVoteDelegateBefore, "Assert 3";
+ assert prevVoteDelegate != 0 => mkrBalanceOfPrevVoteDelegateAfter == mkrBalanceOfPrevVoteDelegateBefore - vatUrnsIlkUrnInk, "Assert 4";
+ assert voteDelegate_ == 0 => mkrBalanceOfNewVoteDelegateAfter == mkrBalanceOfNewVoteDelegateBefore, "Assert 5";
+ assert voteDelegate_ != 0 => mkrBalanceOfNewVoteDelegateAfter == mkrBalanceOfNewVoteDelegateBefore + vatUrnsIlkUrnInk, "Assert 6";
+ assert prevVoteDelegate == 0 && voteDelegate_ == 0 || prevVoteDelegate != 0 && voteDelegate_ != 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore, "Assert 7";
+ assert prevVoteDelegate == 0 && voteDelegate_ != 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore - vatUrnsIlkUrnInk, "Assert 8";
+ assert prevVoteDelegate != 0 && voteDelegate_ == 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore + vatUrnsIlkUrnInk, "Assert 9";
+ assert mkrBalanceOfOtherAfter == mkrBalanceOfOtherBefore, "Assert 10";
+}
+
+// Verify revert rules on selectVoteDelegate
+rule selectVoteDelegate_revert(address owner, uint256 index, address voteDelegate_) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address prevVoteDelegate = urnVoteDelegates(urn);
+ require prevVoteDelegate == 0 || prevVoteDelegate == voteDelegate2;
+
+ mathint urnCanUrnSender = urnCan(urn, e.msg.sender);
+ mathint urnAuctions = urnAuctions(urn);
+ mathint voteDelegateFactoryCreatedVoteDelegate = voteDelegateFactory.created(voteDelegate_);
+ bytes32 ilk = ilk();
+ mathint vatIlksIlkSpot; mathint a;
+ a, a, vatIlksIlkSpot, a, a = vat.ilks(ilk);
+ mathint vatUrnsIlkUrnInk; mathint vatUrnsIlkUrnArt;
+ vatUrnsIlkUrnInk, vatUrnsIlkUrnArt = vat.urns(ilk, urn);
+ mathint calcVatIlksIlkRateAfter = dripSummary(ilk);
+
+ // Tokens invariants
+ require to_mathint(mkr.totalSupply()) >= mkr.balanceOf(prevVoteDelegate) + mkr.balanceOf(voteDelegate_) + mkr.balanceOf(currentContract);
+ // Practical Vat assumptions
+ require vatUrnsIlkUrnInk * vatIlksIlkSpot <= max_uint256;
+ require vatUrnsIlkUrnArt * calcVatIlksIlkRateAfter <= max_uint256;
+ // TODO: this might be nice to prove in some sort
+ require prevVoteDelegate == 0 && to_mathint(mkr.balanceOf(currentContract)) >= vatUrnsIlkUrnInk || prevVoteDelegate != 0 && to_mathint(mkr.balanceOf(prevVoteDelegate)) >= vatUrnsIlkUrnInk && to_mathint(voteDelegate2.stake(currentContract)) >= vatUrnsIlkUrnInk; // TODO: this might be interesting to be proved
+ require voteDelegate.stake(currentContract) + vatUrnsIlkUrnInk <= max_uint256;
+
+ selectVoteDelegate@withrevert(e, owner, index, voteDelegate_);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = owner != e.msg.sender && urnCanUrnSender != 1;
+ bool revert4 = urnAuctions > 0;
+ bool revert5 = voteDelegate_ != 0 && voteDelegateFactoryCreatedVoteDelegate != 1;
+ bool revert6 = voteDelegate_ == prevVoteDelegate;
+ bool revert7 = vatUrnsIlkUrnArt > 0 && voteDelegate_ != 0 && vatUrnsIlkUrnInk * vatIlksIlkSpot < vatUrnsIlkUrnArt * calcVatIlksIlkRateAfter;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5 || revert6 ||
+ revert7, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting selectFarm
+rule selectFarm(address owner, uint256 index, address farm, uint16 ref) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ require farm == 0 || farm == stakingRewards;
+ address prevFarm = urnFarms(urn);
+ require prevFarm == 0 || prevFarm == stakingRewards2;
+
+ address other;
+ require other != urn;
+ address other2;
+ require other2 != farm && other2 != prevFarm && other2 != urn;
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInk; mathint a;
+ vatUrnsIlkUrnInk, a = vat.urns(ilk, urn);
+
+ address urnFarmsOtherBefore = urnFarms(other);
+ mathint lsmkrBalanceOfPrevFarmBefore = lsmkr.balanceOf(prevFarm);
+ mathint lsmkrBalanceOfNewFarmBefore = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfUrnBefore = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfOtherBefore = lsmkr.balanceOf(other2);
+
+ // Tokens invariants
+ require to_mathint(lsmkr.totalSupply()) >= lsmkrBalanceOfPrevFarmBefore + lsmkrBalanceOfNewFarmBefore + lsmkrBalanceOfUrnBefore + lsmkrBalanceOfOtherBefore;
+
+ selectFarm(e, owner, index, farm, ref);
+
+ address urnFarmsUrnAfter = urnFarms(urn);
+ address urnFarmsOtherAfter = urnFarms(other);
+ mathint lsmkrBalanceOfPrevFarmAfter = lsmkr.balanceOf(prevFarm);
+ mathint lsmkrBalanceOfNewFarmAfter = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfUrnAfter = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfOtherAfter = lsmkr.balanceOf(other2);
+
+ assert urnFarmsUrnAfter == farm, "Assert 1";
+ assert urnFarmsOtherAfter == urnFarmsOtherBefore, "Assert 2";
+ assert prevFarm == 0 => lsmkrBalanceOfPrevFarmAfter == lsmkrBalanceOfPrevFarmBefore, "Assert 3";
+ assert prevFarm != 0 => lsmkrBalanceOfPrevFarmAfter == lsmkrBalanceOfPrevFarmBefore - vatUrnsIlkUrnInk, "Assert 4";
+ assert farm == 0 => lsmkrBalanceOfNewFarmAfter == lsmkrBalanceOfNewFarmBefore, "Assert 5";
+ assert farm != 0 => lsmkrBalanceOfNewFarmAfter == lsmkrBalanceOfNewFarmBefore + vatUrnsIlkUrnInk, "Assert 6";
+ assert prevFarm == 0 && farm == 0 || prevFarm != 0 && farm != 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore, "Assert 7";
+ assert prevFarm == 0 && farm != 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore - vatUrnsIlkUrnInk, "Assert 8";
+ assert prevFarm != 0 && farm == 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore + vatUrnsIlkUrnInk, "Assert 9";
+ assert lsmkrBalanceOfOtherAfter == lsmkrBalanceOfOtherBefore, "Assert 10";
+}
+
+// Verify revert rules on selectFarm
+rule selectFarm_revert(address owner, uint256 index, address farm, uint16 ref) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ require farm == 0 || farm == stakingRewards;
+ address prevFarm = urnFarms(urn);
+ require prevFarm == 0 || prevFarm == stakingRewards2;
+
+ address urnOwnersUrn = urnOwners(urn);
+ mathint urnCanUrnSender = urnCan(urn, e.msg.sender);
+ mathint urnAuctions = urnAuctions(urn);
+ LockstakeEngine.FarmStatus farmsFarm = farms(farm);
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInk; mathint a;
+ vatUrnsIlkUrnInk, a = vat.urns(ilk, urn);
+
+ // TODO: this might be nice to prove in some sort
+ require prevFarm == 0 && to_mathint(lsmkr.balanceOf(urn)) >= vatUrnsIlkUrnInk || prevFarm != 0 && to_mathint(lsmkr.balanceOf(prevFarm)) >= vatUrnsIlkUrnInk && to_mathint(stakingRewards2.balanceOf(urn)) >= vatUrnsIlkUrnInk;
+ // Token invariants
+ require to_mathint(lsmkr.totalSupply()) >= lsmkr.balanceOf(prevFarm) + lsmkr.balanceOf(farm) + lsmkr.balanceOf(urn);
+ require stakingRewards2.totalSupply() >= stakingRewards2.balanceOf(urn);
+ require stakingRewards.totalSupply() >= stakingRewards.balanceOf(urn);
+ // Assumption
+ require stakingRewards.totalSupply() + vatUrnsIlkUrnInk <= max_uint256;
+
+ selectFarm@withrevert(e, owner, index, farm, ref);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = owner != e.msg.sender && urnCanUrnSender != 1;
+ bool revert4 = urnAuctions > 0;
+ bool revert5 = farm != 0 && farmsFarm != LockstakeEngine.FarmStatus.ACTIVE;
+ bool revert6 = farm == prevFarm;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5 || revert6, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting lock
+rule lock(address owner, uint256 index, uint256 wad, uint16 ref) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ address voteDelegate_ = urnVoteDelegates(urn);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address farm = urnFarms(urn);
+ require farm == 0 || farm == stakingRewards;
+
+ require e.msg.sender != voteDelegate_ && e.msg.sender != currentContract;
+
+ address other;
+ require other != e.msg.sender && other != currentContract && other != voteDelegate_;
+ address other2;
+ require other2 != urn && other2 != farm;
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInkBefore; mathint a;
+ vatUrnsIlkUrnInkBefore, a = vat.urns(ilk, urn);
+ mathint mkrBalanceOfSenderBefore = mkr.balanceOf(e.msg.sender);
+ mathint mkrBalanceOfEngineBefore = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfVoteDelegateBefore = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfOtherBefore = mkr.balanceOf(other);
+ mathint lsmkrTotalSupplyBefore = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUrnBefore = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfFarmBefore = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfOtherBefore = lsmkr.balanceOf(other2);
+
+ // Tokens invariants
+ require to_mathint(mkr.totalSupply()) >= mkrBalanceOfSenderBefore + mkrBalanceOfEngineBefore + mkrBalanceOfVoteDelegateBefore + mkrBalanceOfOtherBefore;
+ require lsmkrTotalSupplyBefore >= lsmkrBalanceOfUrnBefore + lsmkrBalanceOfFarmBefore + lsmkrBalanceOfOtherBefore;
+
+ lock(e, owner, index, wad, ref);
+
+ mathint vatUrnsIlkUrnInkAfter;
+ vatUrnsIlkUrnInkAfter, a = vat.urns(ilk, urn);
+ mathint mkrBalanceOfSenderAfter = mkr.balanceOf(e.msg.sender);
+ mathint mkrBalanceOfVoteDelegateAfter = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfEngineAfter = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfOtherAfter = mkr.balanceOf(other);
+ mathint lsmkrTotalSupplyAfter = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfFarmAfter = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfUrnAfter = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfOtherAfter = lsmkr.balanceOf(other2);
+
+ assert vatUrnsIlkUrnInkAfter == vatUrnsIlkUrnInkBefore + wad, "Assert 1";
+ assert mkrBalanceOfSenderAfter == mkrBalanceOfSenderBefore - wad, "Assert 2";
+ assert voteDelegate_ == 0 => mkrBalanceOfVoteDelegateAfter == mkrBalanceOfVoteDelegateBefore, "Assert 3";
+ assert voteDelegate_ != 0 => mkrBalanceOfVoteDelegateAfter == mkrBalanceOfVoteDelegateBefore + wad, "Assert 4";
+ assert voteDelegate_ == 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore + wad, "Assert 5";
+ assert voteDelegate_ != 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore, "Assert 6";
+ assert mkrBalanceOfOtherAfter == mkrBalanceOfOtherBefore, "Assert 7";
+ assert lsmkrTotalSupplyAfter == lsmkrTotalSupplyBefore + wad, "Assert 8";
+ assert farm == 0 => lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore, "Assert 9";
+ assert farm != 0 => lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore + wad, "Assert 10";
+ assert farm == 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore + wad, "Assert 11";
+ assert farm != 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore, "Assert 12";
+ assert lsmkrBalanceOfOtherAfter == lsmkrBalanceOfOtherBefore, "Assert 13";
+}
+
+// Verify revert rules on lock
+rule lock_revert(address owner, uint256 index, uint256 wad, uint16 ref) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ address voteDelegate_ = urnVoteDelegates(urn);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address farm = urnFarms(urn);
+ require farm == 0 || farm == stakingRewards;
+
+ require e.msg.sender != voteDelegate_ && e.msg.sender != currentContract;
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInk; mathint vatUrnsIlkUrnArt; mathint vatIlksIlkArt; mathint vatIlksIlkRate; mathint vatIlksIlkSpot; mathint vatIlksIlkDust; mathint a;
+ vatUrnsIlkUrnInk, vatUrnsIlkUrnArt = vat.urns(ilk, urn);
+ vatIlksIlkArt, vatIlksIlkRate, vatIlksIlkSpot, a, vatIlksIlkDust = vat.ilks(ilk);
+
+ // Happening in urn init
+ require vat.can(urn, currentContract) == 1;
+ // Happening in deploy scripts
+ require vat.wards(currentContract) == 1;
+ require lsmkr.wards(currentContract) == 1;
+ // User balance and approval
+ require mkr.balanceOf(e.msg.sender) >= wad && mkr.allowance(e.msg.sender, currentContract) >= wad;
+ // Tokens invariants
+ require to_mathint(mkr.totalSupply()) >= mkr.balanceOf(e.msg.sender) + mkr.balanceOf(currentContract) + mkr.balanceOf(voteDelegate_);
+ require to_mathint(lsmkr.totalSupply()) >= lsmkr.balanceOf(urn) + lsmkr.balanceOf(farm);
+ // TODO: this might be nice to prove in some sort
+ require mkr.balanceOf(voteDelegate_) >= voteDelegate.stake(currentContract);
+ require stakingRewards.totalSupply() == stakingRewards.balanceOf(urn);
+ require lsmkr.balanceOf(farm) == stakingRewards.totalSupply();
+ require lsmkr.totalSupply() + wad <= to_mathint(mkr.totalSupply());
+ // Practical Vat assumptions
+ require vat.live() == 1;
+ require vatIlksIlkRate >= RAY() && vatIlksIlkRate <= max_int256();
+ require (vatUrnsIlkUrnInk + wad) * vatIlksIlkSpot <= max_uint256;
+ require vatIlksIlkRate * vatIlksIlkArt <= max_uint256;
+ require vatIlksIlkArt >= vatUrnsIlkUrnArt;
+ require vatUrnsIlkUrnArt == 0 || vatIlksIlkRate * vatUrnsIlkUrnArt >= vatIlksIlkDust;
+ // Safe to assume as Engine doesn't modify vat.gem(ilk,urn) (rule vatGemKeepsUnchanged)
+ require vat.gem(ilk, urn) == 0;
+ // Safe to assume as Engine keeps the invariant (rule inkMatchesLsmkrFarm)
+ require lsmkr.balanceOf(urn) == 0 || stakingRewards.balanceOf(urn) == 0;
+ require vatUrnsIlkUrnInk == lsmkr.balanceOf(urn) + stakingRewards.balanceOf(urn);
+
+ LockstakeEngine.FarmStatus farmsFarm = farms(farm);
+
+ lock@withrevert(e, owner, index, wad, ref);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = to_mathint(wad) > max_int256();
+ bool revert4 = farm != 0 && farmsFarm != LockstakeEngine.FarmStatus.ACTIVE;
+ bool revert5 = farm != 0 && wad == 0;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting lockSky
+rule lockSky(address owner, uint256 index, uint256 skyWad, uint16 ref) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ address voteDelegate_ = urnVoteDelegates(urn);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address farm = urnFarms(urn);
+ require farm == 0 || farm == stakingRewards;
+
+ require e.msg.sender != voteDelegate_ && e.msg.sender != currentContract;
+
+ address other;
+ require other != e.msg.sender && other != currentContract && other != voteDelegate_;
+ address other2;
+ require other2 != urn && other2 != farm;
+
+ mathint mkrSkyRate = mkrSkyRate();
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInkBefore; mathint a;
+ vatUrnsIlkUrnInkBefore, a = vat.urns(ilk, urn);
+ mathint skyTotalSupplyBefore = sky.totalSupply();
+ mathint skyBalanceOfSenderBefore = sky.balanceOf(e.msg.sender);
+ mathint mkrTotalSupplyBefore = mkr.totalSupply();
+ mathint mkrBalanceOfSenderBefore = mkr.balanceOf(e.msg.sender);
+ mathint mkrBalanceOfEngineBefore = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfVoteDelegateBefore = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfOtherBefore = mkr.balanceOf(other);
+ mathint lsmkrTotalSupplyBefore = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUrnBefore = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfFarmBefore = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfOtherBefore = lsmkr.balanceOf(other2);
+
+ // Happening in constructor
+ require mkrSkyRate == to_mathint(mkrSky.rate());
+ // Tokens invariants
+ require skyTotalSupplyBefore >= skyBalanceOfSenderBefore + sky.balanceOf(currentContract) + sky.balanceOf(mkrSky);
+ require mkrTotalSupplyBefore >= mkrBalanceOfSenderBefore + mkrBalanceOfEngineBefore + mkrBalanceOfVoteDelegateBefore + mkrBalanceOfOtherBefore;
+ require lsmkrTotalSupplyBefore >= lsmkrBalanceOfUrnBefore + lsmkrBalanceOfFarmBefore + lsmkrBalanceOfOtherBefore;
+
+ lockSky(e, owner, index, skyWad, ref);
+
+ mathint vatUrnsIlkUrnInkAfter;
+ vatUrnsIlkUrnInkAfter, a = vat.urns(ilk, urn);
+ mathint skyTotalSupplyAfter = sky.totalSupply();
+ mathint skyBalanceOfSenderAfter = sky.balanceOf(e.msg.sender);
+ mathint mkrTotalSupplyAfter = mkr.totalSupply();
+ mathint mkrBalanceOfSenderAfter = mkr.balanceOf(e.msg.sender);
+ mathint mkrBalanceOfVoteDelegateAfter = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfEngineAfter = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfOtherAfter = mkr.balanceOf(other);
+ mathint lsmkrTotalSupplyAfter = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfFarmAfter = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfUrnAfter = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfOtherAfter = lsmkr.balanceOf(other2);
+
+ assert vatUrnsIlkUrnInkAfter == vatUrnsIlkUrnInkBefore + skyWad/mkrSkyRate, "Assert 1";
+ assert skyTotalSupplyAfter == skyTotalSupplyBefore - skyWad, "Assert 2";
+ assert skyBalanceOfSenderAfter == skyBalanceOfSenderBefore - skyWad, "Assert 3";
+ assert mkrTotalSupplyAfter == mkrTotalSupplyBefore + skyWad/mkrSkyRate, "Assert 4";
+ assert voteDelegate_ == 0 => mkrBalanceOfVoteDelegateAfter == mkrBalanceOfVoteDelegateBefore, "Assert 5";
+ assert voteDelegate_ != 0 => mkrBalanceOfVoteDelegateAfter == mkrBalanceOfVoteDelegateBefore + skyWad/mkrSkyRate, "Assert 6";
+ assert voteDelegate_ == 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore + skyWad/mkrSkyRate, "Assert 7";
+ assert voteDelegate_ != 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore, "Assert 8";
+ assert mkrBalanceOfOtherAfter == mkrBalanceOfOtherBefore, "Assert 9";
+ assert lsmkrTotalSupplyAfter == lsmkrTotalSupplyBefore + skyWad/mkrSkyRate, "Assert 10";
+ assert farm == 0 => lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore, "Assert 11";
+ assert farm != 0 => lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore + skyWad/mkrSkyRate, "Assert 12";
+ assert farm == 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore + skyWad/mkrSkyRate, "Assert 13";
+ assert farm != 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore, "Assert 14";
+ assert lsmkrBalanceOfOtherAfter == lsmkrBalanceOfOtherBefore, "Assert 15";
+}
+
+// Verify revert rules on lockSky
+rule lockSky_revert(address owner, uint256 index, uint256 skyWad, uint16 ref) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ address voteDelegate_ = urnVoteDelegates(urn);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address farm = urnFarms(urn);
+ require farm == 0 || farm == stakingRewards;
+
+ require e.msg.sender != voteDelegate_ && e.msg.sender != currentContract;
+
+ mathint mkrSkyRate = mkrSkyRate();
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInk; mathint vatUrnsIlkUrnArt; mathint vatIlksIlkArt; mathint vatIlksIlkRate; mathint vatIlksIlkSpot; mathint vatIlksIlkDust; mathint a;
+ vatUrnsIlkUrnInk, vatUrnsIlkUrnArt = vat.urns(ilk, urn);
+ vatIlksIlkArt, vatIlksIlkRate, vatIlksIlkSpot, a, vatIlksIlkDust = vat.ilks(ilk);
+
+ // Happening in constructor
+ require mkrSkyRate == to_mathint(mkrSky.rate());
+ // Avoid division by zero
+ require mkrSkyRate > 0;
+ // Happening in urn init
+ require vat.can(urn, currentContract) == 1;
+ require sky.allowance(currentContract, mkrSky) == max_uint256;
+ // Happening in deploy scripts
+ require vat.wards(currentContract) == 1;
+ require lsmkr.wards(currentContract) == 1;
+ // User balance and approval
+ require sky.balanceOf(e.msg.sender) >= skyWad && sky.allowance(e.msg.sender, currentContract) >= skyWad;
+ // Tokens invariants
+ require to_mathint(sky.totalSupply()) >= sky.balanceOf(e.msg.sender) + sky.balanceOf(currentContract) + sky.balanceOf(mkrSky);
+ require to_mathint(mkr.totalSupply()) >= mkr.balanceOf(e.msg.sender) + mkr.balanceOf(currentContract) + mkr.balanceOf(voteDelegate_);
+ require to_mathint(lsmkr.totalSupply()) >= lsmkr.balanceOf(urn) + lsmkr.balanceOf(farm);
+ // Assumption
+ require to_mathint(mkr.totalSupply()) <= max_uint256 - skyWad/mkrSkyRate;
+ // TODO: this might be nice to prove in some sort
+ require mkr.balanceOf(voteDelegate_) >= voteDelegate.stake(currentContract);
+ require stakingRewards.totalSupply() == stakingRewards.balanceOf(urn);
+ require lsmkr.balanceOf(farm) == stakingRewards.totalSupply();
+ require lsmkr.totalSupply() + skyWad/mkrSkyRate <= to_mathint(mkr.totalSupply());
+ // Practical Vat assumptions
+ require vat.live() == 1;
+ require vatIlksIlkRate >= RAY() && vatIlksIlkRate <= max_int256();
+ require (vatUrnsIlkUrnInk + skyWad/mkrSkyRate) * vatIlksIlkSpot <= max_uint256;
+ require vatIlksIlkRate * vatIlksIlkArt <= max_uint256;
+ require vatIlksIlkArt >= vatUrnsIlkUrnArt;
+ require vatUrnsIlkUrnArt == 0 || vatIlksIlkRate * vatUrnsIlkUrnArt >= vatIlksIlkDust;
+ // Safe to assume as Engine doesn't modify vat.gem(ilk,urn) (rule vatGemKeepsUnchanged)
+ require vat.gem(ilk, urn) == 0;
+ // Safe to assume as Engine keeps the invariant (rule vatUrnsIlkUrnInkMatchesLsmkrFarm)
+ require lsmkr.balanceOf(urn) == 0 || stakingRewards.balanceOf(urn) == 0;
+ require vatUrnsIlkUrnInk == lsmkr.balanceOf(urn) + stakingRewards.balanceOf(urn);
+
+ LockstakeEngine.FarmStatus farmsFarm = farms(farm);
+
+ lockSky@withrevert(e, owner, index, skyWad, ref);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = skyWad/mkrSkyRate > max_int256();
+ bool revert4 = farm != 0 && farmsFarm != LockstakeEngine.FarmStatus.ACTIVE;
+ bool revert5 = farm != 0 && skyWad/mkrSkyRate == 0;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting free
+rule free(address owner, uint256 index, address to, uint256 wad) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ address voteDelegate_ = urnVoteDelegates(urn);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address farm = urnFarms(urn);
+ require farm == 0 || farm == stakingRewards;
+
+ address other;
+ require other != to && other != currentContract && other != voteDelegate_;
+ address other2;
+ require other2 != urn && other2 != farm;
+
+ mathint fee = fee();
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInkBefore; mathint a;
+ vatUrnsIlkUrnInkBefore, a = vat.urns(ilk, urn);
+ mathint mkrTotalSupplyBefore = mkr.totalSupply();
+ mathint mkrBalanceOfToBefore = mkr.balanceOf(to);
+ mathint mkrBalanceOfEngineBefore = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfVoteDelegateBefore = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfOtherBefore = mkr.balanceOf(other);
+ mathint lsmkrTotalSupplyBefore = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUrnBefore = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfFarmBefore = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfOtherBefore = lsmkr.balanceOf(other2);
+
+ // Happening in constructor
+ require fee < WAD();
+ // Tokens invariants
+ require mkrTotalSupplyBefore >= mkrBalanceOfToBefore + mkrBalanceOfEngineBefore + mkrBalanceOfVoteDelegateBefore + mkrBalanceOfOtherBefore;
+ require lsmkrTotalSupplyBefore >= lsmkrBalanceOfUrnBefore + lsmkrBalanceOfFarmBefore + lsmkrBalanceOfOtherBefore;
+
+ free(e, owner, index, to, wad);
+
+ mathint vatUrnsIlkUrnInkAfter;
+ vatUrnsIlkUrnInkAfter, a = vat.urns(ilk, urn);
+ mathint mkrTotalSupplyAfter = mkr.totalSupply();
+ mathint mkrBalanceOfToAfter = mkr.balanceOf(to);
+ mathint mkrBalanceOfVoteDelegateAfter = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfEngineAfter = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfOtherAfter = mkr.balanceOf(other);
+ mathint lsmkrTotalSupplyAfter = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfFarmAfter = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfUrnAfter = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfOtherAfter = lsmkr.balanceOf(other2);
+
+ assert vatUrnsIlkUrnInkAfter == vatUrnsIlkUrnInkBefore - wad, "Assert 1";
+ assert mkrTotalSupplyAfter == mkrTotalSupplyBefore - wad * fee / WAD(), "Assert 2";
+ assert to != currentContract && to != voteDelegate_ ||
+ to == currentContract && voteDelegate_ != 0 ||
+ to == voteDelegate_ && voteDelegate_ == 0 => mkrBalanceOfToAfter == mkrBalanceOfToBefore + (wad - wad * fee / WAD()), "Assert 3";
+ assert to == currentContract && voteDelegate_ == 0 ||
+ to == voteDelegate_ && voteDelegate_ != 0 => mkrBalanceOfToAfter == mkrBalanceOfToBefore - wad * fee / WAD(), "Assert 4";
+ assert to != voteDelegate_ && voteDelegate_ == 0 => mkrBalanceOfVoteDelegateAfter == mkrBalanceOfVoteDelegateBefore, "Assert 5";
+ assert to != voteDelegate_ && voteDelegate_ != 0 => mkrBalanceOfVoteDelegateAfter == mkrBalanceOfVoteDelegateBefore - wad, "Assert 6";
+ assert to != currentContract && voteDelegate_ == 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore - wad, "Assert 7";
+ assert to != currentContract && voteDelegate_ != 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore, "Assert 8";
+ assert mkrBalanceOfOtherAfter == mkrBalanceOfOtherBefore, "Assert 9";
+ assert lsmkrTotalSupplyAfter == lsmkrTotalSupplyBefore - wad, "Assert 10";
+ assert farm == 0 => lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore, "Assert 11";
+ assert farm != 0 => lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore - wad, "Assert 12";
+ assert farm == 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore - wad, "Assert 13";
+ assert farm != 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore, "Assert 14";
+ assert lsmkrBalanceOfOtherAfter == lsmkrBalanceOfOtherBefore, "Assert 15";
+}
+
+// Verify revert rules on free
+rule free_revert(address owner, uint256 index, address to, uint256 wad) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ address voteDelegate_ = urnVoteDelegates(urn);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address farm = urnFarms(urn);
+ require farm == 0 || farm == stakingRewards;
+
+ require e.msg.sender != voteDelegate_ && e.msg.sender != currentContract;
+
+ mathint fee = fee();
+ mathint urnCanUrnSender = urnCan(urn, e.msg.sender);
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInk; mathint vatUrnsIlkUrnArt; mathint vatIlksIlkArt; mathint vatIlksIlkRate; mathint vatIlksIlkSpot; mathint vatIlksIlkDust; mathint a;
+ vatUrnsIlkUrnInk, vatUrnsIlkUrnArt = vat.urns(ilk, urn);
+ vatIlksIlkArt, vatIlksIlkRate, vatIlksIlkSpot, a, vatIlksIlkDust = vat.ilks(ilk);
+
+ // Hapenning in constructor
+ require fee < WAD();
+ // Happening in urn init
+ require vat.can(urn, currentContract) == 1;
+ require lsmkr.allowance(urn, currentContract) == max_uint256;
+ // Happening in deploy scripts
+ require vat.wards(currentContract) == 1;
+ require lsmkr.wards(currentContract) == 1;
+ // Tokens invariants
+ require to_mathint(mkr.totalSupply()) >= mkr.balanceOf(e.msg.sender) + mkr.balanceOf(currentContract) + mkr.balanceOf(voteDelegate_);
+ require to_mathint(lsmkr.totalSupply()) >= lsmkr.balanceOf(urn) + lsmkr.balanceOf(farm);
+ // TODO: this might be nice to prove in some sort
+ require mkr.balanceOf(voteDelegate_) >= voteDelegate.stake(currentContract);
+ require voteDelegate_ != 0 => to_mathint(voteDelegate.stake(currentContract)) >= vatUrnsIlkUrnInk;
+ require voteDelegate_ == 0 => to_mathint(mkr.balanceOf(currentContract)) >= vatUrnsIlkUrnInk;
+ require stakingRewards.totalSupply() == stakingRewards.balanceOf(urn);
+ require lsmkr.balanceOf(farm) == stakingRewards.totalSupply();
+ // Practical Vat assumptions
+ require vat.live() == 1;
+ require vatIlksIlkRate >= RAY() && vatIlksIlkRate <= max_int256();
+ require (vatUrnsIlkUrnInk - wad) * vatIlksIlkSpot <= max_uint256;
+ require vatIlksIlkRate * vatIlksIlkArt <= max_uint256;
+ require vatIlksIlkArt >= vatUrnsIlkUrnArt;
+ require vatUrnsIlkUrnArt == 0 || vatIlksIlkRate * vatUrnsIlkUrnArt >= vatIlksIlkDust;
+ // Safe to assume as Engine doesn't modify vat.gem(ilk,urn) (rule vatGemKeepsUnchanged)
+ require vat.gem(ilk, urn) == 0;
+ // Safe to assume as Engine keeps the invariant (rule inkMatchesLsmkrFarm)
+ require lsmkr.balanceOf(urn) == 0 || stakingRewards.balanceOf(urn) == 0;
+ require lsmkr.balanceOf(urn) > 0 => farm == 0;
+ require stakingRewards.balanceOf(urn) > 0 => farm != 0;
+ require vatUrnsIlkUrnInk == lsmkr.balanceOf(urn) + stakingRewards.balanceOf(urn);
+
+ free@withrevert(e, owner, index, to, wad);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = owner != e.msg.sender && urnCanUrnSender != 1;
+ bool revert4 = to_mathint(wad) > max_int256();
+ bool revert5 = vatUrnsIlkUrnInk < to_mathint(wad) || wad > 0 && (vatUrnsIlkUrnInk - wad) * vatIlksIlkSpot < vatUrnsIlkUrnArt * vatIlksIlkRate;
+ bool revert6 = farm != 0 && wad == 0;
+ bool revert7 = wad * fee > max_uint256;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5 || revert6 ||
+ revert7, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting freeSky
+rule freeSky(address owner, uint256 index, address to, uint256 skyWad) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ address voteDelegate_ = urnVoteDelegates(urn);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address farm = urnFarms(urn);
+ require farm == 0 || farm == stakingRewards;
+
+ address other;
+ require other != currentContract && other != voteDelegate_;
+ address other2;
+ require other2 != urn && other2 != farm;
+ address other3;
+ require other3 != to;
+
+ mathint mkrSkyRate = mkrSkyRate();
+ mathint fee = fee();
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInkBefore; mathint a;
+ vatUrnsIlkUrnInkBefore, a = vat.urns(ilk, urn);
+ mathint skyTotalSupplyBefore = sky.totalSupply();
+ mathint skyBalanceOfToBefore = sky.balanceOf(to);
+ mathint skyBalanceOfOtherBefore = sky.balanceOf(other3);
+ mathint mkrTotalSupplyBefore = mkr.totalSupply();
+ mathint mkrBalanceOfEngineBefore = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfVoteDelegateBefore = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfOtherBefore = mkr.balanceOf(other);
+ mathint lsmkrTotalSupplyBefore = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUrnBefore = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfFarmBefore = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfOtherBefore = lsmkr.balanceOf(other2);
+
+ // Happening in constructor
+ require mkrSkyRate == to_mathint(mkrSky.rate());
+ require fee < WAD();
+ // Tokens invariants
+ require skyTotalSupplyBefore >= skyBalanceOfToBefore + skyBalanceOfOtherBefore;
+ require mkrTotalSupplyBefore >= mkrBalanceOfEngineBefore + mkrBalanceOfVoteDelegateBefore + mkrBalanceOfOtherBefore;
+ require lsmkrTotalSupplyBefore >= lsmkrBalanceOfUrnBefore + lsmkrBalanceOfFarmBefore + lsmkrBalanceOfOtherBefore;
+
+ freeSky(e, owner, index, to, skyWad);
+
+ mathint vatUrnsIlkUrnInkAfter;
+ vatUrnsIlkUrnInkAfter, a = vat.urns(ilk, urn);
+ mathint skyTotalSupplyAfter = sky.totalSupply();
+ mathint skyBalanceOfToAfter = sky.balanceOf(to);
+ mathint skyBalanceOfOtherAfter = sky.balanceOf(other3);
+ mathint mkrTotalSupplyAfter = mkr.totalSupply();
+ mathint mkrBalanceOfVoteDelegateAfter = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfEngineAfter = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfOtherAfter = mkr.balanceOf(other);
+ mathint lsmkrTotalSupplyAfter = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfFarmAfter = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfUrnAfter = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfOtherAfter = lsmkr.balanceOf(other2);
+
+ assert vatUrnsIlkUrnInkAfter == vatUrnsIlkUrnInkBefore - skyWad/mkrSkyRate, "Assert 1";
+ assert skyTotalSupplyAfter == skyTotalSupplyBefore + (skyWad/mkrSkyRate - skyWad/mkrSkyRate * fee / WAD()) * mkrSkyRate, "Assert 2";
+ assert skyBalanceOfToAfter == skyBalanceOfToBefore + (skyWad/mkrSkyRate - skyWad/mkrSkyRate * fee / WAD()) * mkrSkyRate, "Assert 3";
+ assert skyBalanceOfOtherAfter == skyBalanceOfOtherBefore, "Assert 4";
+ assert mkrTotalSupplyAfter == mkrTotalSupplyBefore - skyWad/mkrSkyRate, "Assert 5";
+ assert to != voteDelegate_ && voteDelegate_ == 0 => mkrBalanceOfVoteDelegateAfter == mkrBalanceOfVoteDelegateBefore, "Assert 6";
+ assert to != voteDelegate_ && voteDelegate_ != 0 => mkrBalanceOfVoteDelegateAfter == mkrBalanceOfVoteDelegateBefore - skyWad/mkrSkyRate, "Assert 7";
+ assert to != currentContract && voteDelegate_ == 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore - skyWad/mkrSkyRate, "Assert 8";
+ assert to != currentContract && voteDelegate_ != 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore, "Assert 9";
+ assert mkrBalanceOfOtherAfter == mkrBalanceOfOtherBefore, "Assert 10";
+ assert lsmkrTotalSupplyAfter == lsmkrTotalSupplyBefore - skyWad/mkrSkyRate, "Assert 11";
+ assert farm == 0 => lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore, "Assert 12";
+ assert farm != 0 => lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore - skyWad/mkrSkyRate, "Assert 13";
+ assert farm == 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore - skyWad/mkrSkyRate, "Assert 14";
+ assert farm != 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore, "Assert 15";
+ assert lsmkrBalanceOfOtherAfter == lsmkrBalanceOfOtherBefore, "Assert 16";
+}
+
+// Verify revert rules on freeSky
+rule freeSky_revert(address owner, uint256 index, address to, uint256 skyWad) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ address voteDelegate_ = urnVoteDelegates(urn);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address farm = urnFarms(urn);
+ require farm == 0 || farm == stakingRewards;
+
+ require e.msg.sender != voteDelegate_ && e.msg.sender != currentContract;
+
+ mathint urnCanUrnSender = urnCan(urn, e.msg.sender);
+
+ mathint mkrSkyRate = mkrSkyRate();
+ mathint fee = fee();
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInk; mathint vatUrnsIlkUrnArt; mathint vatIlksIlkArt; mathint vatIlksIlkRate; mathint vatIlksIlkSpot; mathint vatIlksIlkDust; mathint a;
+ vatUrnsIlkUrnInk, vatUrnsIlkUrnArt = vat.urns(ilk, urn);
+ vatIlksIlkArt, vatIlksIlkRate, vatIlksIlkSpot, a, vatIlksIlkDust = vat.ilks(ilk);
+
+ // Happening in constructor
+ require mkrSkyRate == to_mathint(mkrSky.rate());
+ require fee < WAD();
+ require mkr.allowance(currentContract, mkrSky) == max_uint256;
+ // Avoid division by zero
+ require mkrSkyRate > 0;
+ // Happening in urn init
+ require vat.can(urn, currentContract) == 1;
+ require lsmkr.allowance(urn, currentContract) == max_uint256;
+ // Happening in deploy scripts
+ require vat.wards(currentContract) == 1;
+ require lsmkr.wards(currentContract) == 1;
+ // Tokens invariants
+ require sky.totalSupply() >= sky.balanceOf(to);
+ require to_mathint(mkr.totalSupply()) >= mkr.balanceOf(e.msg.sender) + mkr.balanceOf(currentContract) + mkr.balanceOf(voteDelegate_);
+ require to_mathint(lsmkr.totalSupply()) >= lsmkr.balanceOf(urn) + lsmkr.balanceOf(farm);
+ // Practical assumption
+ require sky.totalSupply() + skyWad <= max_uint256;
+ // TODO: this might be nice to prove in some sort
+ require mkr.balanceOf(voteDelegate_) >= voteDelegate.stake(currentContract);
+ require voteDelegate_ != 0 => to_mathint(voteDelegate.stake(currentContract)) >= vatUrnsIlkUrnInk;
+ require voteDelegate_ == 0 => to_mathint(mkr.balanceOf(currentContract)) >= vatUrnsIlkUrnInk;
+ require stakingRewards.totalSupply() == stakingRewards.balanceOf(urn);
+ require lsmkr.balanceOf(farm) == stakingRewards.totalSupply();
+ // Practical Vat assumptions
+ require vat.live() == 1;
+ require vatIlksIlkRate >= RAY() && vatIlksIlkRate <= max_int256();
+ require (vatUrnsIlkUrnInk - skyWad/mkrSkyRate) * vatIlksIlkSpot <= max_uint256;
+ require vatIlksIlkRate * vatIlksIlkArt <= max_uint256;
+ require vatIlksIlkArt >= vatUrnsIlkUrnArt;
+ require vatUrnsIlkUrnArt == 0 || vatIlksIlkRate * vatUrnsIlkUrnArt >= vatIlksIlkDust;
+ // Safe to assume as Engine doesn't modify vat.gem(ilk,urn) (rule vatGemKeepsUnchanged)
+ require vat.gem(ilk, urn) == 0;
+ // Safe to assume as Engine keeps the invariant (rule inkMatchesLsmkrFarm)
+ require lsmkr.balanceOf(urn) == 0 || stakingRewards.balanceOf(urn) == 0;
+ require lsmkr.balanceOf(urn) > 0 => farm == 0;
+ require stakingRewards.balanceOf(urn) > 0 => farm != 0;
+ require vatUrnsIlkUrnInk == lsmkr.balanceOf(urn) + stakingRewards.balanceOf(urn);
+
+ freeSky@withrevert(e, owner, index, to, skyWad);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = owner != e.msg.sender && urnCanUrnSender != 1;
+ bool revert4 = to_mathint(skyWad/mkrSkyRate) > max_int256();
+ bool revert5 = vatUrnsIlkUrnInk < to_mathint(skyWad/mkrSkyRate) || skyWad/mkrSkyRate > 0 && (vatUrnsIlkUrnInk - skyWad/mkrSkyRate) * vatIlksIlkSpot < vatUrnsIlkUrnArt * vatIlksIlkRate;
+ bool revert6 = farm != 0 && skyWad/mkrSkyRate == 0;
+ bool revert7 = skyWad/mkrSkyRate * fee > max_uint256;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5 || revert6 ||
+ revert7, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting freeNoFee
+rule freeNoFee(address owner, uint256 index, address to, uint256 wad) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ address voteDelegate_ = urnVoteDelegates(urn);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address farm = urnFarms(urn);
+ require farm == 0 || farm == stakingRewards;
+
+ address other;
+ require other != to && other != currentContract && other != voteDelegate_;
+ address other2;
+ require other2 != urn && other2 != farm;
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInkBefore; mathint a;
+ vatUrnsIlkUrnInkBefore, a = vat.urns(ilk, urn);
+ mathint mkrTotalSupplyBefore = mkr.totalSupply();
+ mathint mkrBalanceOfToBefore = mkr.balanceOf(to);
+ mathint mkrBalanceOfEngineBefore = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfVoteDelegateBefore = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfOtherBefore = mkr.balanceOf(other);
+ mathint lsmkrTotalSupplyBefore = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUrnBefore = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfFarmBefore = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfOtherBefore = lsmkr.balanceOf(other2);
+
+ // Tokens invariants
+ require mkrTotalSupplyBefore >= mkrBalanceOfToBefore + mkrBalanceOfEngineBefore + mkrBalanceOfVoteDelegateBefore + mkrBalanceOfOtherBefore;
+ require lsmkrTotalSupplyBefore >= lsmkrBalanceOfUrnBefore + lsmkrBalanceOfFarmBefore + lsmkrBalanceOfOtherBefore;
+
+ freeNoFee(e, owner, index, to, wad);
+
+ mathint vatUrnsIlkUrnInkAfter;
+ vatUrnsIlkUrnInkAfter, a = vat.urns(ilk, urn);
+ mathint mkrTotalSupplyAfter = mkr.totalSupply();
+ mathint mkrBalanceOfToAfter = mkr.balanceOf(to);
+ mathint mkrBalanceOfVoteDelegateAfter = mkr.balanceOf(voteDelegate_);
+ mathint mkrBalanceOfEngineAfter = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfOtherAfter = mkr.balanceOf(other);
+ mathint lsmkrTotalSupplyAfter = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfFarmAfter = lsmkr.balanceOf(farm);
+ mathint lsmkrBalanceOfUrnAfter = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfOtherAfter = lsmkr.balanceOf(other2);
+
+ assert vatUrnsIlkUrnInkAfter == vatUrnsIlkUrnInkBefore - wad, "Assert 1";
+ assert mkrTotalSupplyAfter == mkrTotalSupplyBefore, "Assert 2";
+ assert to != currentContract && to != voteDelegate_ ||
+ to == currentContract && voteDelegate_ != 0 ||
+ to == voteDelegate_ && voteDelegate_ == 0 => mkrBalanceOfToAfter == mkrBalanceOfToBefore + wad, "Assert 3";
+ assert to == currentContract && voteDelegate_ == 0 ||
+ to == voteDelegate_ && voteDelegate_ != 0 => mkrBalanceOfToAfter == mkrBalanceOfToBefore, "Assert 4";
+ assert to != voteDelegate_ && voteDelegate_ == 0 => mkrBalanceOfVoteDelegateAfter == mkrBalanceOfVoteDelegateBefore, "Assert 5";
+ assert to != voteDelegate_ && voteDelegate_ != 0 => mkrBalanceOfVoteDelegateAfter == mkrBalanceOfVoteDelegateBefore - wad, "Assert 6";
+ assert to != currentContract && voteDelegate_ == 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore - wad, "Assert 7";
+ assert to != currentContract && voteDelegate_ != 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore, "Assert 8";
+ assert mkrBalanceOfOtherAfter == mkrBalanceOfOtherBefore, "Assert 9";
+ assert lsmkrTotalSupplyAfter == lsmkrTotalSupplyBefore - wad, "Assert 10";
+ assert farm == 0 => lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore, "Assert 11";
+ assert farm != 0 => lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore - wad, "Assert 12";
+ assert farm == 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore - wad, "Assert 13";
+ assert farm != 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore, "Assert 14";
+ assert lsmkrBalanceOfOtherAfter == lsmkrBalanceOfOtherBefore, "Assert 15";
+}
+
+// Verify revert rules on freeNoFee
+rule freeNoFee_revert(address owner, uint256 index, address to, uint256 wad) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require urn == lockstakeUrn;
+
+ mathint wardsSender = wards(e.msg.sender);
+
+ address voteDelegate_ = urnVoteDelegates(urn);
+ require voteDelegate_ == 0 || voteDelegate_ == voteDelegate;
+ address farm = urnFarms(urn);
+ require farm == 0 || farm == stakingRewards;
+
+ require e.msg.sender != voteDelegate_ && e.msg.sender != currentContract;
+
+ mathint urnCanUrnSender = urnCan(urn, e.msg.sender);
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInk; mathint vatUrnsIlkUrnArt; mathint vatIlksIlkArt; mathint vatIlksIlkRate; mathint vatIlksIlkSpot; mathint vatIlksIlkDust; mathint a;
+ vatUrnsIlkUrnInk, vatUrnsIlkUrnArt = vat.urns(ilk, urn);
+ vatIlksIlkArt, vatIlksIlkRate, vatIlksIlkSpot, a, vatIlksIlkDust = vat.ilks(ilk);
+
+ // Happening in urn init
+ require vat.can(urn, currentContract) == 1;
+ require lsmkr.allowance(urn, currentContract) == max_uint256;
+ // Happening in deploy scripts
+ require vat.wards(currentContract) == 1;
+ require lsmkr.wards(currentContract) == 1;
+ // Tokens invariants
+ require to_mathint(mkr.totalSupply()) >= mkr.balanceOf(e.msg.sender) + mkr.balanceOf(currentContract) + mkr.balanceOf(voteDelegate_);
+ require to_mathint(lsmkr.totalSupply()) >= lsmkr.balanceOf(urn) + lsmkr.balanceOf(farm);
+ // TODO: this might be nice to prove in some sort
+ require mkr.balanceOf(voteDelegate_) >= voteDelegate.stake(currentContract);
+ require voteDelegate_ != 0 => to_mathint(voteDelegate.stake(currentContract)) >= vatUrnsIlkUrnInk;
+ require voteDelegate_ == 0 => to_mathint(mkr.balanceOf(currentContract)) >= vatUrnsIlkUrnInk;
+ require stakingRewards.totalSupply() == stakingRewards.balanceOf(urn);
+ require lsmkr.balanceOf(farm) == stakingRewards.totalSupply();
+ // Practical Vat assumptions
+ require vat.live() == 1;
+ require vatIlksIlkRate >= RAY() && vatIlksIlkRate <= max_int256();
+ require (vatUrnsIlkUrnInk - wad) * vatIlksIlkSpot <= max_uint256;
+ require vatIlksIlkRate * vatIlksIlkArt <= max_uint256;
+ require vatIlksIlkArt >= vatUrnsIlkUrnArt;
+ require vatUrnsIlkUrnArt == 0 || vatIlksIlkRate * vatUrnsIlkUrnArt >= vatIlksIlkDust;
+ // Safe to assume as Engine doesn't modify vat.gem(ilk,urn) (rule vatGemKeepsUnchanged)
+ require vat.gem(ilk, urn) == 0;
+ // Safe to assume as Engine keeps the invariant (rule inkMatchesLsmkrFarm)
+ require lsmkr.balanceOf(urn) == 0 || stakingRewards.balanceOf(urn) == 0;
+ require lsmkr.balanceOf(urn) > 0 => farm == 0;
+ require stakingRewards.balanceOf(urn) > 0 => farm != 0;
+ require vatUrnsIlkUrnInk == lsmkr.balanceOf(urn) + stakingRewards.balanceOf(urn);
+
+ freeNoFee@withrevert(e, owner, index, to, wad);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+ bool revert3 = urn == 0;
+ bool revert4 = owner != e.msg.sender && urnCanUrnSender != 1;
+ bool revert5 = to_mathint(wad) > max_int256();
+ bool revert6 = vatUrnsIlkUrnInk < to_mathint(wad) || wad > 0 && (vatUrnsIlkUrnInk - wad) * vatIlksIlkSpot < vatUrnsIlkUrnArt * vatIlksIlkRate;
+ bool revert7 = farm != 0 && wad == 0;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5 || revert6 ||
+ revert7, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting draw
+rule draw(address owner, uint256 index, address to, uint256 wad) {
+ env e;
+
+ address other;
+ require other != to;
+
+ address urn = ownerUrns(owner, index);
+ bytes32 ilk = ilk();
+ mathint vatIlksIlkArtBefore; mathint a;
+ vatIlksIlkArtBefore, a, a, a, a = vat.ilks(ilk);
+ mathint vatUrnsIlkUrnArtBefore;
+ a, vatUrnsIlkUrnArtBefore = vat.urns(ilk, urn);
+ mathint usdsTotalSupplyBefore = usds.totalSupply();
+ mathint usdsBalanceOfToBefore = usds.balanceOf(to);
+ mathint usdsBalanceOfOtherBefore = usds.balanceOf(other);
+
+ // Tokens invariants
+ require usdsTotalSupplyBefore >= usdsBalanceOfToBefore + usdsBalanceOfOtherBefore;
+
+ draw(e, owner, index, to, wad);
+
+ mathint vatIlksIlkArtAfter; mathint vatIlksIlkRateAfter;
+ vatIlksIlkArtAfter, vatIlksIlkRateAfter, a, a, a = vat.ilks(ilk);
+ mathint vatUrnsIlkUrnArtAfter;
+ a, vatUrnsIlkUrnArtAfter = vat.urns(ilk, urn);
+ mathint usdsTotalSupplyAfter = usds.totalSupply();
+ mathint usdsBalanceOfToAfter = usds.balanceOf(to);
+ mathint usdsBalanceOfOtherAfter = usds.balanceOf(other);
+
+ assert vatIlksIlkArtAfter == vatIlksIlkArtBefore + _divup(wad * RAY(), vatIlksIlkRateAfter), "Assert 1";
+ assert vatUrnsIlkUrnArtAfter == vatUrnsIlkUrnArtBefore + _divup(wad * RAY(), vatIlksIlkRateAfter), "Assert 2";
+ assert usdsTotalSupplyAfter == usdsTotalSupplyBefore + wad, "Assert 3";
+ assert usdsBalanceOfToAfter == usdsBalanceOfToBefore + wad, "Assert 4";
+ assert usdsBalanceOfOtherAfter == usdsBalanceOfOtherBefore, "Assert 5";
+}
+
+// Verify revert rules on draw
+rule draw_revert(address owner, uint256 index, address to, uint256 wad) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ mathint urnCanUrnSender = urnCan(urn, e.msg.sender);
+
+ bytes32 ilk = ilk();
+ mathint vatDebt = vat.debt();
+ mathint vatLine = vat.Line();
+ mathint vatIlksIlkArt; mathint vatIlksIlkRate; mathint vatIlksIlkSpot; mathint vatIlksIlkLine; mathint vatIlksIlkDust; mathint a;
+ vatIlksIlkArt, vatIlksIlkRate, vatIlksIlkSpot, vatIlksIlkLine, vatIlksIlkDust = vat.ilks(ilk);
+ mathint vatUrnsIlkUrnInk; mathint vatUrnsIlkUrnArt;
+ vatUrnsIlkUrnInk, vatUrnsIlkUrnArt = vat.urns(ilk, urn);
+ mathint usdsTotalSupply = usds.totalSupply();
+ mathint usdsBalanceOfTo = usds.balanceOf(to);
+
+ storage init = lastStorage;
+ mathint calcVatIlksIlkRateAfter = dripSummary(ilk);
+ // Avoid division by zero
+ require calcVatIlksIlkRateAfter > 0;
+
+ mathint dart = _divup(wad * RAY(), calcVatIlksIlkRateAfter);
+
+ // Happening in constructor
+ require vat.can(currentContract, usdsJoin) == 1;
+ // Happening in urn init
+ require vat.can(urn, currentContract) == 1;
+ // Tokens invariants
+ require usdsTotalSupply >= usdsBalanceOfTo;
+ // Practical token assumtiopns
+ require usdsTotalSupply + wad <= max_uint256;
+ // Practical Vat assumptions
+ require vat.live() == 1;
+ require vat.wards(jug) == 1;
+ require calcVatIlksIlkRateAfter >= RAY() && calcVatIlksIlkRateAfter <= max_int256();
+ require vatUrnsIlkUrnInk * vatIlksIlkSpot <= max_uint256;
+ require calcVatIlksIlkRateAfter * vatIlksIlkArt <= max_uint256;
+ require vatIlksIlkArt >= vatUrnsIlkUrnArt;
+ require vatIlksIlkArt + dart <= max_uint256;
+ require calcVatIlksIlkRateAfter * dart <= max_int256();
+ require vatDebt + vatIlksIlkArt * (calcVatIlksIlkRateAfter - vatIlksIlkRate) + (calcVatIlksIlkRateAfter * dart) <= max_int256();
+ require vat.dai(currentContract) + (dart * calcVatIlksIlkRateAfter) <= max_uint256;
+ require vat.dai(usdsJoin) + (dart * calcVatIlksIlkRateAfter) <= max_uint256;
+ // Other assumptions
+ require wad * RAY() <= max_uint256;
+
+ draw@withrevert(e, owner, index, to, wad) at init;
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = owner != e.msg.sender && urnCanUrnSender != 1;
+ bool revert4 = to_mathint(dart) > max_int256();
+ bool revert5 = dart > 0 && ((vatIlksIlkArt + dart) * calcVatIlksIlkRateAfter > vatIlksIlkLine || vatDebt + vatIlksIlkArt * (calcVatIlksIlkRateAfter - vatIlksIlkRate) + (calcVatIlksIlkRateAfter * dart) > vatLine);
+ bool revert6 = dart > 0 && vatUrnsIlkUrnInk * vatIlksIlkSpot < (vatUrnsIlkUrnArt + dart) * calcVatIlksIlkRateAfter;
+ bool revert7 = vatUrnsIlkUrnArt + dart > 0 && calcVatIlksIlkRateAfter * (vatUrnsIlkUrnArt + dart) < vatIlksIlkDust;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5 || revert6 ||
+ revert7, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting wipe
+rule wipe(address owner, uint256 index, uint256 wad) {
+ env e;
+
+ address other;
+ require other != e.msg.sender;
+
+ address urn = ownerUrns(owner, index);
+ bytes32 ilk = ilk();
+ mathint vatIlksIlkArtBefore; mathint vatIlksIlkRate; mathint a;
+ vatIlksIlkArtBefore, vatIlksIlkRate, a, a, a = vat.ilks(ilk);
+ mathint vatUrnsIlkUrnArtBefore;
+ a, vatUrnsIlkUrnArtBefore = vat.urns(ilk, urn);
+ mathint usdsTotalSupplyBefore = usds.totalSupply();
+ mathint usdsBalanceOfSenderBefore = usds.balanceOf(e.msg.sender);
+ mathint usdsBalanceOfOtherBefore = usds.balanceOf(other);
+
+ // Tokens invariants
+ require usdsTotalSupplyBefore >= usdsBalanceOfSenderBefore + usdsBalanceOfOtherBefore;
+
+ wipe(e, owner, index, wad);
+
+ mathint vatIlksIlkArtAfter;
+ vatIlksIlkArtAfter, a, a, a, a = vat.ilks(ilk);
+ mathint vatUrnsIlkUrnArtAfter;
+ a, vatUrnsIlkUrnArtAfter = vat.urns(ilk, urn);
+ mathint usdsTotalSupplyAfter = usds.totalSupply();
+ mathint usdsBalanceOfSenderAfter = usds.balanceOf(e.msg.sender);
+ mathint usdsBalanceOfOtherAfter = usds.balanceOf(other);
+
+ assert vatIlksIlkArtAfter == vatIlksIlkArtBefore - wad * RAY() / vatIlksIlkRate, "Assert 1";
+ assert vatUrnsIlkUrnArtAfter == vatUrnsIlkUrnArtBefore - wad * RAY() / vatIlksIlkRate, "Assert 2";
+ assert usdsTotalSupplyAfter == usdsTotalSupplyBefore - wad, "Assert 3";
+ assert usdsBalanceOfSenderAfter == usdsBalanceOfSenderBefore - wad, "Assert 4";
+ assert usdsBalanceOfOtherAfter == usdsBalanceOfOtherBefore, "Assert 5";
+}
+
+// Verify revert rules on wipe
+rule wipe_revert(address owner, uint256 index, uint256 wad) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ bytes32 ilk = ilk();
+ mathint vatDebt = vat.debt();
+ mathint vatIlksIlkArt; mathint vatIlksIlkRate; mathint vatIlksIlkSpot; mathint vatIlksIlkLine; mathint vatIlksIlkDust; mathint a;
+ vatIlksIlkArt, vatIlksIlkRate, vatIlksIlkSpot, vatIlksIlkLine, vatIlksIlkDust = vat.ilks(ilk);
+ mathint vatUrnsIlkUrnInk; mathint vatUrnsIlkUrnArt;
+ vatUrnsIlkUrnInk, vatUrnsIlkUrnArt = vat.urns(ilk, urn);
+ mathint usdsTotalSupply = usds.totalSupply();
+ mathint usdsBalanceOfSender = usds.balanceOf(e.msg.sender);
+
+ // Avoid division by zero
+ require vatIlksIlkRate > 0;
+
+ mathint dart = wad * RAY() / vatIlksIlkRate;
+
+ // Happening in constructor
+ require usds.allowance(currentContract, usdsJoin) == max_uint256;
+ // Happening in urn init
+ require vat.can(urn, currentContract) == 1;
+ // Tokens invariants
+ require usdsTotalSupply >= usdsBalanceOfSender + usds.balanceOf(currentContract) + usds.balanceOf(usdsJoin);
+ // Practical token assumtiopns
+ require usdsBalanceOfSender >= to_mathint(wad);
+ require usds.allowance(e.msg.sender, currentContract) >= wad;
+ // Practical Vat assumptions
+ require vat.live() == 1;
+ require vat.wards(jug) == 1;
+ require vatIlksIlkRate >= RAY() && vatIlksIlkRate <= max_int256();
+ require vatUrnsIlkUrnInk * vatIlksIlkSpot <= max_uint256;
+ require vatIlksIlkRate * vatIlksIlkArt <= max_uint256;
+ require vatIlksIlkArt >= vatUrnsIlkUrnArt;
+ require vatIlksIlkRate * -dart >= min_int256();
+ require vatDebt >= vatIlksIlkRate * dart;
+ require vat.dai(currentContract) + wad * RAY() <= max_uint256;
+ require to_mathint(vat.dai(usdsJoin)) >= wad * RAY();
+ // Other assumptions
+ require wad * RAY() <= max_uint256;
+
+ wipe@withrevert(e, owner, index, wad);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = to_mathint(dart) > max_int256();
+ bool revert4 = vatUrnsIlkUrnArt < dart;
+ bool revert5 = vatUrnsIlkUrnArt - dart > 0 && vatIlksIlkRate * (vatUrnsIlkUrnArt - dart) < vatIlksIlkDust;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting wipeAll
+rule wipeAll(address owner, uint256 index) {
+ env e;
+
+ address other;
+ require other != e.msg.sender;
+
+ address urn = ownerUrns(owner, index);
+ bytes32 ilk = ilk();
+ mathint vatIlksIlkArtBefore; mathint vatIlksIlkRate; mathint a;
+ vatIlksIlkArtBefore, vatIlksIlkRate, a, a, a = vat.ilks(ilk);
+ mathint vatUrnsIlkUrnArtBefore;
+ a, vatUrnsIlkUrnArtBefore = vat.urns(ilk, urn);
+ mathint wad = _divup(vatUrnsIlkUrnArtBefore * vatIlksIlkRate, RAY());
+ mathint usdsTotalSupplyBefore = usds.totalSupply();
+ mathint usdsBalanceOfSenderBefore = usds.balanceOf(e.msg.sender);
+ mathint usdsBalanceOfOtherBefore = usds.balanceOf(other);
+
+ // Tokens invariants
+ require usdsTotalSupplyBefore >= usdsBalanceOfSenderBefore + usdsBalanceOfOtherBefore;
+
+ wipeAll(e, owner, index);
+
+ mathint vatIlksIlkArtAfter;
+ vatIlksIlkArtAfter, a, a, a, a = vat.ilks(ilk);
+ mathint vatUrnsIlkUrnArtAfter;
+ a, vatUrnsIlkUrnArtAfter = vat.urns(ilk, urn);
+ mathint usdsTotalSupplyAfter = usds.totalSupply();
+ mathint usdsBalanceOfSenderAfter = usds.balanceOf(e.msg.sender);
+ mathint usdsBalanceOfOtherAfter = usds.balanceOf(other);
+
+ assert vatIlksIlkArtAfter == vatIlksIlkArtBefore - vatUrnsIlkUrnArtBefore, "Assert 1";
+ assert vatUrnsIlkUrnArtAfter == 0, "Assert 2";
+ assert usdsTotalSupplyAfter == usdsTotalSupplyBefore - wad, "Assert 3";
+ assert usdsBalanceOfSenderAfter == usdsBalanceOfSenderBefore - wad, "Assert 4";
+ assert usdsBalanceOfOtherAfter == usdsBalanceOfOtherBefore, "Assert 5";
+}
+
+// Verify revert rules on wipeAll
+rule wipeAll_revert(address owner, uint256 index) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ bytes32 ilk = ilk();
+ mathint vatDebt = vat.debt();
+ mathint vatIlksIlkArt; mathint vatIlksIlkRate; mathint vatIlksIlkSpot; mathint vatIlksIlkLine; mathint vatIlksIlkDust; mathint a;
+ vatIlksIlkArt, vatIlksIlkRate, vatIlksIlkSpot, vatIlksIlkLine, vatIlksIlkDust = vat.ilks(ilk);
+ mathint vatUrnsIlkUrnInk; mathint vatUrnsIlkUrnArt;
+ vatUrnsIlkUrnInk, vatUrnsIlkUrnArt = vat.urns(ilk, urn);
+ mathint usdsTotalSupply = usds.totalSupply();
+ mathint usdsBalanceOfSender = usds.balanceOf(e.msg.sender);
+
+ mathint wad = _divup(vatUrnsIlkUrnArt * vatIlksIlkRate, RAY());
+
+ // Happening in constructor
+ require usds.allowance(currentContract, usdsJoin) == max_uint256;
+ // Happening in urn init
+ require vat.can(urn, currentContract) == 1;
+ // Tokens invariants
+ require usdsTotalSupply >= usdsBalanceOfSender + usds.balanceOf(currentContract) + usds.balanceOf(usdsJoin);
+ // Practical token assumtiopns
+ require usdsBalanceOfSender >= to_mathint(wad);
+ require to_mathint(usds.allowance(e.msg.sender, currentContract)) >= wad;
+ // Practical Vat assumptions
+ require vat.live() == 1;
+ require vat.wards(jug) == 1;
+ require vatIlksIlkRate >= RAY() && vatIlksIlkRate <= max_int256();
+ require vatUrnsIlkUrnInk * vatIlksIlkSpot <= max_uint256;
+ require vatIlksIlkRate * vatIlksIlkArt <= max_uint256;
+ require vatIlksIlkArt >= vatUrnsIlkUrnArt;
+ require vatIlksIlkRate * -vatUrnsIlkUrnArt >= min_int256();
+ require vatDebt >= vatIlksIlkRate * vatUrnsIlkUrnArt;
+ require vat.dai(currentContract) + wad * RAY() <= max_uint256;
+ require to_mathint(vat.dai(usdsJoin)) >= wad * RAY();
+ // Other assumptions
+ require wad * RAY() <= max_uint256;
+
+ wipeAll@withrevert(e, owner, index);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = to_mathint(vatUrnsIlkUrnArt) > max_int256();
+
+ assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting getReward
+rule getReward(address owner, uint256 index, address farm, address to) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ address other;
+ require other != to && other != urn && other != farm;
+
+ require urn == lockstakeUrn;
+ require farm == stakingRewards;
+ require stakingRewards.rewardsToken() == rewardsToken;
+
+ mathint farmRewardsUrnBefore = stakingRewards.rewards(urn);
+ mathint rewardsTokenBalanceOfToBefore = rewardsToken.balanceOf(to);
+ mathint rewardsTokenBalanceOfUrnBefore = rewardsToken.balanceOf(urn);
+ mathint rewardsTokenBalanceOfFarmBefore = rewardsToken.balanceOf(farm);
+ mathint rewardsTokenBalanceOfOtherBefore = rewardsToken.balanceOf(other);
+
+ // Tokens invariants
+ require to_mathint(rewardsToken.totalSupply()) >= rewardsTokenBalanceOfToBefore + rewardsTokenBalanceOfUrnBefore + rewardsTokenBalanceOfFarmBefore + rewardsTokenBalanceOfOtherBefore;
+
+ getReward(e, owner, index, farm, to);
+
+ mathint farmRewardsUrnAfter = stakingRewards.rewards(urn);
+ mathint rewardsTokenBalanceOfToAfter = rewardsToken.balanceOf(to);
+ mathint rewardsTokenBalanceOfUrnAfter = rewardsToken.balanceOf(urn);
+ mathint rewardsTokenBalanceOfFarmAfter = rewardsToken.balanceOf(farm);
+ mathint rewardsTokenBalanceOfOtherAfter = rewardsToken.balanceOf(other);
+
+ assert farmRewardsUrnAfter == 0, "Assert 1";
+ assert to != urn && to != farm => rewardsTokenBalanceOfToAfter == rewardsTokenBalanceOfToBefore + rewardsTokenBalanceOfUrnBefore + farmRewardsUrnBefore, "Assert 2";
+ assert to == urn => rewardsTokenBalanceOfToAfter == rewardsTokenBalanceOfToBefore + farmRewardsUrnBefore, "Assert 3";
+ assert to == farm => rewardsTokenBalanceOfToAfter == rewardsTokenBalanceOfToBefore + rewardsTokenBalanceOfUrnBefore, "Assert 4";
+ assert to != urn => rewardsTokenBalanceOfUrnAfter == 0, "Assert 5";
+ assert to != farm => rewardsTokenBalanceOfFarmAfter == rewardsTokenBalanceOfFarmBefore - farmRewardsUrnBefore, "Assert 6";
+ assert rewardsTokenBalanceOfOtherAfter == rewardsTokenBalanceOfOtherBefore, "Assert 7";
+}
+
+// Verify revert rules on getReward
+rule getReward_revert(address owner, uint256 index, address farm, address to) {
+ env e;
+
+ address urn = ownerUrns(owner, index);
+ require farm == stakingRewards;
+ require stakingRewards.rewardsToken() == rewardsToken;
+
+ mathint urnCanUrnSender = urnCan(urn, e.msg.sender);
+ LockstakeEngine.FarmStatus farmsFarm = farms(farm);
+
+ // Tokens invariants
+ require to_mathint(rewardsToken.totalSupply()) >= rewardsToken.balanceOf(to) + rewardsToken.balanceOf(urn) + rewardsToken.balanceOf(farm);
+
+ // Assumption from the farm
+ require rewardsToken.balanceOf(farm) >= stakingRewards.rewards(urn);
+
+ getReward@withrevert(e, owner, index, farm, to);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = urn == 0;
+ bool revert3 = owner != e.msg.sender && urnCanUrnSender != 1;
+ bool revert4 = farmsFarm == LockstakeEngine.FarmStatus.UNSUPPORTED;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting onKick
+rule onKick(address urn, uint256 wad) {
+ env e;
+
+ require urn == lockstakeUrn;
+ address prevVoteDelegate = urnVoteDelegates(urn);
+ require prevVoteDelegate == 0 || prevVoteDelegate == voteDelegate;
+ address prevFarm = urnFarms(urn);
+ require prevFarm == 0 || prevFarm == stakingRewards;
+
+ address other;
+ require other != urn;
+ address other2;
+ require other2 != prevVoteDelegate && other2 != currentContract;
+ address other3;
+ require other3 != prevFarm && other3 != urn;
+
+ bytes32 ilk = ilk();
+ mathint vatUrnsIlkUrnInk; mathint a;
+ vatUrnsIlkUrnInk, a = vat.urns(ilk, urn);
+
+ address urnVoteDelegatesOtherBefore = urnVoteDelegates(other);
+ address urnFarmsOtherBefore = urnFarms(other);
+ mathint urnAuctionsUrnBefore = urnAuctions(urn);
+ mathint urnAuctionsOtherBefore = urnAuctions(other);
+ mathint mkrBalanceOfPrevVoteDelegateBefore = mkr.balanceOf(prevVoteDelegate);
+ mathint mkrBalanceOfEngineBefore = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfOtherBefore = mkr.balanceOf(other2);
+ mathint lsmkrTotalSupplyBefore = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfPrevFarmBefore = lsmkr.balanceOf(prevFarm);
+ mathint lsmkrBalanceOfUrnBefore = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfOtherBefore = lsmkr.balanceOf(other3);
+
+ // Tokens invariants
+ require to_mathint(mkr.totalSupply()) >= mkrBalanceOfPrevVoteDelegateBefore + mkrBalanceOfEngineBefore + mkrBalanceOfOtherBefore;
+ require lsmkrTotalSupplyBefore >= lsmkrBalanceOfPrevFarmBefore + lsmkrBalanceOfUrnBefore + lsmkrBalanceOfOtherBefore;
+
+ onKick(e, urn, wad);
+
+ address urnVoteDelegatesUrnAfter = urnVoteDelegates(urn);
+ address urnVoteDelegatesOtherAfter = urnVoteDelegates(other);
+ address urnFarmsUrnAfter = urnFarms(urn);
+ address urnFarmsOtherAfter = urnFarms(other);
+ mathint urnAuctionsUrnAfter = urnAuctions(urn);
+ mathint urnAuctionsOtherAfter = urnAuctions(other);
+ mathint mkrBalanceOfPrevVoteDelegateAfter = mkr.balanceOf(prevVoteDelegate);
+ mathint mkrBalanceOfEngineAfter = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfOtherAfter = mkr.balanceOf(other2);
+ mathint lsmkrTotalSupplyAfter = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfPrevFarmAfter = lsmkr.balanceOf(prevFarm);
+ mathint lsmkrBalanceOfUrnAfter = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfOtherAfter = lsmkr.balanceOf(other3);
+
+ assert urnVoteDelegatesUrnAfter == 0, "Assert 1";
+ assert urnVoteDelegatesOtherAfter == urnVoteDelegatesOtherBefore, "Assert 2";
+ assert urnFarmsUrnAfter == 0, "Assert 3";
+ assert urnFarmsOtherAfter == urnFarmsOtherBefore, "Assert 4";
+ assert urnAuctionsUrnAfter == urnAuctionsUrnBefore + 1, "Assert 5";
+ assert urnAuctionsOtherAfter == urnAuctionsOtherBefore, "Assert 6";
+ assert prevVoteDelegate == 0 => mkrBalanceOfPrevVoteDelegateAfter == mkrBalanceOfPrevVoteDelegateBefore, "Assert 7";
+ assert prevVoteDelegate != 0 => mkrBalanceOfPrevVoteDelegateAfter == mkrBalanceOfPrevVoteDelegateBefore - vatUrnsIlkUrnInk - wad, "Assert 8";
+ assert prevVoteDelegate == 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore, "Assert 9";
+ assert prevVoteDelegate != 0 => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore + vatUrnsIlkUrnInk + wad, "Assert 10";
+ assert mkrBalanceOfOtherAfter == mkrBalanceOfOtherBefore, "Assert 11";
+ assert lsmkrTotalSupplyAfter == lsmkrTotalSupplyBefore - wad, "Assert 12";
+ assert prevFarm == 0 => lsmkrBalanceOfPrevFarmAfter == lsmkrBalanceOfPrevFarmBefore, "Assert 13";
+ assert prevFarm != 0 => lsmkrBalanceOfPrevFarmAfter == lsmkrBalanceOfPrevFarmBefore - vatUrnsIlkUrnInk - wad, "Assert 14";
+ assert prevFarm == 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore - wad, "Assert 15";
+ assert prevFarm != 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore + vatUrnsIlkUrnInk, "Assert 16";
+ assert lsmkrBalanceOfOtherAfter == lsmkrBalanceOfOtherBefore, "Assert 17";
+}
+
+// Verify revert rules on onKick
+rule onKick_revert(address urn, uint256 wad) {
+ env e;
+
+ require urn == lockstakeUrn;
+ address prevVoteDelegate = urnVoteDelegates(urn);
+ require prevVoteDelegate == 0 || prevVoteDelegate == voteDelegate;
+ address prevFarm = urnFarms(urn);
+ require prevFarm == 0 || prevFarm == stakingRewards;
+
+ mathint wardsSender = wards(e.msg.sender);
+ mathint urnAuctionsUrn = urnAuctions(urn);
+ mathint vatUrnsIlkUrnInk; mathint vatUrnsIlkUrnArt;
+ vatUrnsIlkUrnInk, vatUrnsIlkUrnArt = vat.urns(ilk(), urn);
+
+ // Happening in urn init
+ require lsmkr.allowance(urn, currentContract) == max_uint256;
+ // Tokens invariants
+ require to_mathint(lsmkr.totalSupply()) >= lsmkr.balanceOf(prevFarm) + lsmkr.balanceOf(urn) + lsmkr.balanceOf(currentContract);
+ require stakingRewards.totalSupply() >= stakingRewards.balanceOf(urn);
+ // VoteDelegate assumptions
+ require prevVoteDelegate == 0 || to_mathint(voteDelegate.stake(currentContract)) >= vatUrnsIlkUrnInk + wad;
+ require prevVoteDelegate == 0 || mkr.balanceOf(voteDelegate) >= voteDelegate.stake(currentContract);
+ // StakingRewards assumptions
+ require prevFarm == 0 && lsmkr.balanceOf(urn) >= wad ||
+ prevFarm != 0 && to_mathint(stakingRewards.balanceOf(urn)) >= vatUrnsIlkUrnInk + wad && to_mathint(lsmkr.balanceOf(prevFarm)) >= vatUrnsIlkUrnInk + wad;
+ // LockstakeClipper assumption
+ require wad > 0;
+ // Practical assumption (vatUrnsIlkUrnInk + wad should be the same than the vatUrnsIlkUrnInk prev to the kick call)
+ require vatUrnsIlkUrnInk + wad <= max_uint256;
+
+ onKick@withrevert(e, urn, wad);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+ bool revert3 = urnAuctionsUrn == max_uint256;
+
+ assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting onTake
+rule onTake(address urn, address who, uint256 wad) {
+ env e;
+
+ address other;
+ require other != currentContract && other != who;
+
+ mathint mkrBalanceOfEngineBefore = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfWhoBefore = mkr.balanceOf(who);
+ mathint mkrBalanceOfOtherBefore = mkr.balanceOf(other);
+
+ // Tokens invariants
+ require to_mathint(mkr.totalSupply()) >= mkrBalanceOfEngineBefore + mkrBalanceOfWhoBefore + mkrBalanceOfOtherBefore;
+
+ onTake(e, urn, who, wad);
+
+ mathint mkrBalanceOfEngineAfter = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfWhoAfter = mkr.balanceOf(who);
+ mathint mkrBalanceOfOtherAfter = mkr.balanceOf(other);
+
+ assert who != currentContract => mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore - wad, "Assert 1";
+ assert who != currentContract => mkrBalanceOfWhoAfter == mkrBalanceOfWhoBefore + wad, "Assert 2";
+ assert who == currentContract => mkrBalanceOfWhoAfter == mkrBalanceOfWhoBefore, "Assert 3";
+}
+
+// Verify revert rules on onTake
+rule onTake_revert(address urn, address who, uint256 wad) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+ mathint mkrBalanceOfEngine = mkr.balanceOf(currentContract);
+
+ // Tokens invariants
+ require to_mathint(mkr.totalSupply()) >= mkrBalanceOfEngine + mkr.balanceOf(who);
+ // LockstakeClipper assumption
+ require mkrBalanceOfEngine >= to_mathint(wad);
+
+ onTake@withrevert(e, urn, who, wad);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting onRemove
+rule onRemove(address urn, uint256 sold, uint256 left) {
+ env e;
+
+ address other;
+ require other != urn;
+ address other2;
+ require other2 != currentContract;
+
+ bytes32 ilk = ilk();
+ mathint fee = fee();
+ mathint urnAuctionsUrnBefore = urnAuctions(urn);
+ mathint urnAuctionsOtherBefore = urnAuctions(other);
+ mathint vatUrnsIlkUrnInkBefore; mathint a;
+ vatUrnsIlkUrnInkBefore, a = vat.urns(ilk, urn);
+ mathint mkrTotalSupplyBefore = mkr.totalSupply();
+ mathint mkrBalanceOfEngineBefore = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfOtherBefore = mkr.balanceOf(other2);
+ mathint lsmkrTotalSupplyBefore = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUrnBefore = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfOtherBefore = lsmkr.balanceOf(other);
+
+ // Happening in constructor
+ require fee < WAD();
+ // Tokens invariants
+ require mkrTotalSupplyBefore >= mkrBalanceOfEngineBefore + mkrBalanceOfOtherBefore;
+ require lsmkrTotalSupplyBefore >= lsmkrBalanceOfUrnBefore + lsmkrBalanceOfOtherBefore;
+
+ mathint burn = _min(sold * fee / (WAD() - fee), left);
+ mathint refund = left - burn;
+
+ onRemove(e, urn, sold, left);
+
+ mathint urnAuctionsUrnAfter = urnAuctions(urn);
+ mathint urnAuctionsOtherAfter = urnAuctions(other);
+ mathint vatUrnsIlkUrnInkAfter;
+ vatUrnsIlkUrnInkAfter, a = vat.urns(ilk, urn);
+ mathint mkrTotalSupplyAfter = mkr.totalSupply();
+ mathint mkrBalanceOfEngineAfter = mkr.balanceOf(currentContract);
+ mathint mkrBalanceOfOtherAfter = mkr.balanceOf(other2);
+ mathint lsmkrTotalSupplyAfter = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUrnAfter = lsmkr.balanceOf(urn);
+ mathint lsmkrBalanceOfOtherAfter = lsmkr.balanceOf(other);
+
+ assert urnAuctionsUrnAfter == urnAuctionsUrnBefore - 1, "Assert 1";
+ assert urnAuctionsOtherAfter == urnAuctionsOtherBefore, "Assert 2";
+ assert refund > 0 => vatUrnsIlkUrnInkAfter == vatUrnsIlkUrnInkBefore + refund, "Assert 3";
+ assert refund == 0 => vatUrnsIlkUrnInkAfter == vatUrnsIlkUrnInkBefore, "Assert 4";
+ assert mkrTotalSupplyAfter == mkrTotalSupplyBefore - burn, "Assert 5";
+ assert mkrBalanceOfEngineAfter == mkrBalanceOfEngineBefore - burn, "Assert 6";
+ assert mkrBalanceOfOtherAfter == mkrBalanceOfOtherBefore, "Assert 7";
+ assert refund > 0 => lsmkrTotalSupplyAfter == lsmkrTotalSupplyBefore + refund, "Assert 8";
+ assert refund == 0 => lsmkrTotalSupplyAfter == lsmkrTotalSupplyBefore, "Assert 9";
+ assert refund > 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore + refund, "Assert 10";
+ assert refund == 0 => lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore, "Assert 11";
+ assert lsmkrBalanceOfOtherAfter == lsmkrBalanceOfOtherBefore, "Assert 12";
+}
+
+// Verify revert rules on onRemove
+rule onRemove_revert(address urn, uint256 sold, uint256 left) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+ bytes32 ilk = ilk();
+ mathint fee = fee();
+ mathint urnAuctionsUrn = urnAuctions(urn);
+ mathint vatIlksIlkArt; mathint vatIlksIlkRate; mathint a;
+ vatIlksIlkArt, vatIlksIlkRate, a, a, a = vat.ilks(ilk);
+ mathint vatUrnsIlkUrnInk; mathint vatUrnsIlkUrnArt;
+ vatUrnsIlkUrnInk, vatUrnsIlkUrnArt = vat.urns(ilk, urn);
+ mathint mkrTotalSupply = mkr.totalSupply();
+ mathint mkrBalanceOfEngine = mkr.balanceOf(currentContract);
+ mathint lsmkrTotalSupply = lsmkr.totalSupply();
+ mathint lsmkrBalanceOfUrn = lsmkr.balanceOf(urn);
+
+ // Happening in constructor
+ require fee < WAD();
+ // Happening in urn init
+ require vat.can(urn, currentContract) == 1;
+ // Happening in deploy scripts
+ require vat.wards(currentContract) == 1;
+ require lsmkr.wards(currentContract) == 1;
+ // Tokens invariants
+ require mkrTotalSupply >= mkrBalanceOfEngine;
+ require lsmkrTotalSupply >= lsmkrBalanceOfUrn;
+
+ require sold * fee < max_uint256;
+ mathint burn = _min(sold * fee / (WAD() - fee), left);
+ mathint refund = left - burn;
+
+ // Practical Vat assumptions
+ require vat.live() == 1;
+ require vat.wards(currentContract) == 1;
+ require vatUrnsIlkUrnInk + refund <= max_uint256;
+ require vatIlksIlkRate <= max_int256();
+ // Safe to assume as Engine doesn't modify vat.gem(ilk,urn) (rule vatGemKeepsUnchanged)
+ require vat.gem(ilk, urn) == 0;
+ // Practical token assumptions
+ require lsmkrTotalSupply + refund <= max_uint256;
+ // Assumption from LockstakeClipper
+ require mkrBalanceOfEngine >= burn;
+ require urn != lsmkr && urn != 0;
+
+ onRemove@withrevert(e, urn, sold, left);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+ bool revert3 = refund > max_int256();
+ bool revert4 = urnAuctionsUrn == 0;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4, "Revert rules failed";
+}
diff --git a/certora/LockstakeEngineMulticall.conf b/certora/LockstakeEngineMulticall.conf
new file mode 100644
index 00000000..b539614d
--- /dev/null
+++ b/certora/LockstakeEngineMulticall.conf
@@ -0,0 +1,57 @@
+{
+ "files": [
+ "src/LockstakeEngine.sol",
+ "src/LockstakeUrn.sol",
+ "src/LockstakeMkr.sol",
+ "certora/harness/dss/Vat.sol",
+ "test/mocks/VoteDelegateMock.sol",
+ "certora/harness/tokens/MkrMock.sol",
+ "test/mocks/StakingRewardsMock.sol",
+ "certora/harness/MulticallExecutor.sol"
+ ],
+ "solc_map": {
+ "LockstakeEngine": "solc-0.8.21",
+ "LockstakeUrn": "solc-0.8.21",
+ "LockstakeMkr": "solc-0.8.21",
+ "Vat": "solc-0.5.12",
+ "VoteDelegateMock": "solc-0.8.21",
+ "MkrMock": "solc-0.8.21",
+ "StakingRewardsMock": "solc-0.8.21",
+ "MulticallExecutor": "solc-0.8.21"
+ },
+ "solc_optimize_map": {
+ "LockstakeEngine": "200",
+ "LockstakeUrn": "200",
+ "LockstakeMkr": "200",
+ "Vat": "0",
+ "MkrMock": "0",
+ "VoteDelegateMock": "0",
+ "StakingRewardsMock": "0",
+ "MulticallExecutor": "0"
+ },
+ "link": [
+ "LockstakeEngine:vat=Vat",
+ "LockstakeEngine:mkr=MkrMock",
+ "LockstakeEngine:lsmkr=LockstakeMkr",
+ "LockstakeUrn:engine=LockstakeEngine",
+ "LockstakeUrn:lsmkr=LockstakeMkr",
+ "LockstakeUrn:vat=Vat",
+ "VoteDelegateMock:gov=MkrMock",
+ "MulticallExecutor:engine=LockstakeEngine"
+ ],
+ "verify": "LockstakeEngine:certora/LockstakeEngineMulticall.spec",
+ "prover_args": [
+ "-rewriteMSizeAllocations true",
+ "-depth 0"
+ ],
+ "smt_timeout": "7000",
+ "rule_sanity": "basic",
+ "optimistic_loop": true,
+ // NOTE: The number of loop iterations should be at least the length of the arrays
+ // given to `multicall`.
+ "loop_iter": "4",
+ "multi_assert_check": true,
+ "parametric_contracts": ["LockstakeEngine"],
+ "build_cache": true,
+ "msg": "LockstakeEngine Multicall"
+}
diff --git a/certora/LockstakeEngineMulticall.spec b/certora/LockstakeEngineMulticall.spec
new file mode 100644
index 00000000..0b9c2048
--- /dev/null
+++ b/certora/LockstakeEngineMulticall.spec
@@ -0,0 +1,95 @@
+// Basic spec checking the `multicall` function
+
+using MulticallExecutor as multicallExecutor;
+using MkrMock as mkr;
+using LockstakeUrn as lockstakeUrn;
+
+methods {
+ function farms(address) external returns (LockstakeEngine.FarmStatus) envfree;
+ function ownerUrns(address,uint256) external returns (address) envfree;
+ function urnCan(address,address) external returns (uint256) envfree;
+ function urnFarms(address) external returns (address) envfree;
+ function mkr.allowance(address,address) external returns (uint256) envfree;
+ function mkr.balanceOf(address) external returns (uint256) envfree;
+ function mkr.totalSupply() external returns (uint256) envfree;
+ //
+ function _.lock(address, uint256, uint256, uint16) external => DISPATCHER(true);
+ function _.lock(uint256) external => DISPATCHER(true);
+ function _.stake(address,uint256,uint16) external => DISPATCHER(true);
+ function _.withdraw(address,uint256) external => DISPATCHER(true);
+ function _.stake(uint256,uint16) external => DISPATCHER(true);
+ function _.withdraw(uint256) external => DISPATCHER(true);
+ function _.mint(address,uint256) external => DISPATCHER(true);
+ function _.transfer(address,uint256) external => DISPATCHER(true);
+ function _.transferFrom(address,address,uint256) external => DISPATCHER(true);
+ // The Prover will attempt to dispatch to the following functions any unresolved
+ // call, if the signature fits. Otherwise it will use the summary defined by the
+ // `default` keyword.
+ function _._ external => DISPATCH [
+ // currentContract.open(uint256),
+ currentContract.hope(address,uint256,address),
+ currentContract.nope(address,uint256,address),
+ // currentContract.selectVoteDelegate(address,uint256,address),
+ currentContract.selectFarm(address,uint256,address,uint16),
+ currentContract.lock(address,uint256,uint256,uint16),
+ // currentContract.lockSky(address,uint256,uint256,uint16),
+ // currentContract.free(address,uint256,address,uint256),
+ // currentContract.freeSky(address,uint256,address,uint256),
+ // currentContract.freeNoFee(address,uint256,address,uint256),
+ // currentContract.draw(address,uint256,address,uint256),
+ // currentContract.wipe(address,uint256,uint256),
+ // currentContract.wipeAll(address,uint256),
+ // currentContract.getReward(address,uint256,address,address)
+ ] default HAVOC_ALL;
+}
+
+rule hopeAndHope(address owner1, uint256 index1, address owner2, uint256 index2, address usr) {
+ env e;
+
+ storage init = lastStorage;
+
+ hope(e, owner1, index1, usr);
+ hope(e, owner2, index2, usr);
+
+ storage twoCalls = lastStorage;
+
+ multicallExecutor.hopeAndHope(e, owner1, index1, owner2, index2, usr) at init;
+
+ assert twoCalls == lastStorage;
+}
+
+rule hopeAndNope(address owner, uint256 index, address usr) {
+ env e;
+
+ multicallExecutor.hopeAndNope(e, owner, index, usr);
+
+ mathint urnCanUrnAfter = urnCan(ownerUrns(owner, index), usr);
+
+ assert urnCanUrnAfter == 0;
+}
+
+
+rule selectFarmAndLock(address owner, uint256 index, address farm, uint16 ref, uint256 wad) {
+ env e;
+
+ mathint mkrBalanceOfExecutorBefore = mkr.balanceOf(multicallExecutor);
+ mathint mkrAllowanceExecutorEngineBefore = mkr.allowance(multicallExecutor, currentContract);
+
+ // Token invariants
+ require to_mathint(mkr.totalSupply()) >= mkrBalanceOfExecutorBefore + mkrAllowanceExecutorEngineBefore;
+
+ multicallExecutor.selectFarmAndLock(e, owner, index, farm, ref, wad);
+
+ mathint mkrBalanceOfExecutorAfter = mkr.balanceOf(multicallExecutor);
+ mathint mkrAllowanceExecutorEngineAfter = mkr.allowance(multicallExecutor, currentContract);
+ address urn = ownerUrns(owner, index);
+ require lockstakeUrn == urn;
+ address urnFarmsUrnAfter = urnFarms(urn);
+
+ assert mkrBalanceOfExecutorAfter == mkrBalanceOfExecutorBefore - wad, "Assert 1";
+ assert mkrAllowanceExecutorEngineBefore < max_uint256 => mkrAllowanceExecutorEngineAfter == mkrAllowanceExecutorEngineBefore - wad, "Assert 2";
+ assert mkrAllowanceExecutorEngineBefore == max_uint256 => mkrAllowanceExecutorEngineAfter == mkrAllowanceExecutorEngineBefore, "Assert 3";
+ assert urnFarmsUrnAfter == farm, "Assert 4";
+
+ assert farm == 0 || farms(farm) == LockstakeEngine.FarmStatus.ACTIVE, "farm is active";
+}
diff --git a/certora/LockstakeMkr.conf b/certora/LockstakeMkr.conf
new file mode 100644
index 00000000..fc9b72ad
--- /dev/null
+++ b/certora/LockstakeMkr.conf
@@ -0,0 +1,12 @@
+{
+ "files": [
+ "src/LockstakeMkr.sol"
+ ],
+ "solc": "solc-0.8.21",
+ "solc_optimize": "200",
+ "verify": "LockstakeMkr:certora/LockstakeMkr.spec",
+ "rule_sanity": "basic",
+ "multi_assert_check": true,
+ "build_cache": true,
+ "msg": "LockstakeMkr"
+}
diff --git a/certora/LockstakeMkr.spec b/certora/LockstakeMkr.spec
new file mode 100644
index 00000000..0d7037b8
--- /dev/null
+++ b/certora/LockstakeMkr.spec
@@ -0,0 +1,329 @@
+// LockstakeMkr.spec
+
+methods {
+ function wards(address) external returns (uint256) envfree;
+ function name() external returns (string) envfree;
+ function symbol() external returns (string) envfree;
+ function version() external returns (string) envfree;
+ function decimals() external returns (uint8) envfree;
+ function totalSupply() external returns (uint256) envfree;
+ function balanceOf(address) external returns (uint256) envfree;
+ function allowance(address, address) external returns (uint256) envfree;
+}
+
+ghost balanceSum() returns mathint {
+ init_state axiom balanceSum() == 0;
+}
+
+hook Sstore balanceOf[KEY address a] uint256 balance (uint256 old_balance) {
+ havoc balanceSum assuming balanceSum@new() == balanceSum@old() + balance - old_balance && balanceSum@new() >= 0;
+}
+
+invariant balanceSum_equals_totalSupply() balanceSum() == to_mathint(totalSupply());
+
+// Verify that each storage layout is only modified in the corresponding functions
+rule storageAffected(method f) {
+ env e;
+
+ address anyAddr;
+ address anyAddr2;
+
+ mathint wardsBefore = wards(anyAddr);
+ mathint totalSupplyBefore = totalSupply();
+ mathint balanceOfBefore = balanceOf(anyAddr);
+ mathint allowanceBefore = allowance(anyAddr, anyAddr2);
+
+ calldataarg args;
+ f(e, args);
+
+ mathint wardsAfter = wards(anyAddr);
+ mathint totalSupplyAfter = totalSupply();
+ mathint balanceOfAfter = balanceOf(anyAddr);
+ mathint allowanceAfter = allowance(anyAddr, anyAddr2);
+
+ assert wardsAfter != wardsBefore => f.selector == sig:rely(address).selector || f.selector == sig:deny(address).selector, "Assert 1";
+ assert totalSupplyAfter != totalSupplyBefore => f.selector == sig:mint(address,uint256).selector || f.selector == sig:burn(address,uint256).selector, "Assert 2";
+ assert balanceOfAfter != balanceOfBefore => f.selector == sig:mint(address,uint256).selector || f.selector == sig:burn(address,uint256).selector || f.selector == sig:transfer(address,uint256).selector || f.selector == sig:transferFrom(address,address,uint256).selector, "Assert 3";
+ assert allowanceAfter != allowanceBefore => f.selector == sig:burn(address,uint256).selector || f.selector == sig:transferFrom(address,address,uint256).selector || f.selector == sig:approve(address,uint256).selector, "Assert 4";
+}
+
+// Verify correct storage changes for non reverting rely
+rule rely(address usr) {
+ env e;
+
+ address other;
+ require other != usr;
+
+ mathint wardsOtherBefore = wards(other);
+
+ rely(e, usr);
+
+ mathint wardsUsrAfter = wards(usr);
+ mathint wardsOtherAfter = wards(other);
+
+ assert wardsUsrAfter == 1, "Assert 1";
+ assert wardsOtherAfter == wardsOtherBefore, "Assert 2";
+}
+
+// Verify revert rules on rely
+rule rely_revert(address usr) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+
+ rely@withrevert(e, usr);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting deny
+rule deny(address usr) {
+ env e;
+
+ address other;
+ require other != usr;
+
+ mathint wardsOtherBefore = wards(other);
+
+ deny(e, usr);
+
+ mathint wardsUsrAfter = wards(usr);
+ mathint wardsOtherAfter = wards(other);
+
+ assert wardsUsrAfter == 0, "Assert 1";
+ assert wardsOtherAfter == wardsOtherBefore, "Assert 2";
+}
+
+// Verify revert rules on deny
+rule deny_revert(address usr) {
+ env e;
+
+ mathint wardsSender = wards(e.msg.sender);
+
+ deny@withrevert(e, usr);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting transfer
+rule transfer(address to, uint256 value) {
+ env e;
+
+ requireInvariant balanceSum_equals_totalSupply();
+
+ address other;
+ require other != e.msg.sender && other != to;
+
+ mathint balanceOfSenderBefore = balanceOf(e.msg.sender);
+ mathint balanceOfToBefore = balanceOf(to);
+ mathint balanceOfOtherBefore = balanceOf(other);
+
+ transfer(e, to, value);
+
+ mathint balanceOfSenderAfter = balanceOf(e.msg.sender);
+ mathint balanceOfToAfter = balanceOf(to);
+ mathint balanceOfOtherAfter = balanceOf(other);
+
+ assert e.msg.sender != to => balanceOfSenderAfter == balanceOfSenderBefore - value, "Assert 1";
+ assert e.msg.sender != to => balanceOfToAfter == balanceOfToBefore + value, "Assert 2";
+ assert e.msg.sender == to => balanceOfSenderAfter == balanceOfSenderBefore, "Assert 3";
+ assert balanceOfOtherAfter == balanceOfOtherBefore, "Assert 4";
+}
+
+// Verify revert rules on transfer
+rule transfer_revert(address to, uint256 value) {
+ env e;
+
+ mathint balanceOfSender = balanceOf(e.msg.sender);
+
+ transfer@withrevert(e, to, value);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = to == 0 || to == currentContract;
+ bool revert3 = balanceOfSender < to_mathint(value);
+
+ assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting transferFrom
+rule transferFrom(address from, address to, uint256 value) {
+ env e;
+
+ requireInvariant balanceSum_equals_totalSupply();
+
+ address other;
+ require other != from && other != to;
+ address other2; address other3;
+ require other2 != from || other3 != e.msg.sender;
+ address anyUsr; address anyUsr2;
+
+ mathint balanceOfFromBefore = balanceOf(from);
+ mathint balanceOfToBefore = balanceOf(to);
+ mathint balanceOfOtherBefore = balanceOf(other);
+ mathint allowanceFromSenderBefore = allowance(from, e.msg.sender);
+ mathint allowanceOtherBefore = allowance(other2, other3);
+
+ transferFrom(e, from, to, value);
+
+ mathint balanceOfFromAfter = balanceOf(from);
+ mathint balanceOfToAfter = balanceOf(to);
+ mathint balanceOfOtherAfter = balanceOf(other);
+ mathint allowanceFromSenderAfter = allowance(from, e.msg.sender);
+ mathint allowanceOtherAfter = allowance(other2, other3);
+
+ assert from != to => balanceOfFromAfter == balanceOfFromBefore - value, "Assert 1";
+ assert from != to => balanceOfToAfter == balanceOfToBefore + value, "Assert 2";
+ assert from == to => balanceOfFromAfter == balanceOfFromBefore, "Assert 3";
+ assert balanceOfOtherAfter == balanceOfOtherBefore, "Assert 4";
+ assert e.msg.sender != from && allowanceFromSenderBefore != max_uint256 => allowanceFromSenderAfter == allowanceFromSenderBefore - value, "Assert 5";
+ assert e.msg.sender == from => allowanceFromSenderAfter == allowanceFromSenderBefore, "Assert 6";
+ assert allowanceFromSenderBefore == max_uint256 => allowanceFromSenderAfter == allowanceFromSenderBefore, "Assert 7";
+ assert allowanceOtherAfter == allowanceOtherBefore, "Assert 8";
+}
+
+// Verify revert rules on transferFrom
+rule transferFrom_revert(address from, address to, uint256 value) {
+ env e;
+
+ mathint balanceOfFrom = balanceOf(from);
+ mathint allowanceFromSender = allowance(from, e.msg.sender);
+
+ transferFrom@withrevert(e, from, to, value);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = to == 0 || to == currentContract;
+ bool revert3 = balanceOfFrom < to_mathint(value);
+ bool revert4 = allowanceFromSender < to_mathint(value) && e.msg.sender != from;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 || revert4, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting approve
+rule approve(address spender, uint256 value) {
+ env e;
+
+ address other; address other2;
+ require other != e.msg.sender || other2 != spender;
+
+ mathint allowanceOtherBefore = allowance(other, other2);
+
+ approve(e, spender, value);
+
+ mathint allowanceSenderSpenderAfter = allowance(e.msg.sender, spender);
+ mathint allowanceOtherAfter = allowance(other, other2);
+
+ assert allowanceSenderSpenderAfter == to_mathint(value), "Assert 1";
+ assert allowanceOtherAfter == allowanceOtherBefore, "Assert 2";
+}
+
+// Verify revert rules on approve
+rule approve_revert(address spender, uint256 value) {
+ env e;
+
+ approve@withrevert(e, spender, value);
+
+ bool revert1 = e.msg.value > 0;
+
+ assert lastReverted <=> revert1, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting mint
+rule mint(address to, uint256 value) {
+ env e;
+
+ requireInvariant balanceSum_equals_totalSupply();
+
+ address other;
+ require other != to;
+
+ bool senderSameAsTo = e.msg.sender == to;
+
+ mathint totalSupplyBefore = totalSupply();
+ mathint balanceOfToBefore = balanceOf(to);
+ mathint balanceOfOtherBefore = balanceOf(other);
+
+ mint(e, to, value);
+
+ mathint totalSupplyAfter = totalSupply();
+ mathint balanceOfToAfter = balanceOf(to);
+ mathint balanceOfOtherAfter = balanceOf(other);
+
+ assert totalSupplyAfter == totalSupplyBefore + value, "Assert 1";
+ assert balanceOfToAfter == balanceOfToBefore + value, "Assert 2";
+ assert balanceOfOtherAfter == balanceOfOtherBefore, "Assert 3";
+}
+
+// Verify revert rules on mint
+rule mint_revert(address to, uint256 value) {
+ env e;
+
+ // Save the totalSupply and sender balance before minting
+ mathint totalSupply = totalSupply();
+ mathint wardsSender = wards(e.msg.sender);
+
+ mint@withrevert(e, to, value);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = wardsSender != 1;
+ bool revert3 = totalSupply + value > max_uint256;
+ bool revert4 = to == 0 || to == currentContract;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 || revert4, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting burn
+rule burn(address from, uint256 value) {
+ env e;
+
+ requireInvariant balanceSum_equals_totalSupply();
+
+ address other;
+ require other != from;
+ address other2; address other3;
+ require other2 != from || other3 != e.msg.sender;
+
+ mathint totalSupplyBefore = totalSupply();
+ mathint balanceOfFromBefore = balanceOf(from);
+ mathint balanceOfOtherBefore = balanceOf(other);
+ mathint allowanceFromSenderBefore = allowance(from, e.msg.sender);
+ mathint allowanceOtherBefore = allowance(other2, other3);
+
+ burn(e, from, value);
+
+ mathint totalSupplyAfter = totalSupply();
+ mathint balanceOfSenderAfter = balanceOf(e.msg.sender);
+ mathint balanceOfFromAfter = balanceOf(from);
+ mathint balanceOfOtherAfter = balanceOf(other);
+ mathint allowanceFromSenderAfter = allowance(from, e.msg.sender);
+ mathint allowanceOtherAfter = allowance(other2, other3);
+
+ assert totalSupplyAfter == totalSupplyBefore - value, "Assert 1";
+ assert balanceOfFromAfter == balanceOfFromBefore - value, "Assert 2";
+ assert balanceOfOtherAfter == balanceOfOtherBefore, "Assert 3";
+ assert e.msg.sender != from && allowanceFromSenderBefore != max_uint256 => allowanceFromSenderAfter == allowanceFromSenderBefore - value, "Assert 4";
+ assert e.msg.sender == from => allowanceFromSenderAfter == allowanceFromSenderBefore, "Assert 5";
+ assert allowanceFromSenderBefore == max_uint256 => allowanceFromSenderAfter == allowanceFromSenderBefore, "Assert 6";
+ assert allowanceOtherAfter == allowanceOtherBefore, "Assert 7";
+}
+
+// Verify revert rules on burn
+rule burn_revert(address from, uint256 value) {
+ env e;
+
+ mathint balanceOfFrom = balanceOf(from);
+ mathint allowanceFromSender = allowance(from, e.msg.sender);
+
+ burn@withrevert(e, from, value);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = balanceOfFrom < to_mathint(value);
+ bool revert3 = from != e.msg.sender && allowanceFromSender < to_mathint(value);
+
+ assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed";
+}
diff --git a/certora/LockstakeUrn.conf b/certora/LockstakeUrn.conf
new file mode 100644
index 00000000..d6b43ebc
--- /dev/null
+++ b/certora/LockstakeUrn.conf
@@ -0,0 +1,34 @@
+{
+ "files": [
+ "src/LockstakeUrn.sol",
+ "src/LockstakeMkr.sol",
+ "certora/harness/dss/Vat.sol",
+ "test/mocks/StakingRewardsMock.sol",
+ "certora/harness/tokens/RewardsMock.sol",
+ ],
+ "solc_map": {
+ "LockstakeUrn": "solc-0.8.21",
+ "LockstakeMkr": "solc-0.8.21",
+ "Vat": "solc-0.5.12",
+ "StakingRewardsMock": "solc-0.8.21",
+ "RewardsMock": "solc-0.8.21",
+ },
+ "solc_optimize_map": {
+ "LockstakeUrn": "200",
+ "LockstakeMkr": "200",
+ "Vat": "0",
+ "StakingRewardsMock": "200",
+ "RewardsMock": "200",
+ },
+ "link": [
+ "StakingRewardsMock:rewardsToken=RewardsMock",
+ "StakingRewardsMock:stakingToken=LockstakeMkr",
+ "LockstakeUrn:lsmkr=LockstakeMkr",
+ "LockstakeUrn:vat=Vat"
+ ],
+ "verify": "LockstakeUrn:certora/LockstakeUrn.spec",
+ "rule_sanity": "basic",
+ "multi_assert_check": true,
+ "build_cache": true,
+ "msg": "LockstakeUrn"
+}
diff --git a/certora/LockstakeUrn.spec b/certora/LockstakeUrn.spec
new file mode 100644
index 00000000..bd96ecc4
--- /dev/null
+++ b/certora/LockstakeUrn.spec
@@ -0,0 +1,180 @@
+// LockstakeUrn.spec
+
+using Vat as vat;
+using LockstakeMkr as lsmkr;
+using StakingRewardsMock as stakingRewards;
+using RewardsMock as rewardsToken;
+
+methods {
+ function engine() external returns (address) envfree;
+ function vat.can(address,address) external returns (uint256) envfree;
+ function lsmkr.allowance(address,address) external returns (uint256) envfree;
+ function lsmkr.balanceOf(address) external returns (uint256) envfree;
+ function lsmkr.totalSupply() external returns (uint256) envfree;
+ function stakingRewards.balanceOf(address) external returns (uint256) envfree;
+ function stakingRewards.totalSupply() external returns (uint256) envfree;
+ function stakingRewards.rewards(address) external returns (uint256) envfree;
+ function _.stake(uint256,uint16) external => DISPATCHER(true);
+ function _.withdraw(uint256) external => DISPATCHER(true);
+ function _.getReward() external => DISPATCHER(true);
+ function _.rewardsToken() external => DISPATCHER(true);
+ function _.balanceOf(address) external => DISPATCHER(true);
+ function _.transfer(address,uint256) external => DISPATCHER(true);
+ function rewardsToken.balanceOf(address) external returns (uint256) envfree;
+ function rewardsToken.totalSupply() external returns (uint256) envfree;
+}
+
+// Verify correct storage changes for non reverting init
+rule init() {
+ env e;
+
+ address engine = engine();
+
+ init(e);
+
+ mathint vatCanUrnEngineAfter = vat.can(currentContract, engine);
+ mathint lsmkrAllowanceUrnEngineAfter = lsmkr.allowance(currentContract, engine);
+
+ assert vatCanUrnEngineAfter == 1, "Assert 1";
+ assert lsmkrAllowanceUrnEngineAfter == max_uint256, "Assert 2";
+}
+
+// Verify revert rules on init
+rule init_revert() {
+ env e;
+
+ address engine = engine();
+
+ init@withrevert(e);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = engine != e.msg.sender;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting stake
+rule stake(address farm, uint256 wad, uint16 ref) {
+ env e;
+
+ require farm == stakingRewards;
+
+ mathint lsmkrBalanceOfUrnBefore = lsmkr.balanceOf(currentContract);
+ mathint lsmkrBalanceOfFarmBefore = lsmkr.balanceOf(farm);
+ require to_mathint(lsmkr.totalSupply()) >= lsmkrBalanceOfUrnBefore + lsmkrBalanceOfFarmBefore;
+ mathint farmBalanceOfUrnBefore = stakingRewards.balanceOf(currentContract);
+
+ stake(e, farm, wad, ref);
+
+ mathint lsmkrAllowanceUrnFarmAfter = lsmkr.allowance(currentContract, farm);
+ mathint lsmkrBalanceOfUrnAfter = lsmkr.balanceOf(currentContract);
+ mathint lsmkrBalanceOfFarmAfter = lsmkr.balanceOf(farm);
+ mathint farmBalanceOfUrnAfter = stakingRewards.balanceOf(currentContract);
+
+ assert lsmkrAllowanceUrnFarmAfter == 0 || lsmkrAllowanceUrnFarmAfter == max_uint256, "Assert 1";
+ assert lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore - wad, "Assert 2";
+ assert lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore + wad, "Assert 3";
+ assert farmBalanceOfUrnAfter == farmBalanceOfUrnBefore + wad, "Assert 4";
+}
+
+// Verify revert rules on stake
+rule stake_revert(address farm, uint256 wad, uint16 ref) {
+ env e;
+
+ require farm == stakingRewards;
+
+ require wad > 0;
+ require lsmkr.balanceOf(currentContract) >= wad;
+ require lsmkr.totalSupply() >= wad;
+ require stakingRewards.balanceOf(currentContract) + wad <= max_uint256;
+ require stakingRewards.totalSupply() + wad <= max_uint256;
+
+ address engine = engine();
+
+ stake@withrevert(e, farm, wad, ref);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = engine != e.msg.sender;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting withdraw
+rule withdraw(address farm, uint256 wad) {
+ env e;
+
+ require farm == stakingRewards;
+
+ mathint lsmkrBalanceOfUrnBefore = lsmkr.balanceOf(currentContract);
+ mathint lsmkrBalanceOfFarmBefore = lsmkr.balanceOf(farm);
+ require to_mathint(lsmkr.totalSupply()) >= lsmkrBalanceOfUrnBefore + lsmkrBalanceOfFarmBefore;
+ mathint farmBalanceOfUrnBefore = stakingRewards.balanceOf(currentContract);
+
+ withdraw(e, farm, wad);
+
+ mathint lsmkrBalanceOfUrnAfter = lsmkr.balanceOf(currentContract);
+ mathint lsmkrBalanceOfFarmAfter = lsmkr.balanceOf(farm);
+ mathint farmBalanceOfUrnAfter = stakingRewards.balanceOf(currentContract);
+
+ assert lsmkrBalanceOfUrnAfter == lsmkrBalanceOfUrnBefore + wad, "Assert 1";
+ assert lsmkrBalanceOfFarmAfter == lsmkrBalanceOfFarmBefore - wad, "Assert 2";
+ assert farmBalanceOfUrnAfter == farmBalanceOfUrnBefore - wad, "Assert 3";
+}
+
+// Verify revert rules on withdraw
+rule withdraw_revert(address farm, uint256 wad) {
+ env e;
+
+ require farm == stakingRewards;
+
+ require wad > 0;
+ require lsmkr.balanceOf(farm) >= wad;
+ require lsmkr.balanceOf(currentContract) + wad <= max_uint256;
+ require lsmkr.totalSupply() + wad <= max_uint256;
+ require stakingRewards.balanceOf(currentContract) >= wad;
+ require stakingRewards.totalSupply() >= wad;
+
+ address engine = engine();
+
+ withdraw@withrevert(e, farm, wad);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = engine != e.msg.sender;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting getReward
+rule getReward(address farm, address to) {
+ env e;
+
+ require farm == stakingRewards;
+
+ mathint rewardsTokenBalanceOfToBefore = rewardsToken.balanceOf(to);
+ require rewardsTokenBalanceOfToBefore <= to_mathint(rewardsToken.totalSupply());
+ require rewardsTokenBalanceOfToBefore + rewardsToken.balanceOf(currentContract) + rewardsToken.balanceOf(stakingRewards) <= to_mathint(rewardsToken.totalSupply());
+
+ getReward(e, farm, to);
+
+ mathint rewardsTokenBalanceOfToAfter = rewardsToken.balanceOf(to);
+
+ assert rewardsTokenBalanceOfToAfter >= rewardsTokenBalanceOfToBefore, "Assert 1";
+}
+
+// Verify revert rules on getReward
+rule getReward_revert(address farm, address to) {
+ env e;
+
+ require farm == stakingRewards;
+
+ require rewardsToken.balanceOf(stakingRewards) >= stakingRewards.rewards(currentContract);
+
+ address engine = engine();
+
+ getReward@withrevert(e, farm, to);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = engine != e.msg.sender;
+
+ assert lastReverted <=> revert1 || revert2, "Revert rules failed";
+}
diff --git a/certora/harness/MulticallExecutor.sol b/certora/harness/MulticallExecutor.sol
new file mode 100644
index 00000000..36d5ccd7
--- /dev/null
+++ b/certora/harness/MulticallExecutor.sol
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+pragma solidity ^0.8.21;
+
+import { LockstakeEngine } from "../../src/LockstakeEngine.sol";
+
+
+contract MulticallExecutor {
+
+ LockstakeEngine public engine;
+
+ function hopeAndHope(address owner1, uint256 index1, address owner2, uint256 index2, address usr) public {
+
+ bytes[] memory calls = new bytes[](2);
+ calls[0] = abi.encodeWithSignature("hope(address,uint256,address)", owner1, index1, usr);
+ calls[1] = abi.encodeWithSignature("hope(address,uint256,address)", owner2, index2, usr);
+ engine.multicall(calls);
+ }
+
+ function hopeAndNope(address owner, uint256 index, address usr) public {
+ bytes[] memory calls = new bytes[](2);
+ calls[0] = abi.encodeWithSignature("hope(address,uint256,address)", owner, index, usr);
+ calls[1] = abi.encodeWithSignature("nope(address,uint256,address)", owner, index, usr);
+ engine.multicall(calls);
+ }
+
+ function selectFarmAndLock(
+ address owner,
+ uint256 index,
+ address farm,
+ uint16 ref,
+ uint256 wad
+ ) public {
+
+ bytes[] memory calls = new bytes[](2);
+ calls[0] = abi.encodeWithSignature(
+ "selectFarm(address,uint256,address,uint16)", owner, index, farm, ref
+ );
+ calls[1] = abi.encodeWithSignature(
+ "lock(address,uint256,uint256,uint16)", owner, index, wad, ref
+ );
+ engine.multicall(calls);
+ }
+}
diff --git a/certora/harness/StakingRewards2Mock.sol b/certora/harness/StakingRewards2Mock.sol
new file mode 100644
index 00000000..251806b6
--- /dev/null
+++ b/certora/harness/StakingRewards2Mock.sol
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+import {StakingRewardsMock} from "../../test/mocks/StakingRewardsMock.sol";
+
+contract StakingRewards2Mock is StakingRewardsMock {
+
+ constructor(address rewardsToken, address stakingToken) StakingRewardsMock(rewardsToken, stakingToken) {
+ }
+}
diff --git a/certora/harness/VoteDelegate2Mock.sol b/certora/harness/VoteDelegate2Mock.sol
new file mode 100644
index 00000000..83595f18
--- /dev/null
+++ b/certora/harness/VoteDelegate2Mock.sol
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+import {VoteDelegateMock} from "../../test/mocks/VoteDelegateMock.sol";
+
+contract VoteDelegate2Mock is VoteDelegateMock {
+
+ constructor(address gov) VoteDelegateMock(gov) {
+ }
+}
diff --git a/certora/harness/dss/ClipperCallee.sol b/certora/harness/dss/ClipperCallee.sol
new file mode 100644
index 00000000..4ff4e53a
--- /dev/null
+++ b/certora/harness/dss/ClipperCallee.sol
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// taken from: ./LockstakeClipper.t.sol
+
+pragma solidity ^0.8.21;
+
+import { LockstakeClipper } from "src/LockstakeClipper.sol";
+
+contract BadGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(address sender, uint256 owe, uint256 slice, bytes calldata data)
+ external {
+ sender; owe; slice; data;
+ clip.take({ // attempt reentrancy
+ id: 1,
+ amt: 25 ether,
+ max: 5 ether * 10E27,
+ who: address(this),
+ data: ""
+ });
+ }
+}
+
+contract RedoGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(
+ address sender, uint256 owe, uint256 slice, bytes calldata data
+ ) external {
+ owe; slice; data;
+ clip.redo(1, sender);
+ }
+}
+
+contract KickGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(
+ address sender, uint256 owe, uint256 slice, bytes calldata data
+ ) external {
+ sender; owe; slice; data;
+ clip.kick(1, 1, address(0), address(0));
+ }
+}
+
+contract FileUintGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(
+ address sender, uint256 owe, uint256 slice, bytes calldata data
+ ) external {
+ sender; owe; slice; data;
+ clip.file("stopped", 1);
+ }
+}
+
+contract FileAddrGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(
+ address sender, uint256 owe, uint256 slice, bytes calldata data
+ ) external {
+ sender; owe; slice; data;
+ clip.file("vow", address(123));
+ }
+}
+
+contract YankGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(
+ address sender, uint256 owe, uint256 slice, bytes calldata data
+ ) external {
+ sender; owe; slice; data;
+ clip.yank(1);
+ }
+}
diff --git a/certora/harness/dss/Dog.sol b/certora/harness/dss/Dog.sol
new file mode 100644
index 00000000..d941d80f
--- /dev/null
+++ b/certora/harness/dss/Dog.sol
@@ -0,0 +1,249 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+/// dog.sol -- Dai liquidation module 2.0
+
+// Copyright (C) 2020-2022 Dai Foundation
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.6.12;
+
+interface ClipperLike {
+ function ilk() external view returns (bytes32);
+ function kick(
+ uint256 tab,
+ uint256 lot,
+ address usr,
+ address kpr
+ ) external returns (uint256);
+}
+
+interface VatLike {
+ function ilks(bytes32) external view returns (
+ uint256 Art, // [wad]
+ uint256 rate, // [ray]
+ uint256 spot, // [ray]
+ uint256 line, // [rad]
+ uint256 dust // [rad]
+ );
+ function urns(bytes32,address) external view returns (
+ uint256 ink, // [wad]
+ uint256 art // [wad]
+ );
+ function grab(bytes32,address,address,address,int256,int256) external;
+ function hope(address) external;
+ function nope(address) external;
+}
+
+interface VowLike {
+ function fess(uint256) external;
+}
+
+contract Dog {
+ // --- Auth ---
+ mapping (address => uint256) public wards;
+ function rely(address usr) external auth { wards[usr] = 1; emit Rely(usr); }
+ function deny(address usr) external auth { wards[usr] = 0; emit Deny(usr); }
+ modifier auth {
+ require(wards[msg.sender] == 1, "Dog/not-authorized");
+ _;
+ }
+
+ // --- Data ---
+ struct Ilk {
+ address clip; // Liquidator
+ uint256 chop; // Liquidation Penalty [wad]
+ uint256 hole; // Max DAI needed to cover debt+fees of active auctions per ilk [rad]
+ uint256 dirt; // Amt DAI needed to cover debt+fees of active auctions per ilk [rad]
+ }
+
+ VatLike immutable public vat; // CDP Engine
+
+ mapping (bytes32 => Ilk) public ilks;
+
+ VowLike public vow; // Debt Engine
+ uint256 public live; // Active Flag
+ uint256 public Hole; // Max DAI needed to cover debt+fees of active auctions [rad]
+ uint256 public Dirt; // Amt DAI needed to cover debt+fees of active auctions [rad]
+
+ // --- Events ---
+ event Rely(address indexed usr);
+ event Deny(address indexed usr);
+
+ event File(bytes32 indexed what, uint256 data);
+ event File(bytes32 indexed what, address data);
+ event File(bytes32 indexed ilk, bytes32 indexed what, uint256 data);
+ event File(bytes32 indexed ilk, bytes32 indexed what, address clip);
+
+ event Bark(
+ bytes32 indexed ilk,
+ address indexed urn,
+ uint256 ink,
+ uint256 art,
+ uint256 due,
+ address clip,
+ uint256 indexed id
+ );
+ event Digs(bytes32 indexed ilk, uint256 rad);
+ event Cage();
+
+ // --- Init ---
+ constructor(address vat_) public {
+ vat = VatLike(vat_);
+ live = 1;
+ wards[msg.sender] = 1;
+ emit Rely(msg.sender);
+ }
+
+ // --- Math ---
+ uint256 constant WAD = 10 ** 18;
+
+ function min(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ z = x <= y ? x : y;
+ }
+ function add(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ require((z = x + y) >= x);
+ }
+ function sub(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ require((z = x - y) <= x);
+ }
+ function mul(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ require(y == 0 || (z = x * y) / y == x);
+ }
+
+ // --- Administration ---
+ function file(bytes32 what, address data) external auth {
+ if (what == "vow") vow = VowLike(data);
+ else revert("Dog/file-unrecognized-param");
+ emit File(what, data);
+ }
+ function file(bytes32 what, uint256 data) external auth {
+ if (what == "Hole") Hole = data;
+ else revert("Dog/file-unrecognized-param");
+ emit File(what, data);
+ }
+ function file(bytes32 ilk, bytes32 what, uint256 data) external auth {
+ if (what == "chop") {
+ require(data >= WAD, "Dog/file-chop-lt-WAD");
+ ilks[ilk].chop = data;
+ } else if (what == "hole") ilks[ilk].hole = data;
+ else revert("Dog/file-unrecognized-param");
+ emit File(ilk, what, data);
+ }
+ function file(bytes32 ilk, bytes32 what, address clip) external auth {
+ if (what == "clip") {
+ require(ilk == ClipperLike(clip).ilk(), "Dog/file-ilk-neq-clip.ilk");
+ ilks[ilk].clip = clip;
+ } else revert("Dog/file-unrecognized-param");
+ emit File(ilk, what, clip);
+ }
+
+ function chop(bytes32 ilk) external view returns (uint256) {
+ return ilks[ilk].chop;
+ }
+
+ // --- CDP Liquidation: all bark and no bite ---
+ //
+ // Liquidate a Vault and start a Dutch auction to sell its collateral for DAI.
+ //
+ // The third argument is the address that will receive the liquidation reward, if any.
+ //
+ // The entire Vault will be liquidated except when the target amount of DAI to be raised in
+ // the resulting auction (debt of Vault + liquidation penalty) causes either Dirt to exceed
+ // Hole or ilk.dirt to exceed ilk.hole by an economically significant amount. In that
+ // case, a partial liquidation is performed to respect the global and per-ilk limits on
+ // outstanding DAI target. The one exception is if the resulting auction would likely
+ // have too little collateral to be interesting to Keepers (debt taken from Vault < ilk.dust),
+ // in which case the function reverts. Please refer to the code and comments within if
+ // more detail is desired.
+ function bark(bytes32 ilk, address urn, address kpr) external returns (uint256 id) {
+ require(live == 1, "Dog/not-live");
+
+ (uint256 ink, uint256 art) = vat.urns(ilk, urn);
+ Ilk memory milk = ilks[ilk];
+ uint256 dart;
+ uint256 rate;
+ uint256 dust;
+ {
+ uint256 spot;
+ (,rate, spot,, dust) = vat.ilks(ilk);
+ require(spot > 0 && mul(ink, spot) < mul(art, rate), "Dog/not-unsafe");
+
+ // Get the minimum value between:
+ // 1) Remaining space in the general Hole
+ // 2) Remaining space in the collateral hole
+ require(Hole > Dirt && milk.hole > milk.dirt, "Dog/liquidation-limit-hit");
+ uint256 room = min(Hole - Dirt, milk.hole - milk.dirt);
+
+ // uint256.max()/(RAD*WAD) = 115,792,089,237,316
+ dart = min(art, mul(room, WAD) / rate / milk.chop);
+
+ // Partial liquidation edge case logic
+ if (art > dart) {
+ if (mul(art - dart, rate) < dust) {
+
+ // If the leftover Vault would be dusty, just liquidate it entirely.
+ // This will result in at least one of dirt_i > hole_i or Dirt > Hole becoming true.
+ // The amount of excess will be bounded above by ceiling(dust_i * chop_i / WAD).
+ // This deviation is assumed to be small compared to both hole_i and Hole, so that
+ // the extra amount of target DAI over the limits intended is not of economic concern.
+ dart = art;
+ } else {
+
+ // In a partial liquidation, the resulting auction should also be non-dusty.
+ require(mul(dart, rate) >= dust, "Dog/dusty-auction-from-partial-liquidation");
+ }
+ }
+ }
+
+ uint256 dink = mul(ink, dart) / art;
+
+ require(dink > 0, "Dog/null-auction");
+ require(dart <= 2**255 && dink <= 2**255, "Dog/overflow");
+
+ vat.grab(
+ ilk, urn, milk.clip, address(vow), -int256(dink), -int256(dart)
+ );
+
+ uint256 due = mul(dart, rate);
+ vow.fess(due);
+
+ { // Avoid stack too deep
+ // This calcuation will overflow if dart*rate exceeds ~10^14
+ uint256 tab = mul(due, milk.chop) / WAD;
+ Dirt = add(Dirt, tab);
+ ilks[ilk].dirt = add(milk.dirt, tab);
+
+ id = ClipperLike(milk.clip).kick({
+ tab: tab,
+ lot: dink,
+ usr: urn,
+ kpr: kpr
+ });
+ }
+
+ emit Bark(ilk, urn, dink, dart, due, milk.clip, id);
+ }
+
+ function digs(bytes32 ilk, uint256 rad) external auth {
+ Dirt = sub(Dirt, rad);
+ ilks[ilk].dirt = sub(ilks[ilk].dirt, rad);
+ emit Digs(ilk, rad);
+ }
+
+ function cage() external auth {
+ live = 0;
+ emit Cage();
+ }
+}
diff --git a/certora/harness/dss/Jug.sol b/certora/harness/dss/Jug.sol
new file mode 100644
index 00000000..c9ec7086
--- /dev/null
+++ b/certora/harness/dss/Jug.sol
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+/// jug.sol -- Dai Lending Rate
+
+// Copyright (C) 2018 Rain
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity >=0.5.12;
+
+contract LibNote {
+ event LogNote(
+ bytes4 indexed sig,
+ address indexed usr,
+ bytes32 indexed arg1,
+ bytes32 indexed arg2,
+ bytes data
+ ) anonymous;
+
+ modifier note {
+ _;
+ assembly {
+ // log an 'anonymous' event with a constant 6 words of calldata
+ // and four indexed topics: selector, caller, arg1 and arg2
+ let mark := msize() // end of memory ensures zero
+ mstore(0x40, add(mark, 288)) // update free memory pointer
+ mstore(mark, 0x20) // bytes type data offset
+ mstore(add(mark, 0x20), 224) // bytes size (padded)
+ calldatacopy(add(mark, 0x40), 0, 224) // bytes payload
+ log4(mark, 288, // calldata
+ shl(224, shr(224, calldataload(0))), // msg.sig
+ caller(), // msg.sender
+ calldataload(4), // arg1
+ calldataload(36) // arg2
+ )
+ }
+ }
+}
+
+interface VatLike {
+ function ilks(bytes32) external returns (
+ uint256 Art, // [wad]
+ uint256 rate // [ray]
+ );
+ function fold(bytes32,address,int) external;
+}
+
+contract Jug is LibNote {
+ // --- Auth ---
+ mapping (address => uint) public wards;
+ function rely(address usr) external note auth { wards[usr] = 1; }
+ function deny(address usr) external note auth { wards[usr] = 0; }
+ modifier auth {
+ require(wards[msg.sender] == 1, "Jug/not-authorized");
+ _;
+ }
+
+ // --- Data ---
+ struct Ilk {
+ uint256 duty; // Collateral-specific, per-second stability fee contribution [ray]
+ uint256 rho; // Time of last drip [unix epoch time]
+ }
+
+ mapping (bytes32 => Ilk) public ilks;
+ VatLike public vat; // CDP Engine
+ address public vow; // Debt Engine
+ uint256 public base; // Global, per-second stability fee contribution [ray]
+
+ // --- Init ---
+ constructor(address vat_) public {
+ wards[msg.sender] = 1;
+ vat = VatLike(vat_);
+ }
+
+ // --- Math ---
+ function rpow(uint x, uint n, uint b) internal pure returns (uint z) {
+ assembly {
+ switch x case 0 {switch n case 0 {z := b} default {z := 0}}
+ default {
+ switch mod(n, 2) case 0 { z := b } default { z := x }
+ let half := div(b, 2) // for rounding.
+ for { n := div(n, 2) } n { n := div(n,2) } {
+ let xx := mul(x, x)
+ if iszero(eq(div(xx, x), x)) { revert(0,0) }
+ let xxRound := add(xx, half)
+ if lt(xxRound, xx) { revert(0,0) }
+ x := div(xxRound, b)
+ if mod(n,2) {
+ let zx := mul(z, x)
+ if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { revert(0,0) }
+ let zxRound := add(zx, half)
+ if lt(zxRound, zx) { revert(0,0) }
+ z := div(zxRound, b)
+ }
+ }
+ }
+ }
+ }
+ uint256 constant ONE = 10 ** 27;
+ function add(uint x, uint y) internal pure returns (uint z) {
+ z = x + y;
+ require(z >= x);
+ }
+ function diff(uint x, uint y) internal pure returns (int z) {
+ z = int(x) - int(y);
+ require(int(x) >= 0 && int(y) >= 0);
+ }
+ function rmul(uint x, uint y) internal pure returns (uint z) {
+ z = x * y;
+ require(y == 0 || z / y == x);
+ z = z / ONE;
+ }
+
+ // --- Administration ---
+ function init(bytes32 ilk) external note auth {
+ Ilk storage i = ilks[ilk];
+ require(i.duty == 0, "Jug/ilk-already-init");
+ i.duty = ONE;
+ i.rho = now;
+ }
+ function file(bytes32 ilk, bytes32 what, uint data) external note auth {
+ require(now == ilks[ilk].rho, "Jug/rho-not-updated");
+ if (what == "duty") ilks[ilk].duty = data;
+ else revert("Jug/file-unrecognized-param");
+ }
+ function file(bytes32 what, uint data) external note auth {
+ if (what == "base") base = data;
+ else revert("Jug/file-unrecognized-param");
+ }
+ function file(bytes32 what, address data) external note auth {
+ if (what == "vow") vow = data;
+ else revert("Jug/file-unrecognized-param");
+ }
+
+ // --- Stability Fee Collection ---
+ function drip(bytes32 ilk) external note returns (uint rate) {
+ require(now >= ilks[ilk].rho, "Jug/invalid-now");
+ (, uint prev) = vat.ilks(ilk);
+ rate = rmul(rpow(add(base, ilks[ilk].duty), now - ilks[ilk].rho, ONE), prev);
+ vat.fold(ilk, vow, diff(rate, prev));
+ ilks[ilk].rho = now;
+ }
+}
diff --git a/certora/harness/dss/Spotter.sol b/certora/harness/dss/Spotter.sol
new file mode 100644
index 00000000..a7785d87
--- /dev/null
+++ b/certora/harness/dss/Spotter.sol
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+/// spot.sol -- Spotter
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity >=0.5.12;
+
+contract LibNote {
+ event LogNote(
+ bytes4 indexed sig,
+ address indexed usr,
+ bytes32 indexed arg1,
+ bytes32 indexed arg2,
+ bytes data
+ ) anonymous;
+
+ modifier note {
+ _;
+ assembly {
+ // log an 'anonymous' event with a constant 6 words of calldata
+ // and four indexed topics: selector, caller, arg1 and arg2
+ let mark := msize() // end of memory ensures zero
+ mstore(0x40, add(mark, 288)) // update free memory pointer
+ mstore(mark, 0x20) // bytes type data offset
+ mstore(add(mark, 0x20), 224) // bytes size (padded)
+ calldatacopy(add(mark, 0x40), 0, 224) // bytes payload
+ log4(mark, 288, // calldata
+ shl(224, shr(224, calldataload(0))), // msg.sig
+ caller(), // msg.sender
+ calldataload(4), // arg1
+ calldataload(36) // arg2
+ )
+ }
+ }
+}
+
+interface VatLike {
+ function file(bytes32, bytes32, uint) external;
+}
+
+interface PipLike {
+ function peek() external returns (bytes32, bool);
+}
+
+contract Spotter is LibNote {
+ // --- Auth ---
+ mapping (address => uint) public wards;
+ function rely(address guy) external note auth { wards[guy] = 1; }
+ function deny(address guy) external note auth { wards[guy] = 0; }
+ modifier auth {
+ require(wards[msg.sender] == 1, "Spotter/not-authorized");
+ _;
+ }
+
+ // --- Data ---
+ struct Ilk {
+ PipLike pip; // Price Feed
+ uint256 mat; // Liquidation ratio [ray]
+ }
+
+ mapping (bytes32 => Ilk) public ilks;
+
+ VatLike public vat; // CDP Engine
+ uint256 public par; // ref per dai [ray]
+
+ uint256 public live;
+
+ // --- Events ---
+ event Poke(
+ bytes32 ilk,
+ bytes32 val, // [wad]
+ uint256 spot // [ray]
+ );
+
+ // --- Init ---
+ constructor(address vat_) public {
+ wards[msg.sender] = 1;
+ vat = VatLike(vat_);
+ par = ONE;
+ live = 1;
+ }
+
+ // --- Math ---
+ uint constant ONE = 10 ** 27;
+
+ function mul(uint x, uint y) internal pure returns (uint z) {
+ require(y == 0 || (z = x * y) / y == x);
+ }
+ function rdiv(uint x, uint y) internal pure returns (uint z) {
+ z = mul(x, ONE) / y;
+ }
+
+ // --- Administration ---
+ function file(bytes32 ilk, bytes32 what, address pip_) external note auth {
+ require(live == 1, "Spotter/not-live");
+ if (what == "pip") ilks[ilk].pip = PipLike(pip_);
+ else revert("Spotter/file-unrecognized-param");
+ }
+ function file(bytes32 what, uint data) external note auth {
+ require(live == 1, "Spotter/not-live");
+ if (what == "par") par = data;
+ else revert("Spotter/file-unrecognized-param");
+ }
+ function file(bytes32 ilk, bytes32 what, uint data) external note auth {
+ require(live == 1, "Spotter/not-live");
+ if (what == "mat") ilks[ilk].mat = data;
+ else revert("Spotter/file-unrecognized-param");
+ }
+
+ // --- Update value ---
+ function poke(bytes32 ilk) external {
+ (bytes32 val, bool has) = ilks[ilk].pip.peek();
+ uint256 spot = has ? rdiv(rdiv(mul(uint(val), 10 ** 9), par), ilks[ilk].mat) : 0;
+ vat.file(ilk, "spot", spot);
+ emit Poke(ilk, val, spot);
+ }
+
+ function cage() external note auth {
+ live = 0;
+ }
+}
diff --git a/certora/harness/dss/Vat.sol b/certora/harness/dss/Vat.sol
new file mode 100644
index 00000000..b3f9cbd2
--- /dev/null
+++ b/certora/harness/dss/Vat.sol
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+/// vat.sol -- Dai CDP database
+
+// Copyright (C) 2018 Rain
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity >=0.5.12;
+
+contract Vat {
+ // --- Auth ---
+ mapping (address => uint) public wards;
+ function rely(address usr) external note auth { require(live == 1, "Vat/not-live"); wards[usr] = 1; }
+ function deny(address usr) external note auth { require(live == 1, "Vat/not-live"); wards[usr] = 0; }
+ modifier auth {
+ require(wards[msg.sender] == 1, "Vat/not-authorized");
+ _;
+ }
+
+ mapping(address => mapping (address => uint)) public can;
+ function hope(address usr) external note { can[msg.sender][usr] = 1; }
+ function nope(address usr) external note { can[msg.sender][usr] = 0; }
+ function wish(address bit, address usr) internal view returns (bool) {
+ return either(bit == usr, can[bit][usr] == 1);
+ }
+
+ // --- Data ---
+ struct Ilk {
+ uint256 Art; // Total Normalised Debt [wad]
+ uint256 rate; // Accumulated Rates [ray]
+ uint256 spot; // Price with Safety Margin [ray]
+ uint256 line; // Debt Ceiling [rad]
+ uint256 dust; // Urn Debt Floor [rad]
+ }
+ struct Urn {
+ uint256 ink; // Locked Collateral [wad]
+ uint256 art; // Normalised Debt [wad]
+ }
+
+ mapping (bytes32 => Ilk) public ilks;
+ mapping (bytes32 => mapping (address => Urn )) public urns;
+ mapping (bytes32 => mapping (address => uint)) public gem; // [wad]
+ mapping (address => uint256) public dai; // [rad]
+ mapping (address => uint256) public sin; // [rad]
+
+ uint256 public debt; // Total Dai Issued [rad]
+ uint256 public vice; // Total Unbacked Dai [rad]
+ uint256 public Line; // Total Debt Ceiling [rad]
+ uint256 public live; // Active Flag
+
+ // --- Logs ---
+ event LogNote(
+ bytes4 indexed sig,
+ bytes32 indexed arg1,
+ bytes32 indexed arg2,
+ bytes32 indexed arg3,
+ bytes data
+ ) anonymous;
+
+ modifier note {
+ _;
+ assembly {
+ // log an 'anonymous' event with a constant 6 words of calldata
+ // and four indexed topics: the selector and the first three args
+ let mark := msize() // end of memory ensures zero
+ mstore(0x40, add(mark, 288)) // update free memory pointer
+ mstore(mark, 0x20) // bytes type data offset
+ mstore(add(mark, 0x20), 224) // bytes size (padded)
+ calldatacopy(add(mark, 0x40), 0, 224) // bytes payload
+ log4(mark, 288, // calldata
+ shl(224, shr(224, calldataload(0))), // msg.sig
+ calldataload(4), // arg1
+ calldataload(36), // arg2
+ calldataload(68) // arg3
+ )
+ }
+ }
+
+ // --- Init ---
+ constructor() public {
+ wards[msg.sender] = 1;
+ live = 1;
+ }
+
+ // --- Math ---
+ function add(uint x, int y) internal pure returns (uint z) {
+ z = x + uint(y);
+ require(y >= 0 || z <= x);
+ require(y <= 0 || z >= x);
+ }
+ function sub(uint x, int y) internal pure returns (uint z) {
+ z = x - uint(y);
+ require(y <= 0 || z <= x);
+ require(y >= 0 || z >= x);
+ }
+ function mul(uint x, int y) internal pure returns (int z) {
+ z = int(x) * y;
+ require(int(x) >= 0);
+ require(y == 0 || z / y == int(x));
+ }
+ function add(uint x, uint y) internal pure returns (uint z) {
+ require((z = x + y) >= x);
+ }
+ function sub(uint x, uint y) internal pure returns (uint z) {
+ require((z = x - y) <= x);
+ }
+ function mul(uint x, uint y) internal pure returns (uint z) {
+ require(y == 0 || (z = x * y) / y == x);
+ }
+
+ // --- Administration ---
+ function init(bytes32 ilk) external note auth {
+ require(ilks[ilk].rate == 0, "Vat/ilk-already-init");
+ ilks[ilk].rate = 10 ** 27;
+ }
+ function file(bytes32 what, uint data) external note auth {
+ require(live == 1, "Vat/not-live");
+ if (what == "Line") Line = data;
+ else revert("Vat/file-unrecognized-param");
+ }
+ function file(bytes32 ilk, bytes32 what, uint data) external note auth {
+ require(live == 1, "Vat/not-live");
+ if (what == "spot") ilks[ilk].spot = data;
+ else if (what == "line") ilks[ilk].line = data;
+ else if (what == "dust") ilks[ilk].dust = data;
+ else revert("Vat/file-unrecognized-param");
+ }
+ function cage() external note auth {
+ live = 0;
+ }
+
+ // --- Fungibility ---
+ function slip(bytes32 ilk, address usr, int256 wad) external note auth {
+ gem[ilk][usr] = add(gem[ilk][usr], wad);
+ }
+ function flux(bytes32 ilk, address src, address dst, uint256 wad) external note {
+ require(wish(src, msg.sender), "Vat/not-allowed");
+ gem[ilk][src] = sub(gem[ilk][src], wad);
+ gem[ilk][dst] = add(gem[ilk][dst], wad);
+ }
+ function move(address src, address dst, uint256 rad) external note {
+ require(wish(src, msg.sender), "Vat/not-allowed");
+ dai[src] = sub(dai[src], rad);
+ dai[dst] = add(dai[dst], rad);
+ }
+
+ function either(bool x, bool y) internal pure returns (bool z) {
+ assembly{ z := or(x, y)}
+ }
+ function both(bool x, bool y) internal pure returns (bool z) {
+ assembly{ z := and(x, y)}
+ }
+
+ // --- CDP Manipulation ---
+ function frob(bytes32 i, address u, address v, address w, int dink, int dart) external note {
+ // system is live
+ require(live == 1, "Vat/not-live");
+
+ Urn memory urn = urns[i][u];
+ Ilk memory ilk = ilks[i];
+ // ilk has been initialised
+ require(ilk.rate != 0, "Vat/ilk-not-init");
+
+ urn.ink = add(urn.ink, dink);
+ urn.art = add(urn.art, dart);
+ ilk.Art = add(ilk.Art, dart);
+
+ int dtab = mul(ilk.rate, dart);
+ uint tab = mul(ilk.rate, urn.art);
+ debt = add(debt, dtab);
+
+ // either debt has decreased, or debt ceilings are not exceeded
+ require(either(dart <= 0, both(mul(ilk.Art, ilk.rate) <= ilk.line, debt <= Line)), "Vat/ceiling-exceeded");
+ // urn is either less risky than before, or it is safe
+ require(either(both(dart <= 0, dink >= 0), tab <= mul(urn.ink, ilk.spot)), "Vat/not-safe");
+
+ // urn is either more safe, or the owner consents
+ require(either(both(dart <= 0, dink >= 0), wish(u, msg.sender)), "Vat/not-allowed-u");
+ // collateral src consents
+ require(either(dink <= 0, wish(v, msg.sender)), "Vat/not-allowed-v");
+ // debt dst consents
+ require(either(dart >= 0, wish(w, msg.sender)), "Vat/not-allowed-w");
+
+ // urn has no debt, or a non-dusty amount
+ require(either(urn.art == 0, tab >= ilk.dust), "Vat/dust");
+
+ gem[i][v] = sub(gem[i][v], dink);
+ dai[w] = add(dai[w], dtab);
+
+ urns[i][u] = urn;
+ ilks[i] = ilk;
+ }
+ // --- CDP Fungibility ---
+ function fork(bytes32 ilk, address src, address dst, int dink, int dart) external note {
+ Urn storage u = urns[ilk][src];
+ Urn storage v = urns[ilk][dst];
+ Ilk storage i = ilks[ilk];
+
+ u.ink = sub(u.ink, dink);
+ u.art = sub(u.art, dart);
+ v.ink = add(v.ink, dink);
+ v.art = add(v.art, dart);
+
+ uint utab = mul(u.art, i.rate);
+ uint vtab = mul(v.art, i.rate);
+
+ // both sides consent
+ require(both(wish(src, msg.sender), wish(dst, msg.sender)), "Vat/not-allowed");
+
+ // both sides safe
+ require(utab <= mul(u.ink, i.spot), "Vat/not-safe-src");
+ require(vtab <= mul(v.ink, i.spot), "Vat/not-safe-dst");
+
+ // both sides non-dusty
+ require(either(utab >= i.dust, u.art == 0), "Vat/dust-src");
+ require(either(vtab >= i.dust, v.art == 0), "Vat/dust-dst");
+ }
+ // --- CDP Confiscation ---
+ function grab(bytes32 i, address u, address v, address w, int dink, int dart) external note auth {
+ Urn storage urn = urns[i][u];
+ Ilk storage ilk = ilks[i];
+
+ urn.ink = add(urn.ink, dink);
+ urn.art = add(urn.art, dart);
+ ilk.Art = add(ilk.Art, dart);
+
+ int dtab = mul(ilk.rate, dart);
+
+ gem[i][v] = sub(gem[i][v], dink);
+ sin[w] = sub(sin[w], dtab);
+ vice = sub(vice, dtab);
+ }
+
+ // --- Settlement ---
+ function heal(uint rad) external note {
+ address u = msg.sender;
+ sin[u] = sub(sin[u], rad);
+ dai[u] = sub(dai[u], rad);
+ vice = sub(vice, rad);
+ debt = sub(debt, rad);
+ }
+ function suck(address u, address v, uint rad) external note auth {
+ sin[u] = add(sin[u], rad);
+ dai[v] = add(dai[v], rad);
+ vice = add(vice, rad);
+ debt = add(debt, rad);
+ }
+
+ // --- Rates ---
+ function fold(bytes32 i, address u, int rate) external note auth {
+ require(live == 1, "Vat/not-live");
+ Ilk storage ilk = ilks[i];
+ ilk.rate = add(ilk.rate, rate);
+ int rad = mul(ilk.Art, rate);
+ dai[u] = add(dai[u], rad);
+ debt = add(debt, rad);
+ }
+}
diff --git a/certora/harness/tokens/MkrMock.sol b/certora/harness/tokens/MkrMock.sol
new file mode 100644
index 00000000..aba48514
--- /dev/null
+++ b/certora/harness/tokens/MkrMock.sol
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+import {GemMock} from "../../../test/mocks/GemMock.sol";
+
+contract MkrMock is GemMock {
+
+ constructor(uint256 initialSupply) GemMock(initialSupply) {
+ }
+}
diff --git a/certora/harness/tokens/RewardsMock.sol b/certora/harness/tokens/RewardsMock.sol
new file mode 100644
index 00000000..0e8e582b
--- /dev/null
+++ b/certora/harness/tokens/RewardsMock.sol
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+import {GemMock} from "../../../test/mocks/GemMock.sol";
+
+contract RewardsMock is GemMock {
+
+ constructor(uint256 initialSupply) GemMock(initialSupply) {
+ }
+}
diff --git a/certora/harness/tokens/SkyMock.sol b/certora/harness/tokens/SkyMock.sol
new file mode 100644
index 00000000..7ea9079b
--- /dev/null
+++ b/certora/harness/tokens/SkyMock.sol
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+import {GemMock} from "../../../test/mocks/GemMock.sol";
+
+contract SkyMock is GemMock {
+
+ constructor(uint256 initialSupply) GemMock(initialSupply) {
+ }
+}
diff --git a/deploy/LockstakeDeploy.sol b/deploy/LockstakeDeploy.sol
new file mode 100644
index 00000000..fde6baaa
--- /dev/null
+++ b/deploy/LockstakeDeploy.sol
@@ -0,0 +1,51 @@
+// SPDX-FileCopyrightText: © 2023 Dai Foundation
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.21;
+
+import { ScriptTools } from "dss-test/ScriptTools.sol";
+import { MCD, DssInstance } from "dss-test/MCD.sol";
+import { LockstakeInstance } from "./LockstakeInstance.sol";
+import { LockstakeMkr } from "src/LockstakeMkr.sol";
+import { LockstakeEngine } from "src/LockstakeEngine.sol";
+import { LockstakeClipper } from "src/LockstakeClipper.sol";
+
+// Deploy a Lockstake instance
+library LockstakeDeploy {
+
+ function deployLockstake(
+ address deployer,
+ address owner,
+ address voteDelegateFactory,
+ address usdsJoin,
+ bytes32 ilk,
+ address mkrSky,
+ bytes4 calcSig
+ ) internal returns (LockstakeInstance memory lockstakeInstance) {
+ DssInstance memory dss = MCD.loadFromChainlog(0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F);
+
+ lockstakeInstance.lsmkr = address(new LockstakeMkr());
+ lockstakeInstance.engine = address(new LockstakeEngine(voteDelegateFactory, usdsJoin, ilk, mkrSky, lockstakeInstance.lsmkr));
+ lockstakeInstance.clipper = address(new LockstakeClipper(address(dss.vat), address(dss.spotter), address(dss.dog), lockstakeInstance.engine));
+ (bool ok, bytes memory returnV) = dss.chainlog.getAddress("CALC_FAB").call(abi.encodeWithSelector(calcSig, owner));
+ require(ok);
+ lockstakeInstance.clipperCalc = abi.decode(returnV, (address));
+
+ ScriptTools.switchOwner(lockstakeInstance.lsmkr, deployer, owner);
+ ScriptTools.switchOwner(lockstakeInstance.engine, deployer, owner);
+ ScriptTools.switchOwner(lockstakeInstance.clipper, deployer, owner);
+ }
+}
diff --git a/deploy/LockstakeInit.sol b/deploy/LockstakeInit.sol
new file mode 100644
index 00000000..57809d25
--- /dev/null
+++ b/deploy/LockstakeInit.sol
@@ -0,0 +1,259 @@
+// SPDX-FileCopyrightText: © 2023 Dai Foundation
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity >=0.8.0;
+
+import { DssInstance } from "dss-test/MCD.sol";
+import { LockstakeInstance } from "./LockstakeInstance.sol";
+
+interface LockstakeMkrLike {
+ function rely(address) external;
+}
+
+interface LockstakeEngineLike {
+ function voteDelegateFactory() external view returns (address);
+ function vat() external view returns (address);
+ function usdsJoin() external view returns (address);
+ function usds() external view returns (address);
+ function ilk() external view returns (bytes32);
+ function mkr() external view returns (address);
+ function lsmkr() external view returns (address);
+ function fee() external view returns (uint256);
+ function mkrSky() external view returns (address);
+ function sky() external view returns (address);
+ function rely(address) external;
+ function file(bytes32, address) external;
+ function file(bytes32, uint256) external;
+ function addFarm(address) external;
+}
+
+interface LockstakeClipperLike {
+ function vat() external view returns (address);
+ function dog() external view returns (address);
+ function spotter() external view returns (address);
+ function engine() external view returns (address);
+ function ilk() external view returns (bytes32);
+ function rely(address) external;
+ function file(bytes32, address) external;
+ function file(bytes32, uint256) external;
+ function upchost() external;
+}
+
+interface PipLike {
+ function kiss(address) external;
+ function rely(address) external;
+}
+
+interface CalcLike {
+ function file(bytes32, uint256) external;
+}
+
+interface AutoLineLike {
+ function setIlk(bytes32, uint256, uint256, uint256) external;
+}
+
+interface OsmMomLike {
+ function setOsm(bytes32, address) external;
+}
+
+interface LineMomLike {
+ function addIlk(bytes32) external;
+}
+
+interface ClipperMomLike {
+ function setPriceTolerance(address, uint256) external;
+}
+
+interface StakingRewardsLike {
+ function stakingToken() external view returns (address);
+}
+
+interface IlkRegistryLike {
+ function put(
+ bytes32 _ilk,
+ address _join,
+ address _gem,
+ uint256 _dec,
+ uint256 _class,
+ address _pip,
+ address _xlip,
+ string memory _name,
+ string memory _symbol
+ ) external;
+}
+
+struct LockstakeConfig {
+ bytes32 ilk;
+ address voteDelegateFactory;
+ address usdsJoin;
+ address usds;
+ address mkr;
+ address mkrSky;
+ address sky;
+ address[] farms;
+ uint256 fee;
+ uint256 maxLine;
+ uint256 gap;
+ uint256 ttl;
+ uint256 dust;
+ uint256 duty;
+ uint256 mat;
+ uint256 buf;
+ uint256 tail;
+ uint256 cusp;
+ uint256 chip;
+ uint256 tip;
+ uint256 stopped;
+ uint256 chop;
+ uint256 hole;
+ uint256 tau;
+ uint256 cut;
+ uint256 step;
+ bool lineMom;
+ uint256 tolerance;
+ string name;
+ string symbol;
+}
+
+library LockstakeInit {
+ uint256 constant internal RATES_ONE_HUNDRED_PCT = 1000000021979553151239153027;
+ uint256 constant internal WAD = 10**18;
+ uint256 constant internal RAY = 10**27;
+ uint256 constant internal RAD = 10**45;
+
+ function initLockstake(
+ DssInstance memory dss,
+ LockstakeInstance memory lockstakeInstance,
+ LockstakeConfig memory cfg
+ ) internal {
+ LockstakeEngineLike engine = LockstakeEngineLike(lockstakeInstance.engine);
+ LockstakeClipperLike clipper = LockstakeClipperLike(lockstakeInstance.clipper);
+ CalcLike calc = CalcLike(lockstakeInstance.clipperCalc);
+
+ // Sanity checks
+ require(engine.voteDelegateFactory() == cfg.voteDelegateFactory, "Engine voteDelegateFactory mismatch");
+ require(engine.vat() == address(dss.vat), "Engine vat mismatch");
+ require(engine.usdsJoin() == cfg.usdsJoin, "Engine usdsJoin mismatch");
+ require(engine.usds() == cfg.usds, "Engine usds mismatch");
+ require(engine.ilk() == cfg.ilk, "Engine ilk mismatch");
+ require(engine.mkr() == cfg.mkr, "Engine mkr mismatch");
+ require(engine.lsmkr() == lockstakeInstance.lsmkr, "Engine lsmkr mismatch");
+ require(engine.mkrSky() == cfg.mkrSky, "Engine mkrSky mismatch");
+ require(engine.sky() == cfg.sky, "Engine sky mismatch");
+ require(clipper.ilk() == cfg.ilk, "Clipper ilk mismatch");
+ require(clipper.vat() == address(dss.vat), "Clipper vat mismatch");
+ require(clipper.engine() == address(engine), "Clipper engine mismatch");
+ require(clipper.dog() == address(dss.dog), "Clipper dog mismatch");
+ require(clipper.spotter() == address(dss.spotter), "Clipper spotter mismatch");
+
+ require(cfg.gap <= cfg.maxLine, "gap greater than max line");
+ require(cfg.dust <= cfg.hole, "dust greater than hole");
+ require(cfg.duty >= RAY && cfg.duty <= RATES_ONE_HUNDRED_PCT, "duty out of boundaries");
+ require(cfg.mat >= RAY && cfg.mat < 10 * RAY, "mat out of boundaries");
+ require(cfg.buf >= RAY && cfg.buf < 10 * RAY, "buf out of boundaries");
+ require(cfg.cusp < RAY, "cusp negative drop value");
+ require(cfg.chip < WAD, "chip equal or greater than 100%");
+ require(cfg.tip <= 1_000 * RAD, "tip out of boundaries");
+ require(cfg.chop >= WAD && cfg.chop < 2 * WAD, "chop out of boundaries");
+ require(cfg.tolerance < RAY, "tolerance equal or greater than 100%");
+
+ dss.vat.init(cfg.ilk);
+ dss.vat.file(cfg.ilk, "line", cfg.gap);
+ dss.vat.file("Line", dss.vat.Line() + cfg.gap);
+ dss.vat.file(cfg.ilk, "dust", cfg.dust);
+ dss.vat.rely(address(engine));
+ dss.vat.rely(address(clipper));
+
+ AutoLineLike(dss.chainlog.getAddress("MCD_IAM_AUTO_LINE")).setIlk(cfg.ilk, cfg.maxLine, cfg.gap, cfg.ttl);
+
+ dss.jug.init(cfg.ilk);
+ dss.jug.file(cfg.ilk, "duty", cfg.duty);
+
+ address pip = dss.chainlog.getAddress("PIP_MKR");
+ address clipperMom = dss.chainlog.getAddress("CLIPPER_MOM");
+ PipLike(pip).kiss(address(dss.spotter));
+ PipLike(pip).kiss(address(clipper));
+ PipLike(pip).kiss(clipperMom);
+ PipLike(pip).kiss(address(dss.end));
+ // This assumes pip is a standard Osm sourced by a Median
+ {
+ address osmMom = dss.chainlog.getAddress("OSM_MOM");
+ PipLike(pip).rely(osmMom);
+ OsmMomLike(osmMom).setOsm(cfg.ilk, pip);
+ }
+
+ dss.spotter.file(cfg.ilk, "mat", cfg.mat);
+ dss.spotter.file(cfg.ilk, "pip", pip);
+ dss.spotter.poke(cfg.ilk);
+
+ dss.dog.file(cfg.ilk, "clip", address(clipper));
+ dss.dog.file(cfg.ilk, "chop", cfg.chop);
+ dss.dog.file(cfg.ilk, "hole", cfg.hole);
+ dss.dog.rely(address(clipper));
+
+ LockstakeMkrLike(lockstakeInstance.lsmkr).rely(address(engine));
+
+ engine.file("jug", address(dss.jug));
+ engine.file("fee", cfg.fee);
+ for (uint256 i = 0; i < cfg.farms.length; i++) {
+ require(StakingRewardsLike(cfg.farms[i]).stakingToken() == lockstakeInstance.lsmkr, "Farm staking token mismatch");
+ engine.addFarm(cfg.farms[i]);
+ }
+ engine.rely(address(clipper));
+
+ clipper.file("buf", cfg.buf);
+ clipper.file("tail", cfg.tail);
+ clipper.file("cusp", cfg.cusp);
+ clipper.file("chip", cfg.chip);
+ clipper.file("tip", cfg.tip);
+ clipper.file("stopped", cfg.stopped);
+ clipper.file("vow", address(dss.vow));
+ clipper.file("calc", address(calc));
+ clipper.upchost();
+ clipper.rely(address(dss.dog));
+ clipper.rely(address(dss.end));
+ clipper.rely(clipperMom);
+
+ if (cfg.tau > 0) calc.file("tau", cfg.tau);
+ if (cfg.cut > 0) calc.file("cut", cfg.cut);
+ if (cfg.step > 0) calc.file("step", cfg.step);
+
+ if (cfg.lineMom) {
+ LineMomLike(dss.chainlog.getAddress("LINE_MOM")).addIlk(cfg.ilk);
+ }
+
+ if (cfg.tolerance > 0) {
+ ClipperMomLike(clipperMom).setPriceTolerance(address(clipper), cfg.tolerance);
+ }
+
+ IlkRegistryLike(dss.chainlog.getAddress("ILK_REGISTRY")).put(
+ cfg.ilk,
+ address(0),
+ cfg.mkr,
+ 18,
+ 7, // New class
+ pip,
+ address(clipper),
+ cfg.name,
+ cfg.symbol
+ );
+
+ dss.chainlog.setAddress("LOCKSTAKE_MKR", lockstakeInstance.lsmkr);
+ dss.chainlog.setAddress("LOCKSTAKE_ENGINE", address(engine));
+ dss.chainlog.setAddress("LOCKSTAKE_CLIP", address(clipper));
+ dss.chainlog.setAddress("LOCKSTAKE_CLIP_CALC", address(calc));
+ }
+}
diff --git a/deploy/LockstakeInstance.sol b/deploy/LockstakeInstance.sol
new file mode 100644
index 00000000..d249711d
--- /dev/null
+++ b/deploy/LockstakeInstance.sol
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: © 2023 Dai Foundation
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity >=0.8.0;
+
+struct LockstakeInstance {
+ address lsmkr;
+ address engine;
+ address clipper;
+ address clipperCalc;
+}
diff --git a/foundry.toml b/foundry.toml
index e883058f..39143a1f 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -2,5 +2,13 @@
src = "src"
out = "out"
libs = ["lib"]
+solc = "0.8.21"
+optimizer = true
+optimizer_runs = 200
+verbosity = 1
+
+invariant.runs = 1
+invariant.depth = 100
+invariant.preserve_state = true # For using cheats in handlers (https://github.com/foundry-rs/foundry/pull/7219)
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
diff --git a/lib/dss-test b/lib/dss-test
deleted file mode 160000
index df7b13ea..00000000
--- a/lib/dss-test
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit df7b13ead253f4b831df2464f7d74f28a091a790
diff --git a/lib/token-tests b/lib/token-tests
new file mode 160000
index 00000000..6bdd5a38
--- /dev/null
+++ b/lib/token-tests
@@ -0,0 +1 @@
+Subproject commit 6bdd5a38586970acf27166c3205093fd3450d530
diff --git a/src/LockstakeClipper.sol b/src/LockstakeClipper.sol
new file mode 100644
index 00000000..d2e61011
--- /dev/null
+++ b/src/LockstakeClipper.sol
@@ -0,0 +1,485 @@
+// SPDX-FileCopyrightText: © 2021 Dai Foundation
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.21;
+
+interface VatLike {
+ function suck(address, address, uint256) external;
+ function move(address, address, uint256) external;
+ function flux(bytes32, address, address, uint256) external;
+ function slip(bytes32, address, int256) external;
+ function ilks(bytes32) external view returns (uint256, uint256, uint256, uint256, uint256);
+}
+
+interface PipLike {
+ function peek() external returns (bytes32, bool);
+}
+
+interface SpotterLike {
+ function par() external returns (uint256);
+ function ilks(bytes32) external returns (PipLike, uint256);
+}
+
+interface DogLike {
+ function chop(bytes32) external returns (uint256);
+ function digs(bytes32, uint256) external;
+}
+
+interface ClipperCallee {
+ function clipperCall(address, uint256, uint256, bytes calldata) external;
+}
+
+interface AbacusLike {
+ function price(uint256, uint256) external view returns (uint256);
+}
+
+interface LockstakeEngineLike {
+ function ilk() external view returns (bytes32);
+ function onKick(address, uint256) external;
+ function onTake(address, address, uint256) external;
+ function onRemove(address, uint256, uint256) external;
+}
+
+// Clipper for use with the Lockstake Engine
+contract LockstakeClipper {
+ // --- Auth ---
+ mapping (address => uint256) public wards;
+ function rely(address usr) external auth { wards[usr] = 1; emit Rely(usr); }
+ function deny(address usr) external auth { wards[usr] = 0; emit Deny(usr); }
+ modifier auth {
+ require(wards[msg.sender] == 1, "LockstakeClipper/not-authorized");
+ _;
+ }
+
+ // --- Data ---
+ bytes32 immutable public ilk; // Collateral type of this LockstakeClipper
+ VatLike immutable public vat; // Core CDP Engine
+ LockstakeEngineLike immutable public engine; // Lockstake Engine
+
+ DogLike public dog; // Liquidation module
+ address public vow; // Recipient of dai raised in auctions
+ SpotterLike public spotter; // Collateral price module
+ AbacusLike public calc; // Current price calculator
+
+ uint256 public buf; // Multiplicative factor to increase starting price [ray]
+ uint256 public tail; // Time elapsed before auction reset [seconds]
+ uint256 public cusp; // Percentage drop before auction reset [ray]
+ uint64 public chip; // Percentage of tab to suck from vow to incentivize keepers [wad]
+ uint192 public tip; // Flat fee to suck from vow to incentivize keepers [rad]
+ uint256 public chost; // Cache the ilk dust times the ilk chop to prevent excessive SLOADs [rad]
+
+ uint256 public kicks; // Total auctions
+ uint256[] public active; // Array of active auction ids
+
+ struct Sale {
+ uint256 pos; // Index in active array
+ uint256 tab; // Dai to raise [rad]
+ uint256 lot; // collateral to sell [wad]
+ uint256 tot; // static registry of total collateral to sell [wad]
+ address usr; // Liquidated CDP
+ uint96 tic; // Auction start time
+ uint256 top; // Starting price [ray]
+ }
+ mapping(uint256 => Sale) public sales;
+
+ uint256 internal locked;
+
+ // Levels for circuit breaker
+ // 0: no breaker
+ // 1: no new kick()
+ // 2: no new kick() or redo()
+ // 3: no new kick(), redo(), or take()
+ uint256 public stopped = 0;
+
+ // --- Events ---
+ event Rely(address indexed usr);
+ event Deny(address indexed usr);
+
+ event File(bytes32 indexed what, uint256 data);
+ event File(bytes32 indexed what, address data);
+
+ event Kick(
+ uint256 indexed id,
+ uint256 top,
+ uint256 tab,
+ uint256 lot,
+ address indexed usr,
+ address indexed kpr,
+ uint256 coin
+ );
+ event Take(
+ uint256 indexed id,
+ uint256 max,
+ uint256 price,
+ uint256 owe,
+ uint256 tab,
+ uint256 lot,
+ address indexed usr
+ );
+ event Redo(
+ uint256 indexed id,
+ uint256 top,
+ uint256 tab,
+ uint256 lot,
+ address indexed usr,
+ address indexed kpr,
+ uint256 coin
+ );
+
+ event Yank(uint256 id);
+
+ // --- Init ---
+ constructor(address vat_, address spotter_, address dog_, address engine_) {
+ vat = VatLike(vat_);
+ spotter = SpotterLike(spotter_);
+ dog = DogLike(dog_);
+ engine = LockstakeEngineLike(engine_);
+ ilk = engine.ilk();
+ buf = RAY;
+ wards[msg.sender] = 1;
+ emit Rely(msg.sender);
+ }
+
+ // --- Synchronization ---
+ modifier lock {
+ require(locked == 0, "LockstakeClipper/system-locked");
+ locked = 1;
+ _;
+ locked = 0;
+ }
+
+ modifier isStopped(uint256 level) {
+ require(stopped < level, "LockstakeClipper/stopped-incorrect");
+ _;
+ }
+
+ // --- Administration ---
+ function file(bytes32 what, uint256 data) external auth lock {
+ if (what == "buf") buf = data;
+ else if (what == "tail") tail = data; // Time elapsed before auction reset
+ else if (what == "cusp") cusp = data; // Percentage drop before auction reset
+ else if (what == "chip") chip = uint64(data); // Percentage of tab to incentivize (max: 2^64 - 1 => 18.xxx WAD = 18xx%)
+ else if (what == "tip") tip = uint192(data); // Flat fee to incentivize keepers (max: 2^192 - 1 => 6.277T RAD)
+ else if (what == "stopped") stopped = data; // Set breaker (0, 1, 2, or 3)
+ else revert("LockstakeClipper/file-unrecognized-param");
+ emit File(what, data);
+ }
+ function file(bytes32 what, address data) external auth lock {
+ if (what == "spotter") spotter = SpotterLike(data);
+ else if (what == "dog") dog = DogLike(data);
+ else if (what == "vow") vow = data;
+ else if (what == "calc") calc = AbacusLike(data);
+ else revert("LockstakeClipper/file-unrecognized-param");
+ emit File(what, data);
+ }
+
+ // --- Math ---
+ uint256 constant BLN = 10 ** 9;
+ uint256 constant WAD = 10 ** 18;
+ uint256 constant RAY = 10 ** 27;
+
+ function min(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ z = x <= y ? x : y;
+ }
+ function wmul(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ z = x * y / WAD;
+ }
+ function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ z = x * y / RAY;
+ }
+ function rdiv(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ z = x * RAY / y;
+ }
+
+ // --- Auction ---
+
+ // get the price directly from the pip
+ // Could get this from rmul(Vat.ilks(ilk).spot, Spotter.mat()) instead, but
+ // if mat has changed since the last poke, the resulting value will be
+ // incorrect.
+ function getFeedPrice() internal returns (uint256 feedPrice) {
+ (PipLike pip, ) = spotter.ilks(ilk);
+ (bytes32 val, bool has) = pip.peek();
+ require(has, "LockstakeClipper/invalid-price");
+ feedPrice = rdiv(uint256(val) * BLN, spotter.par());
+ }
+
+ // start an auction
+ // note: trusts the caller to transfer collateral to the contract
+ // The starting price `top` is obtained as follows:
+ //
+ // top = val * buf / par
+ //
+ // Where `val` is the collateral's unitary value in USD, `buf` is a
+ // multiplicative factor to increase the starting price, and `par` is a
+ // reference per DAI.
+ function kick(
+ uint256 tab, // Debt [rad]
+ uint256 lot, // Collateral [wad]
+ address usr, // Address that will receive any leftover collateral; additionally assumed here to be the liquidated Vault.
+ address kpr // Address that will receive incentives
+ ) external auth lock isStopped(1) returns (uint256 id) {
+ // Input validation
+ require(tab > 0, "LockstakeClipper/zero-tab");
+ require(lot > 0, "LockstakeClipper/zero-lot");
+ require(lot <= uint256(type(int256).max), "LockstakeClipper/over-maxint-lot"); // This is ensured by the dog but we still prefer to be explicit
+ require(usr != address(0), "LockstakeClipper/zero-usr");
+ unchecked { id = ++kicks; }
+ require(id > 0, "LockstakeClipper/overflow");
+
+ active.push(id);
+
+ sales[id].pos = active.length - 1;
+
+ sales[id].tab = tab;
+ sales[id].lot = lot;
+ sales[id].tot = lot;
+ sales[id].usr = usr;
+ sales[id].tic = uint96(block.timestamp);
+
+ uint256 top;
+ top = rmul(getFeedPrice(), buf);
+ require(top > 0, "LockstakeClipper/zero-top-price");
+ sales[id].top = top;
+
+ // incentive to kick auction
+ uint256 _tip = tip;
+ uint256 _chip = chip;
+ uint256 coin;
+ if (_tip > 0 || _chip > 0) {
+ coin = _tip + wmul(tab, _chip);
+ vat.suck(vow, kpr, coin);
+ }
+
+ // Trigger engine liquidation call-back
+ engine.onKick(usr, lot);
+
+ emit Kick(id, top, tab, lot, usr, kpr, coin);
+ }
+
+ // Reset an auction
+ // See `kick` above for an explanation of the computation of `top`.
+ function redo(
+ uint256 id, // id of the auction to reset
+ address kpr // Address that will receive incentives
+ ) external lock isStopped(2) {
+ // Read auction data
+ address usr = sales[id].usr;
+ uint96 tic = sales[id].tic;
+ uint256 top = sales[id].top;
+
+ require(usr != address(0), "LockstakeClipper/not-running-auction");
+
+ // Check that auction needs reset
+ // and compute current price [ray]
+ (bool done,) = status(tic, top);
+ require(done, "LockstakeClipper/cannot-reset");
+
+ uint256 tab = sales[id].tab;
+ uint256 lot = sales[id].lot;
+ sales[id].tic = uint96(block.timestamp);
+
+ uint256 feedPrice = getFeedPrice();
+ top = rmul(feedPrice, buf);
+ require(top > 0, "LockstakeClipper/zero-top-price");
+ sales[id].top = top;
+
+ // incentive to redo auction
+ uint256 _tip = tip;
+ uint256 _chip = chip;
+ uint256 coin;
+ if (_tip > 0 || _chip > 0) {
+ uint256 _chost = chost;
+ if (tab >= _chost && lot * feedPrice >= _chost) {
+ coin = _tip + wmul(tab, _chip);
+ vat.suck(vow, kpr, coin);
+ }
+ }
+
+ emit Redo(id, top, tab, lot, usr, kpr, coin);
+ }
+
+ // Buy up to `amt` of collateral from the auction indexed by `id`.
+ //
+ // Auctions will not collect more DAI than their assigned DAI target,`tab`;
+ // thus, if `amt` would cost more DAI than `tab` at the current price, the
+ // amount of collateral purchased will instead be just enough to collect `tab` DAI.
+ //
+ // To avoid partial purchases resulting in very small leftover auctions that will
+ // never be cleared, any partial purchase must leave at least `LockstakeClipper.chost`
+ // remaining DAI target. `chost` is an asynchronously updated value equal to
+ // (Vat.dust * Dog.chop(ilk) / WAD) where the values are understood to be determined
+ // by whatever they were when LockstakeClipper.upchost() was last called. Purchase amounts
+ // will be minimally decreased when necessary to respect this limit; i.e., if the
+ // specified `amt` would leave `tab < chost` but `tab > 0`, the amount actually
+ // purchased will be such that `tab == chost`.
+ //
+ // If `tab <= chost`, partial purchases are no longer possible; that is, the remaining
+ // collateral can only be purchased entirely, or not at all.
+ function take(
+ uint256 id, // Auction id
+ uint256 amt, // Upper limit on amount of collateral to buy [wad]
+ uint256 max, // Maximum acceptable price (DAI / collateral) [ray]
+ address who, // Receiver of collateral and external call address
+ bytes calldata data // Data to pass in external call; if length 0, no call is done
+ ) external lock isStopped(3) {
+
+ address usr = sales[id].usr;
+ uint96 tic = sales[id].tic;
+
+ require(usr != address(0), "LockstakeClipper/not-running-auction");
+
+ uint256 price;
+ {
+ bool done;
+ (done, price) = status(tic, sales[id].top);
+
+ // Check that auction doesn't need reset
+ require(!done, "LockstakeClipper/needs-reset");
+ }
+
+ // Ensure price is acceptable to buyer
+ require(max >= price, "LockstakeClipper/too-expensive");
+
+ uint256 lot = sales[id].lot;
+ uint256 tab = sales[id].tab;
+ uint256 owe;
+
+ {
+ // Purchase as much as possible, up to amt
+ uint256 slice = min(lot, amt); // slice <= lot
+
+ // DAI needed to buy a slice of this sale
+ owe = slice * price;
+
+ // Don't collect more than tab of DAI
+ if (owe > tab) {
+ // Total debt will be paid
+ owe = tab; // owe' <= owe
+ // Adjust slice
+ slice = owe / price; // slice' = owe' / price <= owe / price == slice <= lot
+ } else if (owe < tab && slice < lot) {
+ // If slice == lot => auction completed => dust doesn't matter
+ uint256 _chost = chost;
+ if (tab - owe < _chost) { // safe as owe < tab
+ // If tab <= chost, buyers have to take the entire lot.
+ require(tab > _chost, "LockstakeClipper/no-partial-purchase");
+ // Adjust amount to pay
+ owe = tab - _chost; // owe' <= owe
+ // Adjust slice
+ slice = owe / price; // slice' = owe' / price < owe / price == slice < lot
+ }
+ }
+
+ // Calculate remaining tab after operation
+ tab = tab - owe; // safe since owe <= tab
+ // Calculate remaining lot after operation
+ lot = lot - slice;
+
+ // Send collateral to who
+ vat.slip(ilk, address(this), -int256(slice));
+ engine.onTake(usr, who, slice);
+
+ // Do external call (if data is defined) but to be
+ // extremely careful we don't allow to do it to the three
+ // contracts which the LockstakeClipper needs to be authorized
+ DogLike dog_ = dog;
+ if (data.length > 0 && who != address(vat) && who != address(dog_) && who != address(engine)) {
+ ClipperCallee(who).clipperCall(msg.sender, owe, slice, data);
+ }
+
+ // Get DAI from caller
+ vat.move(msg.sender, vow, owe);
+
+ // Removes Dai out for liquidation from accumulator
+ dog_.digs(ilk, lot == 0 ? tab + owe : owe);
+ }
+
+ if (lot == 0) {
+ uint256 tot = sales[id].tot;
+ engine.onRemove(usr, tot, 0);
+ _remove(id);
+ } else if (tab == 0) {
+ uint256 tot = sales[id].tot;
+ vat.slip(ilk, address(this), -int256(lot));
+ engine.onRemove(usr, tot - lot, lot);
+ _remove(id);
+ } else {
+ sales[id].tab = tab;
+ sales[id].lot = lot;
+ }
+
+ emit Take(id, max, price, owe, tab, lot, usr);
+ }
+
+ function _remove(uint256 id) internal {
+ uint256 _move = active[active.length - 1];
+ if (id != _move) {
+ uint256 _index = sales[id].pos;
+ active[_index] = _move;
+ sales[_move].pos = _index;
+ }
+ active.pop();
+ delete sales[id];
+ }
+
+ // The number of active auctions
+ function count() external view returns (uint256) {
+ return active.length;
+ }
+
+ // Return the entire array of active auctions
+ function list() external view returns (uint256[] memory) {
+ return active;
+ }
+
+ // Externally returns boolean for if an auction needs a redo and also the current price
+ function getStatus(uint256 id) external view returns (bool needsRedo, uint256 price, uint256 lot, uint256 tab) {
+ // Read auction data
+ address usr = sales[id].usr;
+ uint96 tic = sales[id].tic;
+
+ bool done;
+ (done, price) = status(tic, sales[id].top);
+
+ needsRedo = usr != address(0) && done;
+ lot = sales[id].lot;
+ tab = sales[id].tab;
+ }
+
+ // Internally returns boolean for if an auction needs a redo
+ function status(uint96 tic, uint256 top) internal view returns (bool done, uint256 price) {
+ price = calc.price(top, block.timestamp - tic);
+ done = (block.timestamp - tic > tail || rdiv(price, top) < cusp);
+ }
+
+ // Public function to update the cached dust*chop value.
+ function upchost() external {
+ (,,,, uint256 _dust) = VatLike(vat).ilks(ilk);
+ chost = wmul(_dust, dog.chop(ilk));
+ }
+
+ // Cancel an auction during End.cage or via other governance action.
+ function yank(uint256 id) external auth lock {
+ require(sales[id].usr != address(0), "LockstakeClipper/not-running-auction");
+ dog.digs(ilk, sales[id].tab);
+ uint256 lot = sales[id].lot;
+ vat.flux(ilk, address(this), msg.sender, lot);
+ engine.onRemove(sales[id].usr, 0, 0);
+ _remove(id);
+ emit Yank(id);
+ }
+}
diff --git a/src/LockstakeEngine.sol b/src/LockstakeEngine.sol
new file mode 100644
index 00000000..e4e019bd
--- /dev/null
+++ b/src/LockstakeEngine.sol
@@ -0,0 +1,472 @@
+// SPDX-FileCopyrightText: © 2023 Dai Foundation
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.21;
+
+import { LockstakeUrn } from "src/LockstakeUrn.sol";
+import { Multicall } from "src/Multicall.sol";
+
+interface VoteDelegateFactoryLike {
+ function created(address) external returns (uint256);
+}
+
+interface VoteDelegateLike {
+ function lock(uint256) external;
+ function free(uint256) external;
+}
+
+interface VatLike {
+ function ilks(bytes32) external view returns (uint256, uint256, uint256, uint256, uint256);
+ function urns(bytes32, address) external view returns (uint256, uint256);
+ function hope(address) external;
+ function slip(bytes32, address, int256) external;
+ function frob(bytes32, address, address, address, int256, int256) external;
+ function grab(bytes32, address, address, address, int256, int256) external;
+}
+
+interface UsdsJoinLike {
+ function vat() external view returns (VatLike);
+ function usds() external view returns (GemLike);
+ function join(address, uint256) external;
+ function exit(address, uint256) external;
+}
+
+interface GemLike {
+ function approve(address, uint256) external;
+ function transfer(address, uint256) external;
+ function transferFrom(address, address, uint256) external;
+ function mint(address, uint256) external;
+ function burn(address, uint256) external;
+}
+
+interface JugLike {
+ function drip(bytes32) external returns (uint256);
+}
+
+interface MkrSkyLike {
+ function rate() external view returns (uint256);
+ function mkr() external view returns (GemLike);
+ function sky() external view returns (GemLike);
+ function skyToMkr(address, uint256) external;
+ function mkrToSky(address, uint256) external;
+}
+
+contract LockstakeEngine is Multicall {
+ // --- storage variables ---
+
+ mapping(address usr => uint256 allowed) public wards;
+ mapping(address farm => FarmStatus) public farms;
+ mapping(address owner => uint256 count) public ownerUrnsCount;
+ mapping(address owner => mapping(uint256 index => address urn)) public ownerUrns;
+ mapping(address urn => address owner) public urnOwners;
+ mapping(address urn => mapping(address usr => uint256 allowed)) public urnCan;
+ mapping(address urn => address voteDelegate) public urnVoteDelegates;
+ mapping(address urn => address farm) public urnFarms;
+ mapping(address urn => uint256 auctionsCount) public urnAuctions;
+ JugLike public jug;
+ uint256 public fee;
+
+ // --- constants and enums ---
+
+ uint256 constant WAD = 10**18;
+ uint256 constant RAY = 10**27;
+
+ enum FarmStatus { UNSUPPORTED, ACTIVE, DELETED }
+
+ // --- immutables ---
+
+ VoteDelegateFactoryLike immutable public voteDelegateFactory;
+ VatLike immutable public vat;
+ UsdsJoinLike immutable public usdsJoin;
+ GemLike immutable public usds;
+ bytes32 immutable public ilk;
+ GemLike immutable public mkr;
+ GemLike immutable public lsmkr;
+ MkrSkyLike immutable public mkrSky;
+ GemLike immutable public sky;
+ uint256 immutable public mkrSkyRate;
+ address immutable public urnImplementation;
+
+ // --- events ---
+
+ event Rely(address indexed usr);
+ event Deny(address indexed usr);
+ event File(bytes32 indexed what, address data);
+ event File(bytes32 indexed what, uint256 data);
+ event AddFarm(address farm);
+ event DelFarm(address farm);
+ event Open(address indexed owner, uint256 indexed index, address urn);
+ event Hope(address indexed owner, uint256 indexed index, address indexed usr);
+ event Nope(address indexed owner, uint256 indexed index, address indexed usr);
+ event SelectVoteDelegate(address indexed owner, uint256 indexed index, address indexed voteDelegate);
+ event SelectFarm(address indexed owner, uint256 indexed index, address indexed farm, uint16 ref);
+ event Lock(address indexed owner, uint256 indexed index, uint256 wad, uint16 ref);
+ event LockSky(address indexed owner, uint256 indexed index, uint256 skyWad, uint16 ref);
+ event Free(address indexed owner, uint256 indexed index, address to, uint256 wad, uint256 freed);
+ event FreeSky(address indexed owner, uint256 indexed index, address to, uint256 skyWad, uint256 skyFreed);
+ event FreeNoFee(address indexed owner, uint256 indexed index, address to, uint256 wad);
+ event Draw(address indexed owner, uint256 indexed index, address to, uint256 wad);
+ event Wipe(address indexed owner, uint256 indexed index, uint256 wad);
+ event GetReward(address indexed owner, uint256 indexed index, address indexed farm, address to, uint256 amt);
+ event OnKick(address indexed urn, uint256 wad);
+ event OnTake(address indexed urn, address indexed who, uint256 wad);
+ event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund);
+
+ // --- modifiers ---
+
+ modifier auth {
+ require(wards[msg.sender] == 1, "LockstakeEngine/not-authorized");
+ _;
+ }
+
+ // --- constructor ---
+
+ constructor(address voteDelegateFactory_, address usdsJoin_, bytes32 ilk_, address mkrSky_, address lsmkr_) {
+ voteDelegateFactory = VoteDelegateFactoryLike(voteDelegateFactory_);
+ usdsJoin = UsdsJoinLike(usdsJoin_);
+ vat = usdsJoin.vat();
+ usds = usdsJoin.usds();
+ ilk = ilk_;
+ mkrSky = MkrSkyLike(mkrSky_);
+ mkr = mkrSky.mkr();
+ sky = mkrSky.sky();
+ mkrSkyRate = mkrSky.rate();
+ lsmkr = GemLike(lsmkr_);
+ urnImplementation = address(new LockstakeUrn(address(vat), lsmkr_));
+ vat.hope(usdsJoin_);
+ usds.approve(usdsJoin_, type(uint256).max);
+ sky.approve(address(mkrSky), type(uint256).max);
+ mkr.approve(address(mkrSky), type(uint256).max);
+
+ wards[msg.sender] = 1;
+ emit Rely(msg.sender);
+ }
+
+ // --- internals ---
+
+ function _min(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ z = x <= y ? x : y;
+ }
+
+ function _divup(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ // Note: _divup(0,0) will return 0 differing from natural solidity division
+ unchecked {
+ z = x != 0 ? ((x - 1) / y) + 1 : 0;
+ }
+ }
+
+ function _urnAuth(address owner, address urn, address usr) internal view returns (bool ok) {
+ ok = owner == usr || urnCan[urn][usr] == 1;
+ }
+
+ function _getUrn(address owner, uint256 index) internal view returns (address urn) {
+ urn = ownerUrns[owner][index];
+ require(urn != address(0), "LockstakeEngine/invalid-urn");
+ }
+
+ function _getAuthedUrn(address owner, uint256 index) internal view returns (address urn) {
+ urn = _getUrn(owner, index);
+ require(_urnAuth(owner, urn, msg.sender), "LockstakeEngine/urn-not-authorized");
+ }
+
+ // See the reference implementation in https://eips.ethereum.org/EIPS/eip-1167
+ function _initCode() internal view returns (bytes memory code) {
+ code = new bytes(0x37);
+ bytes20 impl = bytes20(urnImplementation);
+ assembly {
+ mstore(add(code, 0x20), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
+ mstore(add(code, add(0x20, 0x14)), impl)
+ mstore(add(code, add(0x20, 0x28)), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
+ }
+ }
+
+ // --- administration ---
+
+ function rely(address usr) external auth {
+ wards[usr] = 1;
+ emit Rely(usr);
+ }
+
+ function deny(address usr) external auth {
+ wards[usr] = 0;
+ emit Deny(usr);
+ }
+
+ function file(bytes32 what, address data) external auth {
+ if (what == "jug") {
+ jug = JugLike(data);
+ } else revert("LockstakeEngine/file-unrecognized-param");
+ emit File(what, data);
+ }
+
+ function file(bytes32 what, uint256 data) external auth {
+ if (what == "fee") {
+ require(data < WAD, "LockstakeEngine/fee-equal-or-greater-wad");
+ fee = data;
+ } else revert("LockstakeEngine/file-unrecognized-param");
+ emit File(what, data);
+ }
+
+ function addFarm(address farm) external auth {
+ farms[farm] = FarmStatus.ACTIVE;
+ emit AddFarm(farm);
+ }
+
+ function delFarm(address farm) external auth {
+ farms[farm] = FarmStatus.DELETED;
+ emit DelFarm(farm);
+ }
+
+ // --- getters ---
+
+ function isUrnAuth(address owner, uint256 index, address usr) external view returns (bool ok) {
+ ok = _urnAuth(owner, _getUrn(owner, index), usr);
+ }
+
+ // --- urn management functions ---
+
+ function open(uint256 index) external returns (address urn) {
+ require(index == ownerUrnsCount[msg.sender]++, "LockstakeEngine/wrong-urn-index");
+ bytes memory initCode = _initCode();
+ assembly { urn := create(0, add(initCode, 0x20), 0x37) }
+ LockstakeUrn(urn).init(); // would revert if create had failed
+ ownerUrns[msg.sender][index] = urn;
+ urnOwners[urn] = msg.sender;
+ emit Open(msg.sender, index, urn);
+ }
+
+ function hope(address owner, uint256 index, address usr) external {
+ address urn = _getAuthedUrn(owner, index);
+ urnCan[urn][usr] = 1;
+ emit Hope(owner, index, usr);
+ }
+
+ function nope(address owner, uint256 index, address usr) external {
+ address urn = _getAuthedUrn(owner, index);
+ urnCan[urn][usr] = 0;
+ emit Nope(owner, index, usr);
+ }
+
+ // --- delegation/staking functions ---
+
+ function selectVoteDelegate(address owner, uint256 index, address voteDelegate) external {
+ address urn = _getAuthedUrn(owner, index);
+ require(urnAuctions[urn] == 0, "LockstakeEngine/urn-in-auction");
+ require(voteDelegate == address(0) || voteDelegateFactory.created(voteDelegate) == 1, "LockstakeEngine/not-valid-vote-delegate");
+ address prevVoteDelegate = urnVoteDelegates[urn];
+ require(prevVoteDelegate != voteDelegate, "LockstakeEngine/same-vote-delegate");
+ (uint256 ink, uint256 art) = vat.urns(ilk, urn);
+ if (art > 0 && voteDelegate != address(0)) {
+ (,, uint256 spot,,) = vat.ilks(ilk);
+ require(ink * spot >= art * jug.drip(ilk), "LockstakeEngine/urn-unsafe");
+ }
+ _selectVoteDelegate(urn, ink, prevVoteDelegate, voteDelegate);
+ emit SelectVoteDelegate(owner, index, voteDelegate);
+ }
+
+ function _selectVoteDelegate(address urn, uint256 wad, address prevVoteDelegate, address voteDelegate) internal {
+ if (wad > 0) {
+ if (prevVoteDelegate != address(0)) {
+ VoteDelegateLike(prevVoteDelegate).free(wad);
+ }
+ if (voteDelegate != address(0)) {
+ mkr.approve(voteDelegate, wad);
+ VoteDelegateLike(voteDelegate).lock(wad);
+ }
+ }
+ urnVoteDelegates[urn] = voteDelegate;
+ }
+
+ function selectFarm(address owner, uint256 index, address farm, uint16 ref) external {
+ address urn = _getAuthedUrn(owner, index);
+ require(urnAuctions[urn] == 0, "LockstakeEngine/urn-in-auction");
+ require(farm == address(0) || farms[farm] == FarmStatus.ACTIVE, "LockstakeEngine/farm-unsupported-or-deleted");
+ address prevFarm = urnFarms[urn];
+ require(prevFarm != farm, "LockstakeEngine/same-farm");
+ (uint256 ink,) = vat.urns(ilk, urn);
+ _selectFarm(urn, ink, prevFarm, farm, ref);
+ emit SelectFarm(owner, index, farm, ref);
+ }
+
+ function _selectFarm(address urn, uint256 wad, address prevFarm, address farm, uint16 ref) internal {
+ if (wad > 0) {
+ if (prevFarm != address(0)) {
+ LockstakeUrn(urn).withdraw(prevFarm, wad);
+ }
+ if (farm != address(0)) {
+ LockstakeUrn(urn).stake(farm, wad, ref);
+ }
+ }
+ urnFarms[urn] = farm;
+ }
+
+ function lock(address owner, uint256 index, uint256 wad, uint16 ref) external {
+ address urn = _getUrn(owner, index);
+ mkr.transferFrom(msg.sender, address(this), wad);
+ _lock(urn, wad, ref);
+ emit Lock(owner, index, wad, ref);
+ }
+
+ function lockSky(address owner, uint256 index, uint256 skyWad, uint16 ref) external {
+ address urn = _getUrn(owner, index);
+ sky.transferFrom(msg.sender, address(this), skyWad);
+ mkrSky.skyToMkr(address(this), skyWad);
+ _lock(urn, skyWad / mkrSkyRate, ref);
+ emit LockSky(owner, index, skyWad, ref);
+ }
+
+ function _lock(address urn, uint256 wad, uint16 ref) internal {
+ require(wad <= uint256(type(int256).max), "LockstakeEngine/overflow");
+ address voteDelegate = urnVoteDelegates[urn];
+ if (voteDelegate != address(0)) {
+ mkr.approve(voteDelegate, wad);
+ VoteDelegateLike(voteDelegate).lock(wad);
+ }
+ vat.slip(ilk, urn, int256(wad));
+ vat.frob(ilk, urn, urn, address(0), int256(wad), 0);
+ lsmkr.mint(urn, wad);
+ address urnFarm = urnFarms[urn];
+ if (urnFarm != address(0)) {
+ require(farms[urnFarm] == FarmStatus.ACTIVE, "LockstakeEngine/farm-deleted");
+ LockstakeUrn(urn).stake(urnFarm, wad, ref);
+ }
+ }
+
+ function free(address owner, uint256 index, address to, uint256 wad) external returns (uint256 freed) {
+ address urn = _getAuthedUrn(owner, index);
+ freed = _free(urn, wad, fee);
+ mkr.transfer(to, freed);
+ emit Free(owner, index, to, wad, freed);
+ }
+
+ function freeSky(address owner, uint256 index, address to, uint256 skyWad) external returns (uint256 skyFreed) {
+ address urn = _getAuthedUrn(owner, index);
+ uint256 wad = skyWad / mkrSkyRate;
+ uint256 freed = _free(urn, wad, fee);
+ skyFreed = freed * mkrSkyRate;
+ mkrSky.mkrToSky(to, freed);
+ emit FreeSky(owner, index, to, skyWad, skyFreed);
+ }
+
+ function freeNoFee(address owner, uint256 index, address to, uint256 wad) external auth {
+ address urn = _getAuthedUrn(owner, index);
+ _free(urn, wad, 0);
+ mkr.transfer(to, wad);
+ emit FreeNoFee(owner, index, to, wad);
+ }
+
+ function _free(address urn, uint256 wad, uint256 fee_) internal returns (uint256 freed) {
+ require(wad <= uint256(type(int256).max), "LockstakeEngine/overflow");
+ address urnFarm = urnFarms[urn];
+ if (urnFarm != address(0)) {
+ LockstakeUrn(urn).withdraw(urnFarm, wad);
+ }
+ lsmkr.burn(urn, wad);
+ vat.frob(ilk, urn, urn, address(0), -int256(wad), 0);
+ vat.slip(ilk, urn, -int256(wad));
+ address voteDelegate = urnVoteDelegates[urn];
+ if (voteDelegate != address(0)) {
+ VoteDelegateLike(voteDelegate).free(wad);
+ }
+ uint256 burn = wad * fee_ / WAD;
+ if (burn > 0) {
+ mkr.burn(address(this), burn);
+ }
+ unchecked { freed = wad - burn; } // burn <= wad always
+ }
+
+ // --- loan functions ---
+
+ function draw(address owner, uint256 index, address to, uint256 wad) external {
+ address urn = _getAuthedUrn(owner, index);
+ uint256 rate = jug.drip(ilk);
+ uint256 dart = _divup(wad * RAY, rate);
+ require(dart <= uint256(type(int256).max), "LockstakeEngine/overflow");
+ vat.frob(ilk, urn, address(0), address(this), 0, int256(dart));
+ usdsJoin.exit(to, wad);
+ emit Draw(owner, index, to, wad);
+ }
+
+ function wipe(address owner, uint256 index, uint256 wad) external {
+ address urn = _getUrn(owner, index);
+ usds.transferFrom(msg.sender, address(this), wad);
+ usdsJoin.join(address(this), wad);
+ (, uint256 rate,,,) = vat.ilks(ilk);
+ uint256 dart = wad * RAY / rate;
+ require(dart <= uint256(type(int256).max), "LockstakeEngine/overflow");
+ vat.frob(ilk, urn, address(0), address(this), 0, -int256(dart));
+ emit Wipe(owner, index, wad);
+ }
+
+ function wipeAll(address owner, uint256 index) external returns (uint256 wad) {
+ address urn = _getUrn(owner, index);
+ (, uint256 art) = vat.urns(ilk, urn);
+ require(art <= uint256(type(int256).max), "LockstakeEngine/overflow");
+ (, uint256 rate,,,) = vat.ilks(ilk);
+ wad = _divup(art * rate, RAY);
+ usds.transferFrom(msg.sender, address(this), wad);
+ usdsJoin.join(address(this), wad);
+ vat.frob(ilk, urn, address(0), address(this), 0, -int256(art));
+ emit Wipe(owner, index, wad);
+ }
+
+ // --- staking rewards function ---
+
+ function getReward(address owner, uint256 index, address farm, address to) external returns (uint256 amt) {
+ address urn = _getAuthedUrn(owner, index);
+ require(farms[farm] > FarmStatus.UNSUPPORTED, "LockstakeEngine/farm-unsupported");
+ amt = LockstakeUrn(urn).getReward(farm, to);
+ emit GetReward(owner, index, farm, to, amt);
+ }
+
+ // --- liquidation callback functions ---
+
+ function onKick(address urn, uint256 wad) external auth {
+ // Urn confiscation happens in Dog contract where ilk vat.gem is sent to the LockstakeClipper
+ (uint256 ink,) = vat.urns(ilk, urn);
+ uint256 inkBeforeKick = ink + wad;
+ _selectVoteDelegate(urn, inkBeforeKick, urnVoteDelegates[urn], address(0));
+ _selectFarm(urn, inkBeforeKick, urnFarms[urn], address(0), 0);
+ lsmkr.burn(urn, wad);
+ urnAuctions[urn]++;
+ emit OnKick(urn, wad);
+ }
+
+ function onTake(address urn, address who, uint256 wad) external auth {
+ mkr.transfer(who, wad); // Free MKR to the auction buyer
+ emit OnTake(urn, who, wad);
+ }
+
+ function onRemove(address urn, uint256 sold, uint256 left) external auth {
+ uint256 burn;
+ uint256 refund;
+ if (left > 0) {
+ uint256 fee_ = fee;
+ burn = _min(sold * fee_ / (WAD - fee_), left);
+ mkr.burn(address(this), burn);
+ unchecked { refund = left - burn; }
+ if (refund > 0) {
+ // The following is ensured by the dog and clip but we still prefer to be explicit
+ require(refund <= uint256(type(int256).max), "LockstakeEngine/overflow");
+ vat.slip(ilk, urn, int256(refund));
+ vat.grab(ilk, urn, urn, address(0), int256(refund), 0);
+ lsmkr.mint(urn, refund);
+ }
+ }
+ urnAuctions[urn]--;
+ emit OnRemove(urn, sold, burn, refund);
+ }
+}
diff --git a/src/LockstakeMkr.sol b/src/LockstakeMkr.sol
new file mode 100644
index 00000000..92087faf
--- /dev/null
+++ b/src/LockstakeMkr.sol
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+/// LockstakeMkr.sol -- LockstakeMkr token
+
+// Copyright (C) 2017, 2018, 2019 dbrock, rain, mrchico
+// Copyright (C) 2023 Dai Foundation
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.21;
+
+contract LockstakeMkr {
+ mapping (address => uint256) public wards;
+
+ // --- ERC20 Data ---
+ string public constant name = "LockstakeMkr";
+ string public constant symbol = "lsMKR";
+ string public constant version = "1";
+ uint8 public constant decimals = 18;
+ uint256 public totalSupply;
+
+ mapping (address => uint256) public balanceOf;
+ mapping (address => mapping (address => uint256)) public allowance;
+
+ // --- Events ---
+ event Rely(address indexed usr);
+ event Deny(address indexed usr);
+ event Approval(address indexed owner, address indexed spender, uint256 value);
+ event Transfer(address indexed from, address indexed to, uint256 value);
+
+ modifier auth {
+ require(wards[msg.sender] == 1, "LockstakeMkr/not-authorized");
+ _;
+ }
+
+ constructor() {
+ wards[msg.sender] = 1;
+ emit Rely(msg.sender);
+ }
+
+ // --- Administration ---
+ function rely(address usr) external auth {
+ wards[usr] = 1;
+ emit Rely(usr);
+ }
+
+ function deny(address usr) external auth {
+ wards[usr] = 0;
+ emit Deny(usr);
+ }
+
+ // --- ERC20 Mutations ---
+ function transfer(address to, uint256 value) external returns (bool) {
+ require(to != address(0) && to != address(this), "LockstakeMkr/invalid-address");
+ uint256 balance = balanceOf[msg.sender];
+ require(balance >= value, "LockstakeMkr/insufficient-balance");
+
+ unchecked {
+ balanceOf[msg.sender] = balance - value;
+ balanceOf[to] += value; // note: we don't need an overflow check here b/c sum of all balances == totalSupply
+ }
+
+ emit Transfer(msg.sender, to, value);
+
+ return true;
+ }
+
+ function transferFrom(address from, address to, uint256 value) external returns (bool) {
+ require(to != address(0) && to != address(this), "LockstakeMkr/invalid-address");
+ uint256 balance = balanceOf[from];
+ require(balance >= value, "LockstakeMkr/insufficient-balance");
+
+ if (from != msg.sender) {
+ uint256 allowed = allowance[from][msg.sender];
+ if (allowed != type(uint256).max) {
+ require(allowed >= value, "LockstakeMkr/insufficient-allowance");
+
+ unchecked {
+ allowance[from][msg.sender] = allowed - value;
+ }
+ }
+ }
+
+ unchecked {
+ balanceOf[from] = balance - value;
+ balanceOf[to] += value; // note: we don't need an overflow check here b/c sum of all balances == totalSupply
+ }
+
+ emit Transfer(from, to, value);
+
+ return true;
+ }
+
+ function approve(address spender, uint256 value) external returns (bool) {
+ allowance[msg.sender][spender] = value;
+
+ emit Approval(msg.sender, spender, value);
+
+ return true;
+ }
+
+ // --- Mint/Burn ---
+ function mint(address to, uint256 value) external auth {
+ require(to != address(0) && to != address(this), "LockstakeMkr/invalid-address");
+ unchecked {
+ 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
+ }
+ totalSupply = totalSupply + value;
+
+ emit Transfer(address(0), to, value);
+ }
+
+ function burn(address from, uint256 value) external {
+ uint256 balance = balanceOf[from];
+ require(balance >= value, "LockstakeMkr/insufficient-balance");
+
+ if (from != msg.sender) {
+ uint256 allowed = allowance[from][msg.sender];
+ if (allowed != type(uint256).max) {
+ require(allowed >= value, "LockstakeMkr/insufficient-allowance");
+
+ unchecked {
+ allowance[from][msg.sender] = allowed - value;
+ }
+ }
+ }
+
+ unchecked {
+ balanceOf[from] = balance - value; // note: we don't need overflow checks b/c require(balance >= value) and balance <= totalSupply
+ totalSupply = totalSupply - value;
+ }
+
+ emit Transfer(from, address(0), value);
+ }
+}
diff --git a/src/LockstakeUrn.sol b/src/LockstakeUrn.sol
new file mode 100644
index 00000000..c26e3a5e
--- /dev/null
+++ b/src/LockstakeUrn.sol
@@ -0,0 +1,80 @@
+// SPDX-FileCopyrightText: © 2023 Dai Foundation
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.21;
+
+interface VatLike {
+ function hope(address) external;
+}
+
+interface GemLike {
+ function balanceOf(address) external view returns (uint256);
+ function approve(address, uint256) external;
+ function transfer(address, uint256) external;
+}
+
+interface StakingRewardsLike {
+ function rewardsToken() external view returns (GemLike);
+ function stake(uint256, uint16) external;
+ function withdraw(uint256) external;
+ function getReward() external;
+}
+
+contract LockstakeUrn {
+ // --- immutables ---
+
+ address immutable public engine;
+ GemLike immutable public lsmkr;
+ VatLike immutable public vat;
+
+ // --- modifiers ---
+
+ modifier isEngine {
+ require(msg.sender == engine, "LockstakeUrn/not-engine");
+ _;
+ }
+
+ // --- constructor & init ---
+
+ constructor(address vat_, address lsmkr_) {
+ engine = msg.sender;
+ vat = VatLike(vat_);
+ lsmkr = GemLike(lsmkr_);
+ }
+
+ function init() external isEngine {
+ vat.hope(msg.sender);
+ lsmkr.approve(msg.sender, type(uint256).max);
+ }
+
+ // --- staking functions ---
+
+ function stake(address farm, uint256 wad, uint16 ref) external isEngine {
+ lsmkr.approve(farm, wad);
+ StakingRewardsLike(farm).stake(wad, ref);
+ }
+
+ function withdraw(address farm, uint256 wad) external isEngine {
+ StakingRewardsLike(farm).withdraw(wad);
+ }
+
+ function getReward(address farm, address to) external isEngine returns (uint256 amt) {
+ StakingRewardsLike(farm).getReward();
+ GemLike rewardsToken = StakingRewardsLike(farm).rewardsToken();
+ amt = rewardsToken.balanceOf(address(this));
+ rewardsToken.transfer(to, amt);
+ }
+}
diff --git a/src/Multicall.sol b/src/Multicall.sol
new file mode 100644
index 00000000..bc16a50a
--- /dev/null
+++ b/src/Multicall.sol
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on https://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/base/Multicall.sol
+
+pragma solidity ^0.8.21;
+
+// Enables calling multiple methods in a single call to the contract
+abstract contract Multicall {
+ function multicall(bytes[] calldata data) external returns (bytes[] memory results) {
+ results = new bytes[](data.length);
+ for (uint256 i = 0; i < data.length; i++) {
+ (bool success, bytes memory result) = address(this).delegatecall(data[i]);
+
+ if (!success) {
+ if (result.length == 0) revert("multicall failed");
+ assembly ("memory-safe") {
+ revert(add(32, result), mload(result))
+ }
+ }
+
+ results[i] = result;
+ }
+ }
+}
diff --git a/test/LockstakeClipper.t.sol b/test/LockstakeClipper.t.sol
new file mode 100644
index 00000000..1d97e1f9
--- /dev/null
+++ b/test/LockstakeClipper.t.sol
@@ -0,0 +1,1770 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+import "dss-test/DssTest.sol";
+
+import { LockstakeClipper } from "src/LockstakeClipper.sol";
+import { LockstakeEngineMock } from "test/mocks/LockstakeEngineMock.sol";
+import { PipMock } from "test/mocks/PipMock.sol";
+
+contract BadGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(address sender, uint256 owe, uint256 slice, bytes calldata data)
+ external {
+ sender; owe; slice; data;
+ clip.take({ // attempt reentrancy
+ id: 1,
+ amt: 25 ether,
+ max: 5 ether * 10E27,
+ who: address(this),
+ data: ""
+ });
+ }
+}
+
+contract RedoGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(
+ address sender, uint256 owe, uint256 slice, bytes calldata data
+ ) external {
+ owe; slice; data;
+ clip.redo(1, sender);
+ }
+}
+
+contract KickGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(
+ address sender, uint256 owe, uint256 slice, bytes calldata data
+ ) external {
+ sender; owe; slice; data;
+ clip.kick(1, 1, address(0), address(0));
+ }
+}
+
+contract FileUintGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(
+ address sender, uint256 owe, uint256 slice, bytes calldata data
+ ) external {
+ sender; owe; slice; data;
+ clip.file("stopped", 1);
+ }
+}
+
+contract FileAddrGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(
+ address sender, uint256 owe, uint256 slice, bytes calldata data
+ ) external {
+ sender; owe; slice; data;
+ clip.file("vow", address(123));
+ }
+}
+
+contract YankGuy {
+ LockstakeClipper clip;
+
+ constructor(LockstakeClipper clip_) {
+ clip = clip_;
+ }
+
+ function clipperCall(
+ address sender, uint256 owe, uint256 slice, bytes calldata data
+ ) external {
+ sender; owe; slice; data;
+ clip.yank(1);
+ }
+}
+
+contract PublicClip is LockstakeClipper {
+
+ constructor(address vat, address spot, address dog, address engine) LockstakeClipper(vat, spot, dog, engine) {}
+
+ function add() public returns (uint256 id) {
+ id = ++kicks;
+ active.push(id);
+ sales[id].pos = active.length - 1;
+ }
+
+ function remove(uint256 id) public {
+ _remove(id);
+ }
+}
+
+interface VatLike {
+ function dai(address) external view returns (uint256);
+ function gem(bytes32, address) external view returns (uint256);
+ function ilks(bytes32) external view returns (uint256, uint256, uint256, uint256, uint256);
+ function urns(bytes32, address) external view returns (uint256, uint256);
+ function rely(address) external;
+ function file(bytes32, bytes32, uint256) external;
+ function init(bytes32) external;
+ function hope(address) external;
+ function frob(bytes32, address, address, address, int256, int256) external;
+ function slip(bytes32, address, int256) external;
+ function suck(address, address, uint256) external;
+ function fold(bytes32, address, int256) external;
+}
+
+interface GemLike {
+ function balanceOf(address) external view returns (uint256);
+}
+
+interface DogLike {
+ function Dirt() external view returns (uint256);
+ function chop(bytes32) external view returns (uint256);
+ function ilks(bytes32) external view returns (address, uint256, uint256, uint256);
+ function rely(address) external;
+ function file(bytes32, uint256) external;
+ function file(bytes32, bytes32, address) external;
+ function file(bytes32, bytes32, uint256) external;
+ function bark(bytes32, address, address) external returns (uint256);
+}
+
+interface SpotterLike {
+ function file(bytes32, bytes32, address) external;
+ function file(bytes32, bytes32, uint256) external;
+ function poke(bytes32) external;
+}
+
+interface CalcFabLike {
+ function newLinearDecrease(address) external returns (address);
+ function newStairstepExponentialDecrease(address) external returns (address);
+}
+
+interface CalcLike {
+ function file(bytes32, uint256) external;
+}
+
+interface VowLike {
+
+}
+
+contract LockstakeClipperTest is DssTest {
+ using stdStorage for StdStorage;
+
+ DssInstance dss;
+ address pauseProxy;
+ PipMock pip;
+ GemLike dai;
+
+ LockstakeEngineMock engine;
+ LockstakeClipper clip;
+
+ // Exchange exchange;
+
+ address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;
+
+ address ali;
+ address bob;
+ address che;
+
+ bytes32 constant ilk = "LSE";
+ uint256 constant price = 5 ether;
+
+ uint256 constant startTime = 604411200; // Used to avoid issues with `block.timestamp`
+
+ function _ink(bytes32 ilk_, address urn_) internal view returns (uint256) {
+ (uint256 ink_,) = dss.vat.urns(ilk_, urn_);
+ return ink_;
+ }
+ function _art(bytes32 ilk_, address urn_) internal view returns (uint256) {
+ (,uint256 art_) = dss.vat.urns(ilk_, urn_);
+ return art_;
+ }
+
+ function ray(uint256 wad) internal pure returns (uint256) {
+ return wad * 10 ** 9;
+ }
+
+ function rad(uint256 wad) internal pure returns (uint256) {
+ return wad * 10 ** 27;
+ }
+
+ modifier takeSetup {
+ address calc = CalcFabLike(dss.chainlog.getAddress("CALC_FAB")).newStairstepExponentialDecrease(address(this));
+ CalcLike(calc).file("cut", RAY - ray(0.01 ether)); // 1% decrease
+ CalcLike(calc).file("step", 1); // Decrease every 1 second
+
+ clip.file("buf", ray(1.25 ether)); // 25% Initial price buffer
+ clip.file("calc", address(calc)); // File price contract
+ clip.file("cusp", ray(0.3 ether)); // 70% drop before reset
+ clip.file("tail", 3600); // 1 hour before reset
+
+ (uint256 ink, uint256 art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 40 ether);
+ assertEq(art, 100 ether);
+
+ assertEq(clip.kicks(), 0);
+ dss.dog.bark(ilk, address(this), address(this));
+ assertEq(clip.kicks(), 1);
+
+ (ink, art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 0);
+ assertEq(art, 0);
+
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, rad(110 ether));
+ assertEq(sale.lot, 40 ether);
+ assertEq(sale.tot, 40 ether);
+ assertEq(sale.usr, address(this));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, ray(5 ether)); // $4 plus 25%
+
+ assertEq(dss.vat.gem(ilk, ali), 0);
+ assertEq(dss.vat.dai(ali), rad(1000 ether));
+ assertEq(dss.vat.gem(ilk, bob), 0);
+ assertEq(dss.vat.dai(bob), rad(1000 ether));
+
+ _;
+ }
+
+ function setUp() public {
+ vm.createSelectFork(vm.envString("ETH_RPC_URL"));
+ vm.warp(startTime);
+
+ dss = MCD.loadFromChainlog(LOG);
+
+ pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY");
+ dai = GemLike(dss.chainlog.getAddress("MCD_DAI"));
+
+ pip = new PipMock();
+ pip.setPrice(price); // Spot = $2.5
+
+ vm.startPrank(pauseProxy);
+ dss.vat.init(ilk);
+
+ dss.spotter.file(ilk, "pip", address(pip));
+ dss.spotter.file(ilk, "mat", ray(2 ether)); // 200% liquidation ratio for easier test calcs
+ dss.spotter.poke(ilk);
+
+ dss.vat.file(ilk, "dust", rad(20 ether)); // $20 dust
+ dss.vat.file(ilk, "line", rad(10000 ether));
+ dss.vat.file("Line", dss.vat.Line() + rad(10000 ether));
+
+ dss.dog.file(ilk, "chop", 1.1 ether); // 10% chop
+ dss.dog.file(ilk, "hole", rad(1000 ether));
+ dss.dog.file("Hole", dss.dog.Dirt() + rad(1000 ether));
+
+ engine = new LockstakeEngineMock(address(dss.vat), ilk);
+ dss.vat.rely(address(engine));
+ vm.stopPrank();
+
+ // dust and chop filed previously so clip.chost will be set correctly
+ clip = new LockstakeClipper(address(dss.vat), address(dss.spotter), address(dss.dog), address(engine));
+ clip.upchost();
+ clip.rely(address(dss.dog));
+
+ vm.startPrank(pauseProxy);
+ dss.dog.file(ilk, "clip", address(clip));
+ dss.dog.rely(address(clip));
+ dss.vat.rely(address(clip));
+
+ dss.vat.slip(ilk, address(this), int256(1000 ether));
+ vm.stopPrank();
+
+ assertEq(dss.vat.gem(ilk, address(this)), 1000 ether);
+ assertEq(dss.vat.dai(address(this)), 0);
+ dss.vat.frob(ilk, address(this), address(this), address(this), 40 ether, 100 ether);
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ assertEq(dss.vat.dai(address(this)), rad(100 ether));
+
+ pip.setPrice(4 ether); // Spot = $2
+ dss.spotter.poke(ilk); // Now unsafe
+
+ ali = address(111);
+ bob = address(222);
+ che = address(333);
+
+ dss.vat.hope(address(clip));
+ vm.prank(ali); dss.vat.hope(address(clip));
+ vm.prank(bob); dss.vat.hope(address(clip));
+
+ vm.startPrank(pauseProxy);
+ dss.vat.suck(address(0), address(this), rad(1000 ether));
+ dss.vat.suck(address(0), address(ali), rad(1000 ether));
+ dss.vat.suck(address(0), address(bob), rad(1000 ether));
+ vm.stopPrank();
+ }
+
+ function testConstructor() public {
+ vm.expectEmit(true, true, true, true);
+ emit Rely(address(this));
+ LockstakeClipper c = new LockstakeClipper(address(111), address(222), address(333), address(engine));
+ assertEq(address(c.vat()), address(111));
+ assertEq(address(c.spotter()), address(222));
+ assertEq(address(c.dog()), address(333));
+ assertEq(address(c.engine()), address(engine));
+ assertEq(c.ilk(), ilk);
+ assertEq(c.buf(), RAY);
+ assertEq(c.wards(address(this)), 1);
+ }
+
+ function testAuth() public {
+ checkAuth(address(clip), "LockstakeClipper");
+ }
+
+ function testFileUint() public {
+ checkFileUint(address(clip), "LockstakeClipper", ["buf", "tail", "cusp", "chip", "tip", "stopped"]);
+ }
+
+ function testFileAddress() public {
+ checkFileAddress(address(clip), "LockstakeClipper", ["spotter", "dog", "vow", "calc"]);
+ }
+
+ function testAuthModifiers() public {
+ bytes4[] memory authedMethods = new bytes4[](2);
+ authedMethods[0] = clip.kick.selector;
+ authedMethods[1] = clip.yank.selector;
+
+ vm.startPrank(address(0xBEEF));
+ checkModifier(address(clip), "LockstakeClipper/not-authorized", authedMethods);
+ vm.stopPrank();
+ }
+
+ function testChangeDog() public {
+ assertTrue(address(clip.dog()) != address(123));
+ clip.file("dog", address(123));
+ assertEq(address(clip.dog()), address(123));
+ }
+
+ function testGetChop() public view {
+ uint256 chop = dss.dog.chop(ilk);
+ (, uint256 chop2,,) = dss.dog.ilks(ilk);
+ assertEq(chop, chop2);
+ }
+
+ function testKick() public {
+ clip.file("tip", rad(100 ether)); // Flat fee of 100 DAI
+ clip.file("chip", 0); // No linear increase
+
+ assertEq(clip.kicks(), 0);
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ assertEq(dss.vat.dai(ali), rad(1000 ether));
+ (uint256 ink, uint256 art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 40 ether);
+ assertEq(art, 100 ether);
+
+ vm.prank(ali); dss.dog.bark(ilk, address(this), address(ali));
+
+ assertEq(clip.kicks(), 1);
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, rad(110 ether));
+ assertEq(sale.lot, 40 ether);
+ assertEq(sale.tot, 40 ether);
+ assertEq(sale.usr, address(this));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, ray(4 ether));
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ assertEq(dss.vat.dai(ali), rad(1100 ether)); // Paid "tip" amount of DAI for calling bark()
+ (ink, art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 0 ether);
+ assertEq(art, 0 ether);
+
+ pip.setPrice(price); // Spot = $2.5
+ dss.spotter.poke(ilk); // Now safe
+
+ vm.warp(startTime + 100);
+ dss.vat.frob(ilk, address(this), address(this), address(this), 40 ether, 100 ether);
+
+ pip.setPrice(4 ether); // Spot = $2
+ dss.spotter.poke(ilk); // Now unsafe
+
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(2);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+ assertEq(dss.vat.gem(ilk, address(this)), 920 ether);
+
+ clip.file(bytes32("buf"), ray(1.25 ether)); // 25% Initial price buffer
+
+ clip.file("tip", rad(100 ether)); // Flat fee of 100 DAI
+ clip.file("chip", 0.02 ether); // Linear increase of 2% of tab
+
+ assertEq(dss.vat.dai(bob), rad(1000 ether));
+
+ vm.prank(bob); dss.dog.bark(ilk, address(this), address(bob));
+
+ assertEq(clip.kicks(), 2);
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(2);
+ assertEq(sale.pos, 1);
+ assertEq(sale.tab, rad(110 ether));
+ assertEq(sale.lot, 40 ether);
+ assertEq(sale.tot, 40 ether);
+ assertEq(sale.usr, address(this));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, ray(5 ether));
+ assertEq(dss.vat.gem(ilk, address(this)), 920 ether);
+ (ink, art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 0 ether);
+ assertEq(art, 0 ether);
+
+ uint256 bobVatDai = rad(1000 ether) + rad(100 ether) + sale.tab * 0.02 ether / WAD;
+ assertEq(dss.vat.dai(bob), bobVatDai); // Paid (tip + due * chip) amount of DAI for calling bark()
+
+ pip.setPrice(price); // Spot = $2.5
+ dss.spotter.poke(ilk); // Now safe
+
+ dss.vat.frob(ilk, address(this), address(this), address(this), 40 ether, 100 ether);
+
+ pip.setPrice(4 ether); // Spot = $2
+ dss.spotter.poke(ilk); // Now unsafe
+
+ clip.file("tip", 0);
+ clip.file("chip", 0);
+
+ vm.prank(bob); dss.dog.bark(ilk, address(this), address(bob));
+
+ assertEq(clip.kicks(), 3);
+ assertEq(dss.vat.dai(bob), bobVatDai); // no incentive received
+ }
+
+ function testRevertsKickZeroPrice() public {
+ clip.file("buf", 0);
+ vm.expectRevert("LockstakeClipper/zero-top-price");
+ dss.dog.bark(ilk, address(this), address(this));
+ }
+
+ function testRevertsRedoZeroPrice() public {
+ _auctionResetSetup(1 hours);
+
+ clip.file("buf", 0);
+
+ vm.warp(startTime + 1801 seconds);
+ (bool needsRedo,,,) = clip.getStatus(1);
+ assertTrue(needsRedo);
+ vm.expectRevert("LockstakeClipper/zero-top-price");
+ clip.redo(1, address(this));
+ }
+
+ function testKickBasic() public {
+ clip.kick(1 ether, 2 ether, address(1), address(this));
+ }
+
+ function testRevertsKickZeroTab() public {
+ vm.expectRevert("LockstakeClipper/zero-tab");
+ clip.kick(0, 2 ether, address(1), address(this));
+ }
+
+ function testRevertsKickZeroLot() public {
+ vm.expectRevert("LockstakeClipper/zero-lot");
+ clip.kick(1 ether, 0, address(1), address(this));
+ }
+
+ function testRevertsKickLotOverMaxInt() public {
+ vm.expectRevert("LockstakeClipper/over-maxint-lot");
+ clip.kick(1 ether, uint256(type(int256).max) + 1, address(1), address(this));
+ }
+
+ function testRevertsKickZeroUsr() public {
+ vm.expectRevert("LockstakeClipper/zero-usr");
+ clip.kick(1 ether, 2 ether, address(0), address(this));
+ }
+
+ function testRevertsKickKicksOverflow() public {
+ stdstore.target(address(clip)).sig("kicks()").checked_write(type(uint256).max);
+ vm.expectRevert("LockstakeClipper/overflow");
+ clip.kick(1 ether, 2 ether, address(1), address(this));
+ }
+
+ function testRevertsKickInvalidPrice() public {
+ pip.setPrice(0);
+ vm.expectRevert("LockstakeClipper/invalid-price");
+ clip.kick(1 ether, 2 ether, address(1), address(this));
+ }
+
+ function testBarkNotLeavingDust() public {
+ vm.prank(pauseProxy); dss.dog.file(ilk, "hole", rad(80 ether)); // Makes room = 80 WAD
+ vm.prank(pauseProxy); dss.dog.file(ilk, "chop", 1 ether); // 0% chop (for precise calculations)
+
+ assertEq(clip.kicks(), 0);
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ (uint256 ink, uint256 art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 40 ether);
+ assertEq(art, 100 ether);
+
+ dss.dog.bark(ilk, address(this), address(this)); // art - dart = 100 - 80 = dust (= 20)
+
+ assertEq(clip.kicks(), 1);
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, rad(80 ether)); // No chop
+ assertEq(sale.lot, 32 ether);
+ assertEq(sale.tot, 32 ether);
+ assertEq(sale.usr, address(this));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, ray(4 ether));
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ (ink, art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 8 ether);
+ assertEq(art, 20 ether);
+ }
+
+ function testBarkNotLeavingDustOverHole() public {
+ vm.prank(pauseProxy); dss.dog.file(ilk, "hole", rad(80 ether) + ray(1 ether)); // Makes room = 80 WAD + 1 wei
+ vm.prank(pauseProxy); dss.dog.file(ilk, "chop", 1 ether); // 0% chop (for precise calculations)
+
+ assertEq(clip.kicks(), 0);
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ (uint256 ink, uint256 art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 40 ether);
+ assertEq(art, 100 ether);
+
+ dss.dog.bark(ilk, address(this), address(this)); // art - dart = 100 - (80 + 1 wei) < dust (= 20) then the whole debt is taken
+
+ assertEq(clip.kicks(), 1);
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, rad(100 ether)); // No chop
+ assertEq(sale.lot, 40 ether);
+ assertEq(sale.tot, 40 ether);
+ assertEq(sale.usr, address(this));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, ray(4 ether));
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ (ink, art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 0 ether);
+ assertEq(art, 0 ether);
+ }
+
+ function testBarkNotLeavingDustRate() public {
+ vm.prank(pauseProxy); dss.vat.fold(ilk, address(dss.vow), int256(ray(0.02 ether)));
+ (, uint256 rate,,,) = dss.vat.ilks(ilk);
+ assertEq(rate, ray(1.02 ether));
+
+ vm.prank(pauseProxy); dss.dog.file(ilk, "hole", 100 * RAD); // Makes room = 100 RAD
+ vm.prank(pauseProxy); dss.dog.file(ilk, "chop", 1 ether); // 0% chop for precise calculations
+ vm.prank(pauseProxy); dss.vat.file(ilk, "dust", 20 * RAD); // 20 DAI minimum Vault debt
+ clip.upchost();
+
+ assertEq(clip.kicks(), 0);
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ (uint256 ink, uint256 art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 40 ether);
+ assertEq(art, 100 ether); // Full debt is 102 DAI since rate = 1.02 * RAY
+
+ // (art - dart) * rate ~= 2 RAD < dust = 20 RAD
+ // => remnant would be dusty, so a full liquidation occurs.
+ dss.dog.bark(ilk, address(this), address(this));
+
+ assertEq(clip.kicks(), 1);
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 100 ether * rate); // No chop
+ assertEq(sale.lot, 40 ether);
+ assertEq(sale.tot, 40 ether);
+ assertEq(sale.usr, address(this));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, ray(4 ether));
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ (ink, art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 0);
+ assertEq(art, 0);
+ }
+
+ function testBarkOnlyLeavingDustOverHoleRate() public {
+ vm.prank(pauseProxy); dss.vat.fold(ilk, address(dss.vow), int256(ray(0.02 ether)));
+ (, uint256 rate,,,) = dss.vat.ilks(ilk);
+ assertEq(rate, ray(1.02 ether));
+
+ vm.prank(pauseProxy); dss.dog.file(ilk, "hole", 816 * RAD / 10); // Makes room = 81.6 RAD => dart = 80
+ vm.prank(pauseProxy); dss.dog.file(ilk, "chop", 1 ether); // 0% chop for precise calculations
+ vm.prank(pauseProxy); dss.vat.file(ilk, "dust", 204 * RAD / 10); // 20.4 DAI dust
+ clip.upchost();
+
+ assertEq(clip.kicks(), 0);
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ (uint256 ink, uint256 art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 40 ether);
+ assertEq(art, 100 ether);
+
+ // (art - dart) * rate = 20.4 RAD == dust
+ // => marginal threshold at which partial liquidation is acceptable
+ dss.dog.bark(ilk, address(this), address(this));
+
+ assertEq(clip.kicks(), 1);
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 816 * RAD / 10); // Equal to ilk.hole
+ assertEq(sale.lot, 32 ether);
+ assertEq(sale.tot, 32 ether);
+ assertEq(sale.usr, address(this));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, ray(4 ether));
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ (ink, art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 8 ether);
+ assertEq(art, 20 ether);
+ (,,,, uint256 dust) = dss.vat.ilks(ilk);
+ assertEq(art * rate, dust);
+ }
+
+ function testHolehole() public {
+ assertEq(dss.dog.Dirt(), 0);
+ (,,, uint256 dirt) = dss.dog.ilks(ilk);
+ assertEq(dirt, 0);
+
+ dss.dog.bark(ilk, address(this), address(this));
+
+ (, uint256 tab,,,,,) = clip.sales(1);
+
+ assertEq(dss.dog.Dirt(), tab);
+ (,,, dirt) = dss.dog.ilks(ilk);
+ assertEq(dirt, tab);
+
+ bytes32 ilk2 = "LSE2";
+ LockstakeEngineMock engine2 = new LockstakeEngineMock(address(dss.vat), ilk2);
+ vm.prank(pauseProxy); dss.vat.rely(address(engine2));
+ LockstakeClipper clip2 = new LockstakeClipper(address(dss.vat), address(dss.spotter), address(dss.dog), address(engine2));
+ clip2.upchost();
+ clip2.rely(address(dss.dog));
+
+ vm.prank(pauseProxy); dss.dog.file(ilk2, "clip", address(clip2));
+ vm.prank(pauseProxy); dss.dog.file(ilk2, "chop", 1.1 ether);
+ vm.prank(pauseProxy); dss.dog.file(ilk2, "hole", rad(1000 ether));
+ vm.prank(pauseProxy); dss.dog.rely(address(clip2));
+
+ vm.prank(pauseProxy); dss.vat.init(ilk2);
+ vm.prank(pauseProxy); dss.vat.rely(address(clip2));
+ vm.prank(pauseProxy); dss.vat.file(ilk2, "line", rad(100 ether));
+
+ vm.prank(pauseProxy); dss.vat.slip(ilk2, address(this), 40 ether);
+
+ PipMock pip2 = new PipMock();
+ pip2.setPrice(price); // Spot = $2.5
+
+ vm.prank(pauseProxy); dss.spotter.file(ilk2, "pip", address(pip2));
+ vm.prank(pauseProxy); dss.spotter.file(ilk2, "mat", ray(2 ether));
+ dss.spotter.poke(ilk2);
+ dss.vat.frob(ilk2, address(this), address(this), address(this), 40 ether, 100 ether);
+ pip2.setPrice(4 ether); // Spot = $2
+ dss.spotter.poke(ilk2);
+
+ dss.dog.bark(ilk2, address(this), address(this));
+
+ (, uint256 tab2,,,,,) = clip2.sales(1);
+
+ assertEq(dss.dog.Dirt(), tab + tab2);
+ (,,, dirt) = dss.dog.ilks(ilk);
+ (,,, uint256 dirt2) = dss.dog.ilks(ilk2);
+ assertEq(dirt, tab);
+ assertEq(dirt2, tab2);
+ }
+
+ function testPartialLiquidationHoleLimit() public {
+ vm.prank(pauseProxy); dss.dog.file("Hole", rad(75 ether));
+
+ assertEq(_ink(ilk, address(this)), 40 ether);
+ assertEq(_art(ilk, address(this)), 100 ether);
+
+ assertEq(dss.dog.Dirt(), 0);
+ (,uint256 chop,, uint256 dirt) = dss.dog.ilks(ilk);
+ assertEq(dirt, 0);
+
+ dss.dog.bark(ilk, address(this), address(this));
+
+ LockstakeClipper.Sale memory sale;
+ (, sale.tab, sale.lot,,,,) = clip.sales(1);
+
+ (, uint256 rate,,,) = dss.vat.ilks(ilk);
+
+ assertEq(sale.lot, 40 ether * (sale.tab * WAD / rate / chop) / 100 ether);
+ assertEq(sale.tab, rad(75 ether) - ray(0.2 ether)); // 0.2 RAY rounding error
+
+ assertEq(_ink(ilk, address(this)), 40 ether - sale.lot);
+ assertEq(_art(ilk, address(this)), 100 ether - sale.tab * WAD / rate / chop);
+
+ assertEq(dss.dog.Dirt(), sale.tab);
+ (,,, dirt) = dss.dog.ilks(ilk);
+ assertEq(dirt, sale.tab);
+ }
+
+ function testPartialLiquidationholeLimit() public {
+ vm.prank(pauseProxy); dss.dog.file(ilk, "hole", rad(75 ether));
+
+ assertEq(_ink(ilk, address(this)), 40 ether);
+ assertEq(_art(ilk, address(this)), 100 ether);
+
+ assertEq(dss.dog.Dirt(), 0);
+ (,uint256 chop,, uint256 dirt) = dss.dog.ilks(ilk);
+ assertEq(dirt, 0);
+
+ dss.dog.bark(ilk, address(this), address(this));
+
+ LockstakeClipper.Sale memory sale;
+ (, sale.tab, sale.lot,,,,) = clip.sales(1);
+
+ (, uint256 rate,,,) = dss.vat.ilks(ilk);
+
+ assertEq(sale.lot, 40 ether * (sale.tab * WAD / rate / chop) / 100 ether);
+ assertEq(sale.tab, rad(75 ether) - ray(0.2 ether)); // 0.2 RAY rounding error
+
+ assertEq(_ink(ilk, address(this)), 40 ether - sale.lot);
+ assertEq(_art(ilk, address(this)), 100 ether - sale.tab * WAD / rate / chop);
+
+ assertEq(dss.dog.Dirt(), sale.tab);
+ (,,, dirt) = dss.dog.ilks(ilk);
+ assertEq(dirt, sale.tab);
+ }
+
+ function testRevertsTakeZeroUsr() public takeSetup {
+ // Auction id 2 is unpopulated.
+ (,,,, address usr,,) = clip.sales(2);
+ assertEq(usr, address(0));
+ vm.expectRevert("LockstakeClipper/not-running-auction");
+ clip.take(2, 25 ether, ray(5 ether), address(ali), "");
+ }
+
+ function testTakeOverTab() public takeSetup {
+ // Bid so owe (= 25 * 5 = 125 RAD) > tab (= 110 RAD)
+ // Readjusts slice to be tab/top = 25
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 25 ether,
+ max: ray(5 ether),
+ who: address(ali),
+ data: ""
+ });
+
+ assertEq(dss.vat.gem(ilk, ali), 22 ether); // Didn't take whole lot
+ assertEq(dss.vat.dai(ali), rad(890 ether)); // Didn't pay more than tab (110)
+ assertEq(dss.vat.gem(ilk, address(this)), 978 ether); // 960 + (40 - 22) returned to usr
+
+ // Assert auction ends
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+
+ assertEq(dss.dog.Dirt(), 0);
+ (,,, uint256 dirt) = dss.dog.ilks(ilk);
+ assertEq(dirt, 0);
+ }
+
+ function testTakeAtTab() public takeSetup {
+ // Bid so owe (= 22 * 5 = 110 RAD) == tab (= 110 RAD)
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 22 ether,
+ max: ray(5 ether),
+ who: address(ali),
+ data: ""
+ });
+
+ assertEq(dss.vat.gem(ilk, ali), 22 ether); // Didn't take whole lot
+ assertEq(dss.vat.dai(ali), rad(890 ether)); // Paid full tab (110)
+ assertEq(dss.vat.gem(ilk, address(this)), 978 ether); // 960 + (40 - 22) returned to usr
+
+ // Assert auction ends
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+
+ assertEq(dss.dog.Dirt(), 0);
+ (,,, uint256 dirt) = dss.dog.ilks(ilk);
+ assertEq(dirt, 0);
+ }
+
+ function testTakeEmptyDataOrForbiddenWho() public takeSetup {
+ vm.expectRevert(); // Reverts as who is a random address that do not implement clipperCall
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 11 ether,
+ max: ray(5 ether),
+ who: address(123),
+ data: "aaa"
+ });
+ uint256 snapshotId = vm.snapshot();
+ // This one won't revert as has empty data
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 11 ether,
+ max: ray(5 ether),
+ who: address(123),
+ data: ""
+ });
+ vm.revertTo(snapshotId);
+ // The following ones won't revert as are the forbidden addresses and the clipperCall will be ignored
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 11 ether,
+ max: ray(5 ether),
+ who: address(dss.dog),
+ data: "aaa"
+ });
+ vm.revertTo(snapshotId);
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 11 ether,
+ max: ray(5 ether),
+ who: address(dss.vat),
+ data: "aaa"
+ });
+ vm.revertTo(snapshotId);
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 11 ether,
+ max: ray(5 ether),
+ who: address(engine),
+ data: "aaa"
+ });
+ }
+
+ function testTakeUnderTab() public takeSetup {
+ // Bid so owe (= 11 * 5 = 55 RAD) < tab (= 110 RAD)
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 11 ether, // Half of tab at $110
+ max: ray(5 ether),
+ who: address(ali),
+ data: ""
+ });
+
+ assertEq(dss.vat.gem(ilk, ali), 11 ether); // Didn't take whole lot
+ assertEq(dss.vat.dai(ali), rad(945 ether)); // Paid half tab (55)
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether); // Collateral not returned (yet)
+
+ // Assert auction DOES NOT end
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, rad(55 ether)); // 110 - 5 * 11
+ assertEq(sale.lot, 29 ether); // 40 - 11
+ assertEq(sale.tot, 40 ether);
+ assertEq(sale.usr, address(this));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, ray(5 ether));
+
+ assertEq(dss.dog.Dirt(), sale.tab);
+ (,,, uint256 dirt) = dss.dog.ilks(ilk);
+ assertEq(dirt, sale.tab);
+ }
+
+ function testTakeFullLotPartialTab() public takeSetup {
+ vm.warp(block.timestamp + 69); // approx 50% price decline
+ // Bid to purchase entire lot less than tab (~2.5 * 40 ~= 100 < 110)
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 40 ether, // purchase all collateral
+ max: ray(2.5 ether),
+ who: address(ali),
+ data: ""
+ });
+
+ assertEq(dss.vat.gem(ilk, ali), 40 ether); // Took entire lot
+ assertLt(dss.vat.dai(ali) - rad(900 ether), rad(0.1 ether)); // Paid about 100 ether
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether); // Collateral not returned
+
+ // Assert auction ends
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+
+ // All dirt should be cleared, since the auction has ended, even though < 100% of tab was collected
+ assertEq(dss.dog.Dirt(), 0);
+ (,,, uint256 dirt) = dss.dog.ilks(ilk);
+ assertEq(dirt, 0);
+ }
+
+ function testRevertsTakeNeedsReset() public takeSetup {
+ vm.warp(block.timestamp + 3601);
+ vm.expectRevert("LockstakeClipper/needs-reset");
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 22 ether,
+ max: ray(5 ether),
+ who: address(ali),
+ data: ""
+ });
+ }
+
+ function testRevertsTakeBidTooLow() public takeSetup {
+ // Bid so max (= 4) < price (= top = 5) (fails with "Clipper/too-expensive")
+ vm.expectRevert("LockstakeClipper/too-expensive");
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 22 ether,
+ max: ray(4 ether),
+ who: address(ali),
+ data: ""
+ });
+ }
+
+ function testTakeBidRecalculatesDueToChostCheck() public takeSetup {
+ (, uint256 tab, uint256 lot,,,,) = clip.sales(1);
+ assertEq(tab, rad(110 ether));
+ assertEq(lot, 40 ether);
+
+ (, uint256 _price, uint256 _lot, uint256 _tab) = clip.getStatus(1);
+ assertEq(_lot, lot);
+ assertEq(_tab, tab);
+ assertEq(_price, ray(5 ether));
+
+ // Bid for an amount that would leave less than chost remaining tab--bid will be decreased
+ // to leave tab == chost post-execution.
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 18 * WAD, // Costs 90 DAI at current price; 110 - 90 == 20 < 22 == chost
+ max: ray(5 ether),
+ who: address(ali),
+ data: ""
+ });
+
+ (, tab, lot,,,,) = clip.sales(1);
+ assertEq(tab, clip.chost());
+ assertEq(lot, 40 ether - (110 * RAD - clip.chost()) / _price);
+ }
+
+ function testTakeBidAvoidsRecalculateDueNoMoreLot() public takeSetup {
+ vm.warp(block.timestamp + 60); // Reducing the price
+
+ (, uint256 tab, uint256 lot,,,,) = clip.sales(1);
+ assertEq(tab, rad(110 ether));
+ assertEq(lot, 40 ether);
+
+ (, uint256 _price,,) = clip.getStatus(1);
+ assertEq(_price, 2735783211953807380973706855); // 2.73 RAY
+
+ // Bid so owe (= (22 - 1wei) * 5 = 110 RAD - 1) < tab (= 110 RAD)
+ // 1 < 20 RAD => owe = 110 RAD - 20 RAD
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 40 ether,
+ max: ray(2.8 ether),
+ who: address(ali),
+ data: ""
+ });
+
+ // 40 * 2.73 = 109.42...
+ // It means a very low amount of tab (< dust) would remain but doesn't matter
+ // as the auction is finished because there isn't more lot
+ (, tab, lot,,,,) = clip.sales(1);
+ assertEq(tab, 0);
+ assertEq(lot, 0);
+ }
+
+ function testTakeBidFailsNoPartialAllowed() public takeSetup {
+ (, uint256 _price,,) = clip.getStatus(1);
+ assertEq(_price, ray(5 ether));
+
+ clip.take({
+ id: 1,
+ amt: 17.6 ether,
+ max: ray(5 ether),
+ who: address(this),
+ data: ""
+ });
+
+ (, uint256 tab, uint256 lot,,,,) = clip.sales(1);
+ assertEq(tab, rad(22 ether));
+ assertEq(lot, 22.4 ether);
+ assertTrue(!(tab > clip.chost()));
+
+ vm.expectRevert("LockstakeClipper/no-partial-purchase");
+ clip.take({
+ id: 1,
+ amt: 1 ether, // partial purchase attempt when !(tab > chost)
+ max: ray(5 ether),
+ who: address(this),
+ data: ""
+ });
+
+ clip.take({
+ id: 1,
+ amt: tab / _price, // This time take the whole tab
+ max: ray(5 ether),
+ who: address(this),
+ data: ""
+ });
+ }
+
+ function testTakeMultipleBidsDifferentPrices() public takeSetup {
+ // Bid so owe (= 10 * 5 = 50 RAD) < tab (= 110 RAD)
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 10 ether,
+ max: ray(5 ether),
+ who: address(ali),
+ data: ""
+ });
+
+ assertEq(dss.vat.gem(ilk, ali), 10 ether); // Didn't take whole lot
+ assertEq(dss.vat.dai(ali), rad(950 ether)); // Paid some tab (50)
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether); // Collateral not returned (yet)
+
+ // Assert auction DOES NOT end
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, rad(60 ether)); // 110 - 5 * 10
+ assertEq(sale.lot, 30 ether); // 40 - 10
+ assertEq(sale.tot, 40 ether);
+ assertEq(sale.usr, address(this));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, ray(5 ether));
+
+ vm.warp(block.timestamp + 30);
+
+ (, uint256 _price, uint256 _lot,) = clip.getStatus(1);
+ vm.prank(bob); clip.take({
+ id: 1,
+ amt: _lot, // Buy the rest of the lot
+ max: ray(_price), // 5 * 0.99 ** 30 = 3.698501866941401 RAY => max > price
+ who: address(bob),
+ data: ""
+ });
+
+ // Assert auction is over
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+
+ uint256 expectedGem = (RAY * 60 ether) / _price; // tab / price
+ assertEq(dss.vat.gem(ilk, bob), expectedGem); // Didn't take whole lot
+ assertEq(dss.vat.dai(bob), rad(940 ether)); // Paid rest of tab (60)
+
+ uint256 lotReturn = 30 ether - expectedGem; // lot - loaf.tab / max = 15
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether + lotReturn); // Collateral returned (10 WAD)
+ }
+
+ function _auctionResetSetup(uint256 tau) internal {
+ address calc = CalcFabLike(dss.chainlog.getAddress("CALC_FAB")).newLinearDecrease(address(this));
+ CalcLike(calc).file("tau", tau); // tau hours till zero is reached (used to test tail)
+
+ vm.prank(pauseProxy); dss.vat.file(ilk, "dust", rad(20 ether)); // $20 dust
+
+ clip.file("buf", ray(1.25 ether)); // 25% Initial price buffer
+ clip.file("calc", address(calc)); // File price contract
+ clip.file("cusp", ray(0.5 ether)); // 50% drop before reset
+ clip.file("tail", 3600); // 1 hour before reset
+
+ assertEq(clip.kicks(), 0);
+ dss.dog.bark(ilk, address(this), address(this));
+ assertEq(clip.kicks(), 1);
+ }
+
+ function testAuctionResetTail() public {
+ _auctionResetSetup(10 hours); // 10 hours till zero is reached (used to test tail)
+
+ pip.setPrice(3 ether); // Spot = $1.50 (update price before reset is called)
+
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.tic, startTime);
+ assertEq(sale.top, ray(5 ether)); // $4 spot + 25% buffer = $5 (wasn't affected by poke)
+
+ vm.warp(startTime + 3600 seconds);
+ (bool needsRedo,,,) = clip.getStatus(1);
+ assertTrue(!needsRedo);
+ vm.expectRevert("LockstakeClipper/cannot-reset");
+ clip.redo(1, address(this));
+ vm.warp(startTime + 3601 seconds);
+ (needsRedo,,,) = clip.getStatus(1);
+ assertTrue(needsRedo);
+ clip.redo(1, address(this));
+
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.tic, startTime + 3601 seconds); // (block.timestamp)
+ assertEq(sale.top, ray(3.75 ether)); // $3 spot + 25% buffer = $5 (used most recent OSM price)
+ }
+
+ function testAuctionResetCusp() public {
+ _auctionResetSetup(1 hours); // 1 hour till zero is reached (used to test cusp)
+
+ pip.setPrice(3 ether); // Spot = $1.50 (update price before reset is called)
+
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.tic, startTime);
+ assertEq(sale.top, ray(5 ether)); // $4 spot + 25% buffer = $5 (wasn't affected by poke)
+
+ vm.warp(startTime + 1800 seconds);
+ (bool needsRedo,,,) = clip.getStatus(1);
+ assertTrue(!needsRedo);
+ vm.expectRevert("LockstakeClipper/cannot-reset");
+ clip.redo(1, address(this));
+ vm.warp(startTime + 1801 seconds);
+ (needsRedo,,,) = clip.getStatus(1);
+ assertTrue(needsRedo);
+ clip.redo(1, address(this));
+
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.tic, startTime + 1801 seconds); // (block.timestamp)
+ assertEq(sale.top, ray(3.75 ether)); // $3 spot + 25% buffer = $3.75 (used most recent OSM price)
+ }
+
+ function testAuctionResetTailTwice() public {
+ _auctionResetSetup(10 hours); // 10 hours till zero is reached (used to test tail)
+
+ vm.warp(startTime + 3601 seconds);
+ clip.redo(1, address(this));
+
+ vm.expectRevert("LockstakeClipper/cannot-reset");
+ clip.redo(1, address(this));
+ }
+
+ function testAuctionResetCuspTwice() public {
+ _auctionResetSetup(1 hours); // 1 hour till zero is reached (used to test cusp)
+
+ vm.warp(startTime + 1801 seconds); // Price goes below 50% "cusp" after 30min01sec
+ clip.redo(1, address(this));
+
+ vm.expectRevert("LockstakeClipper/cannot-reset");
+ clip.redo(1, address(this));
+ }
+
+ function testRevertsRedoZeroUsr() public {
+ // Can't reset a non-existent auction.
+ vm.expectRevert("LockstakeClipper/not-running-auction");
+ clip.redo(1, address(this));
+ }
+
+ function testRevertsRedoInvalidPrice() public {
+ _auctionResetSetup(1 hours);
+ vm.warp(startTime + 3601 seconds);
+ pip.setPrice(0);
+
+ vm.expectRevert("LockstakeClipper/invalid-price");
+ clip.redo(1, address(this));
+ }
+
+ function testSetBreaker() public {
+ clip.file("stopped", 1);
+ assertEq(clip.stopped(), 1);
+ clip.file("stopped", 2);
+ assertEq(clip.stopped(), 2);
+ clip.file("stopped", 3);
+ assertEq(clip.stopped(), 3);
+ clip.file("stopped", 0);
+ assertEq(clip.stopped(), 0);
+ }
+
+ function testStoppedKick() public {
+ assertEq(clip.kicks(), 0);
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ (uint256 ink, uint256 art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 40 ether);
+ assertEq(art, 100 ether);
+
+ // Any level of stoppage prevents kicking.
+ clip.file("stopped", 1);
+ vm.expectRevert("LockstakeClipper/stopped-incorrect");
+ dss.dog.bark(ilk, address(this), address(this));
+
+ clip.file("stopped", 2);
+ vm.expectRevert("LockstakeClipper/stopped-incorrect");
+ dss.dog.bark(ilk, address(this), address(this));
+
+ clip.file("stopped", 3);
+ vm.expectRevert("LockstakeClipper/stopped-incorrect");
+ dss.dog.bark(ilk, address(this), address(this));
+
+ clip.file("stopped", 0);
+ dss.dog.bark(ilk, address(this), address(this));
+ }
+
+ // At a stopped == 1 we are ok to take
+ function testStopped1Take() public takeSetup {
+ clip.file("stopped", 1);
+ // Bid so owe (= 25 * 5 = 125 RAD) > tab (= 110 RAD)
+ // Readjusts slice to be tab/top = 25
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 25 ether,
+ max: ray(5 ether),
+ who: address(ali),
+ data: ""
+ });
+ }
+
+ function testStopped2Take() public takeSetup {
+ clip.file("stopped", 2);
+ // Bid so owe (= 25 * 5 = 125 RAD) > tab (= 110 RAD)
+ // Readjusts slice to be tab/top = 25
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 25 ether,
+ max: ray(5 ether),
+ who: address(ali),
+ data: ""
+ });
+ }
+
+ function testFailStopped3Take() public takeSetup {
+ clip.file("stopped", 3);
+ // Bid so owe (= 25 * 5 = 125 RAD) > tab (= 110 RAD)
+ // Readjusts slice to be tab/top = 25
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 25 ether,
+ max: ray(5 ether),
+ who: address(ali),
+ data: ""
+ });
+ }
+
+ function testStopped1AuctionResetTail() public {
+ _auctionResetSetup(10 hours); // 10 hours till zero is reached (used to test tail)
+
+ clip.file("stopped", 1);
+
+ pip.setPrice(3 ether); // Spot = $1.50 (update price before reset is called)
+
+ (,,,,, uint96 ticBefore, uint256 topBefore) = clip.sales(1);
+ assertEq(uint256(ticBefore), startTime);
+ assertEq(topBefore, ray(5 ether)); // $4 spot + 25% buffer = $5 (wasn't affected by poke)
+
+ vm.warp(startTime + 3600 seconds);
+ vm.expectRevert("LockstakeClipper/cannot-reset");
+ clip.redo(1, address(this));
+ vm.warp(startTime + 3601 seconds);
+ clip.redo(1, address(this));
+
+ (,,,,, uint96 ticAfter, uint256 topAfter) = clip.sales(1);
+ assertEq(uint256(ticAfter), startTime + 3601 seconds); // (block.timestamp)
+ assertEq(topAfter, ray(3.75 ether)); // $3 spot + 25% buffer = $5 (used most recent OSM price)
+ }
+
+ function testStopped2AuctionResetTail() public {
+ _auctionResetSetup(10 hours); // 10 hours till zero is reached (used to test tail)
+
+ clip.file("stopped", 2);
+
+ pip.setPrice(3 ether); // Spot = $1.50 (update price before reset is called)
+
+ (,,,,, uint96 ticBefore, uint256 topBefore) = clip.sales(1);
+ assertEq(uint256(ticBefore), startTime);
+ assertEq(topBefore, ray(5 ether)); // $4 spot + 25% buffer = $5 (wasn't affected by poke)
+
+ vm.warp(startTime + 3601 seconds);
+ (bool needsRedo,,,) = clip.getStatus(1);
+ assertTrue(needsRedo); // Redo possible if circuit breaker not set
+ vm.expectRevert("LockstakeClipper/stopped-incorrect");
+ clip.redo(1, address(this)); // Redo fails because of circuit breaker
+ }
+
+ function testStopped3AuctionResetTail() public {
+ _auctionResetSetup(10 hours); // 10 hours till zero is reached (used to test tail)
+
+ clip.file("stopped", 3);
+
+ pip.setPrice(3 ether); // Spot = $1.50 (update price before reset is called)
+
+ (,,,,, uint96 ticBefore, uint256 topBefore) = clip.sales(1);
+ assertEq(uint256(ticBefore), startTime);
+ assertEq(topBefore, ray(5 ether)); // $4 spot + 25% buffer = $5 (wasn't affected by poke)
+
+ vm.warp(startTime + 3601 seconds);
+ (bool needsRedo,,,) = clip.getStatus(1);
+ assertTrue(needsRedo); // Redo possible if circuit breaker not set
+ vm.expectRevert("LockstakeClipper/stopped-incorrect");
+ clip.redo(1, address(this)); // Redo fails because of circuit breaker
+ }
+
+ function testRedoIncentive() public takeSetup {
+ clip.file("tip", rad(100 ether)); // Flat fee of 100 DAI
+ clip.file("chip", 0); // No linear increase
+
+ (, uint256 tab, uint256 lot,,,,) = clip.sales(1);
+
+ assertEq(tab, rad(110 ether));
+ assertEq(lot, 40 ether);
+
+ vm.warp(block.timestamp + 300);
+ clip.redo(1, address(123));
+ assertEq(dss.vat.dai(address(123)), clip.tip());
+
+ clip.file("chip", 0.02 ether); // Reward 2% of tab
+ vm.warp(block.timestamp + 300);
+ clip.redo(1, address(234));
+ assertEq(dss.vat.dai(address(234)), clip.tip() + clip.chip() * tab / WAD);
+
+ clip.file("tip", 0); // No more flat fee
+ vm.warp(block.timestamp + 300);
+ clip.redo(1, address(345));
+ assertEq(dss.vat.dai(address(345)), clip.chip() * tab / WAD);
+
+ clip.file("chip", 0); // No more incentive
+ vm.warp(block.timestamp + 300);
+ clip.redo(1, address(456));
+ assertEq(dss.vat.dai(address(456)), 0);
+
+ vm.prank(pauseProxy); dss.vat.file(ilk, "dust", rad(100 ether) + 1); // ensure wmul(dust, chop) > 110 DAI (tab)
+ clip.upchost();
+ assertEq(clip.chost(), 110 * RAD + 1);
+
+ clip.file("tip", rad(100 ether)); // Flat fee of 100 DAI
+ vm.warp(block.timestamp + 300);
+ clip.redo(1, address(567));
+ assertEq(dss.vat.dai(address(567)), 0);
+
+ // Set dust so that wmul(dust, chop) is well below tab to check the dusty lot case.
+ vm.prank(pauseProxy); dss.vat.file(ilk, "dust", rad(20 ether)); // $20 dust
+ clip.upchost();
+ assertEq(clip.chost(), 22 * RAD);
+
+ vm.warp(block.timestamp + 100); // Reducing the price
+
+ (, uint256 _price,,) = clip.getStatus(1);
+ assertEq(_price, 1830161706366147524653080130); // 1.83 RAY
+
+ clip.take({
+ id: 1,
+ amt: 38 ether,
+ max: ray(5 ether),
+ who: address(this),
+ data: ""
+ });
+
+ (, tab, lot,,,,) = clip.sales(1);
+
+ assertEq(tab, rad(110 ether) - 38 ether * _price); // > 22 DAI chost
+ // When auction is reset the current price of lot
+ // is calculated from oracle price ($4) to see if dusty
+ assertEq(lot, 2 ether); // (2 * $4) < $20 quivalent (dusty collateral)
+
+ vm.warp(block.timestamp + 300);
+ clip.redo(1, address(567));
+ assertEq(dss.vat.dai(address(567)), 0);
+ }
+
+ function testIncentiveMaxValues() public {
+ clip.file("chip", 2 ** 64 - 1);
+ clip.file("tip", 2 ** 192 - 1);
+
+ assertEq(uint256(clip.chip()), uint256(18.446744073709551615 * 10 ** 18));
+ assertEq(uint256(clip.tip()), uint256(6277101735386.680763835789423207666416102355444464034512895 * 10 ** 45));
+
+ clip.file("chip", 2 ** 64);
+ clip.file("tip", 2 ** 192);
+
+ assertEq(uint256(clip.chip()), 0);
+ assertEq(uint256(clip.tip()), 0);
+ }
+
+ function testClipperYank() public takeSetup {
+ (,, uint256 lot,, address usr,,) = clip.sales(1);
+ address caller = address(123);
+ clip.rely(caller);
+ uint256 prevUsrGemBalance = dss.vat.gem(ilk, address(usr));
+ uint256 prevCallerGemBalance = dss.vat.gem(ilk, address(caller));
+ uint256 prevClipperGemBalance = dss.vat.gem(ilk, address(clip));
+
+ uint startGas = gasleft();
+ vm.prank(caller); clip.yank(1);
+ uint endGas = gasleft();
+ emit log_named_uint("yank gas", startGas - endGas);
+
+ // Assert that the auction was deleted.
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+
+ // Assert that callback to clear dirt was successful.
+ assertEq(dss.dog.Dirt(), 0);
+ (,,, uint256 dirt) = dss.dog.ilks(ilk);
+ assertEq(dirt, 0);
+
+ // Assert transfer of gem.
+ assertEq(dss.vat.gem(ilk, address(usr)), prevUsrGemBalance);
+ assertEq(dss.vat.gem(ilk, address(caller)), prevCallerGemBalance + lot);
+ assertEq(dss.vat.gem(ilk, address(clip)), prevClipperGemBalance - lot);
+ }
+
+ function testRevertsYankZeroUsr() public takeSetup {
+ // Auction id 2 is unpopulated.
+ (,,,, address usr,,) = clip.sales(2);
+ assertEq(usr, address(0));
+ vm.expectRevert("LockstakeClipper/not-running-auction");
+ clip.yank(2);
+ }
+
+ function testRemoveId() public {
+ LockstakeEngineMock engine2 = new LockstakeEngineMock(address(dss.vat), "random");
+ PublicClip pclip = new PublicClip(address(dss.vat), address(dss.spotter), address(dss.dog), address(engine2));
+ uint256 pos;
+
+ pclip.add();
+ pclip.add();
+ uint256 id = pclip.add();
+ pclip.add();
+ pclip.add();
+
+ // [1,2,3,4,5]
+ assertEq(pclip.count(), 5); // 5 elements added
+ assertEq(pclip.active(0), 1);
+ assertEq(pclip.active(1), 2);
+ assertEq(pclip.active(2), 3);
+ assertEq(pclip.active(3), 4);
+ assertEq(pclip.active(4), 5);
+ uint256[] memory list = pclip.list();
+ assertEq(list[0], 1);
+ assertEq(list[1], 2);
+ assertEq(list[2], 3);
+ assertEq(list[3], 4);
+ assertEq(list[4], 5);
+
+ pclip.remove(id);
+
+ // [1,2,5,4]
+ assertEq(pclip.count(), 4);
+ assertEq(pclip.active(0), 1);
+ assertEq(pclip.active(1), 2);
+ assertEq(pclip.active(2), 5); // Swapped last for middle
+ (pos,,,,,,) = pclip.sales(5);
+ assertEq(pos, 2);
+ assertEq(pclip.active(3), 4);
+
+ pclip.remove(4);
+
+ // [1,2,5]
+ assertEq(pclip.count(), 3);
+
+ (pos,,,,,,) = pclip.sales(1);
+ assertEq(pos, 0); // Sale 1 in slot 0
+ assertEq(pclip.active(0), 1);
+
+ (pos,,,,,,) = pclip.sales(2);
+ assertEq(pos, 1); // Sale 2 in slot 1
+ assertEq(pclip.active(1), 2);
+
+ (pos,,,,,,) = pclip.sales(5);
+ assertEq(pos, 2); // Sale 5 in slot 2
+ assertEq(pclip.active(2), 5); // Final element removed
+
+ (pos,,,,,,) = pclip.sales(4);
+ assertEq(pos, 0); // Sale 4 was deleted. Returns 0
+
+ vm.expectRevert();
+ pclip.active(9); // Fail because id is out of range
+ }
+
+ // function testRevertsNotEnoughDai() public takeSetup {
+ // vm.expectRevert();
+ // vm.prank(che); clip.take({
+ // id: 1,
+ // amt: 25 ether,
+ // max: ray(5 ether),
+ // who: address(che),
+ // data: ""
+ // });
+ // }
+
+ // function testFlashsale() public takeSetup {
+ // address che = address(new Trader(clip, vat, gold, goldJoin, dai, daiJoin, exchange));
+ // assertEq(dss.vat.dai(che), 0);
+ // assertEq(dai.balanceOf(che), 0);
+ // vm.prank(che); clip.take({
+ // id: 1,
+ // amt: 25 ether,
+ // max: ray(5 ether),
+ // who: address(che),
+ // data: "hey"
+ // });
+ // assertEq(dss.vat.dai(che), 0);
+ // assertTrue(dai.balanceOf(che) > 0); // Che turned a profit
+ // }
+
+ function testRevertsReentrancyTake() public takeSetup {
+ BadGuy usr = new BadGuy(clip);
+ vm.prank(address(usr)); dss.vat.hope(address(clip));
+ vm.prank(pauseProxy); dss.vat.suck(address(0), address(usr), rad(1000 ether));
+
+ vm.expectRevert("LockstakeClipper/system-locked");
+ vm.prank(address(usr)); clip.take({
+ id: 1,
+ amt: 25 ether,
+ max: ray(5 ether),
+ who: address(usr),
+ data: "hey"
+ });
+ }
+
+ function testRevertsReentrancyRedo() public takeSetup {
+ RedoGuy usr = new RedoGuy(clip);
+ vm.prank(address(usr)); dss.vat.hope(address(clip));
+ vm.prank(pauseProxy); dss.vat.suck(address(0), address(usr), rad(1000 ether));
+
+ vm.expectRevert("LockstakeClipper/system-locked");
+ vm.prank(address(usr)); clip.take({
+ id: 1,
+ amt: 25 ether,
+ max: ray(5 ether),
+ who: address(usr),
+ data: "hey"
+ });
+ }
+
+ function testRevertsReentrancyKick() public takeSetup {
+ KickGuy usr = new KickGuy(clip);
+ vm.prank(address(usr)); dss.vat.hope(address(clip));
+ vm.prank(pauseProxy); dss.vat.suck(address(0), address(usr), rad(1000 ether));
+ clip.rely(address(usr));
+
+ vm.expectRevert("LockstakeClipper/system-locked");
+ vm.prank(address(usr)); clip.take({
+ id: 1,
+ amt: 25 ether,
+ max: ray(5 ether),
+ who: address(usr),
+ data: "hey"
+ });
+ }
+
+ function testRevertsReentrancyFileUint() public takeSetup {
+ FileUintGuy usr = new FileUintGuy(clip);
+ vm.prank(address(usr)); dss.vat.hope(address(clip));
+ vm.prank(pauseProxy); dss.vat.suck(address(0), address(usr), rad(1000 ether));
+ clip.rely(address(usr));
+
+ vm.expectRevert("LockstakeClipper/system-locked");
+ vm.prank(address(usr)); clip.take({
+ id: 1,
+ amt: 25 ether,
+ max: ray(5 ether),
+ who: address(usr),
+ data: "hey"
+ });
+ }
+
+ function testRevertsReentrancyFileAddr() public takeSetup {
+ FileAddrGuy usr = new FileAddrGuy(clip);
+ vm.prank(address(usr)); dss.vat.hope(address(clip));
+ vm.prank(pauseProxy); dss.vat.suck(address(0), address(usr), rad(1000 ether));
+ clip.rely(address(usr));
+
+ vm.expectRevert("LockstakeClipper/system-locked");
+ vm.prank(address(usr)); clip.take({
+ id: 1,
+ amt: 25 ether,
+ max: ray(5 ether),
+ who: address(usr),
+ data: "hey"
+ });
+ }
+
+ function testRevertsReentrancyYank() public takeSetup {
+ YankGuy usr = new YankGuy(clip);
+ vm.prank(address(usr)); dss.vat.hope(address(clip));
+ vm.prank(pauseProxy); dss.vat.suck(address(0), address(usr), rad(1000 ether));
+ clip.rely(address(usr));
+
+ vm.expectRevert("LockstakeClipper/system-locked");
+ vm.prank(address(usr)); clip.take({
+ id: 1,
+ amt: 25 ether,
+ max: ray(5 ether),
+ who: address(usr),
+ data: "hey"
+ });
+ }
+
+ // function testRevertsTakeImpersonation() public takeSetup { // should fail, but works
+ // vm.expectRevert();
+ // vm.prank(address(bob)); clip.take({
+ // id: 1,
+ // amt: 99999999999999 ether,
+ // max: ray(99999999999999 ether),
+ // who: address(ali),
+ // data: ""
+ // });
+ // }
+
+ function testGasBarkKick() public {
+ // Assertions to make sure setup is as expected.
+ assertEq(clip.kicks(), 0);
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether);
+ assertEq(dss.vat.dai(ali), rad(1000 ether));
+ (uint256 ink, uint256 art) = dss.vat.urns(ilk, address(this));
+ assertEq(ink, 40 ether);
+ assertEq(art, 100 ether);
+
+ uint256 preGas = gasleft();
+ vm.prank(ali); dss.dog.bark(ilk, address(this), address(ali));
+ uint256 diffGas = preGas - gasleft();
+ emit log_named_uint("bark with kick gas", diffGas);
+ }
+
+ function testGasPartialTake() public takeSetup {
+ uint256 preGas = gasleft();
+ // Bid so owe (= 11 * 5 = 55 RAD) < tab (= 110 RAD)
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 11 ether, // Half of tab at $110
+ max: ray(5 ether),
+ who: address(ali),
+ data: ""
+ });
+ uint256 diffGas = preGas - gasleft();
+ emit log_named_uint("partial take gas", diffGas);
+
+ assertEq(dss.vat.gem(ilk, ali), 11 ether); // Didn't take whole lot
+ assertEq(dss.vat.dai(ali), rad(945 ether)); // Paid half tab (55)
+ assertEq(dss.vat.gem(ilk, address(this)), 960 ether); // Collateral not returned (yet)
+
+ // Assert auction DOES NOT end
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, rad(55 ether)); // 110 - 5 * 11
+ assertEq(sale.lot, 29 ether); // 40 - 11
+ assertEq(sale.tot, 40 ether);
+ assertEq(sale.usr, address(this));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, ray(5 ether));
+ }
+
+ function testGasFullTake() public takeSetup {
+ uint256 preGas = gasleft();
+ // Bid so owe (= 25 * 5 = 125 RAD) > tab (= 110 RAD)
+ // Readjusts slice to be tab/top = 25
+ vm.prank(ali); clip.take({
+ id: 1,
+ amt: 25 ether,
+ max: ray(5 ether),
+ who: address(ali),
+ data: ""
+ });
+ uint256 diffGas = preGas - gasleft();
+ emit log_named_uint("full take gas", diffGas);
+
+ assertEq(dss.vat.gem(ilk, ali), 22 ether); // Didn't take whole lot
+ assertEq(dss.vat.dai(ali), rad(890 ether)); // Didn't pay more than tab (110)
+ assertEq(dss.vat.gem(ilk, address(this)), 978 ether); // 960 + (40 - 22) returned to usr
+
+ // Assert auction ends
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(1);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+ }
+}
diff --git a/test/LockstakeEngine-invariants.t.sol b/test/LockstakeEngine-invariants.t.sol
new file mode 100644
index 00000000..855fa91b
--- /dev/null
+++ b/test/LockstakeEngine-invariants.t.sol
@@ -0,0 +1,285 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+import "dss-test/DssTest.sol";
+import { LockstakeEngine } from "src/LockstakeEngine.sol";
+import { LockstakeClipper } from "src/LockstakeClipper.sol";
+import { LockstakeMkr } from "src/LockstakeMkr.sol";
+import { PipMock } from "test/mocks/PipMock.sol";
+import { VoteDelegateFactoryMock, VoteDelegateMock } from "test/mocks/VoteDelegateMock.sol";
+import { GemMock } from "test/mocks/GemMock.sol";
+import { UsdsMock } from "test/mocks/UsdsMock.sol";
+import { UsdsJoinMock } from "test/mocks/UsdsJoinMock.sol";
+import { StakingRewardsMock } from "test/mocks/StakingRewardsMock.sol";
+import { MkrSkyMock } from "test/mocks/MkrSkyMock.sol";
+import { LockstakeHandler } from "test/handlers/LockstakeHandler.sol";
+
+interface ChainlogLike {
+ function getAddress(bytes32) external view returns (address);
+}
+
+interface VatLike {
+ function gem(bytes32, address) external view returns (uint256);
+ function urns(bytes32, address) external view returns (uint256, uint256);
+ function rely(address) external;
+ function file(bytes32, bytes32, uint256) external;
+ function file(bytes32, uint256) external;
+ function init(bytes32) external;
+}
+
+interface SpotterLike {
+ function file(bytes32, bytes32, address) external;
+ function file(bytes32, bytes32, uint256) external;
+ function poke(bytes32) external;
+}
+
+interface JugLike {
+ function file(bytes32, bytes32, uint256) external;
+ function init(bytes32) external;
+}
+
+interface DogLike {
+ function rely(address) external;
+ function file(bytes32, bytes32, address) external;
+ function file(bytes32, bytes32, uint256) external;
+}
+
+interface CalcFabLike {
+ function newStairstepExponentialDecrease(address) external returns (address);
+}
+
+interface CalcLike {
+ function file(bytes32, uint256) external;
+}
+
+contract LockstakeEngineIntegrationTest is DssTest {
+ using stdStorage for StdStorage;
+
+ address public pauseProxy;
+ VatLike public vat;
+ address public spot;
+ DogLike public dog;
+ GemMock public mkr;
+ address public jug;
+ LockstakeEngine public engine;
+ address public urn;
+ LockstakeClipper public clip;
+ PipMock public pip;
+ VoteDelegateFactoryMock public delFactory;
+ UsdsMock public usds;
+ UsdsJoinMock public usdsJoin;
+ LockstakeMkr public lsmkr;
+ GemMock public rTok;
+ StakingRewardsMock public farm0;
+ StakingRewardsMock public farm1;
+ MkrSkyMock public mkrSky;
+ GemMock public sky;
+ bytes32 public ilk = "LSE";
+ address public voter0;
+ address public voter1;
+ address public voterDelegate0;
+ address public voterDelegate1;
+
+ LockstakeHandler public handler;
+
+ address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;
+
+ function _divup(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ unchecked {
+ z = x != 0 ? ((x - 1) / y) + 1 : 0;
+ }
+ }
+
+ function ray(uint256 wad) internal pure returns (uint256) {
+ return wad * 10 ** 9;
+ }
+
+ function rad(uint256 wad) internal pure returns (uint256) {
+ return wad * 10 ** 27;
+ }
+
+ function setUp() public {
+ vm.createSelectFork(vm.envString("ETH_RPC_URL"));
+
+ pauseProxy = ChainlogLike(LOG).getAddress("MCD_PAUSE_PROXY");
+ vat = VatLike(ChainlogLike(LOG).getAddress("MCD_VAT"));
+ spot = ChainlogLike(LOG).getAddress("MCD_SPOT");
+ dog = DogLike(ChainlogLike(LOG).getAddress("MCD_DOG"));
+ mkr = new GemMock(0);
+ jug = ChainlogLike(LOG).getAddress("MCD_JUG");
+ usds = new UsdsMock();
+ usdsJoin = new UsdsJoinMock(address(vat), address(usds));
+ lsmkr = new LockstakeMkr();
+ rTok = new GemMock(0);
+ farm0 = new StakingRewardsMock(address(rTok), address(lsmkr));
+ farm1 = new StakingRewardsMock(address(rTok), address(lsmkr));
+ sky = new GemMock(0);
+ mkrSky = new MkrSkyMock(address(mkr), address(sky), 25_000);
+
+ pip = new PipMock();
+ delFactory = new VoteDelegateFactoryMock(address(mkr));
+ voter0 = address(123);
+ voter1 = address(456);
+ vm.prank(voter0); voterDelegate0 = delFactory.create();
+ vm.prank(voter1); voterDelegate1 = delFactory.create();
+
+ vm.startPrank(pauseProxy);
+ engine = new LockstakeEngine(address(delFactory), address(usdsJoin), ilk, address(mkrSky), address(lsmkr));
+ engine.file("jug", jug);
+ engine.file("fee", 15 * WAD / 100);
+ vat.rely(address(engine));
+ vat.init(ilk);
+ JugLike(jug).init(ilk);
+ JugLike(jug).file(ilk, "duty", 1000000021979553151239153027); // 100% APY
+ SpotterLike(spot).file(ilk, "pip", address(pip));
+ SpotterLike(spot).file(ilk, "mat", 3 * 10**27); // 300% coll ratio
+ pip.setPrice(1000 * 10**18); // 1 MKR = 1000 USD
+ SpotterLike(spot).poke(ilk);
+ vat.file(ilk, "dust", rad(20 ether)); // $20 dust
+
+ vat.file(ilk, "line", type(uint256).max);
+ vat.file("Line", type(uint256).max);
+
+ // dog and clip setup
+ dog.file(ilk, "chop", 1.1 ether); // 10% chop
+ dog.file(ilk, "hole", rad(1_000_000 ether));
+
+ // dust and chop filed previously so clip.chost will be set correctly
+ clip = new LockstakeClipper(address(vat), spot, address(dog), address(engine));
+ clip.upchost();
+ clip.rely(address(dog));
+
+ dog.file(ilk, "clip", address(clip));
+ dog.rely(address(clip));
+ vat.rely(address(clip));
+ engine.rely(address(clip));
+
+ // setup for take
+ address calc = CalcFabLike(ChainlogLike(LOG).getAddress("CALC_FAB")).newStairstepExponentialDecrease(pauseProxy);
+ CalcLike(calc).file("cut", RAY - ray(0.01 ether)); // 1% decrease
+ CalcLike(calc).file("step", 1); // Decrease every 1 second
+
+ clip.file("buf", ray(1.25 ether)); // 25% Initial price buffer
+ clip.file("calc", address(calc)); // File price contract
+ clip.file("cusp", ray(0.3 ether)); // 70% drop before reset
+ clip.file("tail", 3600); // 1 hour before reset
+ vm.stopPrank();
+
+ lsmkr.rely(address(engine));
+
+ deal(address(mkr), address(this), 100_000 * 10**18, true);
+ deal(address(sky), address(this), 100_000 * 25_000 * 10**18, true);
+
+ // Add some existing DAI assigned to usdsJoin to avoid a particular error
+ stdstore.target(address(vat)).sig("dai(address)").with_key(address(usdsJoin)).depth(0).checked_write(100_000 * RAD);
+
+ address[] memory voteDelegates = new address[](2);
+ voteDelegates[0] = voterDelegate0;
+ voteDelegates[1] = voterDelegate1;
+
+ address[] memory farms = new address[](2);
+ farms[0] = address(farm0);
+ farms[1] = address(farm1);
+
+ urn = engine.open(0);
+ handler = new LockstakeHandler(
+ vm,
+ address(engine),
+ address(this),
+ 0,
+ address(spot),
+ address(dog),
+ pauseProxy,
+ voteDelegates,
+ farms
+ );
+
+ // uncomment and fill to only call specific functions
+// bytes4[] memory selectors = new bytes4[](5);
+// selectors[0] = LockstakeHandler.lock.selector;
+// selectors[1] = LockstakeHandler.draw.selector;
+// selectors[2] = LockstakeHandler.selectVoteDelegate.selector;
+// selectors[3] = LockstakeHandler.dropPriceAndBark.selector;
+// selectors[4] = LockstakeHandler.yank.selector;
+//
+// targetSelector(FuzzSelector({
+// addr: address(handler),
+// selectors: selectors
+// }));
+
+ targetContract(address(handler)); // invariant tests should fuzz only handler functions
+ }
+
+ function invariant_system_mkr_equals_ink() public view {
+ (uint256 ink,) = vat.urns(ilk, urn);
+ assertEq(mkr.balanceOf(address(engine)) + handler.sumDelegated() - vat.gem(ilk, address(clip)) - vat.gem(ilk, pauseProxy), ink);
+ }
+
+ function invariant_system_mkr_equals_lsmkr_total_supply() public view {
+ assertEq(mkr.balanceOf(address(engine)) + handler.sumDelegated() - vat.gem(ilk, address(clip)) - vat.gem(ilk, pauseProxy), lsmkr.totalSupply());
+ }
+
+ function invariant_delegation_exclusiveness() public view {
+ assertLe(handler.numDelegated(), 1);
+ }
+
+ function invariant_delegation_all_or_nothing() public view {
+ address urnDelegate = engine.urnVoteDelegates(urn);
+ (uint256 ink,) = vat.urns(ilk, urn);
+
+ if (urnDelegate == address(0)) {
+ assertEq(mkr.balanceOf(address(engine)) - vat.gem(ilk, address(clip)) - vat.gem(ilk, pauseProxy), ink);
+ } else {
+ assertEq(mkr.balanceOf(address(engine)) - vat.gem(ilk, address(clip)) - vat.gem(ilk, pauseProxy), 0);
+ assertEq(handler.delegatedTo(urnDelegate), ink);
+ }
+ }
+
+ function invariant_staking_exclusiveness() public view {
+ assertLe(handler.numStakedForUrn(handler.urn()), 1);
+ }
+
+ function invariant_staking_all_or_nothing() public view {
+ address urnFarm = engine.urnFarms(urn);
+ (uint256 ink,) = vat.urns(ilk, urn);
+
+ if (urnFarm == address(0)) {
+ assertEq(lsmkr.balanceOf(urn), ink);
+ } else {
+ assertEq(lsmkr.balanceOf(urn), 0);
+ assertEq(GemMock(urnFarm).balanceOf(urn), ink);
+ }
+ }
+
+ function invariant_no_delegation_or_staking_during_auction() public view {
+ assertTrue(
+ engine.urnAuctions(urn) == 0 ||
+ engine.urnVoteDelegates(urn) == address(0) && engine.urnFarms(urn) == address(0)
+ );
+ }
+
+ function invariant_call_summary() private view { // make external to enable
+ console.log("------------------");
+
+ console.log("\nCall Summary\n");
+ console.log("addFarm", handler.numCalls("addFarm"));
+ console.log("selectFarm", handler.numCalls("selectFarm"));
+ console.log("selectVoteDelegate", handler.numCalls("selectVoteDelegate"));
+ console.log("lock", handler.numCalls("lock"));
+ console.log("lockSky", handler.numCalls("lockSky"));
+ console.log("free", handler.numCalls("free"));
+ console.log("freeSky", handler.numCalls("freeSky"));
+ console.log("draw", handler.numCalls("draw"));
+ console.log("wipe", handler.numCalls("wipe"));
+ console.log("dropPriceAndBark", handler.numCalls("dropPriceAndBark"));
+ console.log("take", handler.numCalls("take"));
+ console.log("yank", handler.numCalls("yank"));
+ console.log("warp", handler.numCalls("warp"));
+ console.log("total count", handler.numCalls("addFarm") + handler.numCalls("selectFarm") + handler.numCalls("selectVoteDelegate") +
+ handler.numCalls("lock") + handler.numCalls("lockSky") + handler.numCalls("free") +
+ handler.numCalls("freeSky") + handler.numCalls("draw") + handler.numCalls("wipe") +
+ handler.numCalls("dropPriceAndBark") + handler.numCalls("take") + handler.numCalls("yank") +
+ handler.numCalls("warp"));
+ }
+}
diff --git a/test/LockstakeEngine.t.sol b/test/LockstakeEngine.t.sol
new file mode 100644
index 00000000..41aa44d1
--- /dev/null
+++ b/test/LockstakeEngine.t.sol
@@ -0,0 +1,1553 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+import "dss-test/DssTest.sol";
+import "dss-interfaces/Interfaces.sol";
+import { LockstakeDeploy } from "deploy/LockstakeDeploy.sol";
+import { LockstakeInit, LockstakeConfig, LockstakeInstance } from "deploy/LockstakeInit.sol";
+import { LockstakeMkr } from "src/LockstakeMkr.sol";
+import { LockstakeEngine } from "src/LockstakeEngine.sol";
+import { LockstakeClipper } from "src/LockstakeClipper.sol";
+import { LockstakeUrn } from "src/LockstakeUrn.sol";
+import { VoteDelegateFactoryMock, VoteDelegateMock } from "test/mocks/VoteDelegateMock.sol";
+import { GemMock } from "test/mocks/GemMock.sol";
+import { UsdsMock } from "test/mocks/UsdsMock.sol";
+import { UsdsJoinMock } from "test/mocks/UsdsJoinMock.sol";
+import { StakingRewardsMock } from "test/mocks/StakingRewardsMock.sol";
+import { MkrSkyMock } from "test/mocks/MkrSkyMock.sol";
+
+interface CalcFabLike {
+ function newLinearDecrease(address) external returns (address);
+}
+
+interface LineMomLike {
+ function ilks(bytes32) external view returns (uint256);
+}
+
+interface MkrAuthorityLike {
+ function rely(address) external;
+}
+
+contract LockstakeEngineTest is DssTest {
+ using stdStorage for StdStorage;
+
+ DssInstance dss;
+ address pauseProxy;
+ DSTokenAbstract mkr;
+ LockstakeMkr lsmkr;
+ LockstakeEngine engine;
+ LockstakeClipper clip;
+ address calc;
+ OsmAbstract pip;
+ VoteDelegateFactoryMock voteDelegateFactory;
+ UsdsMock usds;
+ UsdsJoinMock usdsJoin;
+ GemMock rTok;
+ StakingRewardsMock farm;
+ StakingRewardsMock farm2;
+ MkrSkyMock mkrSky;
+ GemMock sky;
+ bytes32 ilk = "LSE";
+ address voter;
+ address voteDelegate;
+
+ LockstakeConfig cfg;
+
+ uint256 prevLine;
+
+ address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;
+
+ event AddFarm(address farm);
+ event DelFarm(address farm);
+ event Open(address indexed owner, uint256 indexed index, address urn);
+ event Hope(address indexed owner, uint256 indexed index, address indexed usr);
+ event Nope(address indexed owner, uint256 indexed index, address indexed usr);
+ event SelectVoteDelegate(address indexed owner, uint256 indexed index, address indexed voteDelegate_);
+ event SelectFarm(address indexed owner, uint256 indexed index, address indexed farm, uint16 ref);
+ event Lock(address indexed owner, uint256 indexed index, uint256 wad, uint16 ref);
+ event LockSky(address indexed owner, uint256 indexed index, uint256 skyWad, uint16 ref);
+ event Free(address indexed owner, uint256 indexed index, address to, uint256 wad, uint256 freed);
+ event FreeSky(address indexed owner, uint256 indexed index, address to, uint256 skyWad, uint256 skyFreed);
+ event FreeNoFee(address indexed owner, uint256 indexed index, address to, uint256 wad);
+ event Draw(address indexed owner, uint256 indexed index, address to, uint256 wad);
+ event Wipe(address indexed owner, uint256 indexed index, uint256 wad);
+ event GetReward(address indexed owner, uint256 indexed index, address indexed farm, address to, uint256 amt);
+ event OnKick(address indexed urn, uint256 wad);
+ event OnTake(address indexed urn, address indexed who, uint256 wad);
+ event OnRemove(address indexed urn, uint256 sold, uint256 burn, uint256 refund);
+
+ function _divup(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ // Note: _divup(0,0) will return 0 differing from natural solidity division
+ unchecked {
+ z = x != 0 ? ((x - 1) / y) + 1 : 0;
+ }
+ }
+
+ function _setMedianPrice(uint256 price) internal {
+ vm.store(pip.src(), bytes32(uint256(1)), bytes32(price));
+ vm.warp(block.timestamp + 1 hours);
+ pip.poke();
+ vm.warp(block.timestamp + 1 hours);
+ pip.poke();
+ }
+
+ function setUp() public {
+ vm.createSelectFork(vm.envString("ETH_RPC_URL"));
+
+ dss = MCD.loadFromChainlog(LOG);
+
+ pauseProxy = dss.chainlog.getAddress("MCD_PAUSE_PROXY");
+ pip = OsmAbstract(dss.chainlog.getAddress("PIP_MKR"));
+ mkr = DSTokenAbstract(dss.chainlog.getAddress("MCD_GOV"));
+ usds = new UsdsMock();
+ usdsJoin = new UsdsJoinMock(address(dss.vat), address(usds));
+ rTok = new GemMock(0);
+ sky = new GemMock(0);
+ mkrSky = new MkrSkyMock(address(mkr), address(sky), 24_000);
+ vm.startPrank(pauseProxy);
+ MkrAuthorityLike(mkr.authority()).rely(address(mkrSky));
+ vm.stopPrank();
+
+ voteDelegateFactory = new VoteDelegateFactoryMock(address(mkr));
+ voter = address(123);
+ vm.prank(voter); voteDelegate = voteDelegateFactory.create();
+
+ vm.prank(pauseProxy); pip.kiss(address(this));
+ _setMedianPrice(1_500 * 10**18);
+
+ LockstakeInstance memory instance = LockstakeDeploy.deployLockstake(
+ address(this),
+ pauseProxy,
+ address(voteDelegateFactory),
+ address(usdsJoin),
+ ilk,
+ address(mkrSky),
+ bytes4(abi.encodeWithSignature("newLinearDecrease(address)"))
+ );
+
+ engine = LockstakeEngine(instance.engine);
+ clip = LockstakeClipper(instance.clipper);
+ calc = instance.clipperCalc;
+ lsmkr = LockstakeMkr(instance.lsmkr);
+ farm = new StakingRewardsMock(address(rTok), address(lsmkr));
+ farm2 = new StakingRewardsMock(address(rTok), address(lsmkr));
+
+ address[] memory farms = new address[](2);
+ farms[0] = address(farm);
+ farms[1] = address(farm2);
+
+ cfg = LockstakeConfig({
+ ilk: ilk,
+ voteDelegateFactory: address(voteDelegateFactory),
+ usdsJoin: address(usdsJoin),
+ usds: address(usdsJoin.usds()),
+ mkr: address(mkr),
+ mkrSky: address(mkrSky),
+ sky: address(sky),
+ farms: farms,
+ fee: 15 * WAD / 100,
+ maxLine: 10_000_000 * 10**45,
+ gap: 1_000_000 * 10**45,
+ ttl: 1 days,
+ dust: 50,
+ duty: 100000001 * 10**27 / 100000000,
+ mat: 3 * 10**27,
+ buf: 1.25 * 10**27, // 25% Initial price buffer
+ tail: 3600, // 1 hour before reset
+ cusp: 0.2 * 10**27, // 80% drop before reset
+ chip: 2 * WAD / 100,
+ tip: 3,
+ stopped: 0,
+ chop: 1 ether,
+ hole: 10_000 * 10**45,
+ tau: 100,
+ cut: 0,
+ step: 0,
+ lineMom: true,
+ tolerance: 0.5 * 10**27,
+ name: "LOCKSTAKE",
+ symbol: "LMKR"
+ });
+
+ prevLine = dss.vat.Line();
+
+ vm.startPrank(pauseProxy);
+ LockstakeInit.initLockstake(dss, instance, cfg);
+ vm.stopPrank();
+
+ deal(address(mkr), address(this), 100_000 * 10**18, true);
+ deal(address(sky), address(this), 100_000 * 24_000 * 10**18, true);
+
+ // Add some existing DAI assigned to usdsJoin to avoid a particular error
+ stdstore.target(address(dss.vat)).sig("dai(address)").with_key(address(usdsJoin)).depth(0).checked_write(100_000 * RAD);
+ }
+
+ function _ink(bytes32 ilk_, address urn) internal view returns (uint256 ink) {
+ (ink,) = dss.vat.urns(ilk_, urn);
+ }
+
+ function _art(bytes32 ilk_, address urn) internal view returns (uint256 art) {
+ (, art) = dss.vat.urns(ilk_, urn);
+ }
+
+ function _rate(bytes32 ilk_) internal view returns (uint256 rate) {
+ (, rate,,,) = dss.vat.ilks(ilk_);
+ }
+
+ function _spot(bytes32 ilk_) internal view returns (uint256 spot) {
+ (,, spot,,) = dss.vat.ilks(ilk_);
+ }
+
+ function _line(bytes32 ilk_) internal view returns (uint256 line) {
+ (,,, line,) = dss.vat.ilks(ilk_);
+ }
+
+ function _dust(bytes32 ilk_) internal view returns (uint256 dust) {
+ (,,,, dust) = dss.vat.ilks(ilk_);
+ }
+
+ function _duty(bytes32 ilk_) internal view returns (uint256 duty) {
+ (duty,) = dss.jug.ilks(ilk_);
+ }
+
+ function _rho(bytes32 ilk_) internal view returns (uint256 rho) {
+ (, rho) = dss.jug.ilks(ilk_);
+ }
+
+ function _pip(bytes32 ilk_) internal view returns (address pipV) {
+ (pipV,) = dss.spotter.ilks(ilk_);
+ }
+
+ function _mat(bytes32 ilk_) internal view returns (uint256 mat) {
+ (, mat) = dss.spotter.ilks(ilk_);
+ }
+
+ function _clip(bytes32 ilk_) internal view returns (address clipV) {
+ (clipV,,,) = dss.dog.ilks(ilk_);
+ }
+
+ function _chop(bytes32 ilk_) internal view returns (uint256 chop) {
+ (, chop,,) = dss.dog.ilks(ilk_);
+ }
+
+ function _hole(bytes32 ilk_) internal view returns (uint256 hole) {
+ (,, hole,) = dss.dog.ilks(ilk_);
+ }
+
+ function testDeployAndInit() public {
+ assertEq(address(engine.voteDelegateFactory()), address(voteDelegateFactory));
+ assertEq(address(engine.vat()), address(dss.vat));
+ assertEq(address(engine.usdsJoin()), address(usdsJoin));
+ assertEq(address(engine.usds()), address(usds));
+ assertEq(engine.ilk(), ilk);
+ assertEq(address(engine.mkr()), address(mkr));
+ assertEq(engine.fee(), 15 * WAD / 100);
+ assertEq(address(engine.mkrSky()), address(mkrSky));
+ assertEq(address(engine.sky()), address(sky));
+ assertEq(engine.mkrSkyRate(), 24_000);
+ assertEq(LockstakeUrn(engine.urnImplementation()).engine(), address(engine));
+ assertEq(address(LockstakeUrn(engine.urnImplementation()).vat()), address(dss.vat));
+ assertEq(address(LockstakeUrn(engine.urnImplementation()).lsmkr()), address(lsmkr));
+
+ assertEq(clip.ilk(), ilk);
+ assertEq(address(clip.vat()), address(dss.vat));
+ assertEq(address(clip.engine()), address(engine));
+
+ assertEq(_rate(ilk), 10**27);
+ assertEq(dss.vat.Line(), prevLine + 1_000_000 * 10**45);
+ assertEq(_line(ilk), 1_000_000 * 10**45);
+ assertEq(_dust(ilk), 50);
+ assertEq(dss.vat.wards(address(engine)), 1);
+ assertEq(dss.vat.wards(address(clip)), 1);
+ (uint256 maxline, uint256 gap, uint256 ttl,,) = DssAutoLineAbstract(dss.chainlog.getAddress("MCD_IAM_AUTO_LINE")).ilks(ilk);
+ assertEq(maxline, 10_000_000 * 10**45);
+ assertEq(gap, 1_000_000 * 10**45);
+ assertEq(ttl, 1 days);
+ assertEq(_rho(ilk), block.timestamp);
+ assertEq(_duty(ilk), 100000001 * 10**27 / 100000000);
+ address osmMom = dss.chainlog.getAddress("OSM_MOM");
+ address clipperMom = dss.chainlog.getAddress("CLIPPER_MOM");
+ assertEq(OsmMomAbstract(osmMom).osms(ilk), address(pip));
+ assertEq(pip.wards(osmMom), 1);
+ assertEq(pip.bud(address(dss.spotter)), 1);
+ assertEq(pip.bud(address(clip)), 1);
+ assertEq(pip.bud(clipperMom), 1);
+ assertEq(pip.bud(address(dss.end)), 1);
+ assertEq(_mat(ilk), 3 * 10**27);
+ assertEq(_pip(ilk), address(pip));
+ assertEq(_spot(ilk), (1500 / 3) * 10**27);
+ assertEq(_clip(ilk), address(clip));
+ assertEq(_chop(ilk), 1 ether);
+ assertEq(_hole(ilk), 10_000 * 10**45);
+ assertEq(dss.dog.wards(address(clip)), 1);
+ assertEq(address(engine.jug()), address(dss.jug));
+ assertTrue(engine.farms(address(farm)) == LockstakeEngine.FarmStatus.ACTIVE);
+ assertTrue(engine.farms(address(farm2)) == LockstakeEngine.FarmStatus.ACTIVE);
+ assertEq(engine.wards(address(clip)), 1);
+ assertEq(clip.buf(), 1.25 * 10**27);
+ assertEq(clip.tail(), 3600);
+ assertEq(clip.cusp(), 0.2 * 10**27);
+ assertEq(clip.chip(), 2 * WAD / 100);
+ assertEq(clip.tip(), 3);
+ assertEq(clip.stopped(), 0);
+ assertEq(clip.vow(), address(dss.vow));
+ assertEq(address(clip.calc()), calc);
+ assertEq(clip.chost(), 50 * 1 ether / 10**18);
+ assertEq(clip.wards(address(dss.dog)), 1);
+ assertEq(clip.wards(address(dss.end)), 1);
+ assertEq(clip.wards(clipperMom), 1);
+ assertEq(LinearDecreaseAbstract(calc).tau(), 100);
+ assertEq(LineMomLike(dss.chainlog.getAddress("LINE_MOM")).ilks(ilk), 1);
+ assertEq(ClipperMomAbstract(clipperMom).tolerance(address(clip)), 0.5 * 10**27);
+
+ (
+ string memory name,
+ string memory symbol,
+ uint256 class,
+ uint256 dec,
+ address gem,
+ address pipV,
+ address join,
+ address xlip
+ ) = IlkRegistryAbstract(dss.chainlog.getAddress("ILK_REGISTRY")).info(ilk);
+ assertEq(name, "LOCKSTAKE");
+ assertEq(symbol, "LMKR");
+ assertEq(class, 7);
+ assertEq(gem, address(mkr));
+ assertEq(dec, 18);
+ assertEq(pipV, address(pip));
+ assertEq(join, address(0));
+ assertEq(xlip, address(clip));
+
+ assertEq(dss.chainlog.getAddress("LOCKSTAKE_MKR"), address(lsmkr));
+ assertEq(dss.chainlog.getAddress("LOCKSTAKE_ENGINE"), address(engine));
+ assertEq(dss.chainlog.getAddress("LOCKSTAKE_CLIP"), address(clip));
+ assertEq(dss.chainlog.getAddress("LOCKSTAKE_CLIP_CALC"), address(calc));
+
+ LockstakeInstance memory instance2 = LockstakeDeploy.deployLockstake(
+ address(this),
+ pauseProxy,
+ address(voteDelegateFactory),
+ address(usdsJoin),
+ "eee",
+ address(mkrSky),
+ bytes4(abi.encodeWithSignature("newStairstepExponentialDecrease(address)"))
+ );
+ cfg.ilk = "eee";
+ cfg.tau = 0;
+ cfg.cut = 10**27;
+ cfg.step = 1;
+ cfg.farms[0] = address(new StakingRewardsMock(address(rTok), address(instance2.lsmkr)));
+ cfg.farms[1] = address(new StakingRewardsMock(address(rTok), address(instance2.lsmkr)));
+ vm.startPrank(pauseProxy);
+ LockstakeInit.initLockstake(dss, instance2, cfg);
+ vm.stopPrank();
+ assertEq(StairstepExponentialDecreaseAbstract(instance2.clipperCalc).cut(), 10**27);
+ assertEq(StairstepExponentialDecreaseAbstract(instance2.clipperCalc).step(), 1);
+ }
+
+ function testConstructor() public {
+ address lsmkr2 = address(new GemMock(0));
+ vm.expectEmit(true, true, true, true);
+ emit Rely(address(this));
+ LockstakeEngine e = new LockstakeEngine(address(voteDelegateFactory), address(usdsJoin), "aaa", address(mkrSky), lsmkr2);
+ assertEq(address(e.voteDelegateFactory()), address(voteDelegateFactory));
+ assertEq(address(e.usdsJoin()), address(usdsJoin));
+ assertEq(address(e.vat()), address(dss.vat));
+ assertEq(address(e.usds()), address(usds));
+ assertEq(e.ilk(), "aaa");
+ assertEq(address(e.mkr()), address(mkr));
+ assertEq(address(e.lsmkr()), lsmkr2);
+ assertEq(address(e.mkrSky()), address(mkrSky));
+ assertEq(address(e.sky()), address(sky));
+ assertEq(e.mkrSkyRate(), 24_000);
+ assertEq(LockstakeUrn(e.urnImplementation()).engine(), address(e));
+ assertEq(address(LockstakeUrn(e.urnImplementation()).vat()), address(dss.vat));
+ assertEq(address(LockstakeUrn(e.urnImplementation()).lsmkr()), lsmkr2);
+ assertEq(dss.vat.can(address(e), address(usdsJoin)), 1);
+ assertEq(usds.allowance(address(e), address(usdsJoin)), type(uint256).max);
+ assertEq(sky.allowance(address(e), address(mkrSky)), type(uint256).max);
+ assertEq(mkr.allowance(address(e), address(mkrSky)), type(uint256).max);
+ assertEq(e.wards(address(this)), 1);
+ }
+
+ function testAuth() public {
+ checkAuth(address(engine), "LockstakeEngine");
+ }
+
+ function testFile() public {
+ checkFileAddress(address(engine), "LockstakeEngine", ["jug"]);
+ checkFileUint(address(engine), "LockstakeEngine", ["fee"]);
+
+ vm.expectRevert("LockstakeEngine/fee-equal-or-greater-wad");
+ vm.prank(pauseProxy); engine.file("fee", WAD);
+ }
+
+ function testModifiers() public {
+ bytes4[] memory authedMethods = new bytes4[](6);
+ authedMethods[0] = engine.addFarm.selector;
+ authedMethods[1] = engine.delFarm.selector;
+ authedMethods[2] = engine.freeNoFee.selector;
+ authedMethods[3] = engine.onKick.selector;
+ authedMethods[4] = engine.onTake.selector;
+ authedMethods[5] = engine.onRemove.selector;
+
+ // this checks the case where sender is not authed
+ vm.startPrank(address(0xBEEF));
+ checkModifier(address(engine), "LockstakeEngine/not-authorized", authedMethods);
+ vm.stopPrank();
+ }
+
+ function testAddDelFarm() public {
+ assertTrue(engine.farms(address(1111)) == LockstakeEngine.FarmStatus.UNSUPPORTED);
+ vm.expectEmit(true, true, true, true);
+ emit AddFarm(address(1111));
+ vm.prank(pauseProxy); engine.addFarm(address(1111));
+ assertTrue(engine.farms(address(1111)) == LockstakeEngine.FarmStatus.ACTIVE);
+ vm.expectEmit(true, true, true, true);
+ emit DelFarm(address(1111));
+ vm.prank(pauseProxy); engine.delFarm(address(1111));
+ assertTrue(engine.farms(address(1111)) == LockstakeEngine.FarmStatus.DELETED);
+ }
+
+ function testOpen() public {
+ assertEq(engine.ownerUrnsCount(address(this)), 0);
+ address urn = vm.computeCreateAddress(address(engine), vm.getNonce(address(engine)));
+ vm.expectRevert("LockstakeEngine/wrong-urn-index");
+ engine.open(1);
+
+ assertEq(dss.vat.can(urn, address(engine)), 0);
+ assertEq(lsmkr.allowance(urn, address(engine)), 0);
+ vm.expectEmit(true, true, true, true);
+ emit Open(address(this), 0, urn);
+ assertEq(engine.open(0), urn);
+ assertEq(engine.ownerUrnsCount(address(this)), 1);
+ assertEq(dss.vat.can(urn, address(engine)), 1);
+ assertEq(lsmkr.allowance(urn, address(engine)), type(uint256).max);
+ assertEq(LockstakeUrn(urn).engine(), address(engine));
+ assertEq(address(LockstakeUrn(urn).lsmkr()), address(lsmkr));
+ assertEq(address(LockstakeUrn(urn).vat()), address(dss.vat));
+ vm.expectRevert("LockstakeUrn/not-engine");
+ LockstakeUrn(urn).init();
+
+ vm.expectRevert("LockstakeEngine/wrong-urn-index");
+ engine.open(2);
+
+ address urn2 = vm.computeCreateAddress(address(engine), vm.getNonce(address(engine)));
+ vm.expectEmit(true, true, true, true);
+ emit Open(address(this), 1, urn2);
+ assertEq(engine.open(1), urn2);
+ assertEq(engine.ownerUrnsCount(address(this)), 2);
+ address urn3 = vm.computeCreateAddress(address(engine), vm.getNonce(address(engine)));
+ vm.expectEmit(true, true, true, true);
+ emit Open(address(this), 2, urn3);
+ assertEq(engine.open(2), urn3);
+ assertEq(engine.ownerUrnsCount(address(this)), 3);
+ }
+
+ function testInvalidUrn() public {
+ assertEq(engine.ownerUrns(address(this), 0), address(0));
+ address urn = engine.open(0);
+ assertEq(engine.ownerUrns(address(this), 0), urn);
+ assertEq(engine.ownerUrns(address(this), 1), address(0));
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.isUrnAuth(address(this), 1, address(123));
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.hope(address(this), 1, address(123));
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.nope(address(this), 1, address(123));
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.selectVoteDelegate(address(this), 1, address(123));
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.selectFarm(address(this), 1, address(123), 1);
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.lock(address(this), 1, 1, 1);
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.lockSky(address(this), 1, 1, 1);
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.free(address(this), 1, address(123), 1);
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.freeSky(address(this), 1, address(123), 1);
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ vm.prank(pauseProxy); engine.freeNoFee(address(this), 1, address(123), 1);
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.draw(address(this), 1, address(123), 1);
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.wipe(address(this), 1, 1);
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.wipeAll(address(this), 1);
+ vm.expectRevert("LockstakeEngine/invalid-urn");
+ engine.getReward(address(this), 1, address(123), address(456));
+ }
+
+ function testUrnNotAuthorized() public {
+ vm.prank(address(123)); engine.open(0);
+
+ vm.expectRevert("LockstakeEngine/urn-not-authorized");
+ engine.hope(address(123), 0, address(this));
+ vm.expectRevert("LockstakeEngine/urn-not-authorized");
+ engine.nope(address(123), 0, address(this));
+ vm.expectRevert("LockstakeEngine/urn-not-authorized");
+ engine.selectVoteDelegate(address(123), 0, address(123));
+ vm.expectRevert("LockstakeEngine/urn-not-authorized");
+ engine.selectFarm(address(123), 0, address(123), 1);
+ vm.expectRevert("LockstakeEngine/urn-not-authorized");
+ engine.free(address(123), 0, address(123), 1);
+ vm.expectRevert("LockstakeEngine/urn-not-authorized");
+ engine.freeSky(address(123), 0, address(123), 1);
+ vm.expectRevert("LockstakeEngine/urn-not-authorized");
+ vm.prank(pauseProxy); engine.freeNoFee(address(123), 0, address(123), 1);
+ vm.expectRevert("LockstakeEngine/urn-not-authorized");
+ engine.draw(address(123), 0, address(123), 1);
+ vm.expectRevert("LockstakeEngine/urn-not-authorized");
+ engine.getReward(address(123), 0, address(123), address(456));
+ }
+
+ function testHopeNope() public {
+ address urnOwner = address(123);
+ address urnAuthed = address(456);
+ address authedAndUrnAuthed = address(789);
+ vm.startPrank(pauseProxy);
+ engine.rely(authedAndUrnAuthed);
+ vm.stopPrank();
+ mkr.transfer(urnAuthed, 100_000 * 10**18);
+ sky.transfer(urnAuthed, 100_000 * 24_000 * 10**18);
+ vm.startPrank(urnOwner);
+ address urn = engine.open(0);
+ assertTrue(engine.isUrnAuth(urnOwner, 0, urnOwner));
+ assertTrue(!engine.isUrnAuth(urnOwner, 0, urnAuthed));
+ assertEq(engine.urnCan(urn, urnAuthed), 0);
+ vm.expectEmit(true, true, true, true);
+ emit Hope(urnOwner, 0, urnAuthed);
+ engine.hope(urnOwner, 0, urnAuthed);
+ assertEq(engine.urnCan(urn, urnAuthed), 1);
+ assertTrue(engine.isUrnAuth(urnOwner, 0, urnAuthed));
+ engine.hope(urnOwner, 0, authedAndUrnAuthed);
+ vm.stopPrank();
+ vm.startPrank(urnAuthed);
+ vm.expectEmit(true, true, true, true);
+ emit Hope(urnOwner, 0, address(1111));
+ engine.hope(urnOwner, 0, address(1111));
+ mkr.approve(address(engine), 100_000 * 10**18);
+ engine.lock(urnOwner, 0, 100_000 * 10**18, 0);
+ assertEq(_ink(ilk, urn), 100_000 * 10**18);
+ engine.free(urnOwner, 0, address(this), 50_000 * 10**18);
+ assertEq(_ink(ilk, urn), 50_000 * 10**18);
+ sky.approve(address(engine), 100_000 * 24_000 * 10**18);
+ engine.lockSky(urnOwner, 0, 100_000 * 24_000 * 10**18, 0);
+ assertEq(_ink(ilk, urn), 150_000 * 10**18);
+ engine.freeSky(urnOwner, 0, address(this), 50_000 * 24_000 * 10**18);
+ assertEq(_ink(ilk, urn), 100_000 * 10**18);
+ engine.selectVoteDelegate(urnOwner, 0, voteDelegate);
+ assertEq(engine.urnVoteDelegates(urn), voteDelegate);
+ engine.draw(urnOwner, 0, address(urnAuthed), 1);
+ usds.approve(address(engine), 1);
+ engine.wipe(urnOwner, 0, 1);
+ engine.selectFarm(urnOwner, 0, address(farm), 0);
+ engine.getReward(urnOwner, 0, address(farm), address(0));
+ vm.expectEmit(true, true, true, true);
+ emit Nope(urnOwner, 0, urnAuthed);
+ engine.nope(urnOwner, 0, urnAuthed);
+ assertEq(engine.urnCan(urn, urnAuthed), 0);
+ assertTrue(!engine.isUrnAuth(urnOwner, 0, urnAuthed));
+ vm.stopPrank();
+ vm.prank(authedAndUrnAuthed); engine.freeNoFee(urnOwner, 0, address(this), 50_000 * 10**18);
+ assertEq(_ink(ilk, urn), 50_000 * 10**18);
+ }
+
+ function testSelectVoteDelegate() public {
+ address urn = engine.open(0);
+ vm.expectRevert("LockstakeEngine/not-valid-vote-delegate");
+ engine.selectVoteDelegate(address(this), 0, address(111));
+ vm.expectEmit(true, true, true, true);
+ emit SelectVoteDelegate(address(this), 0, voteDelegate);
+ engine.selectVoteDelegate(address(this), 0, voteDelegate);
+ vm.expectRevert("LockstakeEngine/same-vote-delegate");
+ engine.selectVoteDelegate(address(this), 0, voteDelegate);
+ assertEq(engine.urnVoteDelegates(urn), voteDelegate);
+ vm.prank(address(888)); address voteDelegate2 = voteDelegateFactory.create();
+ mkr.approve(address(engine), 100_000 * 10**18);
+ engine.lock(address(this), 0, 100_000 * 10**18, 5);
+ engine.draw(address(this), 0, address(this), 10_000 * 10**18);
+ assertEq(VoteDelegateMock(voteDelegate).stake(address(engine)), 100_000 * 10**18);
+ assertEq(VoteDelegateMock(voteDelegate2).stake(address(engine)), 0);
+ assertEq(mkr.balanceOf(voteDelegate), 100_000 * 10**18);
+ assertEq(mkr.balanceOf(voteDelegate2), 0);
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ dss.jug.drip(ilk);
+ (, uint256 rateA,,,) = dss.vat.ilks(ilk);
+ vm.warp(block.timestamp + 20);
+ vm.expectEmit(true, true, true, true);
+ emit SelectVoteDelegate(address(this), 0, voteDelegate2);
+ engine.selectVoteDelegate(address(this), 0, voteDelegate2);
+ (, uint256 rateB,,,) = dss.vat.ilks(ilk);
+ assertGt(rateB, rateA);
+ assertEq(engine.urnVoteDelegates(urn), voteDelegate2);
+ assertEq(VoteDelegateMock(voteDelegate).stake(address(engine)), 0);
+ assertEq(VoteDelegateMock(voteDelegate2).stake(address(engine)), 100_000 * 10**18);
+ assertEq(mkr.balanceOf(voteDelegate), 0);
+ assertEq(mkr.balanceOf(voteDelegate2), 100_000 * 10**18);
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ engine.selectVoteDelegate(address(this), 0, address(0));
+ assertEq(engine.urnVoteDelegates(urn), address(0));
+ assertEq(VoteDelegateMock(voteDelegate).stake(address(engine)), 0);
+ assertEq(VoteDelegateMock(voteDelegate2).stake(address(engine)), 0);
+ assertEq(mkr.balanceOf(voteDelegate), 0);
+ assertEq(mkr.balanceOf(voteDelegate2), 0);
+ assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18);
+ }
+
+ function testSelectFarm() public {
+ StakingRewardsMock farm3 = new StakingRewardsMock(address(rTok), address(lsmkr));
+ address urn = engine.open(0);
+ assertEq(engine.urnFarms(urn), address(0));
+ vm.expectRevert("LockstakeEngine/farm-unsupported-or-deleted");
+ engine.selectFarm(address(this), 0, address(farm3), 5);
+ vm.prank(pauseProxy); engine.addFarm(address(farm3));
+ vm.expectEmit(true, true, true, true);
+ emit SelectFarm(address(this), 0, address(farm3), 5);
+ engine.selectFarm(address(this), 0, address(farm3), 5);
+ assertEq(engine.urnFarms(urn), address(farm3));
+ vm.expectRevert("LockstakeEngine/same-farm");
+ engine.selectFarm(address(this), 0, address(farm3), 5);
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+ assertEq(lsmkr.balanceOf(address(farm3)), 0);
+ mkr.approve(address(engine), 100_000 * 10**18);
+ engine.lock(address(this), 0, 100_000 * 10**18, 5);
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+ assertEq(lsmkr.balanceOf(address(farm3)), 100_000 * 10**18);
+ assertEq(farm.balanceOf(urn), 0);
+ assertEq(farm3.balanceOf(urn), 100_000 * 10**18);
+ engine.selectFarm(address(this), 0, address(farm), 5);
+ assertEq(lsmkr.balanceOf(address(farm)), 100_000 * 10**18);
+ assertEq(lsmkr.balanceOf(address(farm3)), 0);
+ assertEq(farm.balanceOf(urn), 100_000 * 10**18);
+ assertEq(farm3.balanceOf(urn), 0);
+ vm.prank(pauseProxy); engine.delFarm(address(farm3));
+ vm.expectRevert("LockstakeEngine/farm-unsupported-or-deleted");
+ engine.selectFarm(address(this), 0, address(farm3), 5);
+ }
+
+ function _testLockFree(bool withDelegate, bool withStaking) internal {
+ uint256 initialMkrSupply = mkr.totalSupply();
+ address urn = engine.open(0);
+ deal(address(mkr), address(this), uint256(type(int256).max) + 1); // deal mkr to allow reaching the overflow revert
+ mkr.approve(address(engine), uint256(type(int256).max) + 1);
+ vm.expectRevert("LockstakeEngine/overflow");
+ engine.lock(address(this), 0, uint256(type(int256).max) + 1, 5);
+ deal(address(mkr), address(this), 100_000 * 10**18); // back to normal mkr balance and allowance
+ mkr.approve(address(engine), 100_000 * 10**18);
+ vm.expectRevert("LockstakeEngine/overflow");
+ engine.free(address(this), 0, address(this), uint256(type(int256).max) + 1);
+ if (withDelegate) {
+ engine.selectVoteDelegate(address(this), 0, voteDelegate);
+ }
+ if (withStaking) {
+ engine.selectFarm(address(this), 0, address(farm), 0);
+ }
+ assertEq(_ink(ilk, urn), 0);
+ assertEq(lsmkr.balanceOf(urn), 0);
+ mkr.transfer(address(123), 100_000 * 10**18);
+ vm.prank(address(123)); mkr.approve(address(engine), 100_000 * 10**18);
+ vm.expectEmit(true, true, true, true);
+ emit Lock(address(this), 0, 100_000 * 10**18, 5);
+ vm.prank(address(123)); engine.lock(address(this), 0, 100_000 * 10**18, 5);
+ assertEq(_ink(ilk, urn), 100_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 100_000 * 10**18);
+ assertEq(farm.balanceOf(urn), 100_000 * 10**18);
+ } else {
+ assertEq(lsmkr.balanceOf(urn), 100_000 * 10**18);
+ }
+ assertEq(mkr.balanceOf(address(this)), 0);
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ assertEq(mkr.balanceOf(voteDelegate), 100_000 * 10**18); // Remains in voteDelegate as it is a mock (otherwise it would be in the Chief)
+ } else {
+ assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18);
+ }
+ assertEq(mkr.totalSupply(), initialMkrSupply);
+ vm.expectEmit(true, true, true, true);
+ emit Free(address(this), 0, address(this), 40_000 * 10**18, 40_000 * 10**18 * 85 / 100);
+ assertEq(engine.free(address(this), 0, address(this), 40_000 * 10**18), 40_000 * 10**18 * 85 / 100);
+ assertEq(_ink(ilk, urn), 60_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 60_000 * 10**18);
+ assertEq(farm.balanceOf(urn), 60_000 * 10**18);
+ } else {
+ assertEq(lsmkr.balanceOf(urn), 60_000 * 10**18);
+ }
+ assertEq(mkr.balanceOf(address(this)), 40_000 * 10**18 - 40_000 * 10**18 * 15 / 100);
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ assertEq(mkr.balanceOf(voteDelegate), 60_000 * 10**18);
+ } else {
+ assertEq(mkr.balanceOf(address(engine)), 60_000 * 10**18);
+ }
+ vm.expectEmit(true, true, true, true);
+ emit Free(address(this), 0, address(123), 10_000 * 10**18, 10_000 * 10**18 * 85 / 100);
+ assertEq(engine.free(address(this), 0, address(123), 10_000 * 10**18), 10_000 * 10**18 * 85 / 100);
+ assertEq(_ink(ilk, urn), 50_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 50_000 * 10**18);
+ assertEq(farm.balanceOf(urn), 50_000 * 10**18);
+ } else {
+ assertEq(lsmkr.balanceOf(urn), 50_000 * 10**18);
+ }
+ assertEq(mkr.balanceOf(address(123)), 10_000 * 10**18 - 10_000 * 10**18 * 15 / 100);
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ assertEq(mkr.balanceOf(voteDelegate), 50_000 * 10**18);
+ } else {
+ assertEq(mkr.balanceOf(address(engine)), 50_000 * 10**18);
+ }
+ assertEq(mkr.totalSupply(), initialMkrSupply - 50_000 * 10**18 * 15 / 100);
+ if (withStaking) {
+ mkr.approve(address(engine), 1);
+ vm.prank(pauseProxy); engine.delFarm(address(farm));
+ vm.expectRevert("LockstakeEngine/farm-deleted");
+ engine.lock(address(this), 0, 1, 0);
+ }
+ }
+
+ function testLockFreeNoDelegateNoStaking() public {
+ _testLockFree(false, false);
+ }
+
+ function testLockFreeWithDelegateNoStaking() public {
+ _testLockFree(true, false);
+ }
+
+ function testLockFreeNoDelegateWithStaking() public {
+ _testLockFree(false, true);
+ }
+
+ function testLockFreeWithDelegateWithStaking() public {
+ _testLockFree(true, true);
+ }
+
+ function _testLockFreeSky(bool withDelegate, bool withStaking) internal {
+ uint256 initialSkySupply = sky.totalSupply();
+ address urn = engine.open(0);
+ // Note: overflow cannot be reached for lockSky and freeSky as with these functions and the value of rate (>=3) the MKR amount will be always lower
+ if (withDelegate) {
+ engine.selectVoteDelegate(address(this), 0, voteDelegate);
+ }
+ if (withStaking) {
+ engine.selectFarm(address(this), 0, address(farm), 0);
+ }
+ assertEq(_ink(ilk, urn), 0);
+ assertEq(lsmkr.balanceOf(urn), 0);
+ sky.approve(address(engine), 100_000 * 24_000 * 10**18);
+ vm.expectEmit(true, true, true, true);
+ emit LockSky(address(this), 0, 100_000 * 24_000 * 10**18, 5);
+ engine.lockSky(address(this), 0, 100_000 * 24_000 * 10**18, 5);
+ assertEq(_ink(ilk, urn), 100_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 100_000 * 10**18);
+ assertEq(farm.balanceOf(urn), 100_000 * 10**18);
+ } else {
+ assertEq(lsmkr.balanceOf(urn), 100_000 * 10**18);
+ }
+ assertEq(sky.balanceOf(address(this)), 0);
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ assertEq(mkr.balanceOf(voteDelegate), 100_000 * 10**18); // Remains in voteDelegate as it is a mock (otherwise it would be in the Chief)
+ } else {
+ assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18);
+ }
+ assertEq(sky.totalSupply(), initialSkySupply - 100_000 * 24_000 * 10**18);
+ vm.expectEmit(true, true, true, true);
+ emit FreeSky(address(this), 0, address(this), 40_000 * 24_000 * 10**18, 40_000 * 24_000 * 10**18 * 85 / 100);
+ assertEq(engine.freeSky(address(this), 0, address(this), 40_000 * 24_000 * 10**18), 40_000 * 24_000 * 10**18 * 85 / 100);
+ assertEq(_ink(ilk, urn), 60_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 60_000 * 10**18);
+ assertEq(farm.balanceOf(urn), 60_000 * 10**18);
+ } else {
+ assertEq(lsmkr.balanceOf(urn), 60_000 * 10**18);
+ }
+ assertEq(sky.balanceOf(address(this)), 40_000 * 24_000 * 10**18 - 40_000 * 24_000 * 10**18 * 15 / 100);
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ assertEq(mkr.balanceOf(voteDelegate), 60_000 * 10**18);
+ } else {
+ assertEq(mkr.balanceOf(address(engine)), 60_000 * 10**18);
+ }
+ vm.expectEmit(true, true, true, true);
+ emit FreeSky(address(this), 0, address(123), 10_000 * 24_000 * 10**18, 10_000 * 24_000 * 10**18 * 85 / 100);
+ assertEq(engine.freeSky(address(this), 0, address(123), 10_000 * 24_000 * 10**18), 10_000 * 24_000 * 10**18 * 85 / 100);
+ assertEq(_ink(ilk, urn), 50_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 50_000 * 10**18);
+ assertEq(farm.balanceOf(urn), 50_000 * 10**18);
+ } else {
+ assertEq(lsmkr.balanceOf(urn), 50_000 * 10**18);
+ }
+ assertEq(sky.balanceOf(address(123)), 10_000 * 24_000 * 10**18 - 10_000 * 24_000 * 10**18 * 15 / 100);
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ assertEq(mkr.balanceOf(voteDelegate), 50_000 * 10**18);
+ } else {
+ assertEq(mkr.balanceOf(address(engine)), 50_000 * 10**18);
+ }
+ assertEq(sky.totalSupply(), initialSkySupply - (100_000 - 50_000) * 24_000 * 10**18 - 50_000 * 24_000 * 10**18 * 15 / 100);
+ if (withStaking) {
+ sky.approve(address(engine), 24_000);
+ vm.prank(pauseProxy); engine.delFarm(address(farm));
+ vm.expectRevert("LockstakeEngine/farm-deleted");
+ engine.lockSky(address(this), 0, 24_000, 0);
+ }
+ }
+
+ function testLockFreeSkyNoDelegateNoStaking() public {
+ _testLockFreeSky(false, false);
+ }
+
+ function testLockFreeSkyWithDelegateNoStaking() public {
+ _testLockFreeSky(true, false);
+ }
+
+ function testLockFreeSkyNoDelegateWithStaking() public {
+ _testLockFreeSky(false, true);
+ }
+
+ function testLockFreeSkyWithDelegateWithStaking() public {
+ _testLockFreeSky(true, true);
+ }
+
+ function _testFreeNoFee(bool withDelegate, bool withStaking) internal {
+ vm.prank(pauseProxy); engine.rely(address(this));
+ uint256 initialMkrSupply = mkr.totalSupply();
+ address urn = engine.open(0);
+ deal(address(mkr), address(this), 100_000 * 10**18);
+ mkr.approve(address(engine), 100_000 * 10**18);
+ vm.expectRevert("LockstakeEngine/overflow");
+ engine.freeNoFee(address(this), 0, address(this), uint256(type(int256).max) + 1);
+ if (withDelegate) {
+ engine.selectVoteDelegate(address(this), 0, voteDelegate);
+ }
+ if (withStaking) {
+ engine.selectFarm(address(this), 0, address(farm), 0);
+ }
+ engine.lock(address(this), 0, 100_000 * 10**18, 5);
+ assertEq(_ink(ilk, urn), 100_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 100_000 * 10**18);
+ assertEq(farm.balanceOf(urn), 100_000 * 10**18);
+ } else {
+ assertEq(lsmkr.balanceOf(urn), 100_000 * 10**18);
+ }
+ assertEq(mkr.balanceOf(address(this)), 0);
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ assertEq(mkr.balanceOf(voteDelegate), 100_000 * 10**18); // Remains in voteDelegate as it is a mock (otherwise it would be in the Chief)
+ } else {
+ assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18);
+ }
+ assertEq(mkr.totalSupply(), initialMkrSupply);
+ vm.expectEmit(true, true, true, true);
+ emit FreeNoFee(address(this), 0, address(this), 40_000 * 10**18);
+ engine.freeNoFee(address(this), 0, address(this), 40_000 * 10**18);
+ assertEq(_ink(ilk, urn), 60_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 60_000 * 10**18);
+ assertEq(farm.balanceOf(urn), 60_000 * 10**18);
+ } else {
+ assertEq(lsmkr.balanceOf(urn), 60_000 * 10**18);
+ }
+ assertEq(mkr.balanceOf(address(this)), 40_000 * 10**18);
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ assertEq(mkr.balanceOf(voteDelegate), 60_000 * 10**18);
+ } else {
+ assertEq(mkr.balanceOf(address(engine)), 60_000 * 10**18);
+ }
+ vm.expectEmit(true, true, true, true);
+ emit FreeNoFee(address(this), 0, address(123), 10_000 * 10**18);
+ engine.freeNoFee(address(this), 0, address(123), 10_000 * 10**18);
+ assertEq(_ink(ilk, urn), 50_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 50_000 * 10**18);
+ assertEq(farm.balanceOf(urn), 50_000 * 10**18);
+ } else {
+ assertEq(lsmkr.balanceOf(urn), 50_000 * 10**18);
+ }
+ assertEq(mkr.balanceOf(address(123)), 10_000 * 10**18);
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ assertEq(mkr.balanceOf(voteDelegate), 50_000 * 10**18);
+ } else {
+ assertEq(mkr.balanceOf(address(engine)), 50_000 * 10**18);
+ }
+ assertEq(mkr.totalSupply(), initialMkrSupply);
+ }
+
+ function testFreeNoFeeNoDelegateNoStaking() public {
+ _testFreeNoFee(false, false);
+ }
+
+ function testFreeNoFeeWithDelegateNoStaking() public {
+ _testFreeNoFee(true, false);
+ }
+
+ function testFreeNoFeeNoDelegateWithStaking() public {
+ _testFreeNoFee(false, true);
+ }
+
+ function testFreeNoFeeWithDelegateWithStaking() public {
+ _testFreeNoFee(true, true);
+ }
+
+ function testDrawWipe() public {
+ deal(address(mkr), address(this), 100_000 * 10**18, true);
+ address urn = engine.open(0);
+ mkr.approve(address(engine), 100_000 * 10**18);
+ engine.lock(address(this), 0, 100_000 * 10**18, 5);
+ assertEq(_art(ilk, urn), 0);
+ vm.expectEmit(true, true, true, true);
+ emit Draw(address(this), 0, address(this), 50 * 10**18);
+ engine.draw(address(this), 0, address(this), 50 * 10**18);
+ assertEq(_art(ilk, urn), 50 * 10**18);
+ assertEq(_rate(ilk), 10**27);
+ assertEq(usds.balanceOf(address(this)), 50 * 10**18);
+ vm.warp(block.timestamp + 1);
+ vm.expectEmit(true, true, true, true);
+ emit Draw(address(this), 0, address(this), 50 * 10**18);
+ engine.draw(address(this), 0, address(this), 50 * 10**18);
+ uint256 art = _art(ilk, urn);
+ uint256 expectedArt = 50 * 10**18 + _divup(50 * 10**18 * 100000000, 100000001);
+ assertEq(art, expectedArt);
+ uint256 rate = _rate(ilk);
+ assertEq(rate, 100000001 * 10**27 / 100000000);
+ assertEq(usds.balanceOf(address(this)), 100 * 10**18);
+ assertGt(art * rate, 100.0000005 * 10**45);
+ assertLt(art * rate, 100.0000006 * 10**45);
+ vm.expectRevert("Usds/insufficient-balance");
+ engine.wipe(address(this), 0, 100.0000006 * 10**18);
+ address anyone = address(1221121);
+ deal(address(usds), anyone, 100.0000006 * 10**18, true);
+ assertEq(usds.balanceOf(anyone), 100.0000006 * 10**18);
+ vm.prank(anyone); usds.approve(address(engine), 100.0000006 * 10**18);
+ vm.expectRevert();
+ vm.prank(anyone); engine.wipe(address(this), 0, 100.0000006 * 10**18); // It will try to wipe more art than existing, then reverts
+ vm.expectEmit(true, true, true, true);
+ emit Wipe(address(this), 0, 100.0000005 * 10**18);
+ vm.prank(anyone); engine.wipe(address(this), 0, 100.0000005 * 10**18);
+ assertEq(usds.balanceOf(anyone), 0.0000001 * 10**18);
+ assertEq(_art(ilk, urn), 1); // Dust which is impossible to wipe via this regular function
+ emit Wipe(address(this), 0, _divup(rate, RAY));
+ vm.prank(anyone); assertEq(engine.wipeAll(address(this), 0), _divup(rate, RAY));
+ assertEq(_art(ilk, urn), 0);
+ assertEq(usds.balanceOf(anyone), 0.0000001 * 10**18 - _divup(rate, RAY));
+ address other = address(123);
+ assertEq(usds.balanceOf(other), 0);
+ emit Draw(address(this), 0, other, 50 * 10**18);
+ engine.draw(address(this), 0, other, 50 * 10**18);
+ assertEq(usds.balanceOf(other), 50 * 10**18);
+ // Check overflows
+ stdstore.target(address(dss.vat)).sig("ilks(bytes32)").with_key(ilk).depth(1).checked_write(1);
+ assertEq(_rate(ilk), 1);
+ vm.expectRevert("LockstakeEngine/overflow");
+ engine.draw(address(this), 0, address(this), uint256(type(int256).max) / RAY + 1);
+ stdstore.target(address(dss.vat)).sig("dai(address)").with_key(address(usdsJoin)).depth(0).checked_write(uint256(type(int256).max) + RAY);
+ deal(address(usds), address(this), uint256(type(int256).max) / RAY + 1, true);
+ usds.approve(address(engine), uint256(type(int256).max) / RAY + 1);
+ vm.expectRevert("LockstakeEngine/overflow");
+ engine.wipe(address(this), 0, uint256(type(int256).max) / RAY + 1);
+ stdstore.target(address(dss.vat)).sig("urns(bytes32,address)").with_key(ilk).with_key(urn).depth(1).checked_write(uint256(type(int256).max) + 1);
+ assertEq(_art(ilk, urn), uint256(type(int256).max) + 1);
+ vm.expectRevert("LockstakeEngine/overflow");
+ engine.wipeAll(address(this), 0);
+ }
+
+ function testOpenLockStakeMulticall() public {
+ mkr.approve(address(engine), 100_000 * 10**18);
+
+ address urn = vm.computeCreateAddress(address(engine), vm.getNonce(address(engine)));
+
+ assertEq(engine.ownerUrnsCount(address(this)), 0);
+ assertEq(_ink(ilk, urn), 0);
+ assertEq(farm.balanceOf(address(urn)), 0);
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+
+ vm.expectEmit(true, true, true, true);
+ emit Open(address(this), 0 , urn);
+ vm.expectEmit(true, true, true, true);
+ emit Lock(address(this), 0, 100_000 * 10**18, uint16(5));
+ vm.expectEmit(true, true, true, true);
+ emit SelectFarm(address(this), 0, address(farm), uint16(5));
+ bytes[] memory callsToExecute = new bytes[](3);
+ callsToExecute[0] = abi.encodeWithSignature("open(uint256)", 0);
+ callsToExecute[1] = abi.encodeWithSignature("lock(address,uint256,uint256,uint16)", address(this), 0, 100_000 * 10**18, uint16(5));
+ callsToExecute[2] = abi.encodeWithSignature("selectFarm(address,uint256,address,uint16)", address(this), 0, address(farm), uint16(5));
+ engine.multicall(callsToExecute);
+
+ assertEq(engine.ownerUrnsCount(address(this)), 1);
+ assertEq(_ink(ilk, urn), 100_000 * 10**18);
+ assertEq(farm.balanceOf(address(urn)), 100_000 * 10**18);
+ assertEq(lsmkr.balanceOf(address(farm)), 100_000 * 10**18);
+
+ bytes[] memory revertExecute = new bytes[](1);
+ revertExecute[0] = abi.encodeWithSignature("open(uint256)", 2);
+ vm.expectRevert("LockstakeEngine/wrong-urn-index");
+ engine.multicall(revertExecute);
+
+ revertExecute[0] = abi.encodeWithSignature("onRemove(address,uint256,uint256)", urn, uint256(0), uint256(0));
+ vm.expectRevert(stdError.arithmeticError);
+ vm.prank(pauseProxy); engine.multicall(revertExecute);
+ }
+
+ function testGetReward() public {
+ address urn = engine.open(0);
+ vm.expectRevert("LockstakeEngine/farm-unsupported");
+ engine.getReward(address(this), 0, address(456), address(123));
+ farm.setReward(address(urn), 20_000);
+ assertEq(GemMock(address(farm.rewardsToken())).balanceOf(address(123)), 0);
+ vm.expectEmit(true, true, true, true);
+ emit GetReward(address(this), 0, address(farm), address(123), 20_000);
+ assertEq(engine.getReward(address(this), 0, address(farm), address(123)), 20_000);
+ assertEq(GemMock(address(farm.rewardsToken())).balanceOf(address(123)), 20_000);
+ vm.prank(pauseProxy); engine.delFarm(address(farm));
+ farm.setReward(address(urn), 30_000);
+ assertEq(engine.getReward(address(this), 0, address(farm), address(123)), 30_000); // Can get reward after farm is deleted
+ assertEq(GemMock(address(farm.rewardsToken())).balanceOf(address(123)), 50_000);
+ }
+
+ function _urnSetUp(bool withDelegate, bool withStaking) internal returns (address urn) {
+ urn = engine.open(0);
+ if (withDelegate) {
+ engine.selectVoteDelegate(address(this), 0, voteDelegate);
+ }
+ if (withStaking) {
+ engine.selectFarm(address(this), 0, address(farm), 0);
+ }
+ mkr.approve(address(engine), 100_000 * 10**18);
+ engine.lock(address(this), 0, 100_000 * 10**18, 5);
+ engine.draw(address(this), 0, address(this), 2_000 * 10**18);
+ assertEq(_ink(ilk, urn), 100_000 * 10**18);
+ assertEq(_art(ilk, urn), 2_000 * 10**18);
+
+ if (withDelegate) {
+ assertEq(engine.urnVoteDelegates(urn), voteDelegate);
+ assertEq(mkr.balanceOf(voteDelegate), 100_000 * 10**18);
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ } else {
+ assertEq(engine.urnVoteDelegates(urn), address(0));
+ assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18);
+ }
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(urn)), 0);
+ assertEq(lsmkr.balanceOf(address(farm)), 100_000 * 10**18);
+ assertEq(farm.balanceOf(address(urn)), 100_000 * 10**18);
+ } else {
+ assertEq(lsmkr.balanceOf(address(urn)), 100_000 * 10**18);
+ }
+ }
+
+ function _forceLiquidation(address urn) internal returns (uint256 id) {
+ _setMedianPrice(0.05 * 10**18); // Force liquidation
+ dss.spotter.poke(ilk);
+ assertEq(clip.kicks(), 0);
+ assertEq(engine.urnAuctions(urn), 0);
+ (,, uint256 hole,) = dss.dog.ilks(ilk);
+ uint256 kicked = hole < 2_000 * 10**45 ? 100_000 * 10**18 * hole / (2_000 * 10**45) : 100_000 * 10**18;
+ vm.expectEmit(true, true, true, true);
+ emit OnKick(urn, kicked);
+ id = dss.dog.bark(ilk, address(urn), address(this));
+ assertEq(clip.kicks(), 1);
+ assertEq(engine.urnAuctions(urn), 1);
+ }
+
+ function _testOnKickFull(bool withDelegate, bool withStaking) internal {
+ address urn = _urnSetUp(withDelegate, withStaking);
+ uint256 lsmkrInitialSupply = lsmkr.totalSupply();
+ uint256 id = _forceLiquidation(urn);
+
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 2_000 * 10**45);
+ assertEq(sale.lot, 100_000 * 10**18);
+ assertEq(sale.tot, 100_000 * 10**18);
+ assertEq(sale.usr, address(urn));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, uint256(pip.read()) * (1.25 * 10**9));
+
+ assertEq(_ink(ilk, urn), 0);
+ assertEq(_art(ilk, urn), 0);
+ assertEq(dss.vat.gem(ilk, address(clip)), 100_000 * 10**18);
+
+ if (withDelegate) {
+ assertEq(engine.urnVoteDelegates(urn), address(0));
+ assertEq(mkr.balanceOf(voteDelegate), 0);
+ }
+ assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+ assertEq(farm.balanceOf(address(urn)), 0);
+ }
+ assertEq(lsmkr.balanceOf(address(urn)), 0);
+ assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18);
+ }
+
+ function testOnKickFullNoStakingNoDelegate() public {
+ _testOnKickFull(false, false);
+ }
+
+ function testOnKickFullNoStakingWithDelegate() public {
+ _testOnKickFull(true, false);
+ }
+
+ function testOnKickFullWithStakingNoDelegate() public {
+ _testOnKickFull(false, true);
+ }
+
+ function testOnKickFullWithStakingWithDelegate() public {
+ _testOnKickFull(true, true);
+ }
+
+ function _testOnKickPartial(bool withDelegate, bool withStaking) internal {
+ address urn = _urnSetUp(withDelegate, withStaking);
+ uint256 lsmkrInitialSupply = lsmkr.totalSupply();
+ vm.prank(pauseProxy); dss.dog.file(ilk, "hole", 500 * 10**45);
+ uint256 id = _forceLiquidation(urn);
+
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 500 * 10**45);
+ assertEq(sale.lot, 25_000 * 10**18);
+ assertEq(sale.tot, 25_000 * 10**18);
+ assertEq(sale.usr, address(urn));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, uint256(pip.read()) * (1.25 * 10**9));
+
+ assertEq(_ink(ilk, urn), 75_000 * 10**18);
+ assertEq(_art(ilk, urn), 1_500 * 10**18);
+ assertEq(dss.vat.gem(ilk, address(clip)), 25_000 * 10**18);
+
+ if (withDelegate) {
+ assertEq(engine.urnVoteDelegates(urn), address(0));
+ assertEq(mkr.balanceOf(voteDelegate), 0);
+ }
+ assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+ assertEq(farm.balanceOf(address(urn)), 0);
+ }
+ assertEq(lsmkr.balanceOf(address(urn)), 75_000 * 10**18);
+ assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 25_000 * 10**18);
+ }
+
+ function testOnKickPartialNoStakingNoDelegate() public {
+ _testOnKickPartial(false, false);
+ }
+
+ function testOnKickPartialNoStakingWithDelegate() public {
+ _testOnKickPartial(true, false);
+ }
+
+ function testOnKickPartialWithStakingNoDelegate() public {
+ _testOnKickPartial(false, true);
+ }
+
+ function testOnKickPartialWithStakingWithDelegate() public {
+ _testOnKickPartial(true, true);
+ }
+
+ function _testOnTake(bool withDelegate, bool withStaking) internal {
+ address urn = _urnSetUp(withDelegate, withStaking);
+ uint256 mkrInitialSupply = mkr.totalSupply();
+ uint256 lsmkrInitialSupply = lsmkr.totalSupply();
+ address vow = address(dss.vow);
+ uint256 vowInitialBalance = dss.vat.dai(vow);
+ uint256 id = _forceLiquidation(urn);
+
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 2_000 * 10**45);
+ assertEq(sale.lot, 100_000 * 10**18);
+ assertEq(sale.tot, 100_000 * 10**18);
+ assertEq(sale.usr, address(urn));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, uint256(pip.read()) * (1.25 * 10**9));
+
+ assertEq(_ink(ilk, urn), 0);
+ assertEq(_art(ilk, urn), 0);
+ assertEq(dss.vat.gem(ilk, address(clip)), 100_000 * 10**18);
+
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(voteDelegate), 0);
+ }
+ assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+ assertEq(farm.balanceOf(address(urn)), 0);
+ }
+ assertEq(lsmkr.balanceOf(address(urn)), 0);
+ assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18);
+
+ address buyer = address(888);
+ vm.prank(pauseProxy); dss.vat.suck(address(0), buyer, 2_000 * 10**45);
+ vm.prank(buyer); dss.vat.hope(address(clip));
+ assertEq(mkr.balanceOf(buyer), 0);
+ vm.expectEmit(true, true, true, true);
+ emit OnTake(urn, buyer, 20_000 * 10**18);
+ vm.prank(buyer); clip.take(id, 20_000 * 10**18, type(uint256).max, buyer, "");
+ assertEq(mkr.balanceOf(buyer), 20_000 * 10**18);
+
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, (2_000 - 20_000 * 0.05 * 1.25) * 10**45);
+ assertEq(sale.lot, 80_000 * 10**18);
+ assertEq(sale.tot, 100_000 * 10**18);
+ assertEq(sale.usr, address(urn));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, uint256(pip.read()) * (1.25 * 10**9));
+
+ assertEq(_ink(ilk, urn), 0);
+ assertEq(_art(ilk, urn), 0);
+ assertEq(dss.vat.gem(ilk, address(clip)), 80_000 * 10**18);
+
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(voteDelegate), 0);
+ }
+ assertEq(mkr.balanceOf(address(engine)), 80_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+ assertEq(farm.balanceOf(address(urn)), 0);
+ }
+ assertEq(lsmkr.balanceOf(address(urn)), 0);
+ assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18);
+
+ uint256 burn = 32_000 * 10**18 * engine.fee() / (WAD - engine.fee());
+ vm.expectEmit(true, true, true, true);
+ emit OnTake(urn, buyer, 12_000 * 10**18);
+ vm.expectEmit(true, true, true, true);
+ emit OnRemove(urn, 32_000 * 10**18, burn, 100_000 * 10**18 - 32_000 * 10**18 - burn);
+ vm.prank(buyer); clip.take(id, 12_000 * 10**18, type(uint256).max, buyer, "");
+ assertEq(burn, (32_000 * 10**18 + burn) * engine.fee() / WAD);
+ assertEq(mkr.balanceOf(buyer), 32_000 * 10**18);
+ assertEq(engine.urnAuctions(urn), 0);
+
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 0);
+ assertEq(sale.lot, 0);
+ assertEq(sale.tot, 0);
+ assertEq(sale.usr, address(0));
+ assertEq(sale.tic, 0);
+ assertEq(sale.top, 0);
+
+ assertEq(_ink(ilk, urn), 100_000 * 10**18 - 32_000 * 10**18 - burn);
+ assertEq(_art(ilk, urn), 0);
+ assertEq(dss.vat.gem(ilk, address(clip)), 0);
+
+ assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18 - 32_000 * 10**18 - burn);
+ assertEq(mkr.totalSupply(), mkrInitialSupply - burn);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+ assertEq(farm.balanceOf(address(urn)), 0);
+ }
+ assertEq(lsmkr.balanceOf(address(urn)), 100_000 * 10**18 - 32_000 * 10**18 - burn);
+ assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 32_000 * 10**18 - burn);
+ assertEq(dss.vat.dai(vow), vowInitialBalance + 2_000 * 10**45);
+ }
+
+ function testOnTakeNoWithStakingNoDelegate() public {
+ _testOnTake(false, false);
+ }
+
+ function testOnTakeNoWithStakingWithDelegate() public {
+ _testOnTake(true, false);
+ }
+
+ function testOnTakeWithStakingNoDelegate() public {
+ _testOnTake(false, true);
+ }
+
+ function testOnTakeWithStakingWithDelegate() public {
+ _testOnTake(true, true);
+ }
+
+ function _testOnTakePartialBurn(bool withDelegate, bool withStaking) internal {
+ address urn = _urnSetUp(withDelegate, withStaking);
+ uint256 mkrInitialSupply = mkr.totalSupply();
+ uint256 lsmkrInitialSupply = lsmkr.totalSupply();
+ address vow = address(dss.vow);
+ uint256 vowInitialBalance = dss.vat.dai(vow);
+ uint256 id = _forceLiquidation(urn);
+
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 2_000 * 10**45);
+ assertEq(sale.lot, 100_000 * 10**18);
+ assertEq(sale.tot, 100_000 * 10**18);
+ assertEq(sale.usr, address(urn));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, uint256(pip.read()) * (1.25 * 10**9));
+
+ assertEq(_ink(ilk, urn), 0);
+ assertEq(_art(ilk, urn), 0);
+ assertEq(dss.vat.gem(ilk, address(clip)), 100_000 * 10**18);
+
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(voteDelegate), 0);
+ }
+ assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+ assertEq(farm.balanceOf(address(urn)), 0);
+ }
+ assertEq(lsmkr.balanceOf(address(urn)), 0);
+ assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18);
+
+ vm.warp(block.timestamp + 65); // Time passes to let the auction price to crash
+
+ address buyer = address(888);
+ vm.prank(pauseProxy); dss.vat.suck(address(0), buyer, 2_000 * 10**45);
+ vm.prank(buyer); dss.vat.hope(address(clip));
+ assertEq(mkr.balanceOf(buyer), 0);
+ vm.expectEmit(true, true, true, true);
+ emit OnTake(urn, buyer, 91428571428571428571428);
+ vm.expectEmit(true, true, true, true);
+ emit OnRemove(urn, 91428571428571428571428, 100_000 * 10**18 - 91428571428571428571428, 0);
+ vm.prank(buyer); clip.take(id, 100_000 * 10**18, type(uint256).max, buyer, "");
+ assertEq(mkr.balanceOf(buyer), 91428571428571428571428);
+ assertEq(engine.urnAuctions(urn), 0);
+
+ assertEq(_ink(ilk, urn), 0);
+ assertEq(_art(ilk, urn), 0);
+ assertEq(dss.vat.gem(ilk, address(clip)), 0);
+
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(voteDelegate), 0);
+ }
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ assertEq(mkr.totalSupply(), mkrInitialSupply - (100_000 * 10**18 - 91428571428571428571428)); // Can't burn 15% of 91428571428571428571428
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+ assertEq(farm.balanceOf(address(urn)), 0);
+ }
+ assertEq(lsmkr.balanceOf(address(urn)), 0);
+ assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18);
+ assertEq(dss.vat.dai(vow), vowInitialBalance + 2_000 * 10**45);
+ }
+
+ function testOnTakePartialBurnNoStakingNoDelegate() public {
+ _testOnTakePartialBurn(false, false);
+ }
+
+ function testOnTakePartialBurnNoStakingWithDelegate() public {
+ _testOnTakePartialBurn(true, false);
+ }
+
+ function testOnTakePartialBurnWithStakingNoDelegate() public {
+ _testOnTakePartialBurn(false, true);
+ }
+
+ function testOnTakePartialBurnWithStakingWithDelegate() public {
+ _testOnTakePartialBurn(true, true);
+ }
+
+ function _testOnTakeNoBurn(bool withDelegate, bool withStaking) internal {
+ address urn = _urnSetUp(withDelegate, withStaking);
+ uint256 mkrInitialSupply = mkr.totalSupply();
+ uint256 lsmkrInitialSupply = lsmkr.totalSupply();
+ address vow = address(dss.vow);
+ uint256 vowInitialBalance = dss.vat.dai(vow);
+ uint256 id = _forceLiquidation(urn);
+
+ LockstakeClipper.Sale memory sale;
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(id);
+ assertEq(sale.pos, 0);
+ assertEq(sale.tab, 2_000 * 10**45);
+ assertEq(sale.lot, 100_000 * 10**18);
+ assertEq(sale.tot, 100_000 * 10**18);
+ assertEq(sale.usr, address(urn));
+ assertEq(sale.tic, block.timestamp);
+ assertEq(sale.top, uint256(pip.read()) * (1.25 * 10**9));
+
+ assertEq(_ink(ilk, urn), 0);
+ assertEq(_art(ilk, urn), 0);
+ assertEq(dss.vat.gem(ilk, address(clip)), 100_000 * 10**18);
+
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(voteDelegate), 0);
+ }
+ assertEq(mkr.balanceOf(address(engine)), 100_000 * 10**18);
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+ assertEq(farm.balanceOf(address(urn)), 0);
+ }
+ assertEq(lsmkr.balanceOf(address(urn)), 0);
+ assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18);
+
+ vm.warp(block.timestamp + 80); // Time passes to let the auction price to crash
+
+ address buyer = address(888);
+ vm.prank(pauseProxy); dss.vat.suck(address(0), buyer, 2_000 * 10**45);
+ vm.prank(buyer); dss.vat.hope(address(clip));
+ assertEq(mkr.balanceOf(buyer), 0);
+ vm.expectEmit(true, true, true, true);
+ emit OnTake(urn, buyer, 100_000 * 10**18);
+ vm.expectEmit(true, true, true, true);
+ emit OnRemove(urn, 100_000 * 10**18, 0, 0);
+ vm.prank(buyer); clip.take(id, 100_000 * 10**18, type(uint256).max, buyer, "");
+ assertEq(mkr.balanceOf(buyer), 100_000 * 10**18);
+ assertEq(engine.urnAuctions(urn), 0);
+
+ assertEq(_ink(ilk, urn), 0);
+ assertEq(_art(ilk, urn), 0);
+ assertEq(dss.vat.gem(ilk, address(clip)), 0);
+
+ if (withDelegate) {
+ assertEq(mkr.balanceOf(voteDelegate), 0);
+ }
+ assertEq(mkr.balanceOf(address(engine)), 0);
+ assertEq(mkr.totalSupply(), mkrInitialSupply); // Can't burn anything
+ if (withStaking) {
+ assertEq(lsmkr.balanceOf(address(farm)), 0);
+ assertEq(farm.balanceOf(address(urn)), 0);
+ }
+ assertEq(lsmkr.balanceOf(address(urn)), 0);
+ assertEq(lsmkr.totalSupply(), lsmkrInitialSupply - 100_000 * 10**18);
+ assertLt(dss.vat.dai(vow), vowInitialBalance + 2_000 * 10**45); // Doesn't recover full debt
+ }
+
+ function testOnTakeNoBurnNoStakingNoDelegate() public {
+ _testOnTakeNoBurn(false, false);
+ }
+
+ function testOnTakeNoBurnNoStakingWithDelegate() public {
+ _testOnTakeNoBurn(true, false);
+ }
+
+ function testOnTakeNoBurnWithStakingNoDelegate() public {
+ _testOnTakeNoBurn(false, true);
+ }
+
+ function testOnTakeNoBurnWithStakingWithDelegate() public {
+ _testOnTakeNoBurn(true, true);
+ }
+
+ function testCannotSelectDuringAuction() public {
+ address urn = _urnSetUp(true, true);
+
+ assertEq(engine.urnVoteDelegates(urn), voteDelegate);
+ assertEq(engine.urnFarms(urn), address(farm));
+
+ vm.prank(pauseProxy); dss.dog.file(ilk, "hole", 500 * 10**45);
+ uint256 id1 = _forceLiquidation(urn);
+
+ assertEq(engine.urnVoteDelegates(urn), address(0));
+ assertEq(engine.urnFarms(urn), address(0));
+
+ vm.expectRevert("LockstakeEngine/urn-in-auction");
+ engine.selectVoteDelegate(address(this), 0, voteDelegate);
+ vm.expectRevert("LockstakeEngine/urn-in-auction");
+ engine.selectFarm(address(this), 0, address(farm), 0);
+
+ vm.prank(pauseProxy); dss.dog.file(ilk, "hole", 1000 * 10**45);
+ uint256 id2 = dss.dog.bark(ilk, urn, address(this));
+
+ assertEq(engine.urnAuctions(urn), 2);
+
+ vm.expectRevert("LockstakeEngine/urn-in-auction");
+ engine.selectVoteDelegate(address(this), 0, voteDelegate);
+ vm.expectRevert("LockstakeEngine/urn-in-auction");
+ engine.selectFarm(address(this), 0, address(farm), 0);
+
+ // Take with left > 0
+ address buyer = address(888);
+ vm.prank(pauseProxy); dss.vat.suck(address(0), buyer, 4_000 * 10**45);
+ vm.prank(buyer); dss.vat.hope(address(clip));
+ uint256 burn = 8_000 * 10**18 * engine.fee() / (WAD - engine.fee());
+ vm.expectEmit(true, true, true, true);
+ emit OnTake(urn, buyer, 8_000 * 10**18); // 500 / (0.05 * 1.25 )
+ vm.expectEmit(true, true, true, true);
+ emit OnRemove(urn, 8_000 * 10**18, burn, 25_000 * 10**18 - 8_000 * 10**18 - burn);
+ vm.prank(buyer); clip.take(id1, 25_000 * 10**18, type(uint256).max, buyer, "");
+ assertEq(engine.urnAuctions(urn), 1);
+
+ vm.expectRevert("LockstakeEngine/urn-in-auction");
+ engine.selectVoteDelegate(address(this), 0, voteDelegate);
+ vm.expectRevert("LockstakeEngine/urn-in-auction");
+ engine.selectFarm(address(this), 0, address(farm), 0);
+
+ vm.warp(block.timestamp + 80); // Time passes to let the auction price to crash
+
+ // Take with left == 0
+ vm.expectEmit(true, true, true, true);
+ emit OnTake(urn, buyer, 25_000 * 10**18);
+ vm.expectEmit(true, true, true, true);
+ emit OnRemove(urn, 25_000 * 10**18, 0, 0);
+ vm.prank(buyer); clip.take(id2, 25_000 * 10**18, type(uint256).max, buyer, "");
+ assertEq(engine.urnAuctions(urn), 0);
+
+ // Can select voteDelegate and farm again
+ engine.selectVoteDelegate(address(this), 0, voteDelegate);
+ engine.selectFarm(address(this), 0, address(farm), 0);
+ }
+
+ function testUrnUnsafe() public {
+ address urn = _urnSetUp(true, true);
+
+ assertEq(engine.urnVoteDelegates(urn), voteDelegate);
+
+ address voteDelegate2 = voteDelegateFactory.create();
+
+ _setMedianPrice(0.05 * 10**18); // Force urn unsafe
+ dss.spotter.poke(ilk);
+
+ vm.expectRevert("LockstakeEngine/urn-unsafe");
+ engine.selectVoteDelegate(address(this), 0, voteDelegate2);
+
+ engine.selectVoteDelegate(address(this), 0, address(0));
+
+ vm.expectRevert("LockstakeEngine/urn-unsafe");
+ engine.selectVoteDelegate(address(this), 0, voteDelegate2);
+
+ _setMedianPrice(1_500 * 10**18); // Back to safety
+ dss.spotter.poke(ilk);
+
+ engine.selectVoteDelegate(address(this), 0, voteDelegate2);
+
+ assertEq(engine.urnVoteDelegates(urn), voteDelegate2);
+ }
+
+ function testOnRemoveOverflow() public {
+ vm.expectRevert("LockstakeEngine/overflow");
+ vm.prank(pauseProxy); engine.onRemove(address(1), 0, uint256(type(int256).max) + 1);
+ }
+
+ function _testYank(bool withDelegate, bool withStaking) internal {
+ address urn = _urnSetUp(withDelegate, withStaking);
+ uint256 id = _forceLiquidation(urn);
+
+ vm.expectEmit(true, true, true, true);
+ emit OnRemove(urn, 0, 0, 0);
+ vm.prank(pauseProxy); clip.yank(id);
+ assertEq(engine.urnAuctions(urn), 0);
+ }
+
+ function testYankNoStakingNoDelegate() public {
+ _testYank(false, false);
+ }
+
+ function testYankNoStakingWithDelegate() public {
+ _testYank(true, false);
+ }
+
+ function testYankWithStakingNoDelegate() public {
+ _testYank(false, true);
+ }
+
+ function testYankWithStakingWithDelegate() public {
+ _testYank(true, true);
+ }
+}
diff --git a/test/LockstakeMkr.t.sol b/test/LockstakeMkr.t.sol
new file mode 100644
index 00000000..ec712fe9
--- /dev/null
+++ b/test/LockstakeMkr.t.sol
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+import "token-tests/TokenChecks.sol";
+import { LockstakeMkr } from "src/LockstakeMkr.sol";
+
+contract LockstakeMkrTest is TokenChecks {
+ address internal lockstakeMkr = address(new LockstakeMkr());
+
+ function testBulkMintBurn() public {
+ checkBulkMintBurn(lockstakeMkr, "LockstakeMkr");
+ }
+
+ function testBulkERC20() public {
+ checkBulkERC20(lockstakeMkr, "LockstakeMkr", "LockstakeMkr", "lsMKR", "1", 18);
+ }
+}
diff --git a/test/handlers/LockstakeHandler.sol b/test/handlers/LockstakeHandler.sol
new file mode 100644
index 00000000..6e0068ee
--- /dev/null
+++ b/test/handlers/LockstakeHandler.sol
@@ -0,0 +1,354 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.21;
+
+import "dss-test/DssTest.sol";
+
+import { LockstakeEngine } from "src/LockstakeEngine.sol";
+import { LockstakeClipper } from "src/LockstakeClipper.sol";
+import { GemMock } from "test/mocks/GemMock.sol";
+import { PipMock } from "test/mocks/PipMock.sol";
+
+interface VatLike {
+ function urns(bytes32, address) external view returns (uint256, uint256);
+ function ilks(bytes32) external view returns (uint256, uint256, uint256, uint256, uint256);
+ function hope(address) external;
+ function suck(address, address, uint256) external;
+}
+
+interface JugLike {
+ function ilks(bytes32) external view returns (uint256, uint256);
+}
+
+interface SpotterLike {
+ function ilks(bytes32) external view returns (address, uint256);
+ function poke(bytes32) external;
+}
+
+interface VoteDelegateLike {
+ function stake(address) external view returns (uint256);
+}
+
+interface DogLike {
+ function bark(bytes32, address, address) external returns (uint256);
+ function ilks(bytes32) external view returns (address, uint256, uint256, uint256);
+}
+
+contract LockstakeHandler is StdUtils, StdCheats {
+ Vm vm;
+
+ LockstakeEngine public engine;
+ GemMock public mkr;
+ GemMock public sky;
+ GemMock public usds;
+ bytes32 public ilk;
+ VatLike public vat;
+ JugLike public jug;
+ SpotterLike public spot;
+ DogLike public dog;
+ LockstakeClipper public clip;
+
+ address public pauseProxy;
+ address public owner;
+ uint256 public index;
+ address public urn;
+ address[] public voteDelegates;
+ address[] public farms;
+ uint256 public mkrSkyRate;
+ address public anyone = address(123);
+
+ mapping(bytes32 => uint256) public numCalls;
+
+ uint256 constant RAY = 10 ** 27;
+
+ modifier callAsAnyone() {
+ vm.startPrank(anyone);
+ _;
+ vm.stopPrank();
+ }
+
+ modifier callAsUrnOwner() {
+ vm.startPrank(owner);
+ _;
+ vm.stopPrank();
+ }
+
+ modifier callAsPauseProxy() {
+ vm.startPrank(pauseProxy);
+ _;
+ vm.stopPrank();
+ }
+
+ constructor(
+ Vm vm_,
+ address engine_,
+ address owner_,
+ uint256 index_,
+ address spot_,
+ address dog_,
+ address pauseProxy_,
+ address[] memory voteDelegates_,
+ address[] memory farms_
+ ) {
+ vm = vm_;
+ engine = LockstakeEngine(engine_);
+ mkr = GemMock(address(engine.mkr()));
+ sky = GemMock(address(engine.sky()));
+ usds = GemMock(address(engine.usds()));
+ pauseProxy = pauseProxy_;
+ ilk = engine.ilk();
+ vat = VatLike(address(engine.vat()));
+ jug = JugLike(address(engine.jug()));
+ spot = SpotterLike(spot_);
+ dog = DogLike(dog_);
+
+ (address clip_, , , ) = dog.ilks(ilk);
+ clip = LockstakeClipper(clip_);
+ owner = owner_;
+ index = index_;
+ urn = engine.ownerUrns(owner, index);
+ mkrSkyRate = engine.mkrSkyRate();
+
+ vat.hope(address(clip));
+
+ for (uint256 i = 0; i < voteDelegates_.length ; i++) {
+ voteDelegates.push(voteDelegates_[i]);
+ }
+ voteDelegates.push(address(0));
+
+ for (uint256 i = 0; i < farms_.length ; i++) {
+ farms.push(farms_[i]);
+ }
+ farms.push(address(0));
+ }
+
+ function _rpow(uint256 x, uint256 n, uint256 b) internal pure returns (uint256 z) {
+ assembly {
+ switch x case 0 {switch n case 0 {z := b} default {z := 0}}
+ default {
+ switch mod(n, 2) case 0 { z := b } default { z := x }
+ let half := div(b, 2) // for rounding.
+ for { n := div(n, 2) } n { n := div(n,2) } {
+ let xx := mul(x, x)
+ if iszero(eq(div(xx, x), x)) { revert(0,0) }
+ let xxRound := add(xx, half)
+ if lt(xxRound, xx) { revert(0,0) }
+ x := div(xxRound, b)
+ if mod(n,2) {
+ let zx := mul(z, x)
+ if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { revert(0,0) }
+ let zxRound := add(zx, half)
+ if lt(zxRound, zx) { revert(0,0) }
+ z := div(zxRound, b)
+ }
+ }
+ }
+ }
+ }
+
+ function _divup(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ // Note: _divup(0,0) will return 0 differing from natural solidity division
+ unchecked {
+ z = x != 0 ? ((x - 1) / y) + 1 : 0;
+ }
+ }
+
+ function _min(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ z = x < y ? x : y;
+ }
+
+ function _getRandomVoteDelegate(uint256 voteDelegateIndex) internal view returns (address) {
+ return voteDelegates[bound(voteDelegateIndex, 0, voteDelegates.length - 1)];
+ }
+
+ function _getRandomFarm(uint256 farmIndex) internal view returns (address) {
+ return farms[bound(farmIndex, 0, farms.length - 1)];
+ }
+
+ function _getRandomAuctionId(uint256 auctionIndex) internal view returns (uint256) {
+ uint256[] memory active = clip.list();
+ return active[bound(auctionIndex, 0, active.length - 1)];
+ }
+
+ function delegatedTo(address voteDelegate) external view returns (uint256) {
+ return VoteDelegateLike(voteDelegate).stake(address(engine));
+ }
+
+ function sumDelegated() external view returns (uint256 sum) {
+ for (uint256 i = 0; i < voteDelegates.length; i++) {
+ if (voteDelegates[i] == address(0)) continue;
+ sum += VoteDelegateLike(voteDelegates[i]).stake(address(engine));
+ }
+ }
+
+ // note: There is no way to get the amount delegated per urn from the actual unmodified vote delegate contract,
+ // so we currently just return the total num of voteDelegates that anyone delegated to.
+ // In practice it means that invariant_delegation_exclusiveness can only work when there is one urn.
+ function numDelegated() external view returns (uint256 num) {
+ for (uint256 i = 0; i < voteDelegates.length; i++) {
+ if (voteDelegates[i] == address(0)) continue;
+ if (VoteDelegateLike(voteDelegates[i]).stake(address(engine)) > 0) num++;
+ }
+ }
+
+ function numStakedForUrn(address urn_) external view returns (uint256 num) {
+ for (uint256 i = 0; i < farms.length; i++) {
+ if (farms[i] == address(0)) continue;
+ if (GemMock(farms[i]).balanceOf(urn_) > 0) num++;
+ }
+ }
+
+ function addFarm(uint256 farmIndex) callAsPauseProxy() external {
+ numCalls["addFarm"]++;
+ engine.addFarm(_getRandomFarm(farmIndex));
+ }
+
+ function selectFarm(uint16 ref, uint256 farmIndex) callAsUrnOwner() external {
+ numCalls["selectFarm"]++;
+ engine.selectFarm(owner, index, _getRandomFarm(farmIndex), ref);
+ }
+
+ function selectVoteDelegate(uint256 voteDelegateIndex) callAsUrnOwner() external {
+ numCalls["selectVoteDelegate"]++;
+ engine.selectVoteDelegate(owner, index, _getRandomVoteDelegate(voteDelegateIndex));
+ }
+
+ function lock(uint256 wad, uint16 ref) external callAsAnyone {
+ numCalls["lock"]++;
+
+ // wad = bound(wad, 0, uint256(type(int256).max) / 10**18) * 10**18;
+ (uint256 ink,) = vat.urns(ilk, urn);
+ (,, uint256 spotPrice,,) = vat.ilks(ilk);
+ wad = bound(wad, 0, _min(
+ uint256(type(int256).max),
+ type(uint256).max / spotPrice - ink
+ ) / 10**18
+ ) * 10**18;
+
+ deal(address(mkr), anyone, wad);
+ mkr.approve(address(engine), wad);
+
+ engine.lock(owner, index, wad, ref);
+ }
+
+ function lockSky(uint256 skyWad, uint16 ref) external callAsAnyone {
+ numCalls["lockSky"]++;
+
+ // skyWad = bound(skyWad, 0, uint256(type(int256).max) / 10**18) * 10**18;
+ (uint256 ink,) = vat.urns(ilk, urn);
+ (,, uint256 spotPrice,,) = vat.ilks(ilk);
+ skyWad = bound(skyWad, 0, _min(
+ uint256(type(int256).max),
+ _min(
+ type(uint256).max / spotPrice - ink,
+ type(uint256).max / mkrSkyRate
+ )
+ ) / 10**18
+ ) * 10**18 * mkrSkyRate;
+
+ deal(address(sky), anyone, skyWad);
+ sky.approve(address(engine), skyWad);
+
+ engine.lockSky(owner, index, skyWad, ref);
+ }
+
+ function free(address to, uint256 wad) external callAsUrnOwner() {
+ numCalls["free"]++;
+
+ if (to == address(engine)) { revert("free-to-engine-unsupported"); }
+
+ (uint256 ink, uint256 art) = vat.urns(ilk, urn);
+ (, uint256 rate, uint256 spotPrice,,) = vat.ilks(ilk);
+ wad = bound(wad, 0, ink - _divup(art * rate, spotPrice));
+
+ engine.free(owner, index, to, wad);
+ }
+
+ function freeSky(address to, uint256 skyWad) external callAsUrnOwner() {
+ numCalls["freeSky"]++;
+
+ (uint256 ink, uint256 art ) = vat.urns(ilk, urn);
+ (, uint256 rate, uint256 spotPrice,,) = vat.ilks(ilk);
+ skyWad = bound(skyWad, 0, (ink - _divup(art * rate, spotPrice)) * mkrSkyRate);
+
+ engine.freeSky(owner, index, to, skyWad);
+ }
+
+ function draw(uint256 wad) external callAsUrnOwner() {
+ numCalls["draw"]++;
+
+ (uint256 ink, uint256 art) = vat.urns(ilk, urn);
+ (uint256 Art, uint256 rate, uint256 spotPrice,, uint256 dust) = vat.ilks(ilk);
+ (uint256 duty, uint256 rho) = jug.ilks(ilk);
+ rate = _rpow(duty, block.timestamp - rho, RAY) * rate / RAY;
+ if (ink * spotPrice < art * rate) { revert("unsafe-fee-accumulation-or-price-drop"); }
+ wad = bound(wad, art > 0 ? 0 : dust / RAY, _min(
+ (ink * spotPrice - art * rate) / RAY,
+ _min(
+ uint256(type(int256).max) / RAY,
+ (type(uint256).max - Art) * rate / RAY
+ )
+ ));
+
+ engine.draw(owner, index, address(this), wad);
+ }
+
+ function wipe(uint256 wad) external callAsAnyone {
+ numCalls["wipe"]++;
+
+ (, uint256 art) = vat.urns(ilk, urn);
+ (, uint256 rate,,, uint256 dust) = vat.ilks(ilk);
+ wad = bound(wad, 0, art > 0 ? _min(
+ (art * rate - dust) / RAY ,
+ uint256(type(int256).max) / RAY
+ )
+ : 0);
+
+ deal(address(usds), anyone, wad);
+ usds.approve(address(engine), wad);
+
+ engine.wipe(owner, index, wad);
+ }
+
+ function dropPriceAndBark() external {
+ numCalls["dropPriceAndBark"]++;
+ (uint256 ink, uint256 art) = vat.urns(ilk, urn);
+ (address pip, uint256 mat) = spot.ilks(ilk);
+ (, uint256 rate,,,) = vat.ilks(ilk);
+
+ uint256 minCollateralizedPrice = ((art * rate / RAY) * mat / ink) / 10**9;
+ PipMock(pip).setPrice(minCollateralizedPrice - 1);
+ spot.poke(ilk);
+
+ dog.bark(ilk, urn, address(0));
+ }
+
+ function take(uint256 auctionIndex) external {
+ numCalls["take"]++;
+ LockstakeClipper.Sale memory sale;
+ uint256 auctionId = _getRandomAuctionId(auctionIndex);
+ (sale.pos, sale.tab, sale.lot, sale.tot, sale.usr, sale.tic, sale.top) = clip.sales(auctionId);
+
+ vm.startPrank(pauseProxy); // we use startPrank as cannot override an ongoing prank with a single vm.prank
+ vat.suck(address(0), address(this), sale.tab);
+ vm.stopPrank();
+
+ clip.take({
+ id: auctionId,
+ amt: sale.lot,
+ max: type(uint256).max,
+ who: address(this),
+ data: ""
+ });
+ }
+
+ function yank(uint256 auctionIndex) external callAsPauseProxy() {
+ numCalls["yank"]++;
+ clip.yank(_getRandomAuctionId(auctionIndex));
+ }
+
+ function warp(uint256 secs) external {
+ numCalls["warp"]++;
+ secs = bound(secs, 0, 365 days);
+ vm.warp(block.timestamp + secs);
+ }
+}
diff --git a/test/mocks/GemMock.sol b/test/mocks/GemMock.sol
new file mode 100644
index 00000000..853605b7
--- /dev/null
+++ b/test/mocks/GemMock.sol
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+contract GemMock {
+ mapping (address => uint256) public balanceOf;
+ mapping (address => mapping (address => uint256)) public allowance;
+
+ uint256 public totalSupply;
+
+ constructor(uint256 initialSupply) {
+ mint(msg.sender, initialSupply);
+ }
+
+ function approve(address spender, uint256 value) external returns (bool) {
+ allowance[msg.sender][spender] = value;
+ return true;
+ }
+
+ function transfer(address to, uint256 value) external returns (bool) {
+ uint256 balance = balanceOf[msg.sender];
+ require(balance >= value, "Gem/insufficient-balance");
+
+ unchecked {
+ balanceOf[msg.sender] = balance - value;
+ balanceOf[to] += value;
+ }
+ return true;
+ }
+
+ function transferFrom(address from, address to, uint256 value) external returns (bool) {
+ uint256 balance = balanceOf[from];
+ require(balance >= value, "Gem/insufficient-balance");
+
+ if (from != msg.sender) {
+ uint256 allowed = allowance[from][msg.sender];
+ if (allowed != type(uint256).max) {
+ require(allowed >= value, "Gem/insufficient-allowance");
+
+ unchecked {
+ allowance[from][msg.sender] = allowed - value;
+ }
+ }
+ }
+
+ unchecked {
+ balanceOf[from] = balance - value;
+ balanceOf[to] += value;
+ }
+ return true;
+ }
+
+ function mint(address to, uint256 value) public {
+ unchecked {
+ balanceOf[to] = balanceOf[to] + value;
+ }
+ totalSupply = totalSupply + value;
+ }
+
+ function burn(address from, uint256 value) external {
+ uint256 balance = balanceOf[from];
+ require(balance >= value, "Gem/insufficient-balance");
+
+ if (from != msg.sender) {
+ uint256 allowed = allowance[from][msg.sender];
+ if (allowed != type(uint256).max) {
+ require(allowed >= value, "Gem/insufficient-allowance");
+
+ unchecked {
+ allowance[from][msg.sender] = allowed - value;
+ }
+ }
+ }
+
+ unchecked {
+ balanceOf[from] = balance - value;
+ totalSupply = totalSupply - value;
+ }
+ }
+}
diff --git a/test/mocks/LockstakeEngineMock.sol b/test/mocks/LockstakeEngineMock.sol
new file mode 100644
index 00000000..41c01fe8
--- /dev/null
+++ b/test/mocks/LockstakeEngineMock.sol
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+interface VatLike {
+ function slip(bytes32, address, int256) external;
+}
+
+contract LockstakeEngineMock {
+ VatLike immutable public vat;
+ bytes32 immutable public ilk;
+
+ constructor(address vat_, bytes32 ilk_) {
+ vat = VatLike(vat_);
+ ilk = ilk_;
+ }
+
+ function onKick(address, uint256) external {
+ }
+
+ function onTake(address, address who, uint256 wad) external {
+ VatLike(vat).slip(ilk, who, int256(wad));
+ }
+
+ function onRemove(address urn, uint256, uint256 left) external {
+ VatLike(vat).slip(ilk, urn, int256(left));
+ }
+}
diff --git a/test/mocks/MkrSkyMock.sol b/test/mocks/MkrSkyMock.sol
new file mode 100644
index 00000000..f86c054e
--- /dev/null
+++ b/test/mocks/MkrSkyMock.sol
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+interface GemLike {
+ function burn(address, uint256) external;
+ function mint(address, uint256) external;
+}
+
+contract MkrSkyMock {
+ GemLike public immutable mkr;
+ GemLike public immutable sky;
+ uint256 public immutable rate;
+
+ constructor(address mkr_, address sky_, uint256 rate_) {
+ mkr = GemLike(mkr_);
+ sky = GemLike(sky_);
+ rate = rate_;
+ }
+
+ function mkrToSky(address usr, uint256 mkrAmt) external {
+ mkr.burn(msg.sender, mkrAmt);
+ uint256 skyAmt = mkrAmt * rate;
+ sky.mint(usr, skyAmt);
+ }
+
+ function skyToMkr(address usr, uint256 skyAmt) external {
+ sky.burn(msg.sender, skyAmt);
+ uint256 mkrAmt = skyAmt / rate;
+ mkr.mint(usr, mkrAmt);
+ }
+}
diff --git a/test/mocks/PipMock.sol b/test/mocks/PipMock.sol
new file mode 100644
index 00000000..68d90ee8
--- /dev/null
+++ b/test/mocks/PipMock.sol
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+contract PipMock {
+ uint256 price;
+
+ function setPrice(uint256 price_) external {
+ price = price_;
+ }
+
+ function read() external view returns (uint256 price_) {
+ price_ = price;
+ }
+
+ function peek() external view returns (uint256 price_, bool ok) {
+ ok = price > 0;
+ price_ = price;
+ }
+}
diff --git a/test/mocks/StakingRewardsMock.sol b/test/mocks/StakingRewardsMock.sol
new file mode 100644
index 00000000..e95e5852
--- /dev/null
+++ b/test/mocks/StakingRewardsMock.sol
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+interface GemLike {
+ function transfer(address, uint256) external;
+ function transferFrom(address, address, uint256) external;
+ function mint(address, uint256) external;
+}
+
+contract StakingRewardsMock {
+ GemLike public immutable rewardsToken;
+ GemLike public immutable stakingToken;
+
+ uint256 public totalSupply;
+ mapping(address => uint256) public balanceOf;
+ mapping(address => uint256) public rewards;
+
+ constructor(
+ address _rewardsToken,
+ address _stakingToken
+ ) {
+ rewardsToken = GemLike(_rewardsToken);
+ stakingToken = GemLike(_stakingToken);
+ }
+
+ function stake(uint256 amount, uint16) external {
+ require(amount > 0, "Cannot stake 0");
+ totalSupply = totalSupply + amount;
+ balanceOf[msg.sender] = balanceOf[msg.sender] + amount;
+ stakingToken.transferFrom(msg.sender, address(this), amount);
+ }
+
+ function withdraw(uint256 amount) external {
+ require(amount > 0, "Cannot withdraw 0");
+ totalSupply = totalSupply - amount;
+ balanceOf[msg.sender] = balanceOf[msg.sender] - amount;
+ stakingToken.transfer(msg.sender, amount);
+ }
+
+ function setReward(address usr, uint256 amount) public {
+ rewardsToken.mint(address(this), amount);
+ rewards[usr] += amount;
+ }
+
+ function getReward() public {
+ uint256 reward = rewards[msg.sender];
+ if (reward > 0) {
+ rewards[msg.sender] = 0;
+ rewardsToken.transfer(msg.sender, reward);
+ }
+ }
+}
diff --git a/test/mocks/UsdsJoinMock.sol b/test/mocks/UsdsJoinMock.sol
new file mode 100644
index 00000000..6ab21bb2
--- /dev/null
+++ b/test/mocks/UsdsJoinMock.sol
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+import { GemMock } from "test/mocks/GemMock.sol";
+
+interface VatLike {
+ function move(address, address, uint256) external;
+}
+
+contract UsdsJoinMock {
+ VatLike public vat;
+ GemMock public usds;
+
+ constructor(address vat_, address usds_) {
+ vat = VatLike(vat_);
+ usds = GemMock(usds_);
+ }
+
+ function join(address usr, uint256 wad) external {
+ vat.move(address(this), usr, wad * 10**27);
+ usds.burn(msg.sender, wad);
+ }
+
+ function exit(address usr, uint256 wad) external {
+ vat.move(msg.sender, address(this), wad * 10**27);
+ usds.mint(usr, wad);
+ }
+}
diff --git a/test/mocks/UsdsMock.sol b/test/mocks/UsdsMock.sol
new file mode 100644
index 00000000..45bcdc19
--- /dev/null
+++ b/test/mocks/UsdsMock.sol
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+contract UsdsMock {
+ mapping (address => uint256) public balanceOf;
+ mapping (address => mapping (address => uint256)) public allowance;
+
+ uint256 public totalSupply;
+
+ constructor() {
+ mint(msg.sender, 0);
+ }
+
+ function approve(address spender, uint256 value) external returns (bool) {
+ allowance[msg.sender][spender] = value;
+ return true;
+ }
+
+ function transfer(address to, uint256 value) external returns (bool) {
+ uint256 balance = balanceOf[msg.sender];
+ require(balance >= value, "Usds/insufficient-balance");
+
+ unchecked {
+ balanceOf[msg.sender] = balance - value;
+ balanceOf[to] += value;
+ }
+ return true;
+ }
+
+ function transferFrom(address from, address to, uint256 value) external returns (bool) {
+ uint256 balance = balanceOf[from];
+ require(balance >= value, "Usds/insufficient-balance");
+
+ if (from != msg.sender) {
+ uint256 allowed = allowance[from][msg.sender];
+ if (allowed != type(uint256).max) {
+ require(allowed >= value, "Usds/insufficient-allowance");
+
+ unchecked {
+ allowance[from][msg.sender] = allowed - value;
+ }
+ }
+ }
+
+ unchecked {
+ balanceOf[from] = balance - value;
+ balanceOf[to] += value;
+ }
+ return true;
+ }
+
+ function mint(address to, uint256 value) public {
+ unchecked {
+ balanceOf[to] = balanceOf[to] + value;
+ }
+ totalSupply = totalSupply + value;
+ }
+
+ function burn(address from, uint256 value) external {
+ uint256 balance = balanceOf[from];
+ require(balance >= value, "Usds/insufficient-balance");
+
+ if (from != msg.sender) {
+ uint256 allowed = allowance[from][msg.sender];
+ if (allowed != type(uint256).max) {
+ require(allowed >= value, "Usds/insufficient-allowance");
+
+ unchecked {
+ allowance[from][msg.sender] = allowed - value;
+ }
+ }
+ }
+
+ unchecked {
+ balanceOf[from] = balance - value;
+ totalSupply = totalSupply - value;
+ }
+ }
+}
diff --git a/test/mocks/VoteDelegateMock.sol b/test/mocks/VoteDelegateMock.sol
new file mode 100644
index 00000000..0d724ddb
--- /dev/null
+++ b/test/mocks/VoteDelegateMock.sol
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+interface GemLike {
+ function transfer(address, uint256) external;
+ function transferFrom(address, address, uint256) external;
+}
+
+contract VoteDelegateFactoryMock {
+ mapping(address => uint256) public created;
+ address immutable private gov;
+
+ constructor(address _gov) {
+ gov = _gov;
+ }
+
+ function create() external returns (address voteDelegate) {
+ voteDelegate = address(new VoteDelegateMock(gov));
+ created[voteDelegate] = 1;
+ }
+}
+
+contract VoteDelegateMock {
+ mapping(address => uint256) public stake;
+
+ GemLike immutable public gov;
+
+ constructor(address gov_) {
+ gov = GemLike(gov_);
+ }
+
+ // --- GOV owner functions
+
+ function lock(uint256 wad) external {
+ gov.transferFrom(msg.sender, address(this), wad);
+ stake[msg.sender] += wad;
+ }
+
+ function free(uint256 wad) external {
+ stake[msg.sender] -= wad;
+ gov.transfer(msg.sender, wad);
+ }
+}