Skip to content

Commit

Permalink
contracts-bedrock: add GovernanceToken updated version
Browse files Browse the repository at this point in the history
Merging to parent PR so that it's easier to test.
  • Loading branch information
cairoeth authored Jul 22, 2024
2 parents 8c740df + c756097 commit a0afc38
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 4 deletions.
131 changes: 127 additions & 4 deletions packages/contracts-bedrock/src/governance/GovernanceToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,153 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "src/libraries/Predeploys.sol";
import "src/governance/Alligator.sol";

/// @custom:predeploy 0x4200000000000000000000000000000000000042
/// @title GovernanceToken
/// @notice The Optimism token used in governance and supporting voting and delegation. Implements
/// EIP 2612 allowing signed approvals. Contract is "owned" by a `MintManager` instance with
/// permission to the `mint` function only, for the purposes of enforcing the token
/// inflation schedule.
/// inflation schedule. If an account has already been migrated to the Alligator contract,
/// the GovernanceToken contract uses the state from the Alligator by calling the corresponding
/// functions in the Alligator contract. If the account has not been migrated, the
/// GovernanceToken contract uses its own state.
contract GovernanceToken is ERC20Burnable, ERC20Votes, Ownable {
/// @notice Thrown when a function undefined post migration is called.
error UndefinedPostMigration();

/// @notice The typehash for the delegation struct used by the contract.
bytes32 private constant _DELEGATION_TYPEHASH =
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

/// @notice Constructs the GovernanceToken contract.
constructor() ERC20("Optimism", "OP") ERC20Permit("Optimism") { }

/// @notice Allows the owner to mint tokens.
/// @notice Allows owner to mint tokens.
/// @param _account The account receiving minted tokens.
/// @param _amount The amount of tokens to mint.
function mint(address _account, uint256 _amount) public onlyOwner {
_mint(_account, _amount);
}

/// @notice Callback called after a token transfer.
/// @notice Returns the checkpoint for a given account at a given position.
/// @param _account The account to get the checkpoints for.
/// @param _pos The psition to get the checkpoints at.
/// @return The checkpoint at the given position.
function checkpoints(address _account, uint32 _pos) public view override(ERC20Votes) returns (Checkpoint memory) {
if (_migrated(_account)) {
return Alligator(Predeploys.ALLIGATOR).checkpoints(address(this), _account)[_pos];
} else {
return super.checkpoints(_account, _pos);
}
}

/// @notice Returns the number of checkpoints for a given account.
/// @param _account The account to get the number of checkpoints for.
/// @return The number of checkpoints for the given account.
function numCheckpoints(address _account) public view override(ERC20Votes) returns (uint32) {
if (_migrated(_account)) {
return Alligator(Predeploys.ALLIGATOR).numCheckpoints(msg.sender, _account);
} else {
return super.numCheckpoints(_account);
}
}

/// @notice Returns the delegatee of an account. This function is unavailable post migration,
/// because the Alligator may hold more than one delegatee for an account, conflicting
/// the return type of this function.
/// @param _account The account to get the delegatee of.
/// @return The delegatee of the given account.
function delegates(address _account) public view override(ERC20Votes) returns (address) {
if (_migrated(_account)) {
revert UndefinedPostMigration();
} else {
return super.delegates(_account);
}
}

/// @notice Delegates votes from the sender to `delegatee`.
/// @param _delegatee The account to delegate votes to.
function delegate(address _delegatee) public override {
if (!_migrated(msg.sender)) _migrate(msg.sender);

Alligator(Predeploys.ALLIGATOR).subdelegate(
msg.sender,
_delegatee,
// Create rule equivalent to basic delegation.
SubdelegationRules({
maxRedelegations: 0,
blocksBeforeVoteCloses: 0,
notValidBefore: 0,
notValidAfter: 0,
allowanceType: AllowanceType.Relative,
allowance: 10e4 // 100%
})
);
}

/// @notice Delegates votes from the sender to `delegatee`.
/// @param _delegatee The account to delegate votes to.
/// @param _nonce The nonce of the transaction.
/// @param _expiry The expiry of the signature.
/// @param _v v of the signature.
/// @param _r r of the signature.
/// @param _s s of the signature.
function delegateBySig(
address _delegatee,
uint256 _nonce,
uint256 _expiry,
uint8 _v,
bytes32 _r,
bytes32 _s
)
public
override
{
if (!_migrated(msg.sender)) _migrate(msg.sender);

// TODO: custom errors. use revert instead of require
require(block.timestamp <= _expiry, "GovernanceToken: signature expired");
address signer = ECDSA.recover(
_hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, _delegatee, _nonce, _expiry))), _v, _r, _s
);
require(_nonce == _useNonce(signer), "GovernanceToken: invalid nonce");
Alligator(Predeploys.ALLIGATOR).subdelegate(
msg.sender,
_delegatee,
// Create rule equivalent to basic delegation.
SubdelegationRules({
maxRedelegations: 0,
blocksBeforeVoteCloses: 0,
notValidBefore: 0,
notValidAfter: 0,
allowanceType: AllowanceType.Relative,
allowance: 10e4 // 100%
})
);
}

