Search code examples
ethereumsolidityevm

Smart contract factory using an array of Struct as a parameter


I am using a CampaignFactory contract to create multiple instances of the Campaign contract and keep track of them. Every campaign is initialized with an array of Struct called rewards. When I try to create a new Campaign with createCampaign in remix, I have the following error:

[vm]from: 0x5B3...eddC4to: CampaignFactory.createCampaign((uint256,uint256,string)[]) 0xd91...39138value: 0 weidata: 0x6d9...00000logs: 0hash: 0xa4d...ad5fd
transact to CampaignFactory.createCampaign errored: VM error: revert.

Here is my code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

struct Reward {
    uint256 contribution;
    uint256 maxNumber;
    string ImageLink;
}

contract CampaignFactory {

    Campaign[] public deployedCampaigns;

    function createCampaign(Reward[] memory _rewards)
        public
    {
        Campaign newCampaign = new Campaign(msg.sender);

        for (uint256 i = 0; i < _rewards.length; i++) {
            newCampaign.createReward(
                _rewards[i].contribution,
                _rewards[i].maxNumber,
                _rewards[i].ImageLink
            );
        }
        deployedCampaigns.push(newCampaign);
    }

    function getDeployedCampaigns()
        public
        view
        returns (Campaign[] memory)
    {
        return deployedCampaigns;
    }
}

contract Campaign {
    Reward[] public rewards;
    address public manager;

    modifier restricted() {
        require(msg.sender == manager);
        _;
    }
    constructor(address creator) {
        manager = creator;
    }

    function createReward(
        uint256 _contribution,
        uint256 _maxNumber,
        string memory _imageLink
    ) public restricted {
        Reward memory newReward = Reward({
            contribution: _contribution,
            maxNumber: _maxNumber,
            ImageLink: _imageLink
        });

        rewards.push(newReward);
    }
}

Solution

  • When you deploy the Campaign contract, the user's address is assigned to manager.

    Then you're invoking the createReward() function. Its restricted modifier allows the function to be invoked only by the manager. But the function is invoked by your contract - not by the user directly. Which fails the validation and reverts the transaction.


    You could get the address of the original transaction sender in tx.origin, but using it for authorization is a security risk - https://swcregistry.io/docs/SWC-115

    So instead, you can pass the user address in the function argument, and verify it there.

    function createCampaign(Reward[] memory _rewards)
        public
    {
        // pass the factory address to later validate `msg.sender`
        Campaign newCampaign = new Campaign(address(this), msg.sender);
    
        for (uint256 i = 0; i < _rewards.length; i++) {
            // pass the user address to validate if they're the `manager`
            newCampaign.createReward(
                _rewards[i].contribution,
                _rewards[i].maxNumber,
                _rewards[i].ImageLink,
                msg.sender
            );
        }
        deployedCampaigns.push(newCampaign);
    }
    
    address factory;
    address public manager;
    
    modifier restrictedThroughFactory(address user) {
        require(msg.sender == factory && user == manager);
        _;
    }
    
    modifier restricted() {
        require(msg.sender == manager);
        _;
    }
    
    constructor(address _factory, address creator) {
        factory = _factory;
        manager = creator;
    }
    
    // original function that can be still executed directly by the user
    function createReward(
        uint256 _contribution,
        uint256 _maxNumber,
        string memory _imageLink
    ) public restricted {
        _createReward(_contribution, _maxNumber, _imageLink);
    }
    
    // overloaded function that can be only executed by the Factory contract
    // reverts if the `user` (can be passed only by the Factory contract) is not the `manager`
    function createReward(
        uint256 _contribution,
        uint256 _maxNumber,
        string memory _imageLink,
        address user
    ) public restrictedThroughFactory(user) {
        _createReward(_contribution, _maxNumber, _imageLink);
    }
    
    function _createReward(
        uint256 _contribution,
        uint256 _maxNumber,
        string memory _imageLink
    ) internal {
        Reward memory newReward = Reward({
            contribution: _contribution,
            maxNumber: _maxNumber,
            ImageLink: _imageLink
        });
    
        rewards.push(newReward);
    }