0

I am working on a full-stack React-NodeJs-Postgres app and combining it with solidity smart contracts managed using Foundry. I am using anvil to create a test blockchain to be able to make repeated transactions. I am using React to request access to Metamask to make web3 transactions on the test blockchain with the anvil test accounts added to Metamask.

As a starting point, I am using a contract factory which I deploy on my test blockchain through a forge script. This is the contract factory:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

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

contract TokenManagerFactory {
    event SharedWalletCreated(address indexed wallet);

    constructor() {}

    function createSharedWallet(uint256 _withdrawalLimit) public {
        SharedWallet sharedWallet = new SharedWallet(_withdrawalLimit);
        emit SharedWalletCreated(address(sharedWallet));
    }
}

The script for deployment is:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Script, console} from "forge-std/Script.sol";

import {TokenManagerFactory} from "./../src/TokenManagerFactory.sol";

contract TokenManagerFactoryScript is Script {
    TokenManagerFactory public tokenManagerFactory;

    function setUp() public {}

    function run() public returns (TokenManagerFactory) {
        vm.startBroadcast();
        tokenManagerFactory = new TokenManagerFactory();
        vm.stopBroadcast();
        return tokenManagerFactory;
    }
}

I use a NodeJS API endpoint to fetch the contract factory deployed address from the broadcast directory and provide it to the React frontend.

The smart contract that I am currently starting with is a simple shared wallet:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import {AccessControl} from "openzeppelin-contracts/contracts/access/AccessControl.sol";

/**
 * @dev Contract for creating a shared wallet for approved withdrawers.
 *
 * The contract owner can add withdrawers and set withdrawal limits
 * for each of them.
 * An attempt to withdraw over the set limit will revert with an error.
 * An attempt by an unauthorized person to withdraw will revert with an error.
 */
contract SharedWallet is Ownable, AccessControl {
    bytes32 public constant SPENDER = keccak256("SPENDER");
    uint256 public withdrawLimit;
    uint256 private balance;

    event SharedWallet__Withdrawal(
        address indexed _from,
        address indexed _to,
        uint256 amount
    );

    error SharedWallet__ExceededLimit(uint256 requestedAmount, uint256 limit);
    error SharedWallet__InsufficientBalance(uint256 requestedAmount);

    constructor(uint256 _withdrawLimit) Ownable(_msgSender()) {
        _grantRole(DEFAULT_ADMIN_ROLE, _msgSender());
        withdrawLimit = _withdrawLimit;
    }

    receive() external payable {
        balance += msg.value;
    }

    function setWithdrawer(address withdrawer) public onlyOwner {
        grantRole(SPENDER, withdrawer);
    }

    function withdraw(address _to, uint256 amount) public {
        _checkRole(SPENDER, _to);
        if (amount > withdrawLimit) {
            revert SharedWallet__ExceededLimit({
                requestedAmount: amount,
                limit: withdrawLimit
            });
        }
        if (amount > balance) {
            revert SharedWallet__InsufficientBalance(amount);
        }
        balance -= amount;
        payable(_to).transfer(amount);
        emit SharedWallet__Withdrawal(address(this), _to, amount);
    }

    function closeWallet() public onlyOwner {
        payable(owner()).transfer(address(this).balance);
    }
}

I create a forge script for this smart contract as well just for testing purposes:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Script, console} from "forge-std/Script.sol";

import {SharedWallet} from "./../src/SharedWallet.sol";

contract SharedWalletScript is Script {
    SharedWallet public sharedWallet;

    function setUp() public {}

    function run(uint256 _withdrawalLimit) public returns(SharedWallet) {
        vm.startBroadcast();
        sharedWallet = new SharedWallet(_withdrawalLimit);
        vm.stopBroadcast();
        return sharedWallet;
    }
}

I am creating a new shared wallet in my React frontend using web3. I am fetching the contract factory address and ABI through a NodeJS API endpoint. The React code for creating the shared wallet is:

const contractFactory = new web3.eth.Contract(
    contractFactoryAbi,
    contractFactoryAddress
);

