1 Executive Summary
This report presents the results of our engagement with Gitcoin to review GTC Token Distribution and Governance.
The review was conducted over two weeks, from April 19, 2021 to April 30, 2021 by Shayan Eskandari and Daniel Luca. A total of 20 person-days were spent.
2 Scope
Our review focused the smart contract files for governance on the commit hash ee5e45a008d65021831de9f3e83053026f2a4dd2
and the Ethereum Signed Message Service (ESMS) repository on the commit hash 5eb22e882e28e6f3192b80f237f7a3bcd15b1ee9
. The list of Solidity files in scope can be found in the Appendix.
3 Security Specification
This section describes, from a security perspective, the expected behavior of the system under audit. It is not a substitute for documentation. The purpose of this section is to identify specific security properties that were validated by the audit team.
3.1 Actors
The relevant actors are listed below with their respective abilities:
-
Signer: Given that not all Gitcoin users have an Ethereum address on their profile, Signer, in combination with
gitcoin.co
, is used to validate the token distribution and the merkle proofs- Signer has the ability to approve (Sign) or reject requests for token claims
-
GTC Minter: Specified at the deployment
- Can change the Minter address
- Can change and set TokenDistribution address
GTCDist
- Minter has the ability to mint
mintCap = 2
percentage of the totalSupply, after the specifiedmintingAllowedAfter = 365 days
-
Users: Users with a valid user-id on
gitcoin.co
who are included in the initial distribution- Can claim their tokens (Given that they have valid signature from Signer)
- Can set delegator on their tokens for governance voting
- Can invoke any ERC20 functionality on GTC token
- Can participate in the governance of GTC token
-
Governance
- Fork of Uniswap governance
- No on-chain system properties could be changed through the governance mechanism at the time of the audit.
-
TokenDistribution
GTCDist
address:- Can delegate votes from
delegator
todelegatee
- Can delegate votes from
-
TimeLock Contract:
- Fork of uniswap TimeLock contract
- All tokens that have not been claimed in 24 weeks (6 months) after launch will be transferred to the TimeLock contract.
- This contract will be in control of the assigned
admin
.
-
TreasuryVester:
- Fork of Uniswap TreasuryVester to vest tokens
3.2 Security Concerns
The following is a non-exhaustive list of security properties and concerns:
-
The entry point to the system is through Github login on Gitcoin.co website. This is outside the scope of this audit, however, it should be noted that if an attacker can gain access to any user’s Github account, they can claim the user’s tokens (given that the user has not claimed the tokens first). It is highly advised to perform a full security audit and penetration testing on
gitcoin.co
. -
The Ethereum Signed Message Service (ESMS), is a micro-service that will be used as the verification method for token claims. It should be noted that this service plays a critical role as it contains the private key of the signer and the database of all the merkle proofs. Anyone with access to all these data, just needs to brute force an integer (
user_amount
) for each user to be able to reconstruct all data needed to claim the user’s tokens.
4 Findings
Each issue has an assigned severity:
- Minor issues are subjective in nature. They are typically suggestions around best practices or readability. Code maintainers should use their own judgment as to whether to address such issues.
- Medium issues are objective in nature but are not security vulnerabilities. These should be addressed unless there is a clear reason not to.
- Major issues are security vulnerabilities that may not be directly exploitable or may require certain conditions in order to be exploited. All major issues should be addressed.
- Critical issues are directly exploitable security vulnerabilities that need to be fixed.
4.1 ESMS use of sanitized user_amount
& user_id
values Medium ✓ Fixed
Resolution
Description
In the Signer service, values are properly checked, however the checked values are not preserved and the user input is passed down in the function.
The values are sanitized here:
code/gtc-request-signer-main-5eb22e882e28e6f3192b80f237f7a3bcd15b1ee9/app.py:L98-L108
try:
int(user_id)
except ValueError:
gtc_sig_app.logger.error('Invalid user_id received!')
return Response('{"message":"ESMS error"}', status=400, mimetype='application/json')
# make sure it's an int
try:
int(user_amount)
except ValueError:
gtc_sig_app.logger.error('Invalid user_amount received!')
return Response('{"message":"ESMS error"}', status=400, mimetype='application/json')
But the original user inputs are being used here:
code/gtc-request-signer-main-5eb22e882e28e6f3192b80f237f7a3bcd15b1ee9/app.py:L110-L113
try:
leaf = proofs[str(user_id)]['leaf']
proof = proofs[str(user_id)]['proof']
leaf_bytes = Web3.toBytes(hexstr=leaf)
code/gtc-request-signer-main-5eb22e882e28e6f3192b80f237f7a3bcd15b1ee9/app.py:L128-L131
# this is a bit of hack to avoid bug in old web3 on frontend
# this means that user_amount is not converted back to wei before tx is broadcast!
user_amount_in_eth = Web3.fromWei(user_amount, 'ether')
Examples
if a float amount is passed for user_amount
, all checks will pass, however the final amount will be slightly different that what it is intended:
>>> print(str(Web3.fromWei(123456789012345, 'ether')))
0.000123456789012345
>>> print(str(Web3.fromWei(123456789012345.123, 'ether')))
0.000123456789012345125
Recommendation
After the sanity check, use the sanitized value for the rest of the code flow.
4.2 Prefer using abi.encode
in TokenDistributor
Medium ✓ Fixed
Resolution
Description
The method _hashLeaf
is called when a user claims their airdrop.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L128-L129
// can we repoduce leaf hash included in the claim?
require(_hashLeaf(user_id, user_amount, leaf), 'TokenDistributor: Leaf Hash Mismatch.');
This method receives the user_id
and the user_amount
as arguments.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L253-L257
/**
* @notice hash user_id + claim amount together & compare results to leaf hash
* @return boolean true on match
*/
function _hashLeaf(uint32 user_id, uint256 user_amount, bytes32 leaf) private returns (bool) {
These arguments are abi encoded and hashed together to produce a unique hash.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L258
bytes32 leaf_hash = keccak256(abi.encodePacked(keccak256(abi.encodePacked(user_id, user_amount))));
This hash is checked against the third argument for equality.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L259
return leaf == leaf_hash;
If the hash matches the third argument, it returns true and considers the provided user_id
and user_amount
are correct.
However, packing differently sized arguments may produce collisions.
The Solidity documentation states that packing dynamic types will produce collisions, but this is also the case if packing uint32
and uint256
.
Examples
Below there’s an example showing that packing uint32
and uint256
in both orders can produce collisions with carefully picked values.
library Encode {
function encode32Plus256(uint32 _a, uint256 _b) public pure returns (bytes memory) {
return abi.encodePacked(_a, _b);
}
function encode256Plus32(uint256 _a, uint32 _b) public pure returns (bytes memory) {
return abi.encodePacked(_a, _b);
}
}
contract Hash {
function checkEqual() public pure returns (bytes32, bytes32) {
// Pack 1
uint32 a1 = 0x12345678;
uint256 b1 = 0x99999999999999999999999999999999999999999999999999999999FFFFFFFF;
// Pack 2
uint256 a2 = 0x1234567899999999999999999999999999999999999999999999999999999999;
uint32 b2 = 0xFFFFFFFF;
// Encode these 2 different values
bytes memory packed1 = Encode.encode32Plus256(a1, b1);
bytes memory packed2 = Encode.encode256Plus32(a2, b2);
// Check if the packed encodings match
require(keccak256(packed1) == keccak256(packed2), "Hash of representation should match");
// The hashes are the same
// 0x9e46e582607c5c6e05587dacf66d311c4ced0819378a41d4b4c5adf99d72408e
return (
keccak256(packed1),
keccak256(packed2)
);
}
}
Changing abi.encodePacked
to abi.encode
in the library will make the transaction fail with error message Hash of representation should match
.
Recommendation
Unless there’s a specific use case to use abi.encodePacked
, you should always use abi.encode
. You might need a few more bytes in the transaction data, but it prevents collisions. Similar fix can be achieved by using unit256
for both values to be packed to prevent any possible collisions.
4.3 Simplify claim tokens for a gas discount and less code Minor ✓ Fixed
Resolution
Fixed in gitcoinco/governance#4
Structure Claim
can still be removed for further optimization.
Description
The method claimTokens
in TokenDistributor
needs to do a few checks before it can distribute the tokens.
A few of these checks can be simplified and optimized.
The method hashMatch
can be removed because it’s only used once and the contents can be moved directly into the parent method.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L125-L126
// can we reproduce the same hash from the raw claim metadata?
require(hashMatch(user_id, user_address, user_amount, delegate_address, leaf, eth_signed_message_hash_hex), 'TokenDistributor: Hash Mismatch.');
Because this method also uses a few other internal calls, they also need to be moved into the parent method.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L211
return getDigest(claim) == eth_signed_message_hash_hex;
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L184
hashClaim(claim)
Moving the code directly in the parent method and removing them will improve gas costs for users.
The structure Claim
can also be removed because it’s not used anywhere else in the code.
Recommendation
Consider simplifying claimTokens
and remove unused methods.
4.4 ESMS use of environment variable for chain info [Optimization] Minor ✓ Fixed
Resolution
Description
Variables to create domain separator are hardcoded in the code, and it requires the modify code on different deployments (e.g. testnet, mainnet, etc).
Examples
code/gtc-request-signer-main-5eb22e882e28e6f3192b80f237f7a3bcd15b1ee9/app.py:L203-L208
domain = make_domain(
name='GTA',
version='1.0.0',
chainId=4,
verifyingContract='0xBD2525B5F0B2a663439a78A99A06605549D25cE5')
Recommendation
Use environment variable for these values. This way there is no need to change the source code on different deployments and it can be scripted to prevent any possible errors on the code base.
4.5 Rename method _hashLeaf
to something that represents the validity of the leaf Minor ✓ Fixed
Resolution
Description
The method _hashLeaf
accepts 3 arguments.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L257
function _hashLeaf(uint32 user_id, uint256 user_amount, bytes32 leaf) private returns (bool) {
The arguments user_id
and user_amount
are used to create a keccak256 hash.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L258
bytes32 leaf_hash = keccak256(abi.encodePacked(keccak256(abi.encodePacked(user_id, user_amount))));
This hash is then checked if it matches the third argument.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L259
return leaf == leaf_hash;
The result of the equality is returned by the method.
The name of the method is confusing because it should say that it returns true if the leaf is considered valid.
Recommendation
Consider renaming the method to something like isValidLeafHash
.
4.6 Method returns bool but result is never used in TokenDistributor.claimTokens
Minor ✓ Fixed
Resolution
Description
The method _delegateTokens
is called when a user claims their tokens to automatically delegate the claimed tokens to their own address or to a different one.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L135
_delegateTokens(user_address, delegate_address);
The method accepts the addresses of the delegator and the delegate and returns a boolean.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L262-L270
/**
* @notice execute call on token contract to delegate tokens
* @return boolean true on success
*/
function _delegateTokens(address delegator, address delegatee) private returns (bool) {
GTCErc20 GTCToken = GTCErc20(token);
GTCToken.delegateOnDist(delegator, delegatee);
return true;
}
But this boolean is never used.
Recommendation
Remove the returned boolean because it’s always returned as true
anyway and the transaction will be a bit cheaper.
4.7 Use a unified compiler version for all contracts Minor ✓ Fixed
Resolution
0.6.12
in gitcoinco/governance#2
Description
Currently the smart contracts for the Gitcoin token and governance use different versions of Solidity compiler (^0.5.16
, 0.6.12
, 0.5.17
).
Recommendation
It is suggested to use a unified compiler version for all contracts (e.g. 0.6.12
).
Note that it is recommended to use the latest version of Solidity compiler with security patches (currently 0.8.3), although given that these contracts are forks of the battle tested Uniswap governance contracts, the Gitcoin team prefer to keep the modifications to the code at minimum.
4.8 Improve efficiency by using immutable in TreasuryVester Minor ✓ Fixed
Resolution
Description
The TreasuryVester
contract when deployed has a few fixed storage variables.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TreasuryVester.sol:L30
gtc = gtc_;
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TreasuryVester.sol:L33-L36
vestingAmount = vestingAmount_;
vestingBegin = vestingBegin_;
vestingCliff = vestingCliff_;
vestingEnd = vestingEnd_;
These storage variables are defined in the contract.
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TreasuryVester.sol:L8
address public gtc;
code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TreasuryVester.sol:L11-L14
uint public vestingAmount;
uint public vestingBegin;
uint public vestingCliff;
uint public vestingEnd;
But they are never changed.
Recommendation
Consider setting storage variables as immutable
type for a considerable gas improvement.
Appendix 1 - Files in Scope
This audit, in addition to the ESMS components, covered the following Solidity files:
File Name | SHA-1 Hash |
---|---|
governance/contracts/GTC.sol | a909f97b7a200d9cf148bc275e48b8e9f800e5e3 |
governance/contracts/Timelock.sol | 501bca9e092f6119425423fbf113dc67537a7872 |
governance/contracts/GovernorAlpha.sol | b52f893c6d6aa0162e0c3c5e9c0ca698217a456f |
governance/contracts/SafeMath.sol | 5a3e130059a4672bd4defa577c6ce292a9ef76d6 |
governance/contracts/TokenDistributor.sol | 3015d9659f613d8b262bc8f35ec5f482797af5c4 |
governance/contracts/TreasuryVester.sol | 344c5a1ea9932b9da3ac2433caa8b40c9b7ebad8 |
Appendix 2 - Disclosure
ConsenSys Diligence (“CD”) typically receives compensation from one or more clients (the “Clients”) for performing the analysis contained in these reports (the “Reports”). The Reports may be distributed through other means, including via ConsenSys publications and other distributions.
The Reports are not an endorsement or indictment of any particular project or team, and the Reports do not guarantee the security of any particular project. This Report does not consider, and should not be interpreted as considering or having any bearing on, the potential economics of a token, token sale or any other product, service or other asset. Cryptographic tokens are emergent technologies and carry with them high levels of technical risk and uncertainty. No Report provides any warranty or representation to any Third-Party in any respect, including regarding the bugfree nature of code, the business model or proprietors of any such business model, and the legal compliance of any such business. No third party should rely on the Reports in any way, including for the purpose of making any decisions to buy or sell any token, product, service or other asset. Specifically, for the avoidance of doubt, this Report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. CD owes no duty to any Third-Party by virtue of publishing these Reports.
PURPOSE OF REPORTS The Reports and the analysis described therein are created solely for Clients and published with their consent. The scope of our review is limited to a review of Solidity code and only the Solidity code we note as being within the scope of our review within this report. The Solidity language itself remains under development and is subject to unknown risks and flaws. The review does not extend to the compiler layer, or any other areas beyond Solidity that could present security risks. Cryptographic tokens are emergent technologies and carry with them high levels of technical risk and uncertainty.
CD makes the Reports available to parties other than the Clients (i.e., “third parties”) – on its website. CD hopes that by making these analyses publicly available, it can help the blockchain ecosystem develop technical best practices in this rapidly evolving area of innovation.
LINKS TO OTHER WEB SITES FROM THIS WEB SITE You may, through hypertext or other computer links, gain access to web sites operated by persons other than ConsenSys and CD. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such web sites’ owners. You agree that ConsenSys and CD are not responsible for the content or operation of such Web sites, and that ConsenSys and CD shall have no liability to you or any other person or entity for the use of third party Web sites. Except as described below, a hyperlink from this web Site to another web site does not imply or mean that ConsenSys and CD endorses the content on that Web site or the operator or operations of that site. You are solely responsible for determining the extent to which you may use any content at any other web sites to which you link from the Reports. ConsenSys and CD assumes no responsibility for the use of third party software on the Web Site and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.
TIMELINESS OF CONTENT The content contained in the Reports is current as of the date appearing on the Report and is subject to change without notice. Unless indicated otherwise, by ConsenSys and CD.