Upgrade of UUPS Contract failing cause of delegatecall

I'm trying to upgrade my UUPS contract using the upgradeTo method provided by UUPSUpgradable.
I'm kinda new to this, from what I understood the UUPS contract is both a proxy and implementation contract, as such, i should be able to directly call upgradeTo on it.

The thing is, when i try to run the Truffle migration script, i get the Function must be called through delegatecall error. This is thrown by the onlyProxy modifier inside the UUPSUpgradable contract:

    modifier onlyProxy() {
        require(address(this) != __self, "Function must be called through delegatecall");
        require(_getImplementation() == __self, "Function must be called through active proxy");
        _;
    }

This is my migration script for the upgrade:

Here CryptoWay is the Proxy contract instance and CryptoWayV2 is the new Implementation.

module.exports = async function (deployer) {
    const CryptoWay = artifacts.require('CryptoWay');
    const CryptoWayV2 = artifacts.require('CryptoWayV2');

    // deploy new contract instance
    await deployer.deploy(CryptoWayV2);
    const contractV2 = await CryptoWayV2.deployed();
    // Get the existing contract instance
    const contract = await CryptoWay.deployed();
    // Perform the upgrade when needed by replacing NEW_ADDRESS with the address of the new implementation contract
    await contract.upgradeTo(contractV2.address);
  
    console.log('-- UPGRADE COMPLETE --');
    console.log('Existing contract address: ', contract.address);
  };

Here's the contract:

pragma solidity ^0.8.0;
//SPDX-License-Identifier: GL3
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

/**
* @title CryptoWay
* @dev This upgradable contract creates payments and allows the owner to withdraw funds.
*/
contract CryptoWay is
    Initializable,
    OwnableUpgradeable,
    UUPSUpgradeable
    {
    event Payment(
        string paymentId,
        address customerAddress,
        uint finalAmount,
        string currency,
        string coinName
    );

    event Withdraw(address indexed owner, uint amount);

    uint public innerBalance;

    /**
    * @dev the initializer works as a constructor for upgradable contracts, this gets executed only once during the initial deployment
    */
    function initialize() initializer public {
        innerBalance = 0;
        __Ownable_init();
        __UUPSUpgradeable_init();
    }

    /**
    * @param paymentId the id of the payment
    * @param currency currency type (fiat/crypto)
    * @param coinName ISO code of the coin that is being used 
    * @dev allows external users or other contracts to initiate payments, the payment amount is then transferred to the owner.
    */
    function triggerPayment (
        string memory paymentId,
        string memory currency,
        string memory coinName
    ) external payable {
        require(msg.value > 0, "Payment amount must be greater than zero");
        emit Payment(paymentId, msg.sender, msg.value, currency, coinName);
        payable(owner()).transfer(msg.value);
    }

    /**
    * @dev gets automatically called when a transaction to this smart contract is made without explicitly calling any function
    */
    receive() external payable {
        innerBalance += msg.value;  
    }

    /**
    * @dev transfers all the smart contract balance to the owner address
    */
    function withdrawAll() external onlyOwner {
        require(innerBalance > 0, "Inner balance must be greater than zero");
        payable(owner()).transfer(innerBalance);
        innerBalance = 0;
        emit Withdraw(msg.sender, innerBalance);
    }

    /**
    * @param amount amount that needs to be withdrawed 
    * @dev transfer the chosen smart contract balance to the owner address
    */
    function withdraw(uint amount) external onlyOwner {
        require(amount <= innerBalance, "Withdrawal amount exceeds inner balance");
        innerBalance -= amount;
        payable(owner()).transfer(amount);
        emit Withdraw(msg.sender, amount);
    }

    /**
    * @param newOwner address of the new owner
    * @dev changes the owner of the contract based on a new address, can only be called by the current owner
    */    
    function changeOwner(address newOwner) external onlyOwner {
        transferOwnership(newOwner);
    }

    /**
    * @param newImplementation address of the new implementation contract
    * @dev upgrades the contract by changing the implementation contract to newImplementation 
    */    
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

The UUPS contract is not both a proxy and implementation contract.

The proxy contract (ERC1967Proxy.sol) is the contract that users should interact with, and it is also the contract where you should invoke the upgradeTo function. However, the code for the upgradeTo function is NOT in the proxy contract, but is actually in the implementation contract!

The error message above is saying that you are trying to call upgradeTo from the implementation address, but you need to call it through the proxy address.

Thanks for reaching out! So i need to write another contract that acts as a proxy and then link it to the implementation contract that I’ve already wrote? Where can i find the docs for the proxy contract?

Generally you don't need to write your own proxy contracts. OpenZeppelin Contracts has the ERC1967Proxy.sol contract (see v4 or v5 docs) which is for the UUPS proxy itself and that can be deployed without changes. You can use the Upgrades Plugins to help with deploying and upgrading the proxy.