diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml new file mode 100644 index 0000000..898e8ed --- /dev/null +++ b/.github/workflows/certora.yml @@ -0,0 +1,45 @@ +name: Certora + +on: [push, pull_request] + +jobs: + certora: + name: Certora + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + vote-delegate: + - vote-delegate + - vote-delegate-factory + + 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: Install Certora + run: pip3 install certora-cli-beta + + - name: Verify ${{ matrix.vote-delegate }} + run: make certora-${{ matrix.vote-delegate }} results=1 + env: + CERTORAKEY: ${{ secrets.CERTORAKEY }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5f538bb --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: test + +on: [push, pull_request] + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test + env: + ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }} diff --git a/.gitignore b/.gitignore index e2e7327..3b7b564 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ /out +/cache + +# Certora +.certora_internal diff --git a/.gitmodules b/.gitmodules index e124719..a2df3f1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "lib/ds-test"] - path = lib/ds-test - url = https://github.com/dapphub/ds-test +[submodule "lib/dss-test"] + path = lib/dss-test + url = https://github.com/makerdao/dss-test diff --git a/Makefile b/Makefile index bfda2da..92abbb3 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,3 @@ -all :; DAPP_BUILD_OPTIMIZE=1 DAPP_BUILD_OPTIMIZE_RUNS=200 dapp --use solc:0.6.12 build -clean :; dapp clean -test :; ./test.sh $(match) $(runs) -deploy-mainnet :; make && dapp create VoteDelegateFactory 0x0a3f6849f78076aefaDf113F5BED87720274dDC0 0xD3A9FE267852281a1e6307a1C37CDfD76d39b133 -deploy-kovan :; make && dapp create VoteDelegateFactory 0x27E0c9567729Ea6e3241DE74B3dE499b7ddd3fe6 0xD931E7c869618dB6FD30cfE4e89248CAA091Ea5f -flatten :; hevm flatten --source-file src/VoteDelegateFactory.sol > out/VoteDelegateFactory-flattened.sol +PATH := ~/.solc-select/artifacts/solc-0.8.21:$(PATH) +certora-vote-delegate :; PATH=${PATH} certoraRun certora/VoteDelegate.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,) +certora-vote-delegate-factory :; PATH=${PATH} certoraRun certora/VoteDelegateFactory.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,) diff --git a/README.md b/README.md index a8c22c7..ad3b224 100644 --- a/README.md +++ b/README.md @@ -1,4 +1 @@ VoteDelegate based on https://github.com/makerdao/vote-proxy - -Kovan Deploy: [0x1740F3bD55b1900C816A0071F8972C201566e3a3](https://kovan.etherscan.io/address/0x1740F3bD55b1900C816A0071F8972C201566e3a3#code) -Mainnet Deploy: [0xD897F108670903D1d6070fcf818f9db3615AF272](https://etherscan.io/address/0xD897F108670903D1d6070fcf818f9db3615AF272#code) diff --git a/audit/20240703-cantina-report-maker-vote-delegate.pdf b/audit/20240703-cantina-report-maker-vote-delegate.pdf new file mode 100644 index 0000000..8897465 Binary files /dev/null and b/audit/20240703-cantina-report-maker-vote-delegate.pdf differ diff --git a/audit/20240909-ChainSecurity_MakerDAO_VoteDelegate_audit.pdf b/audit/20240909-ChainSecurity_MakerDAO_VoteDelegate_audit.pdf new file mode 100644 index 0000000..83c8af5 Binary files /dev/null and b/audit/20240909-ChainSecurity_MakerDAO_VoteDelegate_audit.pdf differ diff --git a/audit/20240909-cantina-report-review-vote-delegate-updates.pdf b/audit/20240909-cantina-report-review-vote-delegate-updates.pdf new file mode 100644 index 0000000..f0cd139 Binary files /dev/null and b/audit/20240909-cantina-report-review-vote-delegate-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 0000000..a1c7bf9 Binary files /dev/null and b/audit/20240917-Sherlock_MakerDAO_Endgame_Audit_Report.pdf differ diff --git a/audits/ABDK-MakerDAO-Vote Delegate.pdf b/audits/ABDK-MakerDAO-Vote Delegate.pdf deleted file mode 100644 index 9731119..0000000 Binary files a/audits/ABDK-MakerDAO-Vote Delegate.pdf and /dev/null differ diff --git a/certora/VoteDelegate.conf b/certora/VoteDelegate.conf new file mode 100644 index 0000000..972b1dc --- /dev/null +++ b/certora/VoteDelegate.conf @@ -0,0 +1,31 @@ +{ + "files": [ + "src/VoteDelegate.sol", + "certora/harness/GovMock.sol", + "certora/harness/IouMock.sol", + "certora/harness/ChiefMock.sol", + "certora/harness/PollingMock.sol" + ], + "solc": "solc-0.8.21", + "solc_optimize_map": { + "VoteDelegate": "200", + "GovMock": "0", + "IouMock": "0", + "ChiefMock": "0", + "PollingMock": "0" + }, + "link": [ + "VoteDelegate:gov=GovMock", + "VoteDelegate:chief=ChiefMock", + "VoteDelegate:polling=PollingMock", + "ChiefMock:GOV=GovMock", + "ChiefMock:IOU=IouMock" + ], + "verify": "VoteDelegate:certora/VoteDelegate.spec", + "rule_sanity": "basic", + "optimistic_loop": true, + "multi_assert_check": true, + "parametric_contracts": ["VoteDelegate"], + "build_cache": true, + "msg": "VoteDelegate" +} diff --git a/certora/VoteDelegate.spec b/certora/VoteDelegate.spec new file mode 100644 index 0000000..3e69347 --- /dev/null +++ b/certora/VoteDelegate.spec @@ -0,0 +1,321 @@ +// VoteDelegate.spec + +using GovMock as gov; +using IouMock as iou; +using ChiefMock as chief; +using PollingMock as polling; + +methods { + // storage variables + function stake(address) external returns (uint256) envfree; + function hatchTrigger() external returns (uint256) envfree; + // immutables + function delegate() external returns (address) envfree; + function gov() external returns (address) envfree; + function chief() external returns (address) envfree; + function polling() external returns (address) envfree; + // constants + function HATCH_SIZE() external returns (uint256) envfree; + function HATCH_COOLDOWN() external returns (uint256) envfree; + // + function gov.allowance(address,address) external returns (uint256) envfree; + function gov.balanceOf(address) external returns (uint256) envfree; + function gov.totalSupply() external returns (uint256) envfree; + function iou.allowance(address,address) external returns (uint256) envfree; + function iou.totalSupply() external returns (uint256) envfree; + function iou.balanceOf(address) external returns (uint256) envfree; + function chief.lastHashYays() external returns (bytes32) envfree; + function chief.calculateHash(address[]) external returns (bytes32) envfree; + function polling.lastPollId() external returns (uint256) envfree; + function polling.lastOptionId() external returns (uint256) envfree; + function polling.lastHashPollIds() external returns (bytes32) envfree; + function polling.lastHashOptionIds() external returns (bytes32) envfree; + function polling.calculateHash(uint256[]) external returns (bytes32) envfree; +} + +// Verify that each storage layout is only modified in the corresponding functions +rule storageAffected(method f) { + env e; + + address anyAddr; + + mathint stakeBefore = stake(anyAddr); + mathint hatchTriggerBefore = hatchTrigger(); + + calldataarg args; + f(e, args); + + mathint stakeAfter = stake(anyAddr); + mathint hatchTriggerAfter = hatchTrigger(); + + assert stakeAfter != stakeBefore => f.selector == sig:lock(uint256).selector || f.selector == sig:free(uint256).selector, "Assert 1"; + assert hatchTriggerAfter != hatchTriggerBefore => f.selector == sig:reserveHatch().selector, "Assert 2"; +} + +// Verify correct storage changes for non reverting lock +rule lock(uint256 wad) { + env e; + + require e.msg.sender != currentContract && e.msg.sender != chief; + + mathint stakeSenderBefore = stake(e.msg.sender); + mathint govBalanceofSenderBefore = gov.balanceOf(e.msg.sender); + mathint govBalanceofVoteDelegateBefore = gov.balanceOf(currentContract); + mathint govBalanceOfChiefBefore = gov.balanceOf(chief); + require gov.totalSupply() >= govBalanceofSenderBefore + govBalanceofVoteDelegateBefore + govBalanceOfChiefBefore; + mathint iouTotalSupplyBefore = iou.totalSupply(); + mathint iouBalanceOfVoteDelegateBefore = iou.balanceOf(currentContract); + require iouTotalSupplyBefore >= iouBalanceOfVoteDelegateBefore; + + lock(e, wad); + + mathint stakeSenderAfter = stake(e.msg.sender); + mathint govBalanceOfSenderAfter = gov.balanceOf(e.msg.sender); + mathint govBalanceOfVoteDelegateAfter = gov.balanceOf(currentContract); + mathint govBalanceOfChiefAfter = gov.balanceOf(chief); + mathint iouTotalSupplyAfter = iou.totalSupply(); + mathint iouBalanceOfVoteDelegateAfter = iou.balanceOf(currentContract); + + assert stakeSenderAfter == stakeSenderBefore + wad, "Assert 1"; + assert govBalanceOfSenderAfter == govBalanceofSenderBefore - wad, "Assert 2"; + assert govBalanceOfVoteDelegateAfter == govBalanceofVoteDelegateBefore, "Assert 3"; + assert govBalanceOfChiefAfter == govBalanceOfChiefBefore + wad, "Assert 4"; + assert iouTotalSupplyAfter == iouTotalSupplyBefore + wad, "Assert 5"; + assert iouBalanceOfVoteDelegateAfter == iouBalanceOfVoteDelegateBefore + wad, "Assert 6"; +} + +// Verify revert rules on lock +rule lock_revert(uint256 wad) { + env e; + + mathint stakeSender = stake(e.msg.sender); + mathint govTotalSupply = gov.totalSupply(); + mathint govBalanceofSender = gov.balanceOf(e.msg.sender); + mathint govBalanceofVoteDelegate = gov.balanceOf(currentContract); + mathint govBalanceOfChief = gov.balanceOf(chief); + mathint iouTotalSupply = iou.totalSupply(); + mathint iouBalanceOfVoteDelegate = iou.balanceOf(currentContract); + // Assumptions from tokens regular behavior + require govTotalSupply >= govBalanceofSender + govBalanceofVoteDelegate + govBalanceOfChief; + require iouTotalSupply >= iouBalanceOfVoteDelegate; + // Assumption from VoteDelegate constructor + require gov.allowance(currentContract, chief) == max_uint256; + // Assumption from Chief functionality + require iouTotalSupply == govBalanceOfChief; + // Assumption from user settings + require govBalanceofSender >= wad; + require gov.allowance(e.msg.sender, currentContract) >= wad; + + mathint hatchTrigger = hatchTrigger(); + mathint hatchSize = HATCH_SIZE(); + + lock@withrevert(e, wad); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.block.number != hatchTrigger && e.block.number <= hatchTrigger + hatchSize; + bool revert3 = stakeSender + wad > max_uint256; + + assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting free +rule free(uint256 wad) { + env e; + + require e.msg.sender != currentContract && e.msg.sender != chief; + + mathint stakeSenderBefore = stake(e.msg.sender); + mathint govBalanceofSenderBefore = gov.balanceOf(e.msg.sender); + mathint govBalanceofVoteDelegateBefore = gov.balanceOf(currentContract); + mathint govBalanceOfChiefBefore = gov.balanceOf(chief); + require gov.totalSupply() >= govBalanceofSenderBefore + govBalanceofVoteDelegateBefore + govBalanceOfChiefBefore; + mathint iouTotalSupplyBefore = iou.totalSupply(); + mathint iouBalanceOfVoteDelegateBefore = iou.balanceOf(currentContract); + require iouTotalSupplyBefore >= iouBalanceOfVoteDelegateBefore; + + free(e, wad); + + mathint stakeSenderAfter = stake(e.msg.sender); + mathint govBalanceOfSenderAfter = gov.balanceOf(e.msg.sender); + mathint govBalanceOfVoteDelegateAfter = gov.balanceOf(currentContract); + mathint govBalanceOfChiefAfter = gov.balanceOf(chief); + mathint iouTotalSupplyAfter = iou.totalSupply(); + mathint iouBalanceOfVoteDelegateAfter = iou.balanceOf(currentContract); + + assert stakeSenderAfter == stakeSenderBefore - wad, "Assert 1"; + assert govBalanceOfSenderAfter == govBalanceofSenderBefore + wad, "Assert 2"; + assert govBalanceOfVoteDelegateAfter == govBalanceofVoteDelegateBefore, "Assert 3"; + assert govBalanceOfChiefAfter == govBalanceOfChiefBefore - wad, "Assert 4"; + assert iouTotalSupplyAfter == iouTotalSupplyBefore - wad, "Assert 5"; + assert iouBalanceOfVoteDelegateAfter == iouBalanceOfVoteDelegateBefore - wad, "Assert 6"; +} + +// Verify revert rules on free +rule free_revert(uint256 wad) { + env e; + + mathint stakeSender = stake(e.msg.sender); + mathint govTotalSupply = gov.totalSupply(); + mathint govBalanceofSender = gov.balanceOf(e.msg.sender); + mathint govBalanceofVoteDelegate = gov.balanceOf(currentContract); + mathint govBalanceOfChief = gov.balanceOf(chief); + mathint iouTotalSupply = iou.totalSupply(); + mathint iouBalanceOfVoteDelegate = iou.balanceOf(currentContract); + // Assumptions from tokens regular behavior + require govTotalSupply >= govBalanceofSender + govBalanceofVoteDelegate + govBalanceOfChief; + require iouTotalSupply >= iouBalanceOfVoteDelegate; + // Assumption from VoteDelegate constructor + require iou.allowance(currentContract, chief) == max_uint256; + // Assumption from chief/voteDelegate functionality // TODO: check in invariant + require govBalanceOfChief >= stakeSender; + require iouBalanceOfVoteDelegate == stakeSender; + + free@withrevert(e, wad); + + bool revert1 = e.msg.value > 0; + bool revert2 = stakeSender < to_mathint(wad); + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting reserveHatch +rule reserveHatch() { + env e; + + reserveHatch(e); + + mathint hatchTriggerAfter = hatchTrigger(); + + assert hatchTriggerAfter == e.block.number, "Assert 1"; +} + +// Verify revert rules on reserveHatch +rule reserveHatch_revert() { + env e; + + mathint hatchTrigger = hatchTrigger(); + mathint hatchSize = HATCH_SIZE(); + mathint hatchCooldown = HATCH_COOLDOWN(); + + reserveHatch@withrevert(e); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.block.number < hatchTrigger + hatchSize + hatchCooldown; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting vote +rule voteYays(address[] yays) { + env e; + + bytes32 hash = chief.calculateHash(yays); + + bytes32 ret = vote(e, yays); + + bytes32 lastHashYaysAfter = chief.lastHashYays(); + + assert lastHashYaysAfter == hash, "Assert 1"; + assert ret == hash, "Assert 2"; +} + +// Verify revert rules on vote +rule voteYays_revert(address[] yays) { + env e; + + // Temporary workaround until tool fixes this issue: + require(forall uint256 i. ( + i >= yays.length || to_mathint(yays[i]) < 2^160 + )); + + address delegate = delegate(); + + vote@withrevert(e, yays); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.msg.sender != delegate; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting vote +rule voteSlate(bytes32 slate) { + env e; + + vote(e, slate); + + bytes32 lastHashYaysAfter = chief.lastHashYays(); + + assert lastHashYaysAfter == slate, "Assert 1"; +} + +// Verify revert rules on votePoll +rule voteSlate_revert(bytes32 slate) { + env e; + + address delegate = delegate(); + + vote@withrevert(e, slate); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.msg.sender != delegate; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting votePoll +rule votePollOne(uint256 pollId, uint256 optionId) { + env e; + + votePoll(e, pollId, optionId); + + uint256 lastPollIdAfter = polling.lastPollId(); + uint256 lastOptionIdAfter = polling.lastOptionId(); + + assert lastPollIdAfter == pollId, "Assert 1"; + assert lastOptionIdAfter == optionId, "Assert 2"; +} + +// Verify revert rules on votePoll +rule votePollOne_revert(uint256 pollId, uint256 optionId) { + env e; + + address delegate = delegate(); + + votePoll@withrevert(e, pollId, optionId); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.msg.sender != delegate; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting votePoll +rule votePollMultiple(uint256[] pollIds, uint256[] optionIds) { + env e; + + bytes32 hashPollIds = polling.calculateHash(pollIds); + bytes32 hashOptionIds = polling.calculateHash(optionIds); + + votePoll(e, pollIds, optionIds); + + bytes32 lastHashPollIdsAfter = polling.lastHashPollIds(); + bytes32 lastHashOptionIdsAfter = polling.lastHashOptionIds(); + + assert lastHashPollIdsAfter == hashPollIds, "Assert 1"; + assert lastHashOptionIdsAfter == hashOptionIds, "Assert 2"; +} + +// Verify revert rules on votePoll +rule votePollMultiple_revert(uint256[] pollIds, uint256[] optionIds) { + env e; + + address delegate = delegate(); + + votePoll@withrevert(e, pollIds, optionIds); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.msg.sender != delegate; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} diff --git a/certora/VoteDelegateFactory.conf b/certora/VoteDelegateFactory.conf new file mode 100644 index 0000000..7799c81 --- /dev/null +++ b/certora/VoteDelegateFactory.conf @@ -0,0 +1,14 @@ +{ + "files": [ + "src/VoteDelegateFactory.sol" + ], + "solc": "solc-0.8.21", + "solc_optimize": "200", + "verify": "VoteDelegateFactory:certora/VoteDelegateFactory.spec", + "rule_sanity": "basic", + "optimistic_loop": true, + "multi_assert_check": true, + "parametric_contracts": ["VoteDelegateFactory"], + "build_cache": true, + "msg": "VoteDelegateFactory" +} diff --git a/certora/VoteDelegateFactory.spec b/certora/VoteDelegateFactory.spec new file mode 100644 index 0000000..866fdf2 --- /dev/null +++ b/certora/VoteDelegateFactory.spec @@ -0,0 +1,38 @@ +// VoteDelegateFactory.spec + +methods { + // storage variables + function delegates(address) external returns (address) envfree; + function created(address) external returns (uint256) envfree; + // immutables + function chief() external returns (address) envfree; + function polling() external returns (address) envfree; +} + +// Verify correct storage changes for non reverting create +rule create() { + env e; + + address voteDelegate = create(e); + + address delegatesSenderAfter = delegates(e.msg.sender); + mathint createdAfter = created(delegatesSenderAfter); + + assert delegatesSenderAfter != 0, "Assert 1"; + assert delegatesSenderAfter == voteDelegate, "Assert 2"; + assert createdAfter == 1, "Assert 3"; +} + +// Verify revert rules on create +rule create_revert() { + env e; + + address delegatesSender = delegates(e.msg.sender); + + create@withrevert(e); + + bool revert1 = e.msg.value > 0; + bool revert2 = delegatesSender != 0; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} diff --git a/certora/harness/ChiefMock.sol b/certora/harness/ChiefMock.sol new file mode 100644 index 0000000..a1c9f80 --- /dev/null +++ b/certora/harness/ChiefMock.sol @@ -0,0 +1,39 @@ +// 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; + function transfer(address, uint256) external; + function transferFrom(address, address, uint256) external; +} + +contract ChiefMock { + GemLike public GOV; + GemLike public IOU; + bytes32 public lastHashYays; + + function calculateHash(address[] memory yays) public returns (bytes32) { + return keccak256(abi.encodePacked(yays)); + } + + function lock(uint256 wad) external { + GOV.transferFrom(msg.sender, address(this), wad); + IOU.mint(msg.sender, wad); + } + + function free(uint256 wad) external { + IOU.burn(msg.sender, wad); + GOV.transfer(msg.sender, wad); + } + + function vote(address[] memory yays) external returns (bytes32) { + lastHashYays = calculateHash(yays); + return lastHashYays; + } + + function vote(bytes32 slate) external { + lastHashYays = slate; + } +} diff --git a/certora/harness/GovMock.sol b/certora/harness/GovMock.sol new file mode 100644 index 0000000..a03e24e --- /dev/null +++ b/certora/harness/GovMock.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.21; + +contract GovMock { + 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/certora/harness/IouMock.sol b/certora/harness/IouMock.sol new file mode 100644 index 0000000..a252d67 --- /dev/null +++ b/certora/harness/IouMock.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.21; + +import { GovMock } from "./GovMock.sol"; + +contract IouMock is GovMock { + constructor(uint256 initialSupply) GovMock(initialSupply) { + } +} diff --git a/certora/harness/PollingMock.sol b/certora/harness/PollingMock.sol new file mode 100644 index 0000000..b4471d9 --- /dev/null +++ b/certora/harness/PollingMock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.21; + +contract PollingMock { + uint256 public lastPollId; + uint256 public lastOptionId; + + bytes32 public lastHashPollIds; + bytes32 public lastHashOptionIds; + + function calculateHash(uint256[] memory v) public returns (bytes32) { + return keccak256(abi.encodePacked(v)); + } + + function vote(uint256 pollId, uint256 optionId) external { + lastPollId = pollId; + lastOptionId = optionId; + } + + function vote(uint256[] calldata pollIds, uint256[] calldata optionIds) external { + lastHashPollIds = calculateHash(pollIds); + lastHashOptionIds = calculateHash(optionIds); + } +} diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..f7e11c6 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,10 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc = "0.8.21" +# Enabling optimizations to improve gas usage. +optimizer = true + +[rpc_endpoints] +mainnet = "${ETH_RPC_URL}" diff --git a/lib/ds-test b/lib/ds-test deleted file mode 160000 index 0a5da56..0000000 --- a/lib/ds-test +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0a5da56b0d65960e6a994d2ec8245e6edd38c248 diff --git a/lib/dss-test b/lib/dss-test new file mode 160000 index 0000000..6d4029d --- /dev/null +++ b/lib/dss-test @@ -0,0 +1 @@ +Subproject commit 6d4029dc373d4e2af2404016e0a834e53d976264 diff --git a/src/VoteDelegate.sol b/src/VoteDelegate.sol index a1727ef..1c1d916 100644 --- a/src/VoteDelegate.sol +++ b/src/VoteDelegate.sol @@ -1,7 +1,6 @@ +// SPDX-FileCopyrightText: © 2021 Dai Foundation // SPDX-License-Identifier: AGPL-3.0-or-later - -// Copyright (C) 2021 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 @@ -15,18 +14,17 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -// VoteDelegate - delegate your vote -pragma solidity 0.6.12; +pragma solidity ^0.8.21; -interface TokenLike { - function approve(address, uint256) external returns (bool); - function pull(address, uint256) external; - function push(address, uint256) external; +interface GemLike { + function approve(address, uint256) external; + function transfer(address, uint256) external; + function transferFrom(address, address, uint256) external; } interface ChiefLike { - function GOV() external view returns (TokenLike); - function IOU() external view returns (TokenLike); + function GOV() external view returns (GemLike); + function IOU() external view returns (GemLike); function lock(uint256) external; function free(uint256) external; function vote(address[] calldata) external returns (bytes32); @@ -34,85 +32,99 @@ interface ChiefLike { } interface PollingLike { - function withdrawPoll(uint256) external; function vote(uint256, uint256) external; - function withdrawPoll(uint256[] calldata) external; function vote(uint256[] calldata, uint256[] calldata) external; } contract VoteDelegate { + // --- storage variables --- + mapping(address => uint256) public stake; - address public immutable delegate; - TokenLike public immutable gov; - TokenLike public immutable iou; - ChiefLike public immutable chief; - PollingLike public immutable polling; - uint256 public immutable expiration; + uint256 public hatchTrigger; + + // --- immutables --- + + address immutable public delegate; + GemLike immutable public gov; + ChiefLike immutable public chief; + PollingLike immutable public polling; + + // --- constants --- + + uint256 public constant HATCH_SIZE = 5; + uint256 public constant HATCH_COOLDOWN = 20; + + // --- events --- event Lock(address indexed usr, uint256 wad); event Free(address indexed usr, uint256 wad); + event ReserveHatch(); - constructor(address _chief, address _polling, address _delegate) public { - chief = ChiefLike(_chief); - polling = PollingLike(_polling); - delegate = _delegate; - expiration = block.timestamp + 365 days; + // --- constructor --- - TokenLike _gov = gov = ChiefLike(_chief).GOV(); - TokenLike _iou = iou = ChiefLike(_chief).IOU(); + constructor(address chief_, address polling_, address delegate_) { + chief = ChiefLike(chief_); + polling = PollingLike(polling_); + delegate = delegate_; - _gov.approve(_chief, type(uint256).max); - _iou.approve(_chief, type(uint256).max); - } + gov = ChiefLike(chief_).GOV(); - function add(uint256 x, uint256 y) internal pure returns (uint256 z) { - require((z = x + y) >= x, "VoteDelegate/add-overflow"); + gov.approve(chief_, type(uint256).max); + ChiefLike(chief_).IOU().approve(chief_, type(uint256).max); } + // --- modifiers --- + modifier delegate_auth() { require(msg.sender == delegate, "VoteDelegate/sender-not-delegate"); _; } - modifier live() { - require(block.timestamp < expiration, "VoteDelegate/delegation-contract-expired"); - _; - } + // --- gov owner functions - function lock(uint256 wad) external live { - stake[msg.sender] = add(stake[msg.sender], wad); - gov.pull(msg.sender, wad); + function lock(uint256 wad) external { + require(block.number == hatchTrigger || block.number > hatchTrigger + HATCH_SIZE, + "VoteDelegate/no-lock-during-hatch"); + gov.transferFrom(msg.sender, address(this), wad); chief.lock(wad); - iou.push(msg.sender, wad); + stake[msg.sender] += wad; emit Lock(msg.sender, wad); } function free(uint256 wad) external { require(stake[msg.sender] >= wad, "VoteDelegate/insufficient-stake"); - - stake[msg.sender] -= wad; - iou.pull(msg.sender, wad); + unchecked { stake[msg.sender] -= wad; } chief.free(wad); - gov.push(msg.sender, wad); + gov.transfer(msg.sender, wad); emit Free(msg.sender, wad); } - function vote(address[] memory yays) external delegate_auth live returns (bytes32 result) { + function reserveHatch() external { + require(block.number >= hatchTrigger + HATCH_SIZE + HATCH_COOLDOWN, "VoteDelegate/cooldown-not-finished"); + hatchTrigger = block.number; + + emit ReserveHatch(); + } + + // --- delegate executive voting functions + + function vote(address[] memory yays) external delegate_auth returns (bytes32 result) { result = chief.vote(yays); } - function vote(bytes32 slate) external delegate_auth live { + function vote(bytes32 slate) external delegate_auth { chief.vote(slate); } - // Polling vote - function votePoll(uint256 pollId, uint256 optionId) external delegate_auth live { + // --- delegate poll voting functions + + function votePoll(uint256 pollId, uint256 optionId) external delegate_auth { polling.vote(pollId, optionId); } - function votePoll(uint256[] calldata pollIds, uint256[] calldata optionIds) external delegate_auth live { + function votePoll(uint256[] calldata pollIds, uint256[] calldata optionIds) external delegate_auth { polling.vote(pollIds, optionIds); } } diff --git a/src/VoteDelegate.t.sol b/src/VoteDelegate.t.sol deleted file mode 100644 index 9c1d7ab..0000000 --- a/src/VoteDelegate.t.sol +++ /dev/null @@ -1,563 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -// Copyright (C) 2021 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; - -import "ds-test/test.sol"; - -import {VoteDelegate, PollingLike} from "./VoteDelegate.sol"; - -interface TokenLike { - function balanceOf(address) external view returns (uint256); - function approve(address, uint256) external returns (bool); - function transfer(address, uint256) external returns (bool); - function mint(address, uint256) external; -} - -interface ChiefLike { - function GOV() external view returns (TokenLike); - function IOU() external view returns (TokenLike); - function approvals(address) external view returns (uint256); - function lock(uint256) external; - function free(uint256) external; - function vote(address[] calldata) external returns (bytes32); - function vote(bytes32) external; -} - -interface Hevm { - function warp(uint256) external; - function roll(uint256) external; - function store(address,bytes32,bytes32) external; - function load(address,bytes32) external view returns (bytes32); -} - -interface AuthLike { - function wards(address) external returns (uint256); -} - -interface OwnerLike { - function owner() external returns (address); -} - -contract Voter { - ChiefLike chief; - PollingLike polling; - TokenLike gov; - TokenLike iou; - VoteDelegate public proxy; - - constructor(ChiefLike chief_, PollingLike polling_) public { - chief = chief_; - polling = polling_; - gov = TokenLike(chief.GOV()); - iou = TokenLike(chief.IOU()); - } - - function expiration() public returns (uint256) { - return proxy.expiration(); - } - - function setProxy(VoteDelegate proxy_) public { - proxy = proxy_; - } - - function doChiefLock(uint amt) public { - chief.lock(amt); - } - - function doChiefFree(uint amt) public { - chief.free(amt); - } - - function doTransfer(address guy, uint amt) public { - gov.transfer(guy, amt); - } - - function approveGov(address guy) public { - gov.approve(guy, uint256(-1)); - } - - function approveIou(address guy) public { - iou.approve(guy, uint256(-1)); - } - - function doProxyLock(uint amt) public { - proxy.lock(amt); - } - - function doProxyFree(uint amt) public { - proxy.free(amt); - } - - function doProxyFreeAll() public { - proxy.free(proxy.stake(address(this))); - } - - function doProxyVote(address[] memory yays) public returns (bytes32 slate) { - return proxy.vote(yays); - } - - function doProxyVote(bytes32 slate) public { - proxy.vote(slate); - } - - function doProxyVotePoll(uint256 pollId, uint256 optionId) public { - proxy.votePoll(pollId, optionId); - } - - function doProxyVotePoll(uint256[] calldata pollIds, uint256[] calldata optionIds) public { - proxy.votePoll(pollIds, optionIds); - } -} - -contract VoteDelegateTest is DSTest { - Hevm hevm; - - uint256 constant electionSize = 3; - address constant c1 = address(0x1); - address constant c2 = address(0x2); - bytes byts; - - VoteDelegate proxy; - TokenLike gov; - TokenLike iou; - ChiefLike chief; - PollingLike polling; - - Voter delegate; - Voter delegator1; - Voter delegator2; - - function setUp() public { - hevm = Hevm(HEVM_ADDRESS); - - chief = ChiefLike(0x0a3f6849f78076aefaDf113F5BED87720274dDC0); - polling = PollingLike(0xD3A9FE267852281a1e6307a1C37CDfD76d39b133); - gov = chief.GOV(); - iou = chief.IOU(); - - // Give us admin access to mint MKR - hevm.store( - address(gov), - bytes32(uint256(4)), - bytes32(uint256(address(this))) - ); - assertEq(OwnerLike(address(gov)).owner(), address(this)); - - delegate = new Voter(chief, polling); - delegator1 = new Voter(chief, polling); - delegator2 = new Voter(chief, polling); - gov.mint(address(delegate), 100 ether); - gov.mint(address(delegator1), 10_000 ether); - gov.mint(address(delegator2), 20_000 ether); - - proxy = new VoteDelegate(address(chief), address(polling), address(delegate)); - - delegate.setProxy(proxy); - delegator1.setProxy(proxy); - delegator2.setProxy(proxy); - } - - function test_proxy_lock_free() public { - uint256 currMKR = gov.balanceOf(address(chief)); - - delegate.approveGov(address(proxy)); - delegate.approveIou(address(proxy)); - - assertEq(gov.balanceOf(address(delegate)), 100 ether); - assertEq(iou.balanceOf(address(delegate)), 0); - - delegate.doProxyLock(100 ether); - assertEq(gov.balanceOf(address(delegate)), 0); - assertEq(gov.balanceOf(address(chief)), currMKR + 100 ether); - assertEq(iou.balanceOf(address(delegate)), 100 ether); - assertEq(proxy.stake(address(delegate)), 100 ether); - - // Comply with Chief's flash loan protection - hevm.roll(block.number + 1); - hevm.warp(block.timestamp + 1); - - delegate.doProxyFree(100 ether); - assertEq(gov.balanceOf(address(delegate)), 100 ether); - assertEq(gov.balanceOf(address(chief)), currMKR); - assertEq(iou.balanceOf(address(delegate)), 0); - assertEq(proxy.stake(address(delegate)), 0); - } - - function test_proxy_lock_free_after_expiration() public { - uint256 currMKR = gov.balanceOf(address(chief)); - - delegate.approveGov(address(proxy)); - delegate.approveIou(address(proxy)); - - assertEq(gov.balanceOf(address(delegate)), 100 ether); - assertEq(iou.balanceOf(address(delegate)), 0); - - delegate.doProxyLock(100 ether); - assertEq(gov.balanceOf(address(delegate)), 0); - assertEq(gov.balanceOf(address(chief)), currMKR + 100 ether); - assertEq(iou.balanceOf(address(delegate)), 100 ether); - assertEq(proxy.stake(address(delegate)), 100 ether); - - // Flash loan protection - hevm.roll(block.number + 1); - - // Warp past expiration - hevm.warp(block.timestamp + 9001 days); - - assertTrue(block.timestamp > delegate.expiration()); - // Always allow freeing after expiration - delegate.doProxyFree(100 ether); - assertEq(gov.balanceOf(address(delegate)), 100 ether); - assertEq(gov.balanceOf(address(chief)), currMKR); - assertEq(iou.balanceOf(address(delegate)), 0); - assertEq(proxy.stake(address(delegate)), 0); - } - - function testFail_proxy_lock_after_expiration() public { - delegate.approveGov(address(proxy)); - delegate.approveIou(address(proxy)); - - // Flash loan protection - hevm.roll(block.number + 1); - - // Warp past expiration - hevm.warp(block.timestamp + 9001 days); - assertTrue(block.timestamp > delegate.expiration()); - - // Fail here. Don't allow locking after expiry. - delegate.doProxyLock(100 ether); - } - - function test_proxy_lock_free_around_expiration() public { - uint256 currMKR = gov.balanceOf(address(chief)); - - delegate.approveGov(address(proxy)); - delegate.approveIou(address(proxy)); - - assertEq(gov.balanceOf(address(delegate)), 100 ether); - assertEq(iou.balanceOf(address(delegate)), 0); - - address[] memory yays = new address[](1); - yays[0] = c1; - uint256[] memory ids = new uint256[](2); - ids[0] = 1; - ids[1] = 2; - uint256[] memory opts = new uint256[](2); - opts[0] = 1; - opts[1] = 3; - - // Flash loan protection - hevm.roll(block.number + 1); - - hevm.warp(delegate.expiration() - 1); - assertTrue(block.timestamp < delegate.expiration()); - - bool ok; - - // Lock will succeed before expiration - //delegate.doProxyLock(10 ether); - (ok,) = address(delegate).call(abi.encodeWithSignature("doProxyLock(uint256)",10 ether)); - assertTrue(ok); - //delegate.doProxyVote(yays); - (ok,) = address(delegate).call(abi.encodeWithSignature("doProxyVote(address[])",yays)); - assertTrue(ok); - (ok,) = address(delegate).call(abi.encodeWithSignature("doProxyVotePoll(uint256[],uint256[])",ids,opts)); - assertTrue(ok); - assertEq(gov.balanceOf(address(delegate)), 90 ether); - assertEq(gov.balanceOf(address(chief)), currMKR + 10 ether); - assertEq(iou.balanceOf(address(delegate)), 10 ether); - assertEq(proxy.stake(address(delegate)), 10 ether); - - - // Lock will fail at expiration - hevm.warp(delegate.expiration()); - assertTrue(block.timestamp == delegate.expiration()); - - (ok,) = address(delegate).call(abi.encodeWithSignature("doProxyLock(uint256)",10 ether)); - assertTrue(!ok); - (ok,) = address(delegate).call(abi.encodeWithSignature("doProxyVote(address[])",yays)); - assertTrue(!ok); - (ok,) = address(delegate).call(abi.encodeWithSignature("doProxyVotePoll(uint256[],uint256[])",ids,opts)); - assertTrue(!ok); - - assertEq(gov.balanceOf(address(delegate)), 90 ether); - assertEq(gov.balanceOf(address(chief)), currMKR + 10 ether); - assertEq(iou.balanceOf(address(delegate)), 10 ether); - assertEq(proxy.stake(address(delegate)), 10 ether); - - - // Lock will fail after expiration - hevm.warp(delegate.expiration() + 1); - assertTrue(block.timestamp > delegate.expiration()); - - (ok,) = address(delegate).call(abi.encodeWithSignature("doProxyLock(uint256)",10 ether)); - assertTrue(!ok); - (ok,) = address(delegate).call(abi.encodeWithSignature("doProxyVote(address[])",yays)); - assertTrue(!ok); - (ok,) = address(delegate).call(abi.encodeWithSignature("doProxyVotePoll(uint256[],uint256[])",ids,opts)); - assertTrue(!ok); - - assertEq(gov.balanceOf(address(delegate)), 90 ether); - assertEq(gov.balanceOf(address(chief)), currMKR + 10 ether); - assertEq(iou.balanceOf(address(delegate)), 10 ether); - assertEq(proxy.stake(address(delegate)), 10 ether); - - - hevm.roll(block.number + 1); - // Always allow freeing after expiration - delegate.doProxyFree(10 ether); - assertEq(gov.balanceOf(address(delegate)), 100 ether); - assertEq(gov.balanceOf(address(chief)), currMKR); - assertEq(iou.balanceOf(address(delegate)), 0); - assertEq(proxy.stake(address(delegate)), 0); - } - - function test_delegator_lock_free() public { - uint256 currMKR = gov.balanceOf(address(chief)); - - delegator1.approveGov(address(proxy)); - delegator1.approveIou(address(proxy)); - - delegator1.doProxyLock(10_000 ether); - assertEq(gov.balanceOf(address(delegator1)), 0); - assertEq(gov.balanceOf(address(chief)), currMKR + 10_000 ether); - assertEq(iou.balanceOf(address(delegator1)), 10_000 ether); - assertEq(proxy.stake(address(delegator1)), 10_000 ether); - - // Comply with Chief's flash loan protection - hevm.roll(block.number + 1); - - delegator1.doProxyFree(10_000 ether); - assertEq(gov.balanceOf(address(delegator1)), 10_000 ether); - assertEq(gov.balanceOf(address(chief)), currMKR); - assertEq(iou.balanceOf(address(delegator1)), 0); - assertEq(proxy.stake(address(delegator1)), 0); - } - - function test_delegator_lock_free_fuzz(uint256 wad_seed) public { - uint256 wad = wad_seed < 1 ether ? wad_seed += 1 ether : wad_seed % 20_000 ether; - uint256 currMKR = gov.balanceOf(address(chief)); - - delegator2.approveGov(address(proxy)); - delegator2.approveIou(address(proxy)); - - uint256 delGovBalance = gov.balanceOf(address(delegator2)); - - delegator2.doProxyLock(wad); - assertEq(gov.balanceOf(address(delegator2)), delGovBalance - wad); - assertEq(gov.balanceOf(address(chief)), currMKR + wad); - assertEq(iou.balanceOf(address(delegator2)), wad); - assertEq(proxy.stake(address(delegator2)), wad); - - // Comply with Chief's flash loan protection - hevm.roll(block.number + 1); - - delegator2.doProxyFree(wad); - assertEq(gov.balanceOf(address(delegator2)), delGovBalance); - assertEq(gov.balanceOf(address(chief)), currMKR); - assertEq(iou.balanceOf(address(delegator2)), 0); - assertEq(proxy.stake(address(delegator2)), 0); - } - - function test_delegator_lock_free_after_expiration() public { - uint256 currMKR = gov.balanceOf(address(chief)); - - delegator1.approveGov(address(proxy)); - delegator1.approveIou(address(proxy)); - - delegator1.doProxyLock(10_000 ether); - assertEq(gov.balanceOf(address(delegator1)), 0); - assertEq(gov.balanceOf(address(chief)), currMKR + 10_000 ether); - assertEq(iou.balanceOf(address(delegator1)), 10_000 ether); - assertEq(proxy.stake(address(delegator1)), 10_000 ether); - - hevm.roll(block.number + 1); - - // Warp past expiration - hevm.warp(block.timestamp + 9001 days); - assertTrue(block.timestamp > delegate.expiration()); - - // Always allow freeing after expiration. - delegator1.doProxyFree(10_000 ether); - assertEq(gov.balanceOf(address(delegator1)), 10_000 ether); - assertEq(gov.balanceOf(address(chief)), currMKR); - assertEq(iou.balanceOf(address(delegator1)), 0); - assertEq(proxy.stake(address(delegator1)), 0); - } - - function test_delegate_voting() public { - uint256 currMKR = gov.balanceOf(address(chief)); - - delegate.approveGov(address(proxy)); - delegate.approveIou(address(proxy)); - delegator1.approveGov(address(proxy)); - delegator1.approveIou(address(proxy)); - - delegate.doProxyLock(100 ether); - delegator1.doProxyLock(10_000 ether); - - assertEq(gov.balanceOf(address(chief)), currMKR + 10_100 ether); - - address[] memory yays = new address[](1); - yays[0] = c1; - delegate.doProxyVote(yays); - assertEq(chief.approvals(c1), 10_100 ether); - assertEq(chief.approvals(c2), 0 ether); - - address[] memory _yays = new address[](1); - _yays[0] = c2; - delegate.doProxyVote(_yays); - assertEq(chief.approvals(c1), 0 ether); - assertEq(chief.approvals(c2), 10_100 ether); - } - - function test_delegate_polling() public { - // We can't test much as they are pure events - // but at least we can check it doesn't revert - - delegate.doProxyVotePoll(1, 1); - - uint256[] memory ids = new uint256[](2); - ids[0] = 1; - ids[1] = 2; - uint256[] memory opts = new uint256[](2); - opts[0] = 1; - opts[1] = 3; - delegate.doProxyVotePoll(ids, opts); - } - - - function testFail_delegate_voting_after_expiration() public { - uint256 currMKR = gov.balanceOf(address(chief)); - - delegate.approveGov(address(proxy)); - delegate.approveIou(address(proxy)); - delegator1.approveGov(address(proxy)); - delegator1.approveIou(address(proxy)); - - delegate.doProxyLock(100 ether); - delegator1.doProxyLock(10_000 ether); - - assertEq(gov.balanceOf(address(chief)), currMKR + 10_100 ether); - - address[] memory yays = new address[](1); - yays[0] = c1; - - hevm.roll(block.number + 1); - - // Warp past expiration - hevm.warp(block.timestamp + 9001 days); - assertTrue(block.timestamp > delegate.expiration()); - - // Fail here after expiration - delegate.doProxyVote(yays); - } - - function test_delegate_voting_fuzz(uint256 wad_seed, uint256 wad2_seed) public { - uint256 wad = wad_seed < 1 ether ? wad_seed += 1 ether : wad_seed % 100 ether; - uint256 wad2 = wad2_seed < 1 ether ? wad2_seed += 1 ether : wad2_seed % 20_000 ether; - uint256 currMKR = gov.balanceOf(address(chief)); - - delegate.approveGov(address(proxy)); - delegate.approveIou(address(proxy)); - delegator2.approveGov(address(proxy)); - delegator2.approveIou(address(proxy)); - - uint256 delGovBalance = gov.balanceOf(address(delegate)); - uint256 del2GovBalance = gov.balanceOf(address(delegator2)); - - delegate.doProxyLock(wad); - delegator2.doProxyLock(wad2); - - assertEq(gov.balanceOf(address(delegate)), delGovBalance - wad); - assertEq(gov.balanceOf(address(delegator2)), del2GovBalance - wad2); - assertEq(iou.balanceOf(address(delegate)), wad); - assertEq(iou.balanceOf(address(delegator2)), wad2); - assertEq(proxy.stake(address(delegate)), wad); - assertEq(proxy.stake(address(delegator2)), wad2); - assertEq(gov.balanceOf(address(chief)), currMKR + wad + wad2); - - address[] memory yays = new address[](1); - yays[0] = c1; - delegate.doProxyVote(yays); - assertEq(chief.approvals(c1), wad + wad2); - assertEq(chief.approvals(c2), 0 ether); - - address[] memory _yays = new address[](1); - _yays[0] = c2; - delegate.doProxyVote(_yays); - assertEq(chief.approvals(c1), 0 ether); - assertEq(chief.approvals(c2), wad + wad2); - } - - function testFail_delegate_attempts_steal() public { - delegate.approveGov(address(proxy)); - delegate.approveIou(address(proxy)); - delegator1.approveGov(address(proxy)); - delegator1.approveIou(address(proxy)); - - delegate.doProxyLock(100 ether); - delegator1.doProxyLock(10_000 ether); - - // Attempting to steal more MKR than you put in - delegate.doProxyFree(101 ether); - } - - function testFail_attempt_steal_with_ious() public { - delegator1.approveGov(address(proxy)); - delegator1.approveIou(address(proxy)); - delegator2.approveGov(address(chief)); - delegator2.approveIou(address(proxy)); - - delegator1.doProxyLock(10_000 ether); - - // You have enough IOU tokens, but you are still not marked as a delegate - delegator2.doChiefLock(20_000 ether); - - delegator2.doProxyFree(10_000 ether); - } - - function testFail_non_delegate_attempts_vote() public { - delegate.approveGov(address(proxy)); - delegate.approveIou(address(proxy)); - delegator1.approveGov(address(proxy)); - delegator1.approveIou(address(proxy)); - - delegate.doProxyLock(100 ether); - delegator1.doProxyLock(10_000 ether); - - // Delegator2 attempts to vote - address[] memory yays = new address[](1); - yays[0] = c1; - delegator2.doProxyVote(yays); - } - - function testFail_non_delegate_attempts_polling_vote() public { - delegator2.doProxyVotePoll(1, 1); - } - - function testFail_non_delegate_attempts_polling_vote_multiple() public { - uint256[] memory ids = new uint256[](2); - ids[0] = 1; - ids[1] = 2; - uint256[] memory opts = new uint256[](2); - opts[0] = 1; - opts[1] = 3; - delegator2.doProxyVotePoll(ids, opts); - } -} diff --git a/src/VoteDelegateFactory.sol b/src/VoteDelegateFactory.sol index 53135d6..765df03 100644 --- a/src/VoteDelegateFactory.sol +++ b/src/VoteDelegateFactory.sol @@ -1,7 +1,6 @@ +// SPDX-FileCopyrightText: © 2021 Dai Foundation // SPDX-License-Identifier: AGPL-3.0-or-later - -// Copyright (C) 2021 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 @@ -15,28 +14,34 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -// VoteDelegateFactory - create and keep record of delegats -pragma solidity 0.6.12; +pragma solidity ^0.8.21; -import "./VoteDelegate.sol"; +import {VoteDelegate} from "src/VoteDelegate.sol"; contract VoteDelegateFactory { - address public immutable chief; - address public immutable polling; - mapping(address => address) public delegates; + // --- storage variables --- + + mapping(address usr => address voteDelegate) public delegates; + mapping(address voteDelegate => uint256 created) public created; + + // --- immutables --- - event CreateVoteDelegate( - address indexed delegate, - address indexed voteDelegate - ); + address immutable public chief; + address immutable public polling; - constructor(address _chief, address _polling) public { + // --- events --- + + event CreateVoteDelegate(address indexed usr, address indexed voteDelegate); + + // --- constructor --- + + constructor(address _chief, address _polling) { chief = _chief; polling = _polling; } - function isDelegate(address guy) public view returns (bool) { - return delegates[guy] != address(0); + function isDelegate(address usr) public view returns (bool ok) { + ok = delegates[usr] != address(0); } function create() external returns (address voteDelegate) { @@ -44,6 +49,8 @@ contract VoteDelegateFactory { voteDelegate = address(new VoteDelegate(chief, polling, msg.sender)); delegates[msg.sender] = voteDelegate; + created[voteDelegate] = 1; + emit CreateVoteDelegate(msg.sender, voteDelegate); } } diff --git a/src/VoteDelegateFactory.t.sol b/src/VoteDelegateFactory.t.sol deleted file mode 100644 index 3bf9410..0000000 --- a/src/VoteDelegateFactory.t.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -// Copyright (C) 2021 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; - -import "ds-test/test.sol"; - -import "./VoteDelegateFactory.sol"; - -interface Hevm { - function warp(uint256) external; - function store(address,bytes32,bytes32) external; - function load(address,bytes32) external view returns (bytes32); -} - -contract VoteUser { - VoteDelegateFactory voteDelegateFactory; - - constructor(VoteDelegateFactory voteDelegateFactory_) public { - voteDelegateFactory = voteDelegateFactory_; - } - - function doCreate() public returns (VoteDelegate) { - return VoteDelegate(voteDelegateFactory.create()); - } -} - - -contract VoteDelegateFactoryTest is DSTest { - Hevm hevm; - - uint256 constant electionSize = 3; - - VoteDelegateFactory voteDelegateFactory; - TokenLike gov; - TokenLike iou; - ChiefLike chief; - PollingLike polling; - - VoteUser delegate; - VoteUser delegator; - - function setUp() public { - hevm = Hevm(HEVM_ADDRESS); - - chief = ChiefLike(0x0a3f6849f78076aefaDf113F5BED87720274dDC0); - polling = PollingLike(0xD3A9FE267852281a1e6307a1C37CDfD76d39b133); - gov = chief.GOV(); - iou = chief.IOU(); - - voteDelegateFactory = new VoteDelegateFactory(address(chief), address(polling)); - delegator = new VoteUser(voteDelegateFactory); - delegate = new VoteUser(voteDelegateFactory); - } - - function test_constructor() public { - assertEq(address(voteDelegateFactory.chief()), address(chief)); - assertEq(address(voteDelegateFactory.polling()), address(polling)); - } - - function test_create() public { - assertTrue(!voteDelegateFactory.isDelegate(address(delegate))); - VoteDelegate voteDelegate = delegate.doCreate(); - assertTrue(voteDelegateFactory.isDelegate(address(delegate))); - assertEq( - address(voteDelegateFactory.delegates(address(delegate))), - address(voteDelegate) - ); - } - - function testFail_create() public { - delegate.doCreate(); - delegate.doCreate(); - } -} diff --git a/test.sh b/test.sh deleted file mode 100755 index 8cc356a..0000000 --- a/test.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -e - -[[ "$ETH_RPC_URL" && "$(seth chain)" == "ethlive" ]] || { echo "Please set a mainnet ETH_RPC_URL"; exit 1; } - -export DAPP_BUILD_OPTIMIZE=1 -export DAPP_BUILD_OPTIMIZE_RUNS=200 - -if [[ -z "$1" && -z "$2" ]]; then - dapp --use solc:0.6.12 test --rpc-url="$ETH_RPC_URL" --fuzz-runs 1 -vv -elif [[ -z "$2" ]]; then - dapp --use solc:0.6.12 test --rpc-url="$ETH_RPC_URL" --match "$1" --fuzz-runs 1 -vv -elif [[ -z "$1" ]]; then - dapp --use solc:0.6.12 test --rpc-url="$ETH_RPC_URL" --fuzz-runs "$2" -vv -else - dapp --use solc:0.6.12 test --rpc-url="$ETH_RPC_URL" --match "$1" --fuzz-runs "$2" -vv -fi diff --git a/test/VoteDelegate.t.sol b/test/VoteDelegate.t.sol new file mode 100644 index 0000000..8d78c74 --- /dev/null +++ b/test/VoteDelegate.t.sol @@ -0,0 +1,311 @@ +// 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; + +import "dss-test/DssTest.sol"; + +import {VoteDelegate, ChiefLike, PollingLike, GemLike} from "src/VoteDelegate.sol"; + +interface ChiefExtendedLike is ChiefLike { + function approvals(address) external view returns (uint256); +} + +interface GemLikeExtended is GemLike { + function allowance(address, address) external view returns (uint256); + function balanceOf(address) external view returns (uint256); + function mint(address, uint256) external; +} + +contract VoteDelegateTest is DssTest { + address constant c1 = address(0x1); + address constant c2 = address(0x2); + + VoteDelegate proxy; + GemLikeExtended gov; + ChiefExtendedLike chief; + PollingLike polling; + + address delegate = address(111); + address delegator1 = address(222); + address delegator2 = address(333); + + event Lock(address indexed usr, uint256 wad); + event Free(address indexed usr, uint256 wad); + event ReserveHatch(); + event Voted(address indexed voter, uint256 indexed pollId, uint256 indexed optionId); + + function setUp() public { + vm.createSelectFork(vm.envString("ETH_RPC_URL")); + + chief = ChiefExtendedLike(0x0a3f6849f78076aefaDf113F5BED87720274dDC0); + polling = PollingLike(0xD3A9FE267852281a1e6307a1C37CDfD76d39b133); + gov = GemLikeExtended(address(chief.GOV())); + + deal(address(gov), address(delegate), 100 ether, true); + deal(address(gov), address(delegator1), 10_000 ether, true); + deal(address(gov), address(delegator2), 20_000 ether, true); + + proxy = new VoteDelegate(address(chief), address(polling), address(delegate)); + } + + function testConstructor() public view { + assertEq(address(proxy.chief()), address(chief)); + assertEq(address(proxy.polling()), address(polling)); + assertEq(proxy.delegate(), delegate); + assertEq(address(proxy.gov()), address(chief.GOV())); + assertEq(gov.allowance(address(proxy), address(chief)), type(uint256).max); + assertEq(GemLikeExtended(address(chief.IOU())).allowance(address(proxy), address(chief)), type(uint256).max); + } + + function testModifiers() public { + bytes4[] memory authedMethods = new bytes4[](4); + authedMethods[0] = bytes4(keccak256("vote(address[])")); + authedMethods[1] = bytes4(keccak256("vote(bytes32)")); + authedMethods[2] = bytes4(keccak256("votePoll(uint256,uint256)")); + authedMethods[3] = bytes4(keccak256("votePoll(uint256[],uint256[])")); + + vm.startPrank(address(0xBEEF)); + checkModifier(address(proxy), "VoteDelegate/sender-not-delegate", authedMethods); + vm.stopPrank(); + } + + function testProxyLockFree() public { + uint256 initialMKR = gov.balanceOf(address(chief)); + + vm.prank(delegate); gov.approve(address(proxy), type(uint256).max); + + assertEq(gov.balanceOf(address(delegate)), 100 ether); + + vm.expectEmit(true, true, true, true); + emit Lock(delegate, 100 ether); + vm.prank(delegate); proxy.lock(100 ether); + assertEq(gov.balanceOf(address(delegate)), 0); + assertEq(gov.balanceOf(address(chief)), initialMKR + 100 ether); + assertEq(proxy.stake(address(delegate)), 100 ether); + + // Comply with Chief's flash loan protection + vm.roll(block.number + 1); + + vm.expectEmit(true, true, true, true); + emit Free(delegate, 100 ether); + vm.prank(delegate); proxy.free(100 ether); + assertEq(gov.balanceOf(address(delegate)), 100 ether); + assertEq(gov.balanceOf(address(chief)), initialMKR); + assertEq(proxy.stake(address(delegate)), 0); + } + + function testDelegatorLockFree() public { + uint256 initialMKR = gov.balanceOf(address(chief)); + + vm.prank(delegator1); gov.approve(address(proxy), type(uint256).max); + + vm.expectEmit(true, true, true, true); + emit Lock(delegator1, 10_000 ether); + vm.prank(delegator1); proxy.lock(10_000 ether); + assertEq(gov.balanceOf(address(delegator1)), 0); + assertEq(gov.balanceOf(address(chief)), initialMKR + 10_000 ether); + assertEq(proxy.stake(address(delegator1)), 10_000 ether); + + // Comply with Chief's flash loan protection + vm.roll(block.number + 1); + + vm.expectEmit(true, true, true, true); + emit Free(delegator1, 10_000 ether); + vm.prank(delegator1); proxy.free(10_000 ether); + assertEq(gov.balanceOf(address(delegator1)), 10_000 ether); + assertEq(gov.balanceOf(address(chief)), initialMKR); + assertEq(proxy.stake(address(delegator1)), 0); + } + + function testDelegatorLockFreeFuzz(uint256 wad_seed) public { + uint256 wad = wad_seed < 1 ether ? wad_seed += 1 ether : wad_seed % 20_000 ether; + uint256 initialMKR = gov.balanceOf(address(chief)); + + vm.prank(delegator2); gov.approve(address(proxy), type(uint256).max); + + uint256 delGovBalance = gov.balanceOf(address(delegator2)); + + vm.expectEmit(true, true, true, true); + emit Lock(delegator2, wad); + vm.prank(delegator2); proxy.lock(wad); + assertEq(gov.balanceOf(address(delegator2)), delGovBalance - wad); + assertEq(gov.balanceOf(address(chief)), initialMKR + wad); + assertEq(proxy.stake(address(delegator2)), wad); + + // Comply with Chief's flash loan protection + vm.roll(block.number + 1); + + vm.expectEmit(true, true, true, true); + emit Free(delegator2, wad); + vm.prank(delegator2); proxy.free(wad); + assertEq(gov.balanceOf(address(delegator2)), delGovBalance); + assertEq(gov.balanceOf(address(chief)), initialMKR); + assertEq(proxy.stake(address(delegator2)), 0); + } + + function testReserveHatch() public { + vm.prank(delegate); gov.approve(address(proxy), type(uint256).max); + + assertEq(gov.balanceOf(address(delegate)), 100 ether); + assertEq(proxy.hatchTrigger(), 0); + + vm.prank(delegate); proxy.lock(10 ether); // can lock + + vm.expectEmit(true, true, true, true); + emit ReserveHatch(); + proxy.reserveHatch(); // can reserve hatch + assertEq(proxy.hatchTrigger(), block.number); + vm.prank(delegate); proxy.lock(10 ether); // can still lock + vm.expectRevert("VoteDelegate/cooldown-not-finished"); // can not reserve hatch again + proxy.reserveHatch(); + + // move to first block of the hatch + vm.roll(block.number + 1); + + vm.expectRevert("VoteDelegate/no-lock-during-hatch"); + vm.prank(delegate); proxy.lock(10 ether); // can not lock + vm.expectRevert("VoteDelegate/cooldown-not-finished"); // can not reserve hatch + proxy.reserveHatch(); + + // move to last block of the hatch + vm.roll(block.number + 4); + + vm.expectRevert("VoteDelegate/no-lock-during-hatch"); + vm.prank(delegate); proxy.lock(10 ether); // can not lock + vm.expectRevert("VoteDelegate/cooldown-not-finished"); // can not reserve hatch + proxy.reserveHatch(); + + // move to first block of the cooldown + vm.roll(block.number + 1); + + vm.prank(delegate); proxy.lock(10 ether); // can lock again + vm.expectRevert("VoteDelegate/cooldown-not-finished"); // can not reserve hatch + proxy.reserveHatch(); + + // move to last block of the cooldown + vm.roll(block.number + 18); + + vm.prank(delegate); proxy.lock(10 ether); // can still lock + vm.expectRevert("VoteDelegate/cooldown-not-finished"); // can not reserve hatch + proxy.reserveHatch(); + + // move to first block after the cooldown + vm.roll(block.number + 1); + + vm.prank(delegate); proxy.lock(10 ether); // can lock + proxy.reserveHatch(); // can reserve hatch again + assertEq(proxy.hatchTrigger(), block.number); + vm.prank(delegate); proxy.lock(10 ether); // can still lock + vm.expectRevert("VoteDelegate/cooldown-not-finished"); // can not reserve hatch again + proxy.reserveHatch(); + + // move to first block of the new hatch + vm.roll(block.number + 1); + + vm.expectRevert("VoteDelegate/no-lock-during-hatch"); + vm.prank(delegate); proxy.lock(10 ether); // can not lock + vm.expectRevert("VoteDelegate/cooldown-not-finished"); // can not reserve hatch + proxy.reserveHatch(); + } + + function testDelegateVoting() public { + uint256 initialMKR = gov.balanceOf(address(chief)); + + vm.prank(delegate); gov.approve(address(proxy), type(uint256).max); + vm.prank(delegator1); gov.approve(address(proxy), type(uint256).max); + + vm.prank(delegate); proxy.lock(100 ether); + vm.prank(delegator1); proxy.lock(10_000 ether); + + assertEq(gov.balanceOf(address(chief)), initialMKR + 10_100 ether); + + address[] memory yays = new address[](1); + yays[0] = c1; + vm.prank(delegate); proxy.vote(yays); + assertEq(chief.approvals(c1), 10_100 ether); + assertEq(chief.approvals(c2), 0 ether); + + address[] memory _yays = new address[](1); + _yays[0] = c2; + vm.prank(delegate); proxy.vote(_yays); + assertEq(chief.approvals(c1), 0 ether); + assertEq(chief.approvals(c2), 10_100 ether); + } + + function testDelegatePolling() public { + // We can't test much as they are pure events + // but at least we can check it doesn't revert and events are emitted + + vm.expectEmit(true, true, true, true); + emit Voted(address(proxy), 1, 1); + vm.prank(delegate); proxy.votePoll(1, 1); + + uint256[] memory ids = new uint256[](2); + ids[0] = 1; + ids[1] = 2; + uint256[] memory opts = new uint256[](2); + opts[0] = 1; + opts[1] = 3; + vm.expectEmit(true, true, true, true); + emit Voted(address(proxy), 1, 1); + vm.prank(delegate); proxy.votePoll(ids, opts); + } + + function testDelegateVotingFuzz(uint256 wad_seed, uint256 wad2_seed) public { + uint256 wad = wad_seed < 1 ether ? wad_seed += 1 ether : wad_seed % 100 ether; + uint256 wad2 = wad2_seed < 1 ether ? wad2_seed += 1 ether : wad2_seed % 20_000 ether; + uint256 initialMKR = gov.balanceOf(address(chief)); + + vm.prank(delegate); gov.approve(address(proxy), type(uint256).max); + vm.prank(delegator2); gov.approve(address(proxy), type(uint256).max); + + uint256 delGovBalance = gov.balanceOf(address(delegate)); + uint256 del2GovBalance = gov.balanceOf(address(delegator2)); + + vm.prank(delegate); proxy.lock(wad); + vm.prank(delegator2); proxy.lock(wad2); + + assertEq(gov.balanceOf(address(delegate)), delGovBalance - wad); + assertEq(gov.balanceOf(address(delegator2)), del2GovBalance - wad2); + assertEq(proxy.stake(address(delegate)), wad); + assertEq(proxy.stake(address(delegator2)), wad2); + assertEq(gov.balanceOf(address(chief)), initialMKR + wad + wad2); + + address[] memory yays = new address[](1); + yays[0] = c1; + vm.prank(delegate); proxy.vote(yays); + assertEq(chief.approvals(c1), wad + wad2); + assertEq(chief.approvals(c2), 0 ether); + + address[] memory _yays = new address[](1); + _yays[0] = c2; + vm.prank(delegate); proxy.vote(_yays); + assertEq(chief.approvals(c1), 0 ether); + assertEq(chief.approvals(c2), wad + wad2); + } + + function testRevertsDelegateAttemptsSteal() public { + vm.prank(delegate); gov.approve(address(proxy), type(uint256).max); + vm.prank(delegator1); gov.approve(address(proxy), type(uint256).max); + vm.prank(delegate); proxy.lock(100 ether); + vm.prank(delegator1); proxy.lock(10_000 ether); + + // Attempting to take more MKR than assigned in the stake mapping (having a greater total) + vm.expectRevert("VoteDelegate/insufficient-stake"); + vm.prank(delegate); proxy.free(100 ether + 1); + } +} diff --git a/test/VoteDelegateFactory.t.sol b/test/VoteDelegateFactory.t.sol new file mode 100644 index 0000000..67db667 --- /dev/null +++ b/test/VoteDelegateFactory.t.sol @@ -0,0 +1,60 @@ +// 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; + +import "dss-test/DssTest.sol"; + +import "src/VoteDelegateFactory.sol"; + +contract VoteDelegateFactoryTest is DssTest { + VoteDelegateFactory factory; + address chief; + address polling; + + event CreateVoteDelegate(address indexed usr, address indexed voteDelegate); + + function setUp() public { + vm.createSelectFork(vm.envString("ETH_RPC_URL")); + + chief = 0x0a3f6849f78076aefaDf113F5BED87720274dDC0; + polling = 0xD3A9FE267852281a1e6307a1C37CDfD76d39b133; + + factory = new VoteDelegateFactory(address(chief), address(polling)); + } + + function testConstructor() public view { + assertEq(address(factory.chief()), address(chief)); + assertEq(address(factory.polling()), address(polling)); + } + + function testCreate() public { + address proxy = vm.computeCreateAddress(address(factory), vm.getNonce(address(factory))); + + assertEq(factory.created(proxy), 0); + assertEq(factory.isDelegate(address(1)), false); + assertEq(factory.delegates(address(1)), address(0)); + vm.expectEmit(true, true, true, true); + emit CreateVoteDelegate(address(1), proxy); + vm.prank(address(1)); address retAddr = factory.create(); + assertEq(retAddr, proxy); + assertEq(factory.created(proxy), 1); + assertEq(factory.isDelegate(address(1)), true); + assertEq(factory.delegates(address(1)), proxy); + vm.expectRevert("VoteDelegateFactory/sender-is-already-delegate"); + vm.prank(address(1)); factory.create(); + } +}