web3/Solodit Report

[Solodit] Public `ContributionNft::mint` leads to cascading issues / loss of funds

Tyojong 2025. 9. 23. 16:45

개요


심각도 : High

언어 : Solidity

프로토콜 : Virtuals Protocol

취약점 유형 : Access Control Issue, Logic Bug

이 리포트는 Virtuals Protocol에서 Code4rena가 발견한 공개적으로 누구나 Contribution NFT를 *mint할 수 있는 기능이 여러 연쇄적 시스템 오작동과 자금손실로 이어지는 문제에 대해 설명한다.

 

*mint : 디지털 자산(특히 NFT)을 블록체인에 새롭게 발행(등록)하는 과정을 의미한다.

 

ContributionNft::mint 함수가 외부에서 누구나 호출할 수 있도록 공개되어 있다.

프론트엔드에서 제안(proposal)을 제출하면서 해당 NFT를 mint하는 것이 정상 플로우지만, 실제로는 제안자가 직접 임의의 값(coreId, datasetId 등)을 넣어 NFT mint가 가능해진다.

 

영향 받는 코드


    function mint(
        address to,
        uint256 virtualId,
        uint8 coreId,
        string memory newTokenURI,
        uint256 proposalId,
        uint256 parentId,
        bool isModel_,
        uint256 datasetId
    ) external returns (uint256) {
        IGovernor personaDAO = getAgentDAO(virtualId);
        require(
            msg.sender == personaDAO.proposalProposer(proposalId),
            "Only proposal proposer can mint Contribution NFT"
        );
        require(parentId != proposalId, "Cannot be parent of itself");

이때 검증은 msg.sender == proposalProposer(proposalId) 만 체크된다.

proposal 제출자가 임의로 여러 값(coreId, datasetId 등)을 지정해서 NFT를 mint할 수 있다.

 

​
    function mint(uint256 virtualId, bytes32 descHash) public returns (uint256) {
        // . . . Rest of the code . . .
        uint256 proposalId = personaDAO.hashProposal(targets, values, calldatas, descHash);
        _mint(info.tba, proposalId);
        _cores[proposalId] = IContributionNft(contributionNft).getCore(proposalId);     <<@ -- // Incorrect core set
        // Calculate maturity
        _maturities[proposalId] = IAgentDAO(info.dao).getMaturity(proposalId);
​
        bool isModel = IContributionNft(contributionNft).isModel(proposalId);               <<@ -- // Incorrect model
        if (isModel) {
            emit CoreServiceUpdated(virtualId, _cores[proposalId], proposalId);
            updateImpact(virtualId, proposalId);
            _coreServices[virtualId][_cores[proposalId]] = proposalId;
        } else {
            _coreDatasets[virtualId][_cores[proposalId]].push(proposalId);
        }
        // . . . Rest of the code . . .
    }
​
    function updateImpact(uint256 virtualId, uint256 proposalId) public {
​
       // . . . Rest of the code . . .
​
        uint256 datasetId = IContributionNft(contributionNft).getDatasetId(proposalId);     <<@ -- // Incorrect datasetId
​
        _impacts[proposalId] = rawImpact;
        if (datasetId > 0) {
            _impacts[datasetId] = (rawImpact * datasetImpactWeight) / 10000;                <<@ -- // Incorrect impact calculated
            _impacts[proposalId] = rawImpact - _impacts[datasetId];                         <<@ -- // Incorrect impact calculated
            emit SetServiceScore(datasetId, _maturities[proposalId], _impacts[datasetId]);
            _maturities[datasetId] = _maturities[proposalId];
        }
​
       // . . . Rest of the code . . .
    }

제안자가 직접 잘못된 값들의 NFT를 mint하면, 나중에 proposalId와 datasetId에 연쇄적으로 잘못된 값들이 저장/연계되어 보상(logic/분배), 핵심 모델 정보, 다양한 컨트랙트 기능에 문제가 생긴다.

 

보상 분배 컨트랙트(AgentRewardV2::_distributeContributorRewards)에서 ServiceNft의 impact값을 참조하고 잘못도니 값으로 보상을 과•소분배 할 수 있다.

토큰 발행 컨트랙트(Minter::mint)에서는 getImpact 값으로 실제 전송 토큰량을 계산하고 잘못된 값에 따라 오/남용이 발생한다.

maturity 계산(AgentDAO::_calcMaturity)에서는 ContributionNft의 core 값에 따라 계정/모델 성숙도 산출이 가능하고 잘못된 core 값에 조작 가능해진다.

 

레퍼런스


 

 

Smart Contract Vulnerability Dataset - Cyfrin Solodit

h-04-public-contributionnftmint-leads-to-cascading-issues-loss-of-funds-code4rena-virtuals-protocol-virtuals-protocol-git

solodit.cyfrin.io

https://code4rena.com/reports/2025-04-virtuals-protocol