/// @notice Callback called after a token transfer. Forwards to the Alligator contract,
/// independently of whether the account has been migrated.
/// @param from The account sending tokens.
/// @param to The account receiving tokens.
/// @param amount The amount of tokens being transfered.
function _afterTokenTransfer(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) {
super._afterTokenTransfer(from, to, amount);
Alligator(Predeploys.ALLIGATOR).afterTokenTransfer(from, to, amount);
}

/// @notice Determines whether an account has been migrated.
/// @param _account The account to check if it has been migrated.
/// @return True if the given account has been migrated, and false otherwise.
function _migrated(address _account) internal view returns (bool) {
return Alligator(Predeploys.ALLIGATOR).migrated(address(this), _account);
}

/// @notice Internal migrate function.
/// @param _account The account to migrate.
function _migrate(address _account) public onlyOwner {
Alligator(Predeploys.ALLIGATOR).migrate(_account);
}

/// @notice Internal mint function.
Expand Down
81 changes: 81 additions & 0 deletions packages/contracts-bedrock/test/governance/GovernanceToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ pragma solidity 0.8.15;

// Testing utilities
import { CommonTest } from "test/setup/CommonTest.sol";
import "src/libraries/Predeploys.sol";
import "src/governance/Alligator.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

contract GovernanceToken_Test is CommonTest {
address owner;
Expand Down Expand Up @@ -166,4 +169,82 @@ contract GovernanceToken_Test is CommonTest {
// Allowances have updated.
assertEq(governanceToken.allowance(rando, owner), 50);
}

/// @dev Test that `checkpoints` returns the correct value when the account is migrated.
function testFuzz_checkpoints_migrated_succeeds(
address _account,
uint32 _pos,
ERC20Votes.Checkpoint calldata _checkpoint
)
public
{
vm.mockCall(Predeploys.ALLIGATOR, abi.encodeWithSignature("migrated(address)", _account), abi.encode(true));
vm.mockCall(
Predeploys.ALLIGATOR,
abi.encodeWithSelector(Alligator.checkpoints.selector, _account, _pos),
abi.encode(_checkpoint)
);

ERC20Votes.Checkpoint memory actualCheckpoint = governanceToken.checkpoints(_account, _pos);
assertEq(actualCheckpoint.fromBlock, _checkpoint.fromBlock);
assertEq(actualCheckpoint.votes, _checkpoint.votes);
}

/// @dev Test that `checkpoints` returns the correct value when the account is not migrated.
function testFuzz_checkpoints_notMigrated_succeeds(
address _account,
uint32 _pos,
ERC20Votes.Checkpoint memory _checkpoint
)
public
{
vm.mockCall(Predeploys.ALLIGATOR, abi.encodeWithSignature("migrated(address)", _account), abi.encode(false));

// Store _pos + 1 (because _pos starts as zero) as length for _checkpoints in slot 8, which stores _checkpoints
vm.store(Predeploys.GOVERNANCE_TOKEN, keccak256(abi.encode(_account, uint256(8))), bytes32(uint256(_pos) + 1));
vm.store(
Predeploys.GOVERNANCE_TOKEN,
bytes32(uint256(keccak256(abi.encode(keccak256(abi.encode(_account, uint256(8)))))) + _pos),
bytes32(abi.encodePacked(_checkpoint.votes, _checkpoint.fromBlock))
);

ERC20Votes.Checkpoint memory actualCheckpoint = governanceToken.checkpoints(_account, _pos);
assertEq(actualCheckpoint.fromBlock, _checkpoint.fromBlock);
assertEq(actualCheckpoint.votes, _checkpoint.votes);
}

function testFuzz_numCheckpoints_migrated_succeeds(address _account, uint32 _numCheckpoints) public {
vm.mockCall(Predeploys.ALLIGATOR, abi.encodeWithSignature("migrated(address)", _account), abi.encode(true));
vm.mockCall(
Predeploys.ALLIGATOR,
abi.encodeWithSelector(Alligator.numCheckpoints.selector, _account),
abi.encode(_numCheckpoints)
);

uint32 actualNumCheckpoints = governanceToken.numCheckpoints(_account);
assertEq(actualNumCheckpoints, _numCheckpoints);
}

function testFuzz_numCheckpoints_notMigrated_succeeds(address _account, uint32 _numCheckpoints) public {
vm.mockCall(Predeploys.ALLIGATOR, abi.encodeWithSignature("migrated(address)", _account), abi.encode(false));

// Store _numCheckpoints as length for _checkpoints in slot 8, which stores _checkpoints
vm.store(
Predeploys.GOVERNANCE_TOKEN, keccak256(abi.encode(_account, uint256(8))), bytes32(uint256(_numCheckpoints))
);

uint32 actualNumCheckpoints = governanceToken.numCheckpoints(_account);
assertEq(actualNumCheckpoints, _numCheckpoints);
}

function testFuzz_delegates_migrated_succeeds(address _account, address _delegatee) public {
vm.mockCall(Predeploys.ALLIGATOR, abi.encodeWithSignature("migrated(address)", _account), abi.encode(true));
vm.mockCall(
Predeploys.ALLIGATOR,
abi.encodeWithSelector(Alligator.numCheckpoints.selector, _account),
abi.encode(_delegatee)
);

assertEq(_delegatee, governanceToken.delegates(_account));
}
}

0 comments on commit a0afc38

Please sign in to comment.