diff --git a/src/chainparams.cpp b/src/chainparams.cpp
--- a/src/chainparams.cpp
+++ b/src/chainparams.cpp
@@ -142,7 +142,7 @@
         consensus.phononHeight = 635258;
 
         // Nov 15, 2020 12:00:00 UTC protocol upgrade
-        consensus.axionActivationTime = 1605441600;
+        consensus.axionHeight = 661647;
 
         // May 15, 2022 12:00:00 UTC protocol upgrade
         consensus.gluonActivationTime = 1652616000;
@@ -304,7 +304,7 @@
         consensus.phononHeight = 1378460;
 
         // Nov 15, 2020 12:00:00 UTC protocol upgrade
-        consensus.axionActivationTime = 1605441600;
+        consensus.axionHeight = 1421481;
 
         // May 15, 2022 12:00:00 UTC protocol upgrade
         consensus.gluonActivationTime = 1652616000;
@@ -440,7 +440,7 @@
         consensus.phononHeight = 0;
 
         // Nov 15, 2020 12:00:00 UTC protocol upgrade
-        consensus.axionActivationTime = 1605441600;
+        consensus.axionHeight = 0;
 
         // May 15, 2022 12:00:00 UTC protocol upgrade
         consensus.gluonActivationTime = 1652616000;
diff --git a/src/consensus/activation.cpp b/src/consensus/activation.cpp
--- a/src/consensus/activation.cpp
+++ b/src/consensus/activation.cpp
@@ -75,14 +75,17 @@
     return IsPhononEnabled(params, pindexPrev->nHeight);
 }
 
+static bool IsAxionEnabled(const Consensus::Params &params, int32_t nHeight) {
+    return nHeight >= params.axionHeight;
+}
+
 bool IsAxionEnabled(const Consensus::Params &params,
                     const CBlockIndex *pindexPrev) {
     if (pindexPrev == nullptr) {
         return false;
     }
 
-    return pindexPrev->GetMedianTimePast() >=
-           gArgs.GetArg("-axionactivationtime", params.axionActivationTime);
+    return IsAxionEnabled(params, pindexPrev->nHeight);
 }
 
 bool IsGluonEnabled(const Consensus::Params &params,
diff --git a/src/consensus/params.h b/src/consensus/params.h
--- a/src/consensus/params.h
+++ b/src/consensus/params.h
@@ -80,8 +80,8 @@
     int gravitonHeight;
     /** Block height at which the phonon activation becomes active */
     int phononHeight;
-    /** Unix time used for MTP activation of 15 Nov 2020 12:00:00 UTC upgrade */
-    int axionActivationTime;
+    /** Block height at which the axion activation becomes active */
+    int axionHeight;
     /** Unix time used for MTP activation of 15 May 2022 12:00:00 UTC upgrade */
     int gluonActivationTime;
     /** Unix time used for MTP activation of 15 Nov 2022 12:00:00 UTC upgrade */
diff --git a/src/init.cpp b/src/init.cpp
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -425,8 +425,6 @@
         "-rootcertificates=<file>",
         "-splash",
         "-uiplatform",
-        // TODO remove after the November 2020 upgrade
-        "-axionactivationtime",
         // TODO remove after the May 2022 upgrade
         "-gluonactivationtime",
     };
diff --git a/src/pow/test/aserti32d_tests.cpp b/src/pow/test/aserti32d_tests.cpp
--- a/src/pow/test/aserti32d_tests.cpp
+++ b/src/pow/test/aserti32d_tests.cpp
@@ -532,11 +532,14 @@
     }
 }
 
