I am testing the ERC20Votes.sol contract from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol" and it seems that if I sign a message with a wrong contract name, the delegator delegating its votes changes as well (it is not the one that originally signed the message).
This is my test code :
import { ethers } from 'hardhat'
import { Contract, ContractFactory } from 'ethers'
async function main() {
console.log(
'π Starting OpenZeppelin ERC20Votes deployment and interaction...'
)
// Get signers
const [deployer, user1, user2] = await ethers.getSigners()
console.log('π Deployer address:', deployer.address)
console.log('π€ User1 address:', user1.address)
console.log('π€ User2 address:', user2.address)
// Constants for contract deployment
const TOKEN_NAME = 'VotingToken'
const TOKEN_SYMBOL = 'VOTE'
const DELEGATE_DEADLINE = Math.floor(Date.now() / 1000) + 3600 // 1 hour from now
try {
console.log('\nπ¦ Deploying TestERC20Votes contract...')
const TestERC20VotesFactory: ContractFactory =
await ethers.getContractFactory('TestERC20Votes')
const testERC20Votes: Contract = await TestERC20VotesFactory.deploy(
TOKEN_NAME,
TOKEN_SYMBOL
)
await testERC20Votes.deployed()
console.log('β
TestERC20Votes deployed to:', testERC20Votes.address)
console.log('\nπ³οΈ Executing delegateBySig on TestERC20Votes...')
const domain = {
name: 'WrongName', // WRONG NAME PROVIDED!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//name: TOKEN_NAME,
version: '1',
chainId: await ethers.provider
.getNetwork()
.then((net) => net.chainId),
verifyingContract: testERC20Votes.address,
}
const types = {
Delegation: [
{ name: 'delegatee', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'expiry', type: 'uint256' },
],
}
const message = {
delegatee: user2.address,
nonce: await testERC20Votes.nonces(user1.address),
expiry: DELEGATE_DEADLINE,
}
const signature = await user1._signTypedData(domain, types, message)
const { v, r, s } = ethers.utils.splitSignature(signature)
const delegateBySigTx = await testERC20Votes.delegateBySig(
user2.address,
message.nonce,
DELEGATE_DEADLINE,
v,
r,
s
)
const response = await delegateBySigTx.wait()
const delegateChangedEvent = response.events?.find(
(e: any) => e.event === 'DelegateChanged'
)
console.log('β
delegateBySig executed successfully')
console.log(' Delegated from:', user1.address)
console.log(' Delegated to:', user2.address)
console.log()
const ErrorMessage =
user1.address == delegateChangedEvent.args.delegator
? 'β
All OK'
: 'β WRONG DELEGATOR!!!!!'
console.log('β
delegateBySig event reply')
console.log(
' Delegated from:',
delegateChangedEvent.args.delegator,
' ',
ErrorMessage
)
console.log(' Delegated to:', delegateChangedEvent.args.toDelegate)
} catch (error) {
console.error('β Error during deployment or execution:', error)
process.exit(1)
}
}
// Execute the script
main()
.then(() => process.exit(0))
.catch((error) => {
console.error('β Script failed:', error)
process.exit(1)
})
And this is its outcome
As you can see the βDelegated fromβ returned by the operation is not the expected one (user 1)
This is the smart contract I used for testing :
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import '@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol';
import '@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol';
contract TestERC20Votes is ERC20Votes {
constructor(
string memory name,
string memory symbol
) ERC20Permit(name) ERC20(name, symbol) {}
}
Could it be that "ECDSA.recover" used by "delegateBySign" is not working as expected (returns a random address instead of reverting when provided with a wrong signature)
function delegateBySig(
address delegatee,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) public virtual override {
require(block.timestamp <= expiry, "ERC20Votes: signature expired");
address signer = ECDSA.recover(
_hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))),
v,
r,
s
);
require(nonce == _useNonce(signer), "ERC20Votes: invalid nonce");
_delegate(signer, delegatee);
}
