Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions contracts/nomina/.gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
NominaMintLock_Test:test_acceptMintAuthority() (gas: 34792)
NominaMintLock_Test:test_acceptMintAuthority_reverts_notPending() (gas: 13525)
NominaMintLock_Test:test_mintLocked() (gas: 36214)
Nomina_Test:testAcceptMintAuthority() (gas: 32024)
Nomina_Test:testAcceptMintAuthorityReverts() (gas: 13134)
Nomina_Test:testBurn() (gas: 105554)
Expand Down
17 changes: 17 additions & 0 deletions contracts/nomina/script/DeployNominaMintLock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.24;

import { Script } from "forge-std/Script.sol";
import { Nomina } from "src/token/Nomina.sol";
import { NominaMintLock } from "src/token/NominaMintLock.sol";

contract DeployNominaMintLock is Script {
function run() public returns (NominaMintLock) {
Nomina nomina = Nomina(0x6e6F6d696e61decd6605bD4a57836c5DB6923340);

vm.broadcast();
NominaMintLock lock = new NominaMintLock(nomina);

return lock;
}
}
31 changes: 31 additions & 0 deletions contracts/nomina/src/token/NominaMintLock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.24;

import { Nomina } from "./Nomina.sol";

/**
* @title NominaMintLock
* @notice Permanently locks the ability to mint NOM tokens.
* @dev Once this contract accepts mint authority from the Nomina token, the mint authority is
* irrevocably held by this contract. Since this contract has no ability to set a minter or
* transfer the mint authority, no new NOM tokens can ever be minted.
*/
contract NominaMintLock {
/**
* @notice The Nomina token contract.
*/
Nomina public immutable NOMINA;

constructor(Nomina _nomina) {
NOMINA = _nomina;
}

/**
* @notice Accepts the mint authority from the Nomina token, permanently locking minting.
* @dev After this call, no new NOM tokens can ever be minted, as this contract has no
* mechanism to set a minter or transfer the mint authority.
*/
function acceptMintAuthority() external {
NOMINA.acceptMintAuthority();
}
}
66 changes: 66 additions & 0 deletions contracts/nomina/test/token/NominaMintLock.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.24;

import { Test } from "forge-std/Test.sol";
import { Nomina } from "src/token/Nomina.sol";
import { NominaMintLock } from "src/token/NominaMintLock.sol";
import { MockOmni } from "test/utils/MockOmni.sol";

contract NominaMintLock_Test is Test {
Nomina public nomina;
NominaMintLock public lock;
MockOmni public omni;

address public mintAuthority = makeAddr("mintAuthority");
address public minter = makeAddr("minter");
address public user = makeAddr("user");

function setUp() public {
omni = new MockOmni(1_000_000 ether, user);
nomina = new Nomina(address(omni), mintAuthority);
lock = new NominaMintLock(nomina);

vm.prank(mintAuthority);
nomina.setMinter(minter);
}

function test_acceptMintAuthority() public {
// Queue the lock contract as pending mint authority
vm.prank(mintAuthority);
nomina.setMintAuthority(address(lock));

// Accept mint authority via the lock contract
lock.acceptMintAuthority();

// Verify the lock contract is now the mint authority
assertEq(nomina.mintAuthority(), address(lock), "mint authority mismatch");
assertEq(nomina.pendingMintAuthority(), address(0), "pending mint authority not cleared");
}

function test_acceptMintAuthority_reverts_notPending() public {
// Revert when lock is not the pending mint authority
vm.expectRevert(Nomina.Unauthorized.selector);
lock.acceptMintAuthority();
}

function test_mintLocked() public {
// Transfer mint authority to the lock contract
vm.prank(mintAuthority);
nomina.setMintAuthority(address(lock));
lock.acceptMintAuthority();

// The lock contract cannot set a minter, so minting is permanently locked.
// Existing minter still works until mint authority sets a new one,
// but the lock contract has no way to call setMinter or setMintAuthority.

// Verify the lock contract has no setMinter function by checking
// that the mint authority (lock) cannot set a new minter.
// Since NominaMintLock has no setMinter or setMintAuthority functions,
// the mint authority is permanently locked.
(bool success,) = address(lock).call(abi.encodeWithSignature("setMinter(address)", user));
assertFalse(success, "lock should not have setMinter");

(success,) = address(lock).call(abi.encodeWithSignature("setMintAuthority(address)", user));
assertFalse(success, "lock should not have setMintAuthority");
}
}
44 changes: 43 additions & 1 deletion e2e/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
libcmd "github.com/omni-network/omni/lib/cmd"
"github.com/omni-network/omni/lib/contracts/solvernet"
"github.com/omni-network/omni/lib/errors"
"github.com/omni-network/omni/lib/ethclient"
"github.com/omni-network/omni/lib/ethclient/ethbackend"
"github.com/omni-network/omni/lib/log"
"github.com/omni-network/omni/lib/netconf"
"github.com/omni-network/omni/lib/tokens"
Expand Down Expand Up @@ -52,7 +54,7 @@ func New() *cobra.Command {
}