const maxLimitWei = web3.utils.toWei(
    formik.values.maxLimit.trim(),
    'ether'
);

const contractEvents = await contractFactory.events.SharedWalletCreated();

contractEvents.on('data', (event: any) => {
    console.log(event)
})

const estimateGas = await contractFactory.methods
    .createSharedWallet(maxLimitWei)
    .estimateGas({ from: ownerAccount });
const actualGas = (estimateGas * BigInt(2)).toString();

const contractFactoryResponse = await contractFactory.methods
    .createSharedWallet(maxLimitWei)
    .send({
    from: ownerAccount,
    gas: actualGas,
    });

console.log(contractFactoryResponse)
const newWalletAddress =
    contractFactoryResponse.events.SharedWalletCreated.returnValues.wallet;

This code is successful. I get the response of the web3 call with the wallet address. And in the event listener, I can verify that the wallet address is the same.

I am trying to now use this wallet address to add a wallet user with the setWithdrawer method in the SharedWallet. This is where I am continuously getting an error. This is the React code for adding withdrawers:

if (web3 !== null && web3 !== undefined) {
    const web3Accounts = await web3.eth.getAccounts();
    const web3Account = web3Accounts[0];
    const walletContract = new web3.eth.Contract(
    wallet.abi,
    wallet.address
    );
    console.log(walletContract);
    // const gasEstimate = await walletContract.methods
    //   .setWithdrawer(accAddresses[0])
    //   .estimateGas({ from: web3Account });
    // console.log(gasEstimate);
    // const actualGas = gasEstimate * BigInt(2);
    const web3Response = await walletContract.methods
    .setWithdrawer('0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC')
    .send({ from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', gas: '500000' });

    console.log(web3Response);
}

This code produces an error:

Error: reverted with: custom error 0x118cdaa7: 000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266

I googled a bit and some responses to similar error said that there might be an error in the SharedWallet smart contract. So I copied the SharedWallet smart contract into Remix IDE and it worked without any errors. I could compile, deploy the contract and also call the setWithdrawer method.

As an additional verification, I ran the SharedWallet.s.sol script that deploys a SharedWallet contract in the test blockchain. I extracted the contract address and hardcoded the React code:

const walletContract = new web3.eth.Contract(
    wallet.abi,
    'wallet-address'
);

And it worked perfectly here as well.

So, it appears as if calling the createSharedWallet method of the TokenManagerFactory smart contract is not really deploying a SharedWallet contract in the test blockchain. Something is being deployed, as I am able to deposit test ether into the wallet and also read the wallet balance. But, am not able to call the setWithdrawer method.

Is there anything that I am missing here?

1 Answer 1

0

There was a bug in the contracts. But I found that out by migrating the contract management to Hardhat. After using Hardhat, the error message in the React console said the error was from OwnableUnauthorizedAccount() error in the Ownable contract in the Openzeppelin library.

Turns out when I was creating the SharedWallet from the TokenManagerFactory contract, I was not passing the msg.sender. Therefore, the contract corresponding to TokenManagerFactory becomes the owner of the shared wallet.

I changed TokenManagerFactory to:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Context} from '@openzeppelin/contracts/utils/Context.sol';
import {SharedWallet} from './SharedWallet.sol';

contract TokenManagerFactory is Context {
    event SharedWalletCreated(address indexed wallet);

    constructor() {}

    function createSharedWallet(uint256 _withdrawalLimit) public {
        SharedWallet sharedWallet = new SharedWallet(_withdrawalLimit, _msgSender());
        emit SharedWalletCreated(address(sharedWallet));
    }
}

And the SharedWallet contract constructor to be:

constructor(
    uint256 _withdrawLimit,
    address _walletOwner
) Ownable(_walletOwner) {
    _grantRole(DEFAULT_ADMIN_ROLE, _walletOwner);
    withdrawLimit = _withdrawLimit;
}

Makes me wonder how this was working in Remix IDE. I understand how it worked when I ran the Forge script for the SharedWallet, as then I was running it as the test account. So then the test account became the owner of the wallet.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.