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?