// Some commands do not require a full definition.
if matchAny(cmd.Use, "hyperliquid-use-big-blocks", "drain-relayer-monitor") {
if matchAny(cmd.Use, "hyperliquid-use-big-blocks", "drain-relayer-monitor", "lock-nom-mint") {
return nil
}

Expand Down Expand Up @@ -106,6 +108,7 @@ func New() *cobra.Command {
fundAccounts(&def),
newFundOpsFromSolverCmd(&def),
newConvertOmniCmd(&def),
newLockNomMintCmd(&defCfg),
newDrainRelayerMonitorCmd(&defCfg),
)

Expand Down Expand Up @@ -526,6 +529,45 @@ func newConvertOmniCmd(def *app.Definition) *cobra.Command {
return cmd
}

func newLockNomMintCmd(defCfg *app.DefinitionConfig) *cobra.Command {
cmd := &cobra.Command{
Use: "lock-nom-mint",
Short: "Sets the NOM mint authority to the NominaMintLock contract",
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()

chain, err := types.PublicChainByName("ethereum")
if err != nil {
return errors.Wrap(err, "public chain")
}

rpc := types.PublicRPCByName(chain.Name)
if override, ok := defCfg.RPCOverrides[chain.Name]; ok {
rpc = override
}

ethCl, err := ethclient.DialContext(ctx, chain.Name, rpc)
if err != nil {
return errors.Wrap(err, "dial ethereum")
}

fireCl, err := app.NewFireblocksClient(*defCfg, netconf.Mainnet, cmd.Name())
if err != nil {
return errors.Wrap(err, "new fireblocks client")
}

backend, err := ethbackend.NewFireBackend(ctx, chain.Name, chain.ChainID, chain.BlockPeriod, ethCl, fireCl)
if err != nil {
return errors.Wrap(err, "new fire backend")
}

return nomina.LockNomMint(ctx, backend)
},
}

return cmd
}

func newDrainRelayerMonitorCmd(defCfg *app.DefinitionConfig) *cobra.Command {
var dryRun bool

Expand Down
50 changes: 50 additions & 0 deletions e2e/nomina/mintlock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package nomina

import (
"context"

"github.com/omni-network/omni/contracts/bindings"
"github.com/omni-network/omni/e2e/app/eoa"
"github.com/omni-network/omni/lib/contracts"
"github.com/omni-network/omni/lib/errors"
"github.com/omni-network/omni/lib/ethclient/ethbackend"
"github.com/omni-network/omni/lib/log"
"github.com/omni-network/omni/lib/netconf"

"github.com/ethereum/go-ethereum/common"
)

// MintLock is the deployed address of the NominaMintLock contract.
var MintLock = common.HexToAddress("0xF9046e60f10000c97316D76Ba0DbAB399C3D8752")

// LockNomMint calls Nomina.setMintAuthority to queue the NominaMintLock contract as the
// pending mint authority, permanently locking the ability to mint NOM once accepted.
func LockNomMint(ctx context.Context, backend *ethbackend.Backend) error {
nomAddr := contracts.NomAddr(netconf.Mainnet)

nomina, err := bindings.NewNomina(nomAddr, backend)
if err != nil {
return errors.Wrap(err, "new nomina")
}

mintAuthority := eoa.MustAddress(netconf.Mainnet, eoa.RoleNomAuthority)

txOpts, err := backend.BindOpts(ctx, mintAuthority)
if err != nil {
return errors.Wrap(err, "bind opts")
}

tx, err := nomina.SetMintAuthority(txOpts, MintLock)
if err != nil {
return errors.Wrap(err, "set mint authority")
}

_, err = backend.WaitMined(ctx, tx)
if err != nil {
return errors.Wrap(err, "wait mined")
}

log.Info(ctx, "NOM mint lock queued", "nom", nomAddr, "mint_lock", MintLock)

return nil
}
Loading