diff --git a/packages/contracts/src/TokenVotingSetup.sol b/packages/contracts/src/TokenVotingSetup.sol index 5feb1c4a..3ce15b8c 100644 --- a/packages/contracts/src/TokenVotingSetup.sol +++ b/packages/contracts/src/TokenVotingSetup.sol @@ -131,18 +131,7 @@ contract TokenVotingSetup is PluginUpgradeableSetup { revert TokenNotERC20(token); } - // [0] = IERC20Upgradeable, [1] = IVotesUpgradeable, [2] = IGovernanceWrappedERC20 - bool[] memory supportedIds = _getTokenInterfaceIds(token); - - if ( - // If token supports none of them - // it's simply ERC20 which gets checked by _isERC20 - // Currently, not a satisfiable check. - (!supportedIds[0] && !supportedIds[1] && !supportedIds[2]) || - // If token supports IERC20, but neither - // IVotes nor IGovernanceWrappedERC20, it needs wrapping. - (supportedIds[0] && !supportedIds[1] && !supportedIds[2]) - ) { + if (!supportsIVotesInterface(token)) { token = governanceWrappedERC20Base.clone(); // User already has a token. We need to wrap it in // GovernanceWrappedERC20 in order to make the token @@ -164,6 +153,7 @@ contract TokenVotingSetup is PluginUpgradeableSetup { ); } + // Prepare and deploy plugin proxy. plugin = address(tokenVotingBase).deployUUPSProxy( abi.encodeWithSignature( @@ -340,15 +330,25 @@ contract TokenVotingSetup is PluginUpgradeableSetup { ); } - /// @notice Retrieves the interface identifiers supported by the token contract. - /// @dev It is crucial to verify if the provided token address represents a valid contract before using the below. - /// @param token The token address - function _getTokenInterfaceIds(address token) private view returns (bool[] memory) { - bytes4[] memory interfaceIds = new bytes4[](3); - interfaceIds[0] = type(IERC20Upgradeable).interfaceId; - interfaceIds[1] = type(IVotesUpgradeable).interfaceId; - interfaceIds[2] = type(IGovernanceWrappedERC20).interfaceId; - return token.getSupportedInterfaces(interfaceIds); + /// @notice Unsatisfiably determines if the token is an IVotes interface. + /// @dev Many tokens don't use ERC165 even though they still support IVotes. + function supportsIVotesInterface(address token) public view returns (bool) { + (bool success1, bytes memory data1) = token.staticcall( + abi.encodeWithSelector(IVotesUpgradeable.getPastTotalSupply.selector, 0) + ); + (bool success2, bytes memory data2) = token.staticcall( + abi.encodeWithSelector(IVotesUpgradeable.getVotes.selector, address(this)) + ); + (bool success3, bytes memory data3) = token.staticcall( + abi.encodeWithSelector(IVotesUpgradeable.getPastVotes.selector, address(this), 0) + ); + + return (success1 && + data1.length == 0x20 && + success2 && + data2.length == 0x20 && + success3 && + data3.length == 0x20); } /// @notice Unsatisfiably determines if the contract is an ERC20 token. diff --git a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts index 3b6effe8..0074cebf 100644 --- a/packages/contracts/test/10_unit-testing/12_plugin-setup.ts +++ b/packages/contracts/test/10_unit-testing/12_plugin-setup.ts @@ -7,6 +7,8 @@ import { GovernanceERC20__factory, GovernanceWrappedERC20, GovernanceWrappedERC20__factory, + IERC20Upgradeable__factory, + IVotesUpgradeable__factory, } from '../../typechain'; import {MajorityVotingBase} from '../../typechain/src/MajorityVotingBase'; import { @@ -27,6 +29,7 @@ import { import {VotingMode} from '../test-utils/voting-helpers'; import { DAO_PERMISSIONS, + getInterfaceId, Operation, PLUGIN_UUPS_UPGRADEABLE_PERMISSIONS, } from '@aragon/osx-commons-sdk'; @@ -39,6 +42,8 @@ import {loadFixture} from '@nomicfoundation/hardhat-network-helpers'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; import {ethers} from 'hardhat'; +import { plugins } from '../../typechain/@aragon/osx-v1.0.0'; +import { IGovernanceWrappedERC20__factory } from '../../typechain/factories/src/ERC20/governance'; const abiCoder = ethers.utils.defaultAbiCoder; const AddressZero = ethers.constants.AddressZero; @@ -371,6 +376,9 @@ describe('TokenVotingSetup', function () { preparedSetupData: {helpers, permissions}, } = await pluginSetup.callStatic.prepareInstallation(dao.address, data); + expect(await pluginSetup.supportsIVotesInterface(erc20.address)) + .to.be.false; + expect(plugin).to.be.equal(anticipatedPluginAddress); expect(helpers.length).to.be.equal(2); expect(helpers).to.be.deep.equal([anticipatedWrappedTokenAddress, anticipatedCondition]); @@ -455,6 +463,36 @@ describe('TokenVotingSetup', function () { expect(await governanceWrappedERC20Contract.underlying()).to.be.equal( erc20.address ); + + expect(await pluginSetup.supportsIVotesInterface(erc20.address)) + .to.be.false; + + // If a token address is not passed, it must have deployed GovernanceERC20. + const ivotesInterfaceId = getInterfaceId( + IVotesUpgradeable__factory.createInterface() + ); + const iERC20InterfaceId = getInterfaceId( + IERC20Upgradeable__factory.createInterface() + ); + const iGovernanceWrappedERC20 = getInterfaceId( + IGovernanceWrappedERC20__factory.createInterface() + ); + + expect( + await governanceWrappedERC20Contract.supportsInterface( + ivotesInterfaceId + ) + ).to.be.true; + expect( + await governanceWrappedERC20Contract.supportsInterface( + iERC20InterfaceId + ) + ).to.be.true; + expect( + await governanceWrappedERC20Contract.supportsInterface( + iGovernanceWrappedERC20 + ) + ).to.be.true; }); it('correctly returns plugin, helpers and permissions, when a governance token address is supplied', async () => { @@ -504,6 +542,10 @@ describe('TokenVotingSetup', function () { preparedSetupData: {helpers, permissions}, } = await pluginSetup.callStatic.prepareInstallation(dao.address, data); + expect( + await pluginSetup.supportsIVotesInterface(governanceERC20.address) + ).to.be.true; + expect(plugin).to.be.equal(anticipatedPluginAddress); expect(helpers.length).to.be.equal(2); expect(helpers).to.be.deep.equal([governanceERC20.address, anticipatedCondition]); @@ -541,7 +583,7 @@ describe('TokenVotingSetup', function () { }); it('correctly returns plugin, helpers and permissions, when a token address is not supplied', async () => { - const {pluginSetup, dao, prepareInstallationInputs} = await loadFixture( + const {pluginSetup, dao, defaultTokenSettings, prepareInstallationInputs} = await loadFixture( fixture ); @@ -571,6 +613,12 @@ describe('TokenVotingSetup', function () { prepareInstallationInputs ); + expect( + await pluginSetup.supportsIVotesInterface( + defaultTokenSettings.addr + ) + ).to.be.false; + expect(plugin).to.be.equal(anticipatedPluginAddress); expect(helpers.length).to.be.equal(2); expect(helpers).to.be.deep.equal([anticipatedTokenAddress, anticipatedCondition]); @@ -691,6 +739,19 @@ describe('TokenVotingSetup', function () { expect(await token.dao()).to.be.equal(daoAddress); expect(await token.name()).to.be.equal(defaultTokenSettings.name); expect(await token.symbol()).to.be.equal(defaultTokenSettings.symbol); + + // If a token address is not passed, it must have deployed GovernanceERC20. + const ivotesInterfaceId = getInterfaceId( + IVotesUpgradeable__factory.createInterface() + ); + const iERC20InterfaceId = getInterfaceId( + IERC20Upgradeable__factory.createInterface() + ); + + expect(await token.supportsInterface(ivotesInterfaceId)) + .to.be.true; + expect(await token.supportsInterface(iERC20InterfaceId)) + .to.be.true; }); });