PeckShield Audit Report VIRTUAL v1.0
PeckShield Audit Report VIRTUAL v1.0
VIRTUAL Protocol
PeckShield
March 10, 2024
Document Properties
Version Info
Contact
For more information about this document and its contents, please contact PeckShield Inc.
Contents
1 Introduction 4
1.1 About VIRTUAL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2 About PeckShield . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 Methodology . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4 Disclaimer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2 Findings 9
2.1 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2 Key Findings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3 Detailed Results 11
3.1 Manipulation of Account Scores/Proposal Maturity in AgentDAO . . . . . . . . . . . 11
3.2 Suggested Adherence of Checks-Effects-Interactions in AgentFactory . . . . . . . . . 14
3.3 Accommodation of Non-ERC20-Compliant Tokens . . . . . . . . . . . . . . . . . . . 15
3.4 Lack of Founder Score Initialization in AgentNft::mint() . . . . . . . . . . . . . . . . 17
3.5 Improved Protocol Parameters Update Logic in TimeLockStaking . . . . . . . . . . . 19
3.6 Improved AgentNft/AgentDAO Initialization Logic . . . . . . . . . . . . . . . . . . . 20
3.7 Trust Issue of Admin Keys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
4 Conclusion 23
References 24
1 | Introduction
Given the opportunity to review the design document and related smart contract source code of
the VIRTUAL protocol, we outline in the report our systematic approach to evaluate potential security
issues in the smart contract implementation, expose possible semantic inconsistencies between smart
contract code and design document, and provide additional suggestions or recommendations for
improvement. Our results show that the given version of smart contracts can be further improved
due to the presence of several issues related to either security or performance. This document outlines
our audit results.
Item Description
Name VIRTUAL Protocol
Website https://virtuals.io/
Type Solidity
Language EVM
Audit Method Whitebox
Latest Audit Report March 10, 2024
In the following, we show the Git repository of reviewed files and the commit hash value used in
this audit.
• https://github.com/Virtual-Protocol/protocol-contracts.git (480b53f)
And here is the commit ID after all fixes for the issues found in the audit have been checked in:
• https://github.com/Virtual-Protocol/protocol-contracts.git (be42a7d)
Likelihood
1.3 Methodology
To standardize the evaluation, we define the following terminology based on OWASP Risk Rating
Methodology [11]:
• Impact measures the technical loss and business damage of a successful attack;
Likelihood and impact are categorized into three ratings: H, M and L, i.e., high, medium and
low respectively. Severity is determined by likelihood and impact, and can be accordingly classified
into four categories, i.e., Critical, High, Medium, Low shown in Table 1.2.
To evaluate the risk, we go through a list of check items and each would be labeled with
a severity category. For one check item, if our tool or analysis does not identify any issue, the
contract is considered safe regarding the check item. For any discovered issue, we might further
deploy contracts on our private testnet and run tests to confirm the findings. If necessary, we would
additionally build a PoC to demonstrate the possibility of exploitation. The concrete list of check
items is shown in Table 1.3.
In particular, we perform the audit according to the following procedure:
• Basic Coding Bugs: We first statically analyze given smart contracts with our proprietary static
code analyzer for known coding bugs, and then manually verify (reject or confirm) all the issues
found by our tool.
• Semantic Consistency Checks: We then manually check the logic of implemented smart con-
tracts and compare with the description in the white paper.
• Advanced DeFi Scrutiny: We further review business logics, examine system operations, and
place DeFi-related aspects under scrutiny to uncover possible pitfalls and/or bugs.
• Additional Recommendations: We also provide additional suggestions regarding the coding and
development of smart contracts from the perspective of proven programming practices.
To better describe each issue we identified, we categorize the findings with Common Weakness
Enumeration (CWE-699) [10], which is a community-developed list of software weakness types to
better delineate and organize weaknesses around concepts frequently encountered in software devel-
opment. Though some categories used in CWE-699 may not be relevant in smart contracts, we use
the CWE categories in Table 1.4 to classify our findings. Moreover, in case there is an issue that
may affect an active protocol that has been deployed, the public version of this report may omit
such issue, but will be amended with full details right after the affected protocol is upgraded with
respective fixes.
1.4 Disclaimer
Note that this security audit is not designed to replace functional tests required before any software
release, and does not give any warranties on finding all possible security issues of the given smart
contract(s) or blockchain software, i.e., the evaluation result does not guarantee the nonexistence
of any further findings of security issues. As one audit-based assessment cannot be considered
comprehensive, we always recommend proceeding with several independent audits and a public bug
bounty program to ensure the security of smart contract(s). Last but not least, this security audit
should not be used as investment advice.
Table 1.4: Common Weakness Enumeration (CWE) Classifications Used in This Audit
Category Summary
Configuration Weaknesses in this category are typically introduced during
the configuration of the software.
Data Processing Issues Weaknesses in this category are typically found in functional-
ity that processes data.
Numeric Errors Weaknesses in this category are related to improper calcula-
tion or conversion of numbers.
Security Features Weaknesses in this category are concerned with topics like
authentication, access control, confidentiality, cryptography,
and privilege management. (Software security is not security
software.)
Time and State Weaknesses in this category are related to the improper man-
agement of time and state in an environment that supports
simultaneous or near-simultaneous computation by multiple
systems, processes, or threads.
Error Conditions, Weaknesses in this category include weaknesses that occur if
Return Values, a function does not generate the correct return/status code,
Status Codes or if the application does not handle all possible return/status
codes that could be generated by a function.
Resource Management Weaknesses in this category are related to improper manage-
ment of system resources.
Behavioral Issues Weaknesses in this category are related to unexpected behav-
iors from code that an application uses.
Business Logics Weaknesses in this category identify some of the underlying
problems that commonly allow attackers to manipulate the
business logic of an application. Errors in business logic can
be devastating to an entire application.
Initialization and Cleanup Weaknesses in this category occur in behaviors that are used
for initialization and breakdown.
Arguments and Parameters Weaknesses in this category are related to improper use of
arguments or parameters within function calls.
Expression Issues Weaknesses in this category are related to incorrectly written
expressions within code.
Coding Practices Weaknesses in this category are related to coding practices
that are deemed unsafe and increase the chances that an ex-
ploitable vulnerability will be present in the application. They
may not directly introduce a vulnerability, but indicate the
product has not been carefully developed or maintained.
2 | Findings
2.1 Summary
Here is a summary of our findings after analyzing the VIRTUAL implementations. During the first
phase of our audit, we study the smart contract source code and run our in-house static code
analyzer through the codebase. The purpose here is to statically identify known coding bugs, and
then manually verify (reject or confirm) issues reported by our tool. We further manually review
business logics, examine system operations, and place DeFi-related aspects under scrutiny to uncover
possible pitfalls and/or bugs.
Severity # of Findings
Critical 0
High 0
Medium 3
Low 4
Total 7
We have so far identified a list of potential issues: some of them involve subtle corner cases that might
not be previously thought of, while others refer to unusual interactions among multiple contracts.
For each uncovered issue, we have therefore developed test cases for reasoning, reproduction, and/or
verification. After further analysis and internal discussion, we determined a few issues of varying
severities need to be brought up and paid more attention to, which are categorized in the above
table. More information can be found in the next subsection, and the detailed discussions of each of
them are in Section 3.
Beside the identified issues, we emphasize that for any user-facing applications and services, it is
always important to develop necessary risk-control mechanisms and make contingency plans, which
may need to be exercised before the mainnet deployment. The risk-control mechanisms should kick
in at the very moment when the contracts are being deployed on mainnet. Please refer to Section 3
for details.
3 | Detailed Results
Description
The VIRTUAL protocol has a key AgentDAO contract that supports the governance on the created virtual
instance. In the process of examining the governance logic, we notice current score calculation for
voters may be manipulated.
In the following, we show the code snippet of the related _castVote() routine. This routine has
a rather straightforward logic in casting the user vote. We notice the vote has the associated voter
score update (lines 163 − 168). The update logic basically counts the number of proposals the voter
has participated and the user participation can be all types of votes, including Against, For, Abstain,
and Deliberate. It comes to our attention that Deliberate type may be abused to inflate the vote
score.
149 function _castVote (
150 uint256 proposalId ,
151 address account ,
152 uint8 support ,
153 string memory reason ,
154 bytes memory params
155 ) internal override returns ( uint256 ) {
156 uint256 weight = super . _castVote (
157 proposalId ,
158 account ,
159 support ,
160 reason ,
161 params
162 );
163 if ( hasVoted ( proposalId , account ) ) {
164 _scores [ account ]. push (
165 SafeCast . toUint48 ( block . number ) ,
166 SafeCast . toUint208 ( scoreOf ( account ) ) + 1
167 );
168 }
169
170 if ( params . length > 0) {
171 _ up da t eM at u ri t y ( account , proposalId , weight , params ) ;
172 }
173
174 return weight ;
175 }
Specifically, a malicious voter Malice may chose a specific proposal and then start to make multiple
votes on the same proposal. The first vote can be For to make the hasVoted(proposalId, account)
call to return true. After that, Malice makes repeated votes with the Deliberate vote type. Each
Deliberate vote will basically increase the Malice vote score by 1. It will also negatively affect the
proposal maturity calculation.
663 function _castVote (
664 uint256 proposalId ,
665 address account ,
666 uint8 support ,
667 string memory reason ,
668 bytes memory params
669 ) internal virtual returns ( uint256 ) {
670 _ v a l i d a t e S t a t e B i t m a p ( proposalId , _ e n c o d e S t a t e B i t m a p ( ProposalState . Active ) ) ;
671
672 uint256 weight = _getVotes ( account , p r o p o s a l S n a p s h o t ( proposalId ) , params ) ;
673 _countVote ( proposalId , account , support , weight , params ) ;
674
675 if ( params . length == 0) {
676 emit VoteCast ( account , proposalId , support , weight , reason ) ;
677 } else {
678 emit V o t e C a s t W i t h P a r a m s ( account , proposalId , support , weight , reason , params
);
679 }
680
681 return weight ;
682 }
Recommendation Revisit the above logic to enable reliable and safe voting.
Status This issue has been fixed by the following commit: 33d61c3.
Description
A common coding best practice in Solidity is the adherence of checks-effects-interactions principle.
This principle is effective in mitigating a serious attack vector known as re-entrancy. Via this
particular attack vector, a malicious contract can be reentering a vulnerable contract in a nested
manner. Specifically, it first calls a function in the vulnerable contract, but before the first instance
of the function call is finished, second call can be arranged to re-enter the vulnerable contract by
invoking functions that should only be executed once. This attack was part of several most prominent
hacks in Ethereum history, including the DAO [14] exploit, and the Uniswap/Lendf.Me hack [13].
We notice there are occasions where the checks-effects-interactions principle is violated. Using
the AgentFactory as an example, the withdraw() function (see the code snippet below) is provided to
externally call a token contract to transfer assets. However, the invocation of an external contract
requires extra care in avoiding the above re-entrancy. For example, the interaction with the external
contract (line 183) start before effecting the update on internal state (lines 187−188), hence violating
the principle. In this particular case, if the external contract has certain hidden logic that may be
capable of launching re-entrancy via the same entry function.
164 function withdraw ( uint256 id ) public {
165 Application storage application = _applications [ id ];
166
167 require (
168 msg . sender == application . proposer
169 hasRole ( WITHDRAW_ROLE , msg . sender ) ,
170 " Not proposer "
171 );
172
173 require (
174 application . status == A p p l i c a t i o n S t a t u s . Active ,
175 " Application is not active "
176 );
177
178 require (
179 block . number > application . proposalEndBlock ,
180 " Application is not matured yet "
181 );
182
183 IERC20 ( assetToken ) . transfer (
184 application . proposer ,
185 application . w i t h d r a w a b l e A m o u n t
186 );
187 application . w i t h d r a w a b l e A m o u n t = 0;
188 application . status = A p p l i c a t i o n S t a t u s . Withdrawn ;
189 }
While the supported tokens in the protocol do implement rather standard ERC20 interfaces and
their related token contracts are not vulnerable or exploitable for re-entrancy, it is important to take
precautions to thwart possible re-entrancy.
Status This issue has been fixed by the following commit: 33d61c3.
Description
Though there is a standardized ERC-20 specification, many token contracts may not strictly follow the
specification or have additional functionalities beyond the specification. In this section, we examine
the transfer() routine and possible idiosyncrasies from current widely-used token contracts.
In particular, we use the popular stablecoin, i.e., USDT, as our example. We show the related
code snippet below. Specifically, the transfer() routine does not have a return value defined and
implemented. However, the IERC20 interface has defined the transfer() interface with a bool return
value. As a result, the call to transfer() may expect a return value. With the lack of return value
of USDT’s transfer(), the call will be unfortunately reverted.
126 function transfer ( address _to , uint _value ) public o n ly Pa y lo ad S iz e (2 * 32) {
127 uint fee = ( _value . mul ( ba s is Po i nt sR a te ) ) . div (10000) ;
Because of that, a normal call to transfer() is suggested to use the safe version, i.e., safeTransfer
(), In essence, it is a wrapper around ERC20 operations that may either throw on failure or return
false without reverts. Moreover, the safe version also supports tokens that return no value (and
instead revert or throw on failure). Note that non-reverting calls are assumed to be successful.
In current implementation, if we examine the AgentFactory::proposePersona() routine that is
designed to propose a new persona. To accommodate the specific idiosyncrasy, there is a need to
user safeTransferFrom(), instead of transferFrom() (line 133).
111 function pro posePe rsona (
112 string memory name ,
113 string memory symbol ,
114 string memory tokenURI ,
115 uint8 [] memory cores ,
116 bytes32 tbaSalt ,
117 address tbaImplementation ,
118 uint32 daoVotingPeriod ,
119 uint256 daoThreshold
120 ) external returns ( uint256 ) {
121 address sender = msg . sender ;
122 require (
123 IERC20 ( assetToken ) . balanceOf ( sender ) >= applicationThreshold ,
124 " Insufficient asset token "
125 );
126 require (
127 IERC20 ( assetToken ) . allowance ( sender , address ( this ) ) >=
128 applicationThreshold ,
129 " Insufficient asset token allowance "
130 );
131 require ( cores . length > 0 , " Cores must be provided " ) ;
161 return id ;
162 }
Status This issue has been fixed by the following commit: be42a7d.
Description
The VIRTUAL protocol has a core AgentNft contract to keep track of every virtual instantiation. Each
virtual has its own DAO that is protected with so-called validators. These validators serve as the
gatekeepers by reviewing and certifying the inputs from contributors. Our analysis shows the founder
will automatically become one validator. Our analysis shows the founder score is not properly
initialized.
In the following, we show the implementation of the related routine, i.e., mint(). As the name indi-
cates, this routine mints the NFT-representation of a new virtual. Notice that the founder is added into
the validator list (line 91). However, it does not properly initialize the score, i.e., _initValidatorScore
(virtualId, founder).
74 function mint (
75 address to ,
76 string memory newTokenURI ,
77 address payable theDAO ,
78 address founder ,
79 uint8 [] memory coreTypes
80 ) external onlyRole ( MINTER_ROLE ) returns ( uint256 ) {
81 uint256 virtualId = _ nextVi rtualI d ++;
82 _mint ( to , virtualId ) ;
83 _setTokenURI ( virtualId , newTokenURI ) ;
84 VirtualInfo storage info = virtualInfos [ virtualId ];
85 info . dao = theDAO ;
86 info . coreTypes = coreTypes ;
87 info . founder = founder ;
88 IERC5805 daoToken = GovernorVotes ( theDAO ) . token () ;
89 info . token = address ( daoToken ) ;
90 _ s t a k i n g T o k e n T o V i r t u a l I d [ address ( daoToken ) ] = virtualId ;
91 _addValidator ( virtualId , founder ) ;
92 return virtualId ;
93 }
Recommendation Revisit the above virtual initialization logic by setting up the initial score of
the first validator, i.e., founder.
Status This issue has been fixed by the following commit: 33d61c3.
Description
DeFi protocols typically have a number of system-wide parameters that can be dynamically configured
on demand. The VIRTUAL protocol is no exception. Specifically, if we examine the TimeLockStaking con-
tract, it has defined a number of protocol-wide risk parameters, such as maxBonus and maxLockDuration.
In the following, we show the corresponding routines that allow for their changes.
147 f u n c t i o n a d j u s t M a x B o n u s ( u i n t 2 5 6 _maxBonus ) e x t e r n a l o n l y G o v {
148 maxBonus = _maxBonus ;
149 }
150
151 f u n c t i o n a d j u s t M a x L o c k P e r i o d ( u i n t 2 5 6 _maxLockDuration ) e x t e r n a l o n l y G o v {
152 m a x L o c k D u r a t i o n = _maxLockDuration ;
153 }
These parameters define various aspects of the protocol operation and maintenance and need
to exercise extra care when configuring or updating them. Our analysis shows the update logic on
these parameters can be improved by applying more rigorous sanity checks. Based on the current
implementation, certain corner cases may lead to an undesirable consequence. For example, an
unlikely mis-configuration of maxBonus may violate the restriction on current curve elements.
157 }
Status This issue has been fixed by the following commit: 33d61c3.
Description
To facilitate possible future upgrade, a number of contracts in VIRTUAL are instantiated as a proxy
with actual logic contracts in the backend. While examining the related contract construction and
initialization logic, we notice current construction can be improved.
In the following, we use the AgentNft contract as an example and show its initialization routine.
We notice its constructor does not have any payload. With that, it can be improved by adding the
following statement, i.e., _disableInitializers();. Note this statement is called in the logic contract
where the initializer is locked. Therefore any user will not able to call the initialize() function in
the state of the logic contract and perform any malicious activity. Note that the proxy contract state
will still be able to call this function since the constructor does not effect the state of the proxy
contract.
52 function initialize ( address defaultAdmin ) public initializer {
53 __ERC721_init ( " Persona " , " PERSONA " ) ;
54 _ _ C o r e R e g i s t r y _ i n i t () ;
55 __ValidatorRegistry_init (
56 _validatorScoreOf ,
57 totalProposals ,
58 _getPastValidatorScore
59 );
60 _ _ A c c e s s C o n t r o l _ i n i t () ;
61 _grantRole ( DEFAULT_ADMIN_ROLE , defaultAdmin ) ;
62 _grantRole ( VALIDATOR_ADMIN_ROLE , defaultAdmin ) ;
63 _ne xtVirt ualId = 1;
64 }
Moreover, the above initialize() routine can be improved by also initializing the inherited con-
tract ERC721URIStorageUpgradeable by calling __ERC721URIStorage_init().
Status This issue has been fixed by the following commit: 33d61c3.
Description
In VIRTUAL, there is a privileged account (with the role ). This account plays a critical role in governing
and regulating the system-wide operations (e.g., configure parameters, create virtuals, execute priv-
ileged operations, etc.). Our analysis shows that this privileged account needs to be scrutinized. In
the following, we use the AgentFactory contract as an example and show the representative functions
potentially affected by the privileged account.
289 function s e t A p p l i c a t i o n T h r e s h o l d (
290 uint256 newThreshold
291 ) public onlyRole ( D E F A U L T _ A D M I N _ R O L E ) {
292 a p p l i c a t i o n T h r e s h o l d = newThreshold ;
293 emit A p p l i c a t i o n T h r e s h o l d U p d a t e d ( newThreshold ) ;
294 }
295
296 function setGov ( address newGov ) public onlyRole ( D E F A U L T _ A D M I N _ R O L E ) {
297 gov = newGov ;
298 emit GovUpdated ( newGov ) ;
299 }
300
301 function setVault (
302 address newVault
303 ) public onlyRole ( D E F A U L T _ A D M I N _ R O L E ) {
304 _vault = newVault ;
305 }
306
307 function s e t I m p l e m e n t a t i o n s (
308 address token ,
309 address dao
310 ) public onlyRole ( D E F A U L T _ A D M I N _ R O L E ) {
311 t o k e n I m p l e m e n t a t i o n = token ;
312 d a o I m p l e m e n t a t i o n = dao ;
313 }
We understand the need of the privileged functions for proper protocol operations, but at the
same time the extra power to the owner may also be a counter-party risk to the protocol users.
Therefore, we list this concern as an issue here from the audit perspective and highly recommend
making these privileges explicit or raising necessary awareness among protocol users.
Recommendation Promptly transfer the privileged account to the intended DAO-like governance
contract. All changed to privileged operations may need to be mediated with necessary timelocks.
Eventually, activate the normal on-chain community-based governance life-cycle and ensure the in-
tended trustless nature and high-quality distributed governance.
Status This issue has been mitigated as the team plans to assign admin role to a multi-sig
wallet managed by Fireblocks.
4 | Conclusion
In this audit, we have analyzed the design and implementation of the VIRTUAL protocol, which is
designed to align incentives for the decentralized creation and monetization of AI personas for every
virtual interaction (gaming, metaverses, online interactions, or beyond). It creates co-owned, human-
curated, plug-and-play gaming AIs by acting as a decentralized factory. In other words, it makes all
sorts of AI characters (that can respond via text, voice and motion) for different virtual worlds,
like games or online spaces. The current code base is well structured and neatly organized. Those
identified issues are promptly confirmed and addressed.
Meanwhile, we need to emphasize that smart contracts as a whole are still in an early, but exciting
stage of development. To improve this report, we greatly appreciate any constructive feedbacks or
suggestions, on our methodology, audit findings, or potential gaps in scope/coverage.
ł
References
[1] MITRE. CWE-1126: Declaration of Variable with Unnecessarily Wide Scope. https://cwe.
mitre.org/data/definitions/1126.html.
mitre.org/data/definitions/663.html.
data/definitions/837.html.
data/definitions/841.html.
254.html.
1006.html.
840.html.
html.
Rating_Methodology.
[13] PeckShield. Uniswap/Lendf.Me Hacks: Root Cause and Loss Analysis. https://medium.com/
@peckshield/uniswap-lendf-me-hacks-root-cause-and-loss-analysis-50f3263dcc09.
understanding-dao-hack-journalists.