Skip to content

Commit

Permalink
Proposal scripts (#50)
Browse files Browse the repository at this point in the history
* A script to set a new v3 factory owner
* A script to set protocol fees on select pools
  • Loading branch information
alexkeating authored Feb 15, 2024
1 parent d745dd2 commit 02d9ffa
Show file tree
Hide file tree
Showing 12 changed files with 497 additions and 2 deletions.
1 change: 1 addition & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
MAINNET_RPC_URL=
FOUNDRY_PROFILE=default
DEPLOYER_PRIVATE_KEY=
PROPOSER_ADDRESS=
1 change: 1 addition & 0 deletions script/DeployInput.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
pragma solidity 0.8.23;

contract DeployInput {
address constant UNISWAP_GOVERNOR = 0x408ED6354d4973f66138C91495F2f2FCbd8724C3;
address constant UNISWAP_GOVERNOR_TIMELOCK = 0x1a9C8182C09F50C8318d769245beA52c32BE35BC;
address constant UNISWAP_V3_FACTORY_ADDRESS = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
// TODO not finalized: currently WETH
Expand Down
52 changes: 52 additions & 0 deletions script/ProposeFactorySetOwner.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.23;

import {Script} from "forge-std/Script.sol";

import {DeployInput} from "script/DeployInput.sol";
import {GovernorBravoDelegate} from "script/interfaces/GovernorBravoInterfaces.sol";

contract ProposeFactorySetOwner is Script, DeployInput {
GovernorBravoDelegate constant GOVERNOR = GovernorBravoDelegate(UNISWAP_GOVERNOR);
// The default proposer is uf.eek.eth.
address _proposer =
vm.envOr("PROPOSER_ADDRESS", address(0x0459f41c5f09BF678D9C07331894dE31d8C22255));

/// @param _newV3FactoryOwner The new factory owner, i.e. the recently deployed `V3FactoryOwner`.
function proposeFactoryOwnerChangeOnGovernor(address _newV3FactoryOwner)
internal
returns (uint256 _proposalId)
{
address[] memory _targets = new address[](1);
uint256[] memory _values = new uint256[](1);
string[] memory _signatures = new string[](1);
bytes[] memory _calldatas = new bytes[](1);

_targets[0] = UNISWAP_V3_FACTORY_ADDRESS;
_values[0] = 0;
_signatures[0] = "setOwner(address)";
_calldatas[0] = abi.encode(address(_newV3FactoryOwner));

return GOVERNOR.propose(
_targets, _values, _signatures, _calldatas, "Change Uniswap V3 factory owner"
);
}

/// @param _newV3FactoryOwner The new factory owner, i.e. the recently deployed `V3FactoryOwner`.
/// @dev After the UniStaker and V3FactoryOwner contracts are deployed, a delegate should run this
/// script to create a proposal to change the Uniswap v3 factory owner.
function run(address _newV3FactoryOwner) public returns (uint256 _proposalId) {
// The expectation is the key loaded here corresponds to the address of the `proposer` above.
// When running as a script, broadcast will fail if the key is not correct.
uint256 _proposerKey = vm.envOr(
"PROPOSER_PRIVATE_KEY",
uint256(0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d)
);
vm.rememberKey(_proposerKey);

vm.startBroadcast(_proposer);
_proposalId = proposeFactoryOwnerChangeOnGovernor(_newV3FactoryOwner);
vm.stopBroadcast();
return _proposalId;
}
}
95 changes: 95 additions & 0 deletions script/ProposeProtocolFeesBase.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.23;

import {Script} from "forge-std/Script.sol";

import {DeployInput} from "script/DeployInput.sol";
import {GovernorBravoDelegate} from "script/interfaces/GovernorBravoInterfaces.sol";

/// @dev A new proposal script that updates a pool's fee settings should inherit this abstract
/// script and implement `getPoolFeeSettings`.
abstract contract ProposeProtocolFeesBase is Script, DeployInput {
GovernorBravoDelegate constant GOVERNOR = GovernorBravoDelegate(UNISWAP_GOVERNOR);
// The default proposer is uf.eek.eth.
address _proposer =
vm.envOr("PROPOSER_ADDRESS", address(0x0459f41c5f09BF678D9C07331894dE31d8C22255));

/// @dev The targets for the proposal which should be the `V3FactoryOwner`.
address[] public targets;
/// @dev The values to pass into the proposal which should all be 0.
uint256[] public values;
/// @dev The function signatures that will be called when a proposal is executed. All of the
/// signatures should be `setFeeProtocol(address,uint8,uint8)`.
string[] public signatures;
/// @dev The calldata for all of function calls in the proposal. These should match the
/// `PoolFeeSettings` defined in `getPoolFeeSettings`.
bytes[] public calldatas;

/// @dev A struct to represent all of the information needed to update a pool's fees. Such as the
/// target pool and the new fees for each token in the pool.
struct PoolFeeSettings {
address pool;
uint8 feeProtocol0;
uint8 feeProtocol1;
}

/// @dev A utility function that updates the targets, values, signatures, and calldatas for a
/// proposal that will only update protocol fees for a list of pools.
function pushFeeSettingToProposalCalldata(
address _v3FactoryOwner,
address _pool,
uint8 _feeProtocol0,
uint8 _feeProtocol1
) internal {
targets.push(_v3FactoryOwner);
values.push(0);
signatures.push("setFeeProtocol(address,uint8,uint8)");
calldatas.push(abi.encode(_pool, _feeProtocol0, _feeProtocol1));
}

/// @return A list of pool settings used to update protocol fees for each pool.
/// @dev A new `ProposeProtocolFees` script should extend this base script and only implement this
/// function to return a list of pools to be updated with their new settings. This function will
/// return the appropriate pool settings in the `run` method and add them to the proposal that
/// will be proposed.
function getPoolFeeSettings() internal pure virtual returns (PoolFeeSettings[] memory);

/// @dev Create a proposal on the Uniswap Governor with targets, values, signatures, and calldatas
/// defined in this contracts. These values should have been built using
/// `pushFeeSettingToProposalCalldata`.
function propose() internal returns (uint256 _proposalId) {
return GOVERNOR.propose(
targets,
values,
signatures,
calldatas,
"Change Uniswap V3 factory owner and set pool protocol fees"
);
}

/// @param _newV3FactoryOwner The new factory owner which should have be the recently deployed.
/// @dev This script sets protocol fees for whatever pools and fees are configured. This script
/// should only be run after `UniStaker` and the `V3FactoryOwner` are deployed, and after the
/// `V3FactoryOwner` becomes the owner of ther Uniswap v3 factory.
function run(address _newV3FactoryOwner) public returns (uint256 _proposalId) {
// The expectation is the key loaded here corresponds to the address of the `proposer` above.
// When running as a script, broadcast will fail if the key is not correct.
uint256 _proposerKey = vm.envUint("PROPOSER_PRIVATE_KEY");
vm.rememberKey(_proposerKey);

PoolFeeSettings[] memory poolFeeSettings = getPoolFeeSettings();
for (uint256 i = 0; i < poolFeeSettings.length; i++) {
pushFeeSettingToProposalCalldata(
_newV3FactoryOwner,
poolFeeSettings[i].pool,
poolFeeSettings[i].feeProtocol0,
poolFeeSettings[i].feeProtocol1
);
}

vm.startBroadcast(_proposer);
_proposalId = propose();
vm.stopBroadcast();
return _proposalId;
}
}
26 changes: 26 additions & 0 deletions script/ProposeProtocolFeesBatch1.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.23;

import {Script} from "forge-std/Script.sol";

import {DeployInput} from "script/DeployInput.sol";
import {GovernorBravoDelegate} from "script/interfaces/GovernorBravoInterfaces.sol";
import {ProposeProtocolFeesBase} from "script/ProposeProtocolFeesBase.s.sol";

/// @dev This script will turn on protocol fees for the following pools: WBTC-WETH-3000,
/// DAI-WETH-300, and DAI-USDC-100.
contract ProposeProtocolFeesBatch1 is ProposeProtocolFeesBase {
// TODO Double check these are the right pools
address constant WBTC_WETH_3000_POOL = 0xCBCdF9626bC03E24f779434178A73a0B4bad62eD;
address constant DAI_WETH_3000_POOL = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
address constant DAI_USDC_100_POOL = 0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168;

/// @return A list of pool settings used to update protocol fees for each pool.
function getPoolFeeSettings() internal pure override returns (PoolFeeSettings[] memory) {
PoolFeeSettings[] memory poolFeeSettings = new PoolFeeSettings[](3);
poolFeeSettings[0] = PoolFeeSettings(WBTC_WETH_3000_POOL, 10, 10);
poolFeeSettings[1] = PoolFeeSettings(DAI_WETH_3000_POOL, 10, 10);
poolFeeSettings[2] = PoolFeeSettings(DAI_USDC_100_POOL, 10, 10);
return poolFeeSettings;
}
}
114 changes: 114 additions & 0 deletions script/interfaces/GovernorBravoInterfaces.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.23;

// This interface was created using cast interface. The contract can be found at
// https://etherscan.io/address/0x53a328f4086d7c0f1fa19e594c9b842125263026#code#F2#L182

interface GovernorBravoDelegate {
type ProposalState is uint8;

struct Receipt {
bool hasVoted;
uint8 support;
uint96 votes;
}

event NewAdmin(address oldAdmin, address newAdmin);
event NewImplementation(address oldImplementation, address newImplementation);
event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin);
event ProposalCanceled(uint256 id);
event ProposalCreated(
uint256 id,
address proposer,
address[] targets,
uint256[] values,
string[] signatures,
bytes[] calldatas,
uint256 startBlock,
uint256 endBlock,
string description
);
event ProposalExecuted(uint256 id);
event ProposalQueued(uint256 id, uint256 eta);
event ProposalThresholdSet(uint256 oldProposalThreshold, uint256 newProposalThreshold);
event VoteCast(
address indexed voter, uint256 proposalId, uint8 support, uint256 votes, string reason
);
event VotingDelaySet(uint256 oldVotingDelay, uint256 newVotingDelay);
event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod);

function BALLOT_TYPEHASH() external view returns (bytes32);
function DOMAIN_TYPEHASH() external view returns (bytes32);
function MAX_PROPOSAL_THRESHOLD() external view returns (uint256);
function MAX_VOTING_DELAY() external view returns (uint256);
function MAX_VOTING_PERIOD() external view returns (uint256);
function MIN_PROPOSAL_THRESHOLD() external view returns (uint256);
function MIN_VOTING_DELAY() external view returns (uint256);
function MIN_VOTING_PERIOD() external view returns (uint256);
function _acceptAdmin() external;
function _initiate(uint256 proposalCount) external;
function _setPendingAdmin(address newPendingAdmin) external;
function _setProposalThreshold(uint256 newProposalThreshold) external;
function _setVotingDelay(uint256 newVotingDelay) external;
function _setVotingPeriod(uint256 newVotingPeriod) external;
function admin() external view returns (address);
function cancel(uint256 proposalId) external;
function castVote(uint256 proposalId, uint8 support) external;
function castVoteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external;
function castVoteWithReason(uint256 proposalId, uint8 support, string memory reason) external;
function execute(uint256 proposalId) external payable;
function getActions(uint256 proposalId)
external
view
returns (
address[] memory targets,
uint256[] memory values,
string[] memory signatures,
bytes[] memory calldatas
);
function getReceipt(uint256 proposalId, address voter) external view returns (Receipt memory);
function implementation() external view returns (address);
function initialProposalId() external view returns (uint256);
function initialize(
address timelock_,
address uni_,
uint256 votingPeriod_,
uint256 votingDelay_,
uint256 proposalThreshold_
) external;
function latestProposalIds(address) external view returns (uint256);
function name() external view returns (string memory);
function pendingAdmin() external view returns (address);
function proposalCount() external view returns (uint256);
function proposalMaxOperations() external view returns (uint256);
function proposalThreshold() external view returns (uint256);
function proposals(uint256)
external
view
returns (
uint256 id,
address proposer,
uint256 eta,
uint256 startBlock,
uint256 endBlock,
uint256 forVotes,
uint256 againstVotes,
uint256 abstainVotes,
bool canceled,
bool executed
);
function propose(
address[] memory targets,
uint256[] memory values,
string[] memory signatures,
bytes[] memory calldatas,
string memory description
) external returns (uint256);
function queue(uint256 proposalId) external;
function quorumVotes() external view returns (uint256);
function state(uint256 proposalId) external view returns (ProposalState);
function timelock() external view returns (address);
function uni() external view returns (address);
function votingDelay() external view returns (uint256);
function votingPeriod() external view returns (uint256);
}
5 changes: 5 additions & 0 deletions src/interfaces/IUniswapV3FactoryOwnerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ pragma solidity ^0.8.23;
/// @dev Stripped down and renamed from:
/// https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/interfaces/IUniswapV3Factory.sol
interface IUniswapV3FactoryOwnerActions {
/// @notice Returns the current owner of the factory
/// @dev Can be changed by the current owner via setOwner
/// @return The address of the factory owner
function owner() external view returns (address);

/// @notice Updates the owner of the factory
/// @dev Must be called by the current owner
/// @param _owner The new owner of the factory
Expand Down
43 changes: 41 additions & 2 deletions test/UniStaker.integration.t.sol
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.23;

import {Vm, Test, console2} from "forge-std/Test.sol";
import {Test, console2} from "forge-std/Test.sol";
import {Deploy} from "script/Deploy.s.sol";
import {DeployInput} from "script/DeployInput.sol";

import {V3FactoryOwner} from "src/V3FactoryOwner.sol";
import {UniStaker} from "src/UniStaker.sol";
import {ProposalTest} from "test/helpers/ProposalTest.sol";
import {IUniswapV3FactoryOwnerActions} from "src/interfaces/IUniswapV3FactoryOwnerActions.sol";
import {IUniswapPool} from "test/helpers/interfaces/IUniswapPool.sol";

contract DeployScriptTest is Test, DeployInput {
function setUp() public {
vm.createSelectFork(vm.rpcUrl("mainnet"));
vm.createSelectFork(vm.rpcUrl("mainnet"), 19_114_228);
}

function testFork_DeployStakingContracts() public {
Expand All @@ -30,3 +33,39 @@ contract DeployScriptTest is Test, DeployInput {
assertTrue(uniStaker.isRewardNotifier(address(v3FactoryOwner)));
}
}

contract Propose is ProposalTest {
function testFork_CorrectlyPassAndExecuteProposal() public {
IUniswapV3FactoryOwnerActions factory =
IUniswapV3FactoryOwnerActions(UNISWAP_V3_FACTORY_ADDRESS);

IUniswapPool wbtcWethPool = IUniswapPool(WBTC_WETH_3000_POOL);
(,,,,, uint8 oldWbtcWethFeeProtocol,) = wbtcWethPool.slot0();

IUniswapPool daiWethPool = IUniswapPool(DAI_WETH_3000_POOL);
(,,,,, uint8 oldDaiWethFeeProtocol,) = daiWethPool.slot0();

IUniswapPool daiUsdcPool = IUniswapPool(DAI_USDC_100_POOL);
(,,,,, uint8 oldDaiUsdcFeeProtocol,) = daiUsdcPool.slot0();

_passQueueAndExecuteProposals();

(,,,,, uint8 newWbtcWethFeeProtocol,) = wbtcWethPool.slot0();
(,,,,, uint8 newDaiWethFeeProtocol,) = daiWethPool.slot0();
(,,,,, uint8 newDaiUsdcFeeProtocol,) = daiUsdcPool.slot0();

assertEq(factory.owner(), address(v3FactoryOwner));

assertEq(oldWbtcWethFeeProtocol, 0);
assertEq(oldDaiWethFeeProtocol, 0);
assertEq(oldDaiUsdcFeeProtocol, 0);

// The below assert is based off of the slot0.feeProtocol calculation which can be found at
// https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L843.
//
// The calculation is feeProtocol0 + (feeProtocol1 << 4)
assertEq(newWbtcWethFeeProtocol, 10 + (10 << 4));
assertEq(newDaiWethFeeProtocol, 10 + (10 << 4));
assertEq(newDaiUsdcFeeProtocol, 10 + (10 << 4));
}
}
9 changes: 9 additions & 0 deletions test/helpers/Constants.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.23;

contract Constants {
address constant UNISWAP_GOVERNOR_ADDRESS = 0x408ED6354d4973f66138C91495F2f2FCbd8724C3;
address constant WBTC_WETH_3000_POOL = 0xCBCdF9626bC03E24f779434178A73a0B4bad62eD;
address constant DAI_WETH_3000_POOL = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
address constant DAI_USDC_100_POOL = 0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168;
}
Loading

0 comments on commit 02d9ffa

Please sign in to comment.