-class ChainParamsWithDAAActivation : public CChainParams {
+class ChainParamsWithCustomActivation : public CChainParams {
 public:
-    ChainParamsWithDAAActivation(const CChainParams &chainParams, int daaHeight)
+    ChainParamsWithCustomActivation(const CChainParams &chainParams,
+                                    int daaHeight, int axionHeight)
         : CChainParams(chainParams) {
+        BOOST_REQUIRE_GT(axionHeight, daaHeight);
         consensus.daaHeight = daaHeight;
+        consensus.axionHeight = axionHeight;
     }
 };
 
@@ -549,11 +552,11 @@
     // at a lower height than usual, so we don't need to waste time making a
     // 504000-long chain.
     const auto mainChainParams = CreateChainParams(CBaseChainParams::MAIN);
-    const ChainParamsWithDAAActivation chainParams(*mainChainParams, 2016);
+    const int asertActivationHeight = 4000;
+    const ChainParamsWithCustomActivation chainParams(*mainChainParams, 2016,
+                                                      asertActivationHeight);
     const Consensus::Params &params = chainParams.GetConsensus();
 
-    const int64_t activationTime =
-        gArgs.GetArg("-axionactivationtime", params.axionActivationTime);
     CBlockHeader blkHeaderDummy;
 
     // an arbitrary compact target for our chain (based on BCH chain ~ Aug 10
@@ -573,32 +576,21 @@
 
     // Pile up a random number of blocks to establish some history of random
     // height. cw144 DAA requires us to have height at least 2016, dunno why
-    // that much.
-    const int initialBlockCount = 2000 + int(InsecureRandRange(1000));
-    for (int i = 1; i < initialBlockCount; i++) {
-        blocks[bidx] = GetBlockIndex(&blocks[bidx - 1], 600, initialBits);
-        bidx++;
+    // that much. Keep going up to 145 blocks prior to ASERT activation.
+    for (int i = 1; i < asertActivationHeight - 145; i++) {
         BOOST_REQUIRE(bidx < int(blocks.size()));
-    }
-
-    // Start making blocks prior to activation. First, make a block about 1 day
-    // before activation. Then put down 145 more blocks with 500 second
-    // solvetime each, such that the MTP on the final block is 1 second short of
-    // activationTime.
-    {
         blocks[bidx] = GetBlockIndex(&blocks[bidx - 1], 600, initialBits);
-        blocks[bidx].nTime = activationTime - 140 * 500 - 1;
         bidx++;
     }
+    // Then put down 145 more blocks with 500 second solvetime each, such that
+    // the final block is the one prior to activation.
     for (int i = 0; i < 145; i++) {
         BOOST_REQUIRE(bidx < int(blocks.size()));
         blocks[bidx] = GetBlockIndex(&blocks[bidx - 1], 500, initialBits);
         bidx++;
     }
     CBlockIndex *pindexPreActivation = &blocks[bidx - 1];
-    BOOST_CHECK_EQUAL(pindexPreActivation->nTime, activationTime + 5 * 500 - 1);
-    BOOST_CHECK_EQUAL(pindexPreActivation->GetMedianTimePast(),
-                      activationTime - 1);
+    BOOST_CHECK_EQUAL(pindexPreActivation->nHeight, asertActivationHeight - 1);
     BOOST_CHECK(IsDAAEnabled(params, pindexPreActivation));
 
     // If we consult DAA, then it uses cw144 which returns a significantly lower
@@ -693,8 +685,7 @@
     // timestamp jump so the resulting target is 1.2% lower.
     CBlockIndex indexActivation4 =
         GetBlockIndex(pindexPreActivation, 0, 0x18011111);
-    indexActivation4.nTime = activationTime;
-    BOOST_CHECK_EQUAL(indexActivation4.GetMedianTimePast(), activationTime);
+    indexActivation4.nTime = pindexPreActivation->GetMedianTimePast();
     BOOST_CHECK(IsAxionEnabled(params, &indexActivation4));
     BOOST_CHECK_EQUAL(
         GetNextWorkRequired(&indexActivation4, &blkHeaderDummy, chainParams),
diff --git a/src/test/activation_tests.cpp b/src/test/activation_tests.cpp
--- a/src/test/activation_tests.cpp
+++ b/src/test/activation_tests.cpp
@@ -47,30 +47,7 @@
 
     testPastActivation(IsGravitonEnabled, consensus, consensus.gravitonHeight);
     testPastActivation(IsPhononEnabled, consensus, consensus.phononHeight);
-}
-
-BOOST_AUTO_TEST_CASE(isaxionenabled) {
-    const Consensus::Params &params = Params().GetConsensus();
-    const auto activation =
-        gArgs.GetArg("-axionactivationtime", params.axionActivationTime);
-    SetMockTime(activation - 1000000);
-
-    BOOST_CHECK(!IsAxionEnabled(params, nullptr));
-
-    std::array<CBlockIndex, 12> blocks;
-    for (size_t i = 1; i < blocks.size(); ++i) {
-        blocks[i].pprev = &blocks[i - 1];
-    }
-    BOOST_CHECK(!IsAxionEnabled(params, &blocks.back()));
-
-    SetMTP(blocks, activation - 1);
-    BOOST_CHECK(!IsAxionEnabled(params, &blocks.back()));
-
-    SetMTP(blocks, activation);
-    BOOST_CHECK(IsAxionEnabled(params, &blocks.back()));
-
-    SetMTP(blocks, activation + 1);
-    BOOST_CHECK(IsAxionEnabled(params, &blocks.back()));
+    testPastActivation(IsAxionEnabled, consensus, consensus.axionHeight);
 }
 
 BOOST_AUTO_TEST_CASE(isgluonenabled) {
diff --git a/test/functional/abc_feature_minerfund.py b/test/functional/abc_feature_minerfund.py
--- a/test/functional/abc_feature_minerfund.py
+++ b/test/functional/abc_feature_minerfund.py
@@ -13,7 +13,6 @@
 from test_framework.txtools import pad_tx
 from test_framework.util import assert_equal, assert_greater_than_or_equal
 
-AXION_ACTIVATION_TIME = 2000000600
 GLUON_ACTIVATION_TIME = 2100000600
 
 MINER_FUND_RATIO = 8
@@ -28,7 +27,6 @@
         self.num_nodes = 1
         self.extra_args = [[
             '-enableminerfund',
-            '-axionactivationtime={}'.format(AXION_ACTIVATION_TIME),
             '-gluonactivationtime={}'.format(GLUON_ACTIVATION_TIME),
         ]]
 
@@ -44,25 +42,23 @@
 
         address = node.get_deterministic_priv_key().address
 
-        # Move MTP forward to axion activation
-        node.setmocktime(AXION_ACTIVATION_TIME)
+        def get_best_coinbase():
+            return node.getblock(node.getbestblockhash(), 2)['tx'][0]
+
+        coinbase = get_best_coinbase()
+        assert_greater_than_or_equal(len(coinbase['vout']), 2)
+        block_reward = sum([vout['value'] for vout in coinbase['vout']])
+
+        # Move MTP forward to gluon activation
+        node.setmocktime(GLUON_ACTIVATION_TIME)
         node.generatetoaddress(6, address)
         assert_equal(
             node.getblockchaininfo()['mediantime'],
-            AXION_ACTIVATION_TIME)
+            GLUON_ACTIVATION_TIME)
 
         # Let's remember the hash of this block for later use.
-        axion_fork_block_hash = int(node.getbestblockhash(), 16)
-
-        def get_best_coinbase():
-            return node.getblock(node.getbestblockhash(), 2)['tx'][0]
-
-        # No money goes to the fund.
-        coinbase = get_best_coinbase()
-        assert_equal(len(coinbase['vout']), 1)
-        block_reward = coinbase['vout'][0]['value']
+        gluon_fork_block_hash = int(node.getbestblockhash(), 16)
 
-        # Now we send part of the coinbase to the fund.
         def check_miner_fund_output(expected_address):
             coinbase = get_best_coinbase()
             assert_equal(len(coinbase['vout']), 2)
@@ -79,12 +75,15 @@
                 coinbase['vout'][1]['value'],
                 (MINER_FUND_RATIO * total) / 100)
 
-        # First block with the new rules.
-        node.generatetoaddress(1, address)
+        # The coinbase has an output to the legacy miner fund address
+        # Now we send part of the coinbase to the fund.
         check_miner_fund_output(MINER_FUND_ADDR_AXION)
 
-        # Invalidate top block, submit a custom block that do not send anything
-        # to the fund and check it is rejected.
+        # First block with the miner fund address.
+        node.generatetoaddress(1, address)
+        check_miner_fund_output(MINER_FUND_ADDR)
+
+        # Invalidate top block.
         node.invalidateblock(node.getbestblockhash())
 
         def check_bad_miner_fund(prev_hash, time, coinbase=None):
@@ -96,29 +95,6 @@
 
             assert_equal(node.submitblock(ToHex(block)), 'bad-cb-minerfund')
 
-        check_bad_miner_fund(axion_fork_block_hash, AXION_ACTIVATION_TIME + 1)
-
-        # Move MTP forward to gluon activation
-        node.setmocktime(GLUON_ACTIVATION_TIME)
-        node.generatetoaddress(6, address)
-        assert_equal(
-            node.getblockchaininfo()['mediantime'],
-            GLUON_ACTIVATION_TIME)
-
-        # Let's remember the hash of this block for later use.
-        gluon_fork_block_hash = int(node.getbestblockhash(), 16)
-
-        # The coinbase has an output to the legacy miner fund address
-        # Now we send part of the coinbase to the fund.
-        check_miner_fund_output(MINER_FUND_ADDR_AXION)
-
-        # First block with the miner fund address.
-        node.generatetoaddress(1, address)
-        check_miner_fund_output(MINER_FUND_ADDR)
-
-        # Invalidate top block.
-        node.invalidateblock(node.getbestblockhash())
-
         # A block with no miner fund coinbase should be rejected.
         check_bad_miner_fund(gluon_fork_block_hash, GLUON_ACTIVATION_TIME + 1)
 
diff --git a/test/functional/abc_mining_basic.py b/test/functional/abc_mining_basic.py
--- a/test/functional/abc_mining_basic.py
+++ b/test/functional/abc_mining_basic.py
@@ -17,7 +17,6 @@
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal, assert_greater_than_or_equal
 
-AXION_ACTIVATION_TIME = 2000000600
 GLUON_ACTIVATION_TIME = 2100000600
 
 MINER_FUND_ADDR_AXION = 'ecregtest:pqnqv9lt7e5vjyp0w88zf2af0l92l8rxdgz0wv9ltl'
@@ -33,12 +32,10 @@
         self.extra_args = [
             [
                 '-enableminerfund',
-                '-axionactivationtime={}'.format(AXION_ACTIVATION_TIME),
                 '-gluonactivationtime={}'.format(GLUON_ACTIVATION_TIME),
             ], [
                 '-enableminerfund',
                 '-usecashaddr=0',
-                '-axionactivationtime={}'.format(AXION_ACTIVATION_TIME),
                 '-gluonactivationtime={}'.format(GLUON_ACTIVATION_TIME),
             ],
         ] * 2
@@ -47,7 +44,8 @@
         self.setup_nodes()
         # Don't connect the nodes
 
-    def run_for_node(self, node, activation_time, expectedMinerFundAddress):
+    def run_for_node(self, node, beforedMinerFundAddress,
+                     afterMinerFundAddress):
         # Connect to a peer so getblocktemplate will return results
         # (getblocktemplate has a sanity check that ensures it's connected to a
         # network).
@@ -68,31 +66,30 @@
                 assert_equal(blockTemplate[key], value)
 
         # Move block time to just before axion activation
-        node.setmocktime(activation_time)
+        node.setmocktime(GLUON_ACTIVATION_TIME)
         node.generatetoaddress(5, address)
 
-        # Before axion activation, the miner fund list is empty
+        def get_best_coinbase():
+            return node.getblock(node.getbestblockhash(), 2)['tx'][0]
+
+        coinbase = get_best_coinbase()
+        assert_greater_than_or_equal(len(coinbase['vout']), 2)
+        block_reward = sum([vout['value'] for vout in coinbase['vout']])
+
         assert_getblocktemplate({
             'coinbasetxn': {
                 'minerfund': {
-                    'addresses': [],
-                    'minimumvalue': 0,
+                    'addresses': [beforedMinerFundAddress],
+                    'minimumvalue': block_reward * 8 // 100 * XEC,
                 },
             },
         })
 
-        # Move MTP forward to axion activation
+        # Move MTP forward to activation
         node.generatetoaddress(1, address)
         assert_equal(
             node.getblockchaininfo()['mediantime'],
-            activation_time)
-
-        def get_best_coinbase():
-            return node.getblock(node.getbestblockhash(), 2)['tx'][0]
-
-        coinbase = get_best_coinbase()
-        assert_equal(len(coinbase['vout']), 1)
-        block_reward = coinbase['vout'][0]['value']
+            GLUON_ACTIVATION_TIME)
 
         # We don't need to test all fields in getblocktemplate since many of
         # them are covered in mining_basic.py
@@ -102,7 +99,7 @@
                 # We expect to start seeing the miner fund addresses since the
                 # next block will start enforcing them.
                 'minerfund': {
-                    'addresses': [expectedMinerFundAddress],
+                    'addresses': [afterMinerFundAddress],
                     'minimumvalue': block_reward * 8 // 100 * XEC,
                 },
             },
@@ -111,7 +108,7 @@
             # since we are not crossing a halving boundary and there are no
             # transactions in the mempool.
             'coinbasevalue': block_reward * XEC,
-            'mintime': activation_time + 1,
+            'mintime': GLUON_ACTIVATION_TIME + 1,
         })
 
         # First block with the new rules
@@ -128,45 +125,37 @@
         assert_getblocktemplate({
             'coinbasetxn': {
                 'minerfund': {
-                    'addresses': [expectedMinerFundAddress],
+                    'addresses': [afterMinerFundAddress],
                     'minimumvalue': block_reward * 8 // 100 * XEC,
                 },
             },
             # Again, we assume the coinbase value is the same as prior blocks.
             'coinbasevalue': block_reward * XEC,
-            'mintime': activation_time + 1,
+            'mintime': GLUON_ACTIVATION_TIME + 1,
         })
 
         # Move MTP forward
-        node.setmocktime(activation_time + 1)
+        node.setmocktime(GLUON_ACTIVATION_TIME + 1)
         node.generatetoaddress(6, address)
         assert_getblocktemplate({
             'coinbasetxn': {
                 'minerfund': {
-                    'addresses': [expectedMinerFundAddress],
+                    'addresses': [afterMinerFundAddress],
                     'minimumvalue': block_reward * 8 // 100 * XEC,
                 },
             },
             'coinbasevalue': block_reward * XEC,
-            'mintime': activation_time + 2,
+            'mintime': GLUON_ACTIVATION_TIME + 2,
         })
 
     def run_test(self):
         self.run_for_node(
             self.nodes[0],
-            AXION_ACTIVATION_TIME,
-            MINER_FUND_ADDR_AXION)
-        self.run_for_node(
-            self.nodes[1],
-            AXION_ACTIVATION_TIME,
-            MINER_FUND_LEGACY_ADDR_AXION)
-        self.run_for_node(
-            self.nodes[2],
-            GLUON_ACTIVATION_TIME,
+            MINER_FUND_ADDR_AXION,
             MINER_FUND_ADDR_GLUON)
         self.run_for_node(
-            self.nodes[3],
-            GLUON_ACTIVATION_TIME,
+            self.nodes[1],
+            MINER_FUND_LEGACY_ADDR_AXION,
             MINER_FUND_LEGACY_ADDR_GLUON)
 
 
diff --git a/test/lint/check-doc.py b/test/lint/check-doc.py
--- a/test/lint/check-doc.py
+++ b/test/lint/check-doc.py
@@ -53,8 +53,6 @@
     '-automaticunparking',
     # Removed arguments that now just print a helpful error message
     '-zapwallettxes',
-    # Remove after November 2020 upgrade
-    '-axionactivationtime',
     # Remove after May 2022 upgrade
     '-gluonactivationtime',
     '-replayprotectionactivationtime',