Prepared by:
HALBORN
Last Updated 04/25/2024
Date of Engagement by: September 19th, 2022 - October 7th, 2022
100% of all REPORTED Findings have been addressed
All findings
6
Critical
0
High
0
Medium
2
Low
2
Informational
2
Affine DeFi engaged Halborn to conduct a security audit on their smart contracts beginning on 2022-09-19 and ending on 2022-10-07. The security assessment was scoped to the smart contracts provided to the Halborn team.
The team at Halborn was provided three weeks for the engagement and assigned a full-time security engineer to audit the security of the smart contract. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.
The purpose of this audit is to:
Ensure that smart contract functions operate as intended
Identify potential security issues with the smart contracts
In summary, Halborn identified some security risks that were mostly addressed by the Affine DeFi team.
Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the bridge code and can quickly identify items that do not follow security best practices. The following phases and associated tools were used throughout the term of the audit:
Research into architecture and purpose
Smart contract manual code review and walkthrough
Graphing out functionality and contract logic/connectivity/functions (solgraph
)
Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes
Manual testing by custom scripts
Scanning of solidity files for vulnerabilities, security hotspots or bugs. (MythX
)
Static Analysis of security for scoped contract, and imported functions. (Slither
)
Testnet deployment (Brownie
, Remix IDE
)
IN-SCOPE: The security assessment was scoped to the following smart contracts:
src/ethereum/L1CompoundStrategy.sol
src/ethereum/L1Vault.sol
src/ethereum/L1WormholeRouter.sol
src/external/Multicall.sol
src/interfaces/*
src/polygon/Detailed.sol
src/polygon/EmergencyWithdrawalQueue.sol
src/polygon/ERC4626Router.sol
src/polygon/ERC4626RouterBase.sol
src/polygon/Forwarder.sol
src/polygon/L2AAVEStrategy.sol
src/polygon/L2Vault.sol
src/polygon/L2WormholeRouter.sol
src/polygon/Router.sol
src/polygon/TwoAssetBasket.sol
src/AffineGovernable.sol
src/BaseStrategy.sol
src/BaseVault.sol
src/BridgeEscrow.sol
src/Constants.sol
src/DollarMath.sol
src/WormholeRouter.sol
Commit ID: 30e93568ca0b0b458f8744bae1e62aaf1e132647
And the following smart contracts:
src/ethereum/CurveStrategy.sol
src/ethereum/ConvexStrategy.sol
src/polygon/DeltaNeutralLp.sol
Commit ID: 06d6bc37fa80f0fdf794a8cb93e8100288d065e0
Fixed Commit ID: 06d6bc37fa80f0fdf794a8cb93e8100288d065e0
And the following smart contracts:
src/BaseVault.sol
src/ethereum/L1Vault.sol
src/polygon/L2Vault.sol
Commit ID: 302ab4e2e54c2666d607be1b88861636fdee311d
Critical
0
High
0
Medium
2
Low
2
Informational
2
Impact x Likelihood
HAL-01
HAL-02
HAL-03
HAL-04
HAL-05
HAL-06
Security analysis | Risk level | Remediation Date |
---|---|---|
IGNORE EXTERNAL CALL FEE | Medium | Solved - 10/26/2022 |
POSSIBLE LOSS OF FUNDS | Medium | Solved - 10/26/2022 |
POSSIBLE UNPREDICTABILITY BETWEEN L2 AND L1 RATIOS | Low | Risk Accepted |
FUNCTION DOES NOT CHECK THE TOKEN BALANCE BEFORE AND AFTER A CALL | Low | Solved - 10/26/2022 |
LACK OF PROPER SLIPPAGE PROTECTION | Informational | Solved - 10/26/2022 |
POSSIBLE MISUSE OF CHAIN ID | Informational | Solved - 10/26/2022 |
// Medium
The wormhole publishMessage
function is payable. Currently, requires no fees, but that can be changed over time. If the wormhole decides to set a fee greater than 0, all those external calls within the protocol would fail. Hence, leaving the wormhole routers unable to perform their critical tasks.
function reportTransferredFund(uint256 amount) external {
require(msg.sender == address(vault), "Only vault");
bytes memory payload = abi.encode(Constants.L2_FUND_TRANSFER_REPORT, amount);
uint64 sequence = wormhole.nextSequence(address(this));
wormhole.publishMessage(uint32(sequence), payload, consistencyLevel);
}
function requestFunds(uint256 amount) external {
require(msg.sender == address(vault), "Only vault");
bytes memory payload = abi.encode(Constants.L2_FUND_REQUEST, amount);
uint64 sequence = wormhole.nextSequence(address(this));
wormhole.publishMessage(uint32(sequence), payload, consistencyLevel);
}
function reportTVL(uint256 tvl, bool received) external {
require(msg.sender == address(vault), "Only vault");
bytes memory payload = abi.encode(Constants.L1_TVL, tvl, received);
// NOTE: We use the current tx count (to wormhole) of this contract
// as a nonce when publishing messages
// This casting is fine so long as we send less than 2 ** 32 - 1 (~ 4 billion) messages
uint64 sequence = wormhole.nextSequence(address(this));
wormhole.publishMessage(uint32(sequence), payload, consistencyLevel);
}
function reportTransferredFund(uint256 amount) external {
require(msg.sender == address(vault), "Only vault");
bytes memory payload = abi.encode(Constants.L1_FUND_TRANSFER_REPORT, amount);
uint64 sequence = wormhole.nextSequence(address(this));
wormhole.publishMessage(uint32(sequence), payload, consistencyLevel);
}
SOLVED: The Affine DeFi team
solved the issue in commit:
06d6bc37fa80f0fdf794a8cb93e8100288d065e0
// Medium
Wormhole does not fail if the destination chain ID is different from the one supposed to be. If the rebalancer bot calls this function directly with a different chain ID, it will not fail, so funds during the transactions can be lost.
You can check the Wormhole Chain IDs on each chain, which is not the same as the network chain ID and can be easily confused.
function _validateWormholeMessageEmitter(IWormhole.VM memory vm) internal view {
require(vm.emitterAddress == bytes32(uint256(uint160(otherLayerRouter))), "Wrong emitter address");
require(vm.emitterChainId == otherLayerChainId, "Wrong emitter chain");
require(vm.nonce >= nextValidNonce, "Old transaction");
}
SOLVED: The Affine DeFi team
solved the issue in commit:
06d6bc37fa80f0fdf794a8cb93e8100288d065e0
// Low
When setLayerRatios
function is used to update the ratio between L1 and L2, an invalid total ratio can be set (more than 100%). Hence, the rebalancer bot could not properly work in those cases.
function setLayerRatios(uint256 _l1Ratio, uint256 _l2Ratio) external onlyGovernance {
l1Ratio = _l1Ratio;
l2Ratio = _l2Ratio;
}
RISK ACCEPTED: The Affine DeFi team
accepted the risk of this finding.
// Low
Whenever the exit function is used, the contract should check the token balance before and after the call. So, the exact amount of tokens sent can be properly checked.
function l1ClearFund(uint256 amount, bytes calldata exitProof) external {
require(msg.sender == wormholeRouter, "Only wormhole router");
// Exit tokens, after that the withdrawn tokens from L2 will be reflected in L1 BridgeEscrow.
rootChainManager.exit(exitProof);
// Transfer exited tokens to L1 Vault.
uint256 balance = token.balanceOf(address(this));
require(balance >= amount, "Funds not received");
IL1Vault l1Vault = IL1Vault(vault);
token.safeTransfer(address(l1Vault), balance);
l1Vault.afterReceive();
}
SOLVED: The Affine DeFi team
solved the issue in commit:
06d6bc37fa80f0fdf794a8cb93e8100288d065e0
// Informational
Within the _claimAndSellRewards
function, the slippage protection of the transaction is set to zero. Hence, if there is tiny liquidity, there is a high risk of losing part of the investment.
function _claimAndSellRewards() internal {
comptroller.claimComp(address(this));
if (rewardToken != address(cToken)) {
uint256 rewardTokenBalance = balanceOfRewardToken();
if (rewardTokenBalance >= minRewardToSell) {
_sellRewardTokenForWant(rewardTokenBalance, 0);
}
}
return;
}
SOLVED: The Affine DeFi team
solved the issue in commit:
06d6bc37fa80f0fdf794a8cb93e8100288d065e0
// Informational
When initializing the wormhole router, the wormhole chain ID can be misused. As can be wrongly set due to confusion with the different deployed chain IDs.
As mentioned on HAL02, you can check the Wormhole Chain IDs on each chain, which is not the same as the network chain ID and can be easily confused.
function initialize(IWormhole _wormhole, L2Vault _vault, address _otherLayerRouter, uint16 _otherLayerChainId)
external
initializer
{
wormhole = _wormhole;
vault = _vault;
governance = vault.governance();
otherLayerRouter = _otherLayerRouter;
otherLayerChainId = _otherLayerChainId;
}
SOLVED: The Affine DeFi team
solved the issue in commit:
06d6bc37fa80f0fdf794a8cb93e8100288d065e0
Halborn used automated testing techniques to enhance the coverage of certain areas of the scoped contracts. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified all the contracts in the repository and was able to compile them correctly into their ABI and binary formats, Slither was run on the all-scoped contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.
src/ethereum/L1CompoundStrategy.sol
src/ethereum/L1Vault.sol
src/ethereum/L1WormholeRouter.sol
src/external/Multicall.sol
src/polygon/Detailed.sol
src/polygon/EmergencyWithdrawalQueue.sol
src/polygon/ERC4626Router.sol
src/polygon/ERC4626RouterBase.sol
src/polygon/Forwarder.sol
src/polygon/L2AAVEStrategy.sol
src/polygon/L2Vault.sol
src/polygon/L2WormholeRouter.sol
src/AffineGovernable.sol
src/BaseStrategy.sol
src/BaseVault.sol
src/BridgeEscrow.sol
src/Constants.sol
src/DollarMath.sol
src/WormholeRouter.sol
As a result of the tests carried out with the Slither tool, some results were obtained and reviewed by Halborn
. Based on the results reviewed, some vulnerabilities were determined to be false positives. The actual vulnerabilities found by Slither are already included in the report findings.
Halborn used automated security scanners to assist with detection of well-known security issues, and to identify low-hanging fruits on the targets for this engagement. Among the tools used was MythX, a security analysis service for Ethereum smart contracts. MythX performed a scan on all the contracts and sent the compiled results to the analyzers to locate any vulnerabilities.
src/ethereum/L1CompoundStrategy.sol
src/ethereum/L1Vault.sol
src/ethereum/L1WormholeRouter.sol
src/external/Multicall.sol
src/polygon/EmergencyWithdrawalQueue.sol
src/polygon/ERC4626Router.sol
src/polygon/ERC4626RouterBase.sol
src/polygon/Forwarder.sol
src/polygon/L2AAVEStrategy.sol
src/polygon/L2Vault.sol
src/polygon/L2WormholeRouter.sol
src/AffineGovernable.sol
src/BaseStrategy.sol
src/BaseVault.sol
src/BridgeEscrow.sol
src/Constants.sol
src/DollarMath.sol
src/WormholeRouter.sol
No major issues found by Mythx. The floating pragma flagged by MythX is a false positive, as the pragma is set in the hardhat.config.ts
file to the 0.8.16
version.
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed