Security Audit
July 8th, 2024
Version 1.0.0
Presented by 0xMacro
This document includes the results of the security audit for Polynomial's smart contract code as found in the section titled ‘Source Code’. The security audit was performed by the Macro security team on June 23rd 10th to June 24th 2024.
The purpose of this audit is to review the source code of certain Polynomial Solidity contracts, and provide feedback on the design, architecture, and quality of the source code with an emphasis on validating the correctness and security of the software in its entirety.
Disclaimer: While Macro’s review is comprehensive and has surfaced some changes that should be made to the source code, this audit should not solely be relied upon for security, as no single audit is guaranteed to catch all possible bugs.
The following is an aggregation of issues found by the Macro Audit team:
Severity | Count | Acknowledged | Won't Do | Addressed |
---|---|---|---|---|
Medium | 1 | - | - | 1 |
Low | 3 | - | - | 3 |
Code Quality | 1 | 1 | - | - |
Polynomial was quick to respond to these issues.
Our understanding of the specification was based on the following sources:
Fee tiers are created by owner of polynomial, and each account requires the owners signature to update their fee tier. The owner is trusted to fairly distribute fee discounts described in fee tiers to appropriate accounts, and update account’s fee tiers appropriately. The fee tiers of an account is also expected to be updated frequently, on a 24 hour basis, and so fee tier updates signatures are expected to have a 24 hour expiry to prevent reusing an account’s beneficial fee tier. Polynomial is also expected to update an accounts fee tier if they do not do so themselves prior to trading.
The following source code was reviewed during the audit:
3c09fa61dbd332c2b9566f2da90176ec5358d211
Specifically, we audited the following contracts within this repository.
Source Code | SHA256 |
---|---|
markets/perps-market/contracts/interfaces/IAsyncOrderModule.sol |
|
markets/perps-market/contracts/interfaces/IFeeTierModule.sol |
|
markets/perps-market/contracts/interfaces/IGlobalPerpsMarketModule.sol |
|
markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol |
|
markets/perps-market/contracts/modules/AsyncOrderModule.sol |
|
markets/perps-market/contracts/modules/FeeTierModule.sol |
|
markets/perps-market/contracts/modules/PerpsAccountModule.sol |
|
markets/perps-market/contracts/storage/AsyncOrder.sol |
|
markets/perps-market/contracts/storage/FeeTier.sol |
|
markets/perps-market/contracts/storage/GlobalPerpsMarketConfiguration.sol |
|
markets/perps-market/contracts/storage/PerpsAccount.sol |
|
Note: This document contains an audit solely of the Solidity contracts listed above. Specifically, the audit pertains only to the contracts themselves, and does not pertain to any other programs or scripts, including deployment scripts.
Click on an issue to jump to it, or scroll down to see them all.
We quantify issues in three parts:
This third part – the severity level – is a summary of how much consideration the client should give to fixing the issue. We assign severity according to the table of guidelines below:
Severity | Description |
---|---|
(C-x) Critical |
We recommend the client must fix the issue, no matter what, because not fixing would mean significant funds/assets WILL be lost. |
(H-x) High |
We recommend the client must address the issue, no matter what, because not fixing would be very bad, or some funds/assets will be lost, or the code’s behavior is against the provided spec. |
(M-x) Medium |
We recommend the client to seriously consider fixing the issue, as the implications of not fixing the issue are severe enough to impact the project significantly, albiet not in an existential manner. |
(L-x) Low |
The risk is small, unlikely, or may not relevant to the project in a meaningful way. Whether or not the project wants to develop a fix is up to the goals and needs of the project. |
(Q-x) Code Quality |
The issue identified does not pose any obvious risk, but fixing could improve overall code quality, on-chain composability, developer ergonomics, or even certain aspects of protocol design. |
(I-x) Informational |
Warnings and things to keep in mind when operating the protocol. No immediate action required. |
(G-x) Gas Optimizations |
The presented optimization suggestion would save an amount of gas significant enough, in our opinion, to be worth the development cost of implementing it. |
In PerpsAccountModule.sol
, signatures are verified to be signed by the owner to update an accounts fee tier:
function _verify(
address _signer,
uint256 _feeTierId,
uint128 _accountId,
uint256 _expiry,
bytes memory signature
) internal pure returns (bool) {
bytes32 messageHash = _getMessageHash(_feeTierId, _accountId, _expiry);
bytes32 ethSignedMessageHash = _getEthSignedMessageHash(messageHash);
return _recoverSigner(ethSignedMessageHash, signature) == _signer;
}
function _recoverSigner(
bytes32 _ethSignedMessageHash,
bytes memory _signature
) internal pure returns (address) {
(bytes32 r, bytes32 s, uint8 v) = _splitSignature(_signature);
return ecrecover(_ethSignedMessageHash, v, r, s);
}
Reference: PerpsAccountModule.sol#L133-L153
However, this directly uses ecrecover()
to pull the signer from the signature, which only works for external owned account signatures. In cases where the signer is a smart contract, like a multi-sig or AA wallet, this method for verifying signatures will not work. The intended owner will be a multi-sig smart contract, which currently will not worth with this setup. EIP1271 was designed to allow smart contracts to sign messages and allow for it’s verification on-chain, so supporting this eip is required.
Remediations to Consider
Add EIP1271 support to allow the owner to sign fee tier updates properly.
As shown in M-1, the signature verification is done by directly using ecrecover()
, and it does not include additional checks on the r, s, and v signature values. As mentioned in openzeppelin’s implementation of signature validation, ecrecover()
allows for signature malleability if the library used to sign the message does not explicitly prevent it. Although most libraries will not generate malleable signatures, explicitly preventing them from being used on-chain is best practice.
Remediations to Consider
Prevent signature malleability by using openzeppelin’s signature checker library, or prevent the s
value from being greater than 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
to resolve the issue yourself.
EIP712 is a standard for signatures for specifying how to sign data with structs, but more importantly describes the standard method for adding a domain to each signature. A domain is additional information that specifies the target contracts address, chain, and version that a signature can be used for, preventing a signature from being used on a differing chain, or unintended contract. Without a domain, signatures made by a signer could potentially be reused maliciously, and in this case prior signatures of the owner could be used to set an unintended fee tier if the prior signature also did not include a domain.
Remediations to Consider
Follow EIP-712 to generate and verify the signatures to be sure the signatures are valid.
FeeTierId
0
unintentionally effects most accounts
In FeeTierModule.sol
, the owner can set the maker and taker fee discounts for a fee tier based on a provided id by calling setFeeTier()
:
function setFeeTier(
uint256 id,
uint256 makerDiscount,
uint256 takerDiscount
) external override {
OwnableStorage.onlyOwner();
FeeTier.Data storage feeTier = FeeTier.load(id);
FeeTier.setFeeTier(feeTier, makerDiscount, takerDiscount);
emit FeeTierSet(id, makerDiscount, takerDiscount);
}
Reference: FeeTierModule.sol#L12-L23
function setFeeTier(Data storage self, uint256 makerDiscount, uint256 takerDiscount) internal {
// check discount should not be more than 100%
if (makerDiscount > 10000) {
revert ParameterError.InvalidParameter(
"makerDiscount",
"Maker discount should not be more than 100%"
);
}
if (takerDiscount > 10000) {
revert ParameterError.InvalidParameter(
"takerDiscount",
"Taker discount should not be more than 100%"
);
}
self.makerDiscount = makerDiscount;
self.takerDiscount = takerDiscount;
}
Reference: FeeTier.sol#L21-L39
However, there are no constraints on the id
set. If the id
set is 0
then since 0
is the default value of feeTier
for each account, setting this will apply the makerDiscount
and takerDiscount
to all users that have no set feeTier
:
// @dev fee tier for this account
uint256 feeTierId;
Reference: PerpsAccount.sol#L53-L54
Remediations to Consider
Prevent setting a feeTier
with an id
of 0
.
PerpsAccountModule
The signature verification functions are currently in the PerpsAccountModule
, however these functions should be moved to their own signature verification library, since they are distinct enough and unrelated to the direct functionality of the PerpsAccountModule
. Also consider using the openzeppelin libraries for signature verification rather than writing your own.
Macro makes no warranties, either express, implied, statutory, or otherwise, with respect to the services or deliverables provided in this report, and Macro specifically disclaims all implied warranties of merchantability, fitness for a particular purpose, noninfringement and those arising from a course of dealing, usage or trade with respect thereto, and all such warranties are hereby excluded to the fullest extent permitted by law.
Macro will not be liable for any lost profits, business, contracts, revenue, goodwill, production, anticipated savings, loss of data, or costs of procurement of substitute goods or services or for any claim or demand by any other party. In no event will Macro be liable for consequential, incidental, special, indirect, or exemplary damages arising out of this agreement or any work statement, however caused and (to the fullest extent permitted by law) under any theory of liability (including negligence), even if Macro has been advised of the possibility of such damages.
The scope of this report and review is limited to a review of only the code presented by the Polynomial team and only the source code Macro notes as being within the scope of Macro’s review within this report. This report does not include an audit of the deployment scripts used to deploy the Solidity contracts in the repository corresponding to this audit. 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. In this report you may through hypertext or other computer links, gain access to websites operated by persons other than Macro. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such websites’ owners. You agree that Macro is not responsible for the content or operation of such websites, and that Macro shall have no liability to your or any other person or entity for the use of third party websites. Macro assumes no responsibility for the use of third party software and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.