diff --git a/contracts/protocol/integration/governance/SnapshotGovernanceAdapter.sol b/contracts/protocol/integration/governance/SnapshotGovernanceAdapter.sol new file mode 100644 index 0000000..a60ca85 --- /dev/null +++ b/contracts/protocol/integration/governance/SnapshotGovernanceAdapter.sol @@ -0,0 +1,69 @@ +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +/** + * @title SnapshotGovernanceAdapter + * @author Mridul Ahuja + * + * Governance adapter that allows a Set to delegate all of its voting power on Snapshot.org + */ + +contract SnapshotGovernanceAdapter { + /* ============ Constants ============ */ + + // Signature of the delegate function in Delegate Registry + string public constant DELEGATE_SIGNATURE = "setDelegate(bytes32,address)"; + + string public constant CLEAR_DELEGATE_SIGNATURE = "clearDelegate(bytes32)"; + + /* ============ State Variables ============ */ + + // Address of the Delegate Registry + address public immutable delegateRegistryAddress; + + /* ============ Constructor ============ */ + + /** + * Set state variables + * + * @param _delegateRegistryAddress Address of Delegate Registry Address + */ + + constructor(address _delegateRegistryAddress) public { + delegateRegistryAddress = _delegateRegistryAddress; + } + + /* ============ External Getter Functions ============ */ + + /** + * Delegates all votes of msg.sender to delegate. If id is left as ZERO_BYTES, then delegates all votes + * + * @param _delegatee Address to which votes will be delegated + * + * @return address Target contract address + * @return uint256 Total quantity of ETH (Set to 0) + * @return bytes Propose calldata + */ + + function getDelegateCalldata(address _delegatee) external view returns (address, uint256, bytes memory){ + + bytes memory callData = abi.encodeWithSignature(DELEGATE_SIGNATURE, bytes32(0), _delegatee); + + return (delegateRegistryAddress, 0, callData); + } + + /** + * Revokes previous delegation from msg.sender. If id is left as ZERO_BYTES, then undelegates all votes + * + * @return address Target contract address + * @return uint256 Total quantity of ETH (Set to 0) + * @return bytes Propose calldata + */ + + function getRevokeCalldata() external view returns (address, uint256, bytes memory){ + + bytes memory callData = abi.encodeWithSignature(CLEAR_DELEGATE_SIGNATURE, bytes32(0)); + + return (delegateRegistryAddress, 0, callData); + } +} \ No newline at end of file diff --git a/external/abi/snapshot/DelegateRegistry.json b/external/abi/snapshot/DelegateRegistry.json new file mode 100644 index 0000000..b32875b --- /dev/null +++ b/external/abi/snapshot/DelegateRegistry.json @@ -0,0 +1,8 @@ +{ + "contractName": "DelegateRegistry", + "abi": [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":true,"internalType":"address","name":"delegate","type":"address"}],"name":"ClearDelegate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":true,"internalType":"address","name":"delegate","type":"address"}],"name":"SetDelegate","type":"event"},{"inputs":[{"internalType":"bytes32","name":"id","type":"bytes32"}],"name":"clearDelegate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"delegation","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"id","type":"bytes32"},{"internalType":"address","name":"delegate","type":"address"}],"name":"setDelegate","outputs":[],"stateMutability":"nonpayable","type":"function"}], + "bytecode": "608060405234801561001057600080fd5b50610794806100206000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c806374c6c45414610046578063bd86e508146100be578063f0bedbe21461010c575b600080fd5b6100926004803603604081101561005c57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061013a565b604051808273ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b61010a600480360360408110156100d457600080fd5b8101908080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919050505061017c565b005b6101386004803603602081101561012257600080fd5b8101908080359060200190929190505050610538565b005b60006020528160005260406000206020528060005260406000206000915091509054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b3373ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16141561021e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260168152602001807f43616e27742064656c656761746520746f2073656c660000000000000000000081525060200191505060405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156102c1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260158152602001807f43616e27742064656c656761746520746f20307830000000000000000000000081525060200191505060405180910390fd5b60008060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600084815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690508073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614156103ba576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602181526020018061073e6021913960400191505060405180910390fd5b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600085815260200190815260200160002060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16146104d8578073ffffffffffffffffffffffffffffffffffffffff16833373ffffffffffffffffffffffffffffffffffffffff167f9c4f00c4291262731946e308dc2979a56bd22cce8f95906b975065e96cd5a06460405160405180910390a45b8173ffffffffffffffffffffffffffffffffffffffff16833373ffffffffffffffffffffffffffffffffffffffff167fa9a7fd460f56bddb880a465a9c3e9730389c70bc53108148f16d55a87a6c468e60405160405180910390a4505050565b60008060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600083815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16141561064f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252600f8152602001807f4e6f2064656c656761746520736574000000000000000000000000000000000081525060200191505060405180910390fd5b60008060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600084815260200190815260200160002060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508073ffffffffffffffffffffffffffffffffffffffff16823373ffffffffffffffffffffffffffffffffffffffff167f9c4f00c4291262731946e308dc2979a56bd22cce8f95906b975065e96cd5a06460405160405180910390a4505056fe416c72656164792064656c65676174656420746f20746869732061646472657373a2646970667358221220b6cd5a8d04426e1189563fbec7dfec4ba70090dc70fe05097a137991fe1b396964736f6c63430007020033", + "deployedBytecode": "", + "linkReferences": {}, + "deployedLinkReferences": {} + } \ No newline at end of file diff --git a/test/protocol/integration/governance/snapshotGovernanceAdapter.spec.ts b/test/protocol/integration/governance/snapshotGovernanceAdapter.spec.ts new file mode 100644 index 0000000..9ac7b46 --- /dev/null +++ b/test/protocol/integration/governance/snapshotGovernanceAdapter.spec.ts @@ -0,0 +1,89 @@ +import "module-alias/register"; +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ZERO, ZERO_BYTES } from "@utils/constants"; +import { + DelegateRegistry, + SnapshotGovernanceAdapter +} from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getWaffleExpect, + getRandomAddress +} from "@utils/test/index"; + +const expect = getWaffleExpect(); + +describe("SnapshotGovernanceAdapter", () => { + let owner: Account; + let deployer: DeployHelper; + let snapshotGovernanceAdapter: SnapshotGovernanceAdapter; + let delegateRegistry: DelegateRegistry; + + before(async () => { + [owner] = await getAccounts(); + deployer = new DeployHelper(owner.wallet); + + delegateRegistry = await deployer.external.deployDelegateRegistry(); + snapshotGovernanceAdapter = await deployer.adapters.deploySnapshotGovernanceAdapter(delegateRegistry.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectDelegateRegistry: Address; + + before(async() => { + subjectDelegateRegistry = delegateRegistry.address; + }); + + async function subject(): Promise { + return await deployer.adapters.deploySnapshotGovernanceAdapter(subjectDelegateRegistry); + } + + it("should have the correct delegateRegistry address", async () => { + const deployedSnapshotGovernanceAdapter = await subject(); + const actualDelegateRegistry = await deployedSnapshotGovernanceAdapter.delegateRegistryAddress(); + + expect(actualDelegateRegistry).to.eq(subjectDelegateRegistry); + }); + }); + + describe("#getDelegateCallData", async () => { + let subjectDelegatee: Address; + + before(async() => { + subjectDelegatee = await getRandomAddress(); + }); + + async function subject(): Promise { + return snapshotGovernanceAdapter.getDelegateCalldata(subjectDelegatee); + } + + it("should return correct data for delegating", async () => { + const [targetAddress, ethValue, callData] = await subject(); + const expectedCallData = delegateRegistry.interface.encodeFunctionData("setDelegate", [ZERO_BYTES, subjectDelegatee]); + + expect(targetAddress).to.eq(delegateRegistry.address); + expect(ethValue).to.eq(ZERO); + expect(callData).to.eq(expectedCallData); + }); + }); + + describe("#getRevokeCallData", async () => { + async function subject(): Promise { + return snapshotGovernanceAdapter.getRevokeCalldata(); + } + + it("shoud return correct data for revoking", async () => { + const [targetAddress, ethValue, callData] = await subject(); + const expectedCallData = delegateRegistry.interface.encodeFunctionData("clearDelegate", [ZERO_BYTES]); + + expect(targetAddress).to.eq(delegateRegistry.address); + expect(ethValue).to.eq(ZERO); + expect(callData).to.eq(expectedCallData); + }); + }); +}); \ No newline at end of file diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 578c03a..3b90419 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -30,6 +30,7 @@ export { CustomSetValuerMock } from "../../typechain/CustomSetValuerMock"; export { DebtIssuanceMock } from "../../typechain/DebtIssuanceMock"; export { DebtIssuanceModule } from "../../typechain/DebtIssuanceModule"; export { DebtModuleMock } from "../../typechain/DebtModuleMock"; +export { DelegateRegistry } from "../../typechain/DelegateRegistry"; export { ExplicitERC20Mock } from "../../typechain/ExplicitERC20Mock"; export { ForceFunderMock } from "../../typechain/ForceFunderMock"; export { GaugeControllerMock } from "../../typechain/GaugeControllerMock"; @@ -67,6 +68,7 @@ export { SetToken } from "../../typechain/SetToken"; export { SetTokenCreator } from "../../typechain/SetTokenCreator"; export { SetValuer } from "../../typechain/SetValuer"; export { SingleIndexModule } from "../../typechain/SingleIndexModule"; +export { SnapshotGovernanceAdapter } from "../../typechain/SnapshotGovernanceAdapter"; export { StakingAdapterMock } from "../../typechain/StakingAdapterMock"; export { StakingModule } from "../../typechain/StakingModule"; export { StakingRewards } from "../../typechain/StakingRewards"; diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index ef18a15..8436a10 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -18,6 +18,7 @@ import { UniswapV2IndexExchangeAdapter, UniswapV2TransferFeeExchangeAdapter, ZeroExApiAdapter, + SnapshotGovernanceAdapter, SynthetixExchangeAdapter, CompoundBravoGovernanceAdapter, CompClaimAdapter, @@ -35,6 +36,7 @@ import { KyberExchangeAdapter__factory } from "../../typechain/factories/KyberEx import { KyberMigrationWrapAdapter__factory } from "../../typechain/factories/KyberMigrationWrapAdapter__factory"; import { OneInchExchangeAdapter__factory } from "../../typechain/factories/OneInchExchangeAdapter__factory"; import { ZeroExApiAdapter__factory } from "../../typechain/factories/ZeroExApiAdapter__factory"; +import { SnapshotGovernanceAdapter__factory } from "../../typechain/factories/SnapshotGovernanceAdapter__factory"; import { AaveMigrationWrapAdapter__factory } from "../../typechain/factories/AaveMigrationWrapAdapter__factory"; import { AaveWrapAdapter__factory } from "../../typechain/factories/AaveWrapAdapter__factory"; import { CompoundWrapAdapter__factory } from "../../typechain/factories/CompoundWrapAdapter__factory"; @@ -143,6 +145,10 @@ export default class DeployAdapters { return await new CompoundBravoGovernanceAdapter__factory(this._deployerSigner).deploy(governorBravo, governanceToken); } + public async deploySnapshotGovernanceAdapter(delegateRegistryAddress: Address): Promise { + return await new SnapshotGovernanceAdapter__factory(this._deployerSigner).deploy(delegateRegistryAddress); + } + public async deployCurveStakingAdapter(gaugeController: Address): Promise { return await new CurveStakingAdapter__factory(this._deployerSigner).deploy(gaugeController); } diff --git a/utils/deploys/deployExternal.ts b/utils/deploys/deployExternal.ts index 58076ef..8241785 100644 --- a/utils/deploys/deployExternal.ts +++ b/utils/deploys/deployExternal.ts @@ -18,6 +18,7 @@ import { } from "./../contracts/compound"; import { WETH9, + DelegateRegistry } from "./../contracts"; import { Address } from "./../types"; @@ -31,6 +32,7 @@ import { CompoundGovernorBravoDelegator__factory } from "../../typechain/factori import { CompoundGovernorBravoDelegate__factory } from "../../typechain/factories/CompoundGovernorBravoDelegate__factory"; import { CompoundTimelock__factory } from "../../typechain/factories/CompoundTimelock__factory"; import { Comptroller__factory } from "../../typechain/factories/Comptroller__factory"; +import { DelegateRegistry__factory } from "../../typechain/factories/DelegateRegistry__factory"; import { PriceOracleProxy__factory } from "../../typechain/factories/PriceOracleProxy__factory"; import { Unitroller__factory } from "../../typechain/factories/Unitroller__factory"; import { WETH9__factory } from "../../typechain/factories/WETH9__factory"; @@ -160,6 +162,11 @@ export default class DeployExternalContracts { this._deployerSigner = deployerSigner; } + // SNAPSHOT + public async deployDelegateRegistry(): Promise { + return await new DelegateRegistry__factory(this._deployerSigner).deploy(); + } + // COMPOUND public async deployComp(_account: Address): Promise { return await new Comp__factory(this._deployerSigner).deploy(_account);