2023-03-asymmetry-findings's People
Forkers
akenta5678 machu6886 kingman978 abaleo kalasforever amiram101 bugsluck kaur-d23 md-assembla mrparsa313 sedbangi sahamdelfi2023-03-asymmetry-findings's Issues
Potential DoS attack due to expensive operation during contract deployment
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L182
Vulnerability details
Impact
During the contract deployment, the addDerivative function iterates over all existing derivatives to calculate the total weight, which can become an expensive operation as the number of derivatives increases. An attacker could potentially exploit this by adding a large number of derivatives to the contract, causing the deployment transaction to fail or run out of gas, leading to a denial-of-service (DoS) attack.
Proof of Concept
function addDerivative(
address _contractAddress,
uint256 _weight
) external onlyOwner {
derivatives[derivativeCount] = IDerivative(_contractAddress);
weights[derivativeCount] = _weight;
derivativeCount++;
uint256 localTotalWeight = 0;
for (uint256 i = 0; i < derivativeCount; i++)
localTotalWeight += weights[i];
totalWeight = localTotalWeight;
emit DerivativeAdded(_contractAddress, _weight, derivativeCount);
}
If the number of derivatives in the contract is large, the for loop that iterates over all derivatives to calculate the total weight can become an expensive operation, leading to a DoS attack during contract deployment.
Tools Used
Manual Review
Recommended Mitigation Steps
One possible solution to mitigate this vulnerability is to implement a limit on the number of derivatives that can be added to the contract. For Emergency situation add deny function for derivatives
DOS - User funds stuck permanently
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L182
Vulnerability details
Impact
If admin mistakenly adds a incorrect derivative contract, then all major contract functionality will permanently fail and User funds will stuck permanently
Proof of Concept
- Admin wants to add a new derivative using
addDerivative
function
function addDerivative(
address _contractAddress,
uint256 _weight
) external onlyOwner {
derivatives[derivativeCount] = IDerivative(_contractAddress);
weights[derivativeCount] = _weight;
derivativeCount++;
...
}
- Admin wants to use contract address as
0xabc
but mistakenly uses0xabd
- A new derivative with contract address
0xabd
gets created - After sometime Admin wants to rebalance weights using
rebalanceToWeights
which fails sincederivatives[i].balance()
does not exist for0xabd
contract
function rebalanceToWeights() external onlyOwner {
uint256 ethAmountBefore = address(this).balance;
for (uint i = 0; i < derivativeCount; i++) {
if (derivatives[i].balance() > 0)
derivatives[i].withdraw(derivatives[i].balance());
}
...
}
- Similarly user stake and unstake operation also fails due to same reason
function unstake(uint256 _safEthAmount) external {
...
for (uint256 i = 0; i < derivativeCount; i++) {
// withdraw a percentage of each asset based on the amount of safETH
uint256 derivativeAmount = (derivatives[i].balance() *
_safEthAmount) / safEthTotalSupply;
if (derivativeAmount == 0) continue; // if derivative empty ignore
derivatives[i].withdraw(derivativeAmount);
}
Recommended Mitigation Steps
Allow admin to remove a derivative, which will prevent such scenarios
Missing deadline checks allow pending transactions to be maliciously executed
Lines of code
Vulnerability details
Impact
AMMs should provide their users with an option to limit the execution of their pending actions, such as swaps or adding and removing liquidity. The most common solution is to include a deadline timestamp as a parameter (for example see Uniswap V2). If such an option is not present, users can unknowingly perform bad trades
Proof of Concept
Alice wants to swap 1 WETH for 1 RETH and later sell the 1 RETH for 1000 DAI. She signs the transaction calling Pair.sell with inputAmount = 1 WETH and minOutputAmount = 0.99 RETH to allow for some slippage.
The transaction is submitted to the mempool, however, Alice chose a transaction fee that is too low for miners to be interested in including her transaction in a block. The transaction stays pending in the mempool for extended periods, which could be hours, days, weeks, or even longer.
When the average gas fee dropped far enough for Alice’s transaction to become interesting again for miners to include it, her swap will be executed. In the meantime, the price of RETH could have drastically changed. She will still at least get 0.99 RETH due to minOutputAmount, but the DAI value of that output might be significantly lower. She has unknowingly performed a bad trade due to the pending transaction she forgot about.
An even worse way this issue can be maliciously exploited is through MEV:
The swap transaction is still pending in the mempool. Average fees are still too high for miners to be interested in it. The price of RETH has gone up significantly since the transaction was signed, meaning Alice would receive a lot more ETH when the swap is executed. But that also means that her minOutputAmount value is outdated and would allow for significant slippage.
A MEV bot detects the pending transaction. Since the outdated minOutputAmount now allows for high slippage, the bot sandwiches Alice, resulting in significant profit for the bot and significant loss for Alice.
Tools Used
Manual review
Recommended Mitigation Steps
Introduce a deadline parameter to the mentioned functions.
Oracle price can be better secured (freshness + tamper-resistance)
Lines of code
Vulnerability details
SfrxEth contract is a derivative contract that enables the conversion of Frax's staked derivative sfrxETH into ETH. The contract relies on the Frax protocol for the conversion of sfrxETH into frxETH, and then the curve pool to convert frxETH into ETH.
The contract includes a function called ethPerDerivative that calculates the current price of the sfrxETH derivative in ETH. The calculation depends on the price oracle of the frxETH-ETH curve pool, which is returned by the price_oracle function of the pool contract.
Impact
The ethPerDerivative function relies on the accuracy of the price oracle returned by the curve pool contract. If the price oracle is manipulated or inaccurate, then the conversion rate from sfrxETH to ETH will be affected, leading to potential losses for users.
The Frax protocol provides a mechanism for the conversion of sfrxETH into frxETH, which introduces an additional layer of risk if the protocol is not secure or trustworthy.
Recommended Mitigation Steps
To mitigate the risk of inaccurate or manipulated price oracles, we recommend the following steps:
Use a price oracle, such as Chainlink oracles, can provide greater security and resistance to manipulation.
Implement a time-based limit on the price oracle: The ethPerDerivative function can be modified to include a check that ensures the price oracle was updated within a certain time limit. For example:
uint256 lastPriceUpdate = IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).lastUpdateTime();
require(block.timestamp - lastPriceUpdate <= 1 hours, 'stale price');
This will prevent the use of outdated or manipulated price oracles.
Implement a price deviation limit: The ethPerDerivative function can be modified to include a check that ensures the last reported price of the curve pool is not too far from the current price oracle. For example:
uint256 lastPrice = IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).getPriceCumulativeLast();
uint256 oraclePrice = IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle();
uint256 percentDiff;
// require difference in prices to be within 5%
if (lastPrice > oraclePrice) {
percentDiff = (lastPrice - oraclePrice) * 1e18 / oraclePrice;
} else {
percentDiff = (oraclePrice - lastPrice) * 1e18 / oraclePrice;
}
require(percentDiff <= 5e16, 'volatile market');
This will prevent the use of price oracles that have deviated too far from the current value.
unstake() will revert if RocketPool deposit pool is empty, causing all funds to be stuck
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L108
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L118
Vulnerability details
Impact
The withdraw function of the reth derivative contract assumes it can always burn rETH for ETH. This is not the case. If Rocket Pool's deposit pool is empty, rETH.burn() will revert. As a result the derivative's withdraw() will revert (See note in RP docs). Subsequently SafETH.unstake() will also revert. This means that no user will be able to unstake their SafETH as long as Rocket Pool's deposit pool is empty (as seen here this can be an extended period of time (e.g., 5 months).
(note: rebalanceToWeights() will also fail)
Proof of Concept
- User deposits 50 ETH into SafETH
- Assume 20 ETH (of the 50) is swapped into rETH
- Rocket Pool uses the 20 ETH to create a minipool.
- Rocket Pool's deposit pool now only has 4 ETH
- User attempts to unstake his SafETH
- We attempt to burn 20ETH worth of rETH -> reverts since only 4 ETH is available
Recommended Mitigation Steps
- Sell rETH on a DEX if rETH is unable to be burned.
QA Report
See the markdown file with the details of this report here.
QA Report
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
Agreements & Disclosures
Agreements
If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:
- C4 Certified Contributor Terms and Conditions,
- C4 Code of Professional Conduct, and
- the disclosure guidelines below.
To signal your agreement to these terms, add a 👍 emoji to this issue.
Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.
Disclosures
Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.
To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:
- any sponsor staff or sponsor contractors who are also participating as wardens
- any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
- any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
- any other case where someone might reasonably infer a possible conflict of interest.
Gas Optimizations
See the markdown file with the details of this report here.
Stake excessive ETH not refunded to user
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L88-L91
Vulnerability details
Impact
From the stake function, we can see that if a user stakes 1 ETH, it will be divided by the number of derivatives and transferred to different derivative accounts based on the weights. However, the remaining balance of the ETH will not be returned to the user, but will be kept in the contract.
The unused balance of the ETH depends on the number of derivatives and their respective weights.
Proof of Concept
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L88-L91
function stake() external payable {
require(pauseStaking == false, "staking is paused");
require(msg.value >= minAmount, "amount too low");
require(msg.value <= maxAmount, "amount too high");
uint256 underlyingValue = 0;
// Getting underlying value in terms of ETH for each derivative
for (uint i = 0; i < derivativeCount; i++)
underlyingValue +=
(derivatives[i].ethPerDerivative(derivatives[i].balance()) *
derivatives[i].balance()) /
10 ** 18;
uint256 totalSupply = totalSupply();
uint256 preDepositPrice; // Price of safETH in regards to ETH
if (totalSupply == 0)
preDepositPrice = 10 ** 18; // initializes with a price of 1
else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;
uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system
for (uint i = 0; i < derivativeCount; i++) {
uint256 weight = weights[i];
IDerivative derivative = derivatives[i];
if (weight == 0) continue;
uint256 ethAmount = (msg.value * weight) / totalWeight;
// This is slightly less than ethAmount because slippage
uint256 depositAmount = derivative.deposit{value: ethAmount}(); //@audit no return rest of funds to user.
uint derivativeReceivedEthValue = (derivative.ethPerDerivative(
depositAmount
) * depositAmount) / 10 ** 18;
totalStakeValueEth += derivativeReceivedEthValue;
}
Test
let depositAmount = ethers.utils.parseEther("100");
safEthProxy.stake({ value: depositAmount });
output:
ethAmount 33333333333333333333
ethAmount 33333333333333333333
ethAmount 33333333333333333333
ethAmountAfter balance left on safETH contract:1
Tools Used
Manual
Recommended Mitigation Steps
Refund any excess back to the user.
Possibly reentrancy attacks in withdraw function
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L107-L114
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/SfrxEth.sol#L60-L88
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/WstEth.sol#L56-L67
Vulnerability details
Impact
Possibly reentrancy attacks in withdraw function
Proof of Concept
For example, Reth.sol calls rethAddress()).burn, which allows the Owner to call function in the fallback.
Tools Used
None.
Recommended Mitigation Steps
add reentrancy guard for critical function.
Gas Optimizations
See the markdown file with the details of this report here.
Missing deadline checks allow pending transactions to be maliciously executed
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L56-L67
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L60-L88
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L156-L204
Vulnerability details
Missing deadline checks allow pending transactions to be maliciously executed
Impact
The WstEth.sol
and SfrxEth.sol
derivatives contracts does not allow users to submit a deadline via withdraw
when unstaking safEth via unstake()
in the SafETH
contract.
Similarly, the Reth.sol
derivative contract does not allow users to submit a deadline via deposit()
when staking safEth via stake()
in the SafETH
contract.
Transaction can be pending in the mempool for a long time and without deadline check, the transaction can be executed a long time after the user submits the transaction. By the time user's transaction is executed, the swap could have been done at a sub-optimal price, resulting in lesser safETH tokens minted or more safeETH tokens burned than expected.
Proof of Concept
1.Alice wants to unstake 1 safEth token for ETH and later sell the 1 ETH for 2000 DAI. She signs the transaction calling SafeEth.unstake
with inputAmount = 1 safEth and minOut
= 0.99 ETH to allow for some slippage.
2.The transaction is submitted to the mempool, however, Alice chose a transaction fee that is too low for miners to be interested in including her transaction in a block. The transaction stays pending in the mempool for extended periods, which could be hours, days, weeks, or even longer.
3.When the average gas fee dropped far enough for Alice's transaction to become interesting again for miners to include it, her swap will be executed. In the meantime, the price of ETH could have drastically decreased. She will still at least get 0.99 ETH due to minOut
, but the DAI value of that output might be significantly lower. She has unknowingly performed a bad trade due to the pending transaction she forgot about.
An even worse way this issue can be maliciously exploited is through MEV:
1.The unstake transaction is still pending in the mempool. Average fees are still too high for miners to be interested in it. The price of safEth has gone up significantly since the transaction was signed, meaning Alice would receive a lot more ETH when the swap is executed. But that also means that her minOutput
value is outdated and would allow for significant slippage.
2.A MEV bot detects the pending transaction. Since the outdated minOut
now allows for high slippage, the bot sandwiches Alice, resulting in significant profit for the bot and significant loss for Alice.
The reverse can happen when staking ETH for Reth.sol
derivatives contract.
Code Snippet
Tools Used
Manual Analysis
Recommendation
Intoduce a deadline
parameter to the functions withdraw()
for WstEth.sol
and SfrxEth.sol
and deposit()
for Reth.sol
. This can be in the form of a modifier such as
modifier ensure(uint deadline) {
require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
_;
}
Incorrect Calculation of Price
Lines of code
Vulnerability details
Impact
The function returns the price of WstETH in terms of stETH. The underlying token which we desire is ETH.
Since stETH does not have the same value as WETH the output price incorrect.
Proof of Concept
function ethPerDerivative(uint256 _amount) public view returns (uint256) {
return IWStETH(WST_ETH).getStETHByWstETH(10 ** 18);
}
Tools Used
Manual review
Recommended Mitigation Steps
Add extra steps to approximate the rate for converting stETH to ETH.
Lack of slippage protection
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L48-L50
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L51-L53
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L58-L60
Vulnerability details
[M-03] Lack of slippage protection
Impact
Users can lose funds when setting maximum slippage via setMaxSlippage()
Proof of Concept
/WstEth.sol
48: function setMaxSlippage(uint256 _slippage) external onlyOwner {
49: maxSlippage = _slippage;
50: }
/SfrxEth.sol
51: function setMaxSlippage(uint256 _slippage) external onlyOwner {
52: maxSlippage = _slippage;
53: }
/Reth.sol
58: function setMaxSlippage(uint256 _slippage) external onlyOwner {
59: maxSlippage = _slippage;
60: }
The default maximum slippage is set at 1% and can be changed via owner only setMaxSlippage()
function. Owner of derivative contract can maliciously set a unbounded or too large maxSlippage
leading to unintended losses by users staking.
Tools
Manual Analysis
Recommendation
Consider adding input validation checks and timelocks to bound _slippage
and to allow users to react to slippage changes. Or add a warning on the UI of the protocol to warn and protect users when setting large slippages or when dealing with large token amounts.
Refer to this link for suggestions on slippage rate protection
Precision Loss that can lead to potential loss of funds through front running and sandwich attacks
Lines of code
Vulnerability details
Impact
60: function withdraw(uint256 _amount) external onlyOwner {
61: IsFrxEth(SFRX_ETH_ADDRESS).redeem(
62: _amount,
63: address(this),
64: address(this)
65: );
66: uint256 frxEthBalance = IERC20(FRX_ETH_ADDRESS).balanceOf(
67: address(this)
68: );
69: IsFrxEth(FRX_ETH_ADDRESS).approve(
70: FRX_ETH_CRV_POOL_ADDRESS,
71: frxEthBalance
72: );
73: uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) *
74: (10 ** 18 - maxSlippage)) / 10 ** 18;
75:
76: IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).exchange(
77: 1,
78: 0,
79: frxEthBalance,
80: minOut
81: );
82: // solhint-disable-next-line
83: (bool sent, ) = address(msg.sender).call{value: address(this).balance}(
84: ""
85: );
86: require(sent, "Failed to send Ether");
87: }
There is a potential precision loss when calculating the minimumOut
representing the minimum coins received during swap of sfrxETH to ETH, minOut
minimum amount of ETH to receive may be set to zero when the _amount
parameter to withdraw is low, such as when amount to withdraw is lower than one Eth.
73: uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) *
74: (10 ** 18 - maxSlippage)) / 10 ** 18;
If minOut is computed as 0, slippage check would fail and the swap is at risk of being front/run or sandwiched when it is triggered when unstake
is called in SafEth.sol
which can lead to loss of funds for users.
Tools Used
Manual Review
Recommendation
Perform multiplication before division to ensure no precision loss and possibility of minOut
to be set as zero.
uint256 minOut = (ethPerDerivative(_amount) * _amount *
(10 ** 18 - maxSlippage)) / 10 ** 18 / 10 ** 18;
QA Report
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
QA Report
See the markdown file with the details of this report here.
QA Report
See the markdown file with the details of this report here.
`SafEth.sol` is intended to be upgradable but inherits from contracts that contain storage and no gaps
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L15
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEthStorage.sol#L15
Vulnerability details
Impact
SafEth.sol
inherits from contractSafEthStorage.sol
that don't contain storage gaps which can be dangerous when upgrading.- Consequences that overwriting storage can lead to
Refer to the bottom part of this article: https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable
Proof of Concept
At the moment, the storage layout for the SafEth.sol contract will look like this:
storage slots:
- Initializable
- ERC20Upgradeable
- OwnableUpgradeable
- SafEthStorage
Since SafEthStorage.sol
does not have a ___gap
, the following situation can now occur:
- The
SafEth
contract is upgraded and starts inheriting a new contract, for example,ReentrancyGuard
- Initializable
- ERC20Upgradeable
- OwnableUpgradeable
- SafEthStorage
- ReentrancyGuard
- The contract is upgraded again, but now a new variable is added to
SafEthStorage.sol
In this case, since we don't have a ___gap
for SafEthStorage.sol
, we will encounter a situation where the new variable overwrites the storage of ReentrancyGuard
- Initializable
- ERC20Upgradeable
- OwnableUpgradeable
- SafEthStorage
- 1 slot from SafEthStorage and ReentrancyGuard on some place
Tools Used
Manual review
Recommended Mitigation Steps
Consider adding a storage gap at the end of the upgradeable abstract contract SafEthStorage.sol
uint256[50] private __gap;
User can lose funds / Funds can stuck
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L138
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L165
Vulnerability details
Impact
"User can lose funds/Funds can stuck" in below 2 scenarios
- Admin mistakenly changes weight of an non existing _derivativeIndex
- Admin has changed weight of all derivatives to 0
Proof of Concept
- Lets say currently
derivativeCount = 3
weights[0] = 10;
weights[1] = 10;
weights[2] = 10;
totalWeight = 30
- Admin calls
adjustWeight
function and instead of using_derivativeIndex
as 2, mistakenly uses 3, with weight of 20
function adjustWeight(
uint256 _derivativeIndex,
uint256 _weight
) external onlyOwner {
weights[_derivativeIndex] = _weight;
uint256 localTotalWeight = 0;
for (uint256 i = 0; i < derivativeCount; i++)
localTotalWeight += weights[i];
totalWeight = localTotalWeight;
emit WeightChange(_derivativeIndex, _weight);
}
- This updates like below:
weights[3] = 20;
totalWeight = 50
derivativeCount = 3 (since this was not updated)
- Now if Admin call
rebalanceToWeights
function
function rebalanceToWeights() external onlyOwner {
...
for (uint i = 0; i < derivativeCount; i++) {
if (weights[i] == 0 || ethAmountToRebalance == 0) continue;
uint256 ethAmount = (ethAmountToRebalance * weights[i]) /
totalWeight;
// Price will change due to slippage
derivatives[i].deposit{value: ethAmount}();
}
...
}
- Since derivativeCount is 3 instead of 4 so loop runs for the initial 3 derivatives.
- The problem is
totalWeight
which is also considering the weight 20 of the mistakenly added derivative, so if ethAmountToRebalance was 50 then
uint256 ethAmount = (ethAmountToRebalance * weights[i]) / totalWeight;
// This becomes
ethAmount = (50 * 10) / 50; = 10
// instead of
ethAmount = (50 * 10) / 30; = 16
- So rebalancing will be incorrect (depositing 6 less tokens to derivative)
- This also causes the excess 50-30=20 balance to get stuck in contract as only amount 30 was deposited and amount 20 was untouched in contract
uint256 ethAmountBefore = address(this).balance;
for (uint i = 0; i < derivativeCount; i++) {
if (derivatives[i].balance() > 0)
derivatives[i].withdraw(derivatives[i].balance());
}
uint256 ethAmountAfter = address(this).balance;
uint256 ethAmountToRebalance = ethAmountAfter - ethAmountBefore;
- Similarly if Admin mistakenly set derivative weight to 0 and calls
rebalanceToWeights
function, then during unstake user get 0 funds due to derivative balance being 0
// withdraw a percentage of each asset based on the amount of safETH
uint256 derivativeAmount = (derivatives[i].balance() *
_safEthAmount) / safEthTotalSupply;
Recommended Mitigation Steps
Add below checks:
function adjustWeight(
uint256 _derivativeIndex,
uint256 _weight
) external onlyOwner {
require(_derivativeIndex<derivativeCount, "Incorrect index provided");
}
function rebalanceToWeights() external onlyOwner {
require(totalWeight>0, "No derivative to rebalance");
}
QA Report
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
QA Report
See the markdown file with the details of this report here.
QA Report
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
set of sqrtPriceLimitX96 is 0 disables the slippage protection.
Lines of code
Vulnerability details
Impact
protect against price impact is disabled by set sqrtPriceLimitX96 to 0 .
Proof of Concept
function swapExactInputSingleHop(
address _tokenIn,
address _tokenOut,
uint24 _poolFee,
uint256 _amountIn,
uint256 _minOut
) private returns (uint256 amountOut) {
IERC20(_tokenIn).approve(UNISWAP_ROUTER, _amountIn);
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
.ExactInputSingleParams({
tokenIn: _tokenIn,
tokenOut: _tokenOut,
fee: _poolFee,
recipient: address(this),
amountIn: _amountIn,
amountOutMinimum: _minOut,
sqrtPriceLimitX96: 0 <- here
});
amountOut = ISwapRouter(UNISWAP_ROUTER).exactInputSingle(params);
}
Tools Used
Manual Review
Recommended Mitigation Steps
Use separate price limit parameters for the perp order and the uniswap swap.
ref
https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps
Derivative contracts are emptied after calling withdraw()
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L63
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L84
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L110
Vulnerability details
Impact
withdraw()
method accepts amount
parameter which is used to calculate how much eth should be send to the owner(SafEth
) but then it sends the whole balance(address(this).balance
) which empties the balance of the contract and will result in incorrect arithmetics wherever the derivative contract's balance()
method is used.
Proof of Concept
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L63
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L84
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L110
Tools Used
Manual review
Recommended Mitigation Steps
Pass only the necessary amount to address(msg.sender).call
Gas Optimizations
See the markdown file with the details of this report here.
miscalculation of ethPerDerivative for SfrxETH can lead to loss of funds when staking due to higher prices
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L72-L75
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L111-L117
Vulnerability details
Impact
When staking , ethPerDerivative for SfrxETH is part of the sum that determines the price of the safETH tokens. However, due to an incorrect computation in ethPerDerivative
, the returned sum will be higher and hence price of safETH tokens will be higher than it should be, causing loss of funds to user as they receive lesser tokens in return.
Proof of Concept
In staking
of safETH
, each derivative will call ethPerDerivative to get the value of the derivative in ETH value. SfrxETH
is on of the derivative.
underlyingValue +=
(derivatives[i].ethPerDerivative(derivatives[i].balance()) *
derivatives[i].balance()) /
10 ** 18;
However, in SfrxETH
, notice return ((10 ** 18 * frxAmount) / IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle());
. This is incorrect as price_oracle
returns how much 1 frxETH is worth in terms of ETH. This can be confirmed by going to https://curve.fi/#/ethereum/swap and seeing the exchange rate of 1 frxETH to ETH. See price_oracle
return value.
Even though frxETH currently worths less than ETH, the computation will cause ethPerDerivative for SfrxETH to be more than frxETH amount. Even if frxETH suddenly becomes worth more than ETH, the problem will flip to become ethPerDerivative for SfrxETH is less than frxETH amount.
function ethPerDerivative(uint256 _amount) public view returns (uint256) {
uint256 frxAmount = IsFrxEth(SFRX_ETH_ADDRESS).convertToAssets(
10 ** 18
);
return ((10 ** 18 * frxAmount) /
IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle());
}
Tools Used
Manual Review
Recommended Mitigation Steps
Recommend using get_virtual_price
that gets the worth of 1 ETH in terms of frxETH. See get_virtual_price
return value.
return ((10 ** 18 * frxAmount) /
(10**18/IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).get_virtual_price()));
Reth derivative is vulnerable to oracle manipulation with flashloan
Lines of code
Vulnerability details
Impact
Reth pool price can be manipulated to cause loss of funds for the protocol and other users
Proof of Concept
Reth poolPrice
uses the UniV3Pool.slot0 to determine the price of reth/eth, slot0 is the most recent data point and can easily be manipulated.
This allows a malicious user to manipulate the valuation of the rETH. An example of this kind of manipulation would be to use large amount of reth to be withdraw.
Tools Used
Manual review
Recommended Mitigation Steps
Consider using TWAP oracle instead of reading from slot0
When stake the user could lose all funds if totalWeight is 0
Lines of code
Vulnerability details
Impact
In adjustWeight
it doesn't validate that totalWeight
must be greater than 0. If all weights are set to 0 accidentally by contract owner somehow, then when user calls stake
it will receive 0 safEth
tokens. In such case he/she can't unstake
any tokens.
For example currently the function adjustWeight can only update one derivative at a time. Let's suppose there are two derivatives, derivative0's weight is 0 and derivative1's weight is 10 * 18, and if we need to adjust derivative0's weight to 10 * 18 and derivative1's weight to 0 then we must pay attention to the order of adjusting the two derivatives. If adjust derivative1's weight to 0 first then there could be a short period during which totalWeight
is 0.
For this protocol there should be an invariant that totalWeight
should always be greater than 0.
Proof of Concept
I added a test in SafEth.test.ts, it will
1. Set all derivatives' weight to 0
2. Stake 200ETH
3. Validate the user's SafETH balance should be greater than 0, which will fail
describe("stake with totalWeight be 0", function () {
it ("should revert if totalWeight is 0", async function () {
const derivativeCount = (await safEthProxy.derivativeCount()).toNumber();
const zeroWeight = BigNumber.from("0");
for (let i = 0; i < derivativeCount; i++) {
const tx1 = await safEthProxy.adjustWeight(i, zeroWeight);
await tx1.wait();
}
const depositAmount = ethers.utils.parseEther("200");
const tx1 = await safEthProxy.stake({ value: depositAmount });
const [signer] = await ethers.getSigners();
// This expect will fail because the totalWeight is 0, however when signer calls
// stake he should always receive SafETH if the tx doesn't revert
expect((await safEthProxy.balanceOf(signer.address)).gt(0)).to.equal(true);
});
});
Tools Used
Manual review
Recommended Mitigation Steps
In stake function add following validation
require(totalWeight > 0, "Weight is not set");
QA Report
See the markdown file with the details of this report here.
QA Report
See the markdown file with the details of this report here.
Attacker can manipulate rETH-derivative's ethPerDerivative() to mint discounted safETH and drain funds.
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L212
Vulnerability details
Impact
An attacker can manipulate ethPerDerivative() for the rETH-derivative, allowing him to mint cheap SafETH which can then be unstaked at normal prices. If repeated, could drain a large amount of funds.
Mechanisms
-
ethPerDerivative() for rETH changes depending on _amount: either returns the poolPrice or the native rETH price.
-
An attacker can manipulate the poolPrice to significantly discount rETH (e.g., using flashloan)
Proof of Concept
Assumptions:
- rETH is the only derivative used (for math simplicity)
- rETH native price is 1 ETH (for math simplicity)
- SafETH holds 5000 rETH (worth 5000 ETH)
Exploit:
- Attacker sells large amount of rETH on uniswap (e.g., using flashloan) -> rETH price is discounted to 0.5ETH
- Attacker stakes 100 ETH on SafETH
- ethPerDerivative on line 73 in stake() returns the poolPrice. This is because SafETH holds 5000 rETH, which is larger than Rocket Pool's deposit pool limit, thus poolCanDeposit() will be false.
- As a result underlyingValue will only be half of the actual (native) value.
- ethPerDerivative on line 92 in stake() will return the native value.
- mintAmount will be double of what it should be. -> Attacker minted SafETH at half the price
- Attacker rebuys large amount of rETH on uniswap
- Attacker unstakes SafETH at normal prices
- Repeat
Recommended Mitigation Steps
ethPerDerivative() should return the same value regardless of amount. Either the market price or the native price.
QA Report
See the markdown file with the details of this report here.
Loss of precision in calc preDepositPrice
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L68-L81
Vulnerability details
Impact
When calculating underlyingVaule
, it gets every derivative price multiplied reverse. But the code firstly converts it's to ether (div 1018), but afterwhile code use underlyingValue multiplies 1018 to calculate preDepositPrice
, it may cause precision loss in price.
Proof of Concept
uint256 underlyingValue = 0;
// Getting underlying value in terms of ETH for each derivative
for (uint i = 0; i < derivativeCount; i++)
underlyingValue +=
(derivatives[i].ethPerDerivative(derivatives[i].balance()) *
derivatives[i].balance()) /
10 ** 18;
uint256 totalSupply = totalSupply();
uint256 preDepositPrice; // Price of safETH in regards to ETH
if (totalSupply == 0)
preDepositPrice = 10 ** 18; // initializes with a price of 1
else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;
Tools Used
manual review
Recommended Mitigation Steps
Remove div 1018 in calc underlyingVaule and also remove multiplies 1018 in calc preDepositPrice.
Protocol susceptible to price manipulation
Lines of code
Vulnerability details
Impact
The price of derivative tokens is obtained from the current pool price, which makes it susceptible to price manipulation through flash loan. which may pose a risk. Price can be easily manipulated by attackers using flash loans to control the pool's price. So attacker can mint more than expected SafEth token. Then attacker can use SafEth to unsatke to make profit. Therefore, it is advisable to use like Uniswap's TWAP to obtain an average price.
The stake() & unstake() functions in SafEth.sol relies:
1.In Reth.sol ethPerDerivative() fetch the current price of Reth from uniswap. Use poolPrice() to calculate Reth's underlying value, derivativeReceivedEthValue, and the derivative.deposit{} of Reth will be affected as well.
2.In SfrxEth.sol ethPerDerivative() fetch the current price of frxETH from curve pool over price_oracle.
3.In WstEth.sol ethPerDerivative() fetch the current price of WstEth from stETH.getPooledEthByShares(_wstETHAmount)
@return the amount of Ether that corresponds to _sharesAmount
token shares.
These three prices are the current prices and are susceptible to price manipulation.
Proof of Concept
@notice - Price of derivative in liquidity pool
*/
function poolPrice() private view returns (uint256) {
address rocketTokenRETHAddress = RocketStorageInterface(
ROCKET_STORAGE_ADDRESS
).getAddress(
keccak256(
abi.encodePacked("contract.address", "rocketTokenRETH")
)
);
IUniswapV3Factory factory = IUniswapV3Factory(UNI_V3_FACTORY);
IUniswapV3Pool pool = IUniswapV3Pool(
factory.getPool(rocketTokenRETHAddress, W_ETH_ADDRESS, 500)
);
(uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
return (sqrtPriceX96 * (uint(sqrtPriceX96)) * (1e18)) >> (96 * 2);
}
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L63-L99
function stake() external payable {
require(pauseStaking == false, "staking is paused");
require(msg.value >= minAmount, "amount too low");
require(msg.value <= maxAmount, "amount too high");
uint256 underlyingValue = 0;
// Getting underlying value in terms of ETH for each derivative
for (uint i = 0; i < derivativeCount; i++)
underlyingValue +=
(derivatives[i].ethPerDerivative(derivatives[i].balance()) * //@audit reth price can be manipulated over flash loan
derivatives[i].balance()) /
10 ** 18;
uint256 totalSupply = totalSupply();
uint256 preDepositPrice; // Price of safETH in regards to ETH
if (totalSupply == 0)
preDepositPrice = 10 ** 18; // initializes with a price of 1
else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;
uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system
for (uint i = 0; i < derivativeCount; i++) {
uint256 weight = weights[i];
IDerivative derivative = derivatives[i];
if (weight == 0) continue;
uint256 ethAmount = (msg.value * weight) / totalWeight;
// This is slightly less than ethAmount because slippage
uint256 depositAmount = derivative.deposit{value: ethAmount}(); //@audit reth price can be manipulated over flash loan
uint derivativeReceivedEthValue = (derivative.ethPerDerivative( //@audit reth price can be manipulated over flash loan
depositAmount
) * depositAmount) / 10 ** 18;
totalStakeValueEth += derivativeReceivedEthValue;
}
// mintAmount represents a percentage of the total assets in the system
uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice;
_mint(msg.sender, mintAmount);
emit Staked(msg.sender, msg.value, mintAmount);
}
IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle());
function ethPerDerivative(uint256 _amount) public view returns (uint256) {
return IWStETH(WST_ETH).getStETHByWstETH(10 ** 18);
}
Tools Used
Manual
Recommended Mitigation Steps
Use TWAP to obtain an average price.
Gas Optimizations
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
Add access control to unstake() function
Lines of code
Vulnerability details
The unstake() function allows any user to withdraw tokens from the contract. This could be a potential security issue if the contract holds a large amount of assets. It would be better to add an access control mechanism, such as a onlyOwner modifier, to restrict who can call this function.
Gas Optimizations
See the markdown file with the details of this report here.
Minimum output size for stETH sell does not account for possible market depeg
Lines of code
Vulnerability details
Impact
The withdraw() function for wstETH-derivative does not account for a possible discount of stETH. This can cause funds to be frozen for a while.
Explanation
wstETH derivative's withdraw() function calculates minOut with the assumption that 1 stETH = 1 ETH. This is not always true, stETH has traded at large discounts (>1%) for extended periods of time. If the maxSlippage is set to 1% while stETH has a large discount, the (stETH sell)[https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L61] would revert, causing unstake() to revert, resulting in all funds being stuck. An operator would have to manually increase slippage for this derivative to fix it.
Recommended Mitigation Steps
- Calculate minOut based on current market price for stETH.
When rocketpool node deposits are not enabled, users will not be able to stake.
Lines of code
Vulnerability details
Impact
When rocketpool node deposits are not enabled, users will not be able to stake.
Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Whenever user stake system check whether rocketpool can be deposited to - poolCanDeposit
function ethPerDerivative(uint256 _amount) public view returns (uint256) {
if (poolCanDeposit(_amount))
return
RocketTokenRETHInterface(rethAddress()).getEthValue(10 ** 18);
else return (poolPrice() * 10 ** 18) / (10 ** 18);
}
SafEth/derivatives/Reth.sol#L212
Here is the function poolCanDeposit
function poolCanDeposit(uint256 _amount) private view returns (bool) {
address rocketDepositPoolAddress = RocketStorageInterface(
ROCKET_STORAGE_ADDRESS
).getAddress(
keccak256(
abi.encodePacked("contract.address", "rocketDepositPool")
)
);
RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(
rocketDepositPoolAddress
);
address rocketProtocolSettingsAddress = RocketStorageInterface(
ROCKET_STORAGE_ADDRESS
).getAddress(
keccak256(
abi.encodePacked(
"contract.address",
"rocketDAOProtocolSettingsDeposit"
)
)
);
RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(
rocketProtocolSettingsAddress
);
return
rocketDepositPool.getBalance() + _amount <=
rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize() &&
_amount >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit();
}
SafEth/derivatives/Reth.sol#L120
I think the whole system was designed to work even when user cannot deposit to rocketpool. Here you can see code from rocketpool, require from line 77 is not implemented, all the other requires are in the place
function deposit() override external payable onlyThisLatestContract {
// Check deposit settings
RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit"));
77: require(rocketDAOProtocolSettingsDeposit.getDepositEnabled(), "Deposits into Rocket Pool are currently disabled");
require(msg.value >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit(), "The deposited amount is less than the minimum deposit size");
RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault"));
require(rocketVault.balanceOf("rocketDepositPool").add(msg.value) <= rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize(), "The deposit pool size after depositing exceeds the maximum size");
// Calculate deposit fee
uint256 depositFee = msg.value.mul(rocketDAOProtocolSettingsDeposit.getDepositFee()).div(calcBase);
uint256 depositNet = msg.value.sub(depositFee);
// Mint rETH to user account
RocketTokenRETHInterface rocketTokenRETH = RocketTokenRETHInterface(getContractAddress("rocketTokenRETH"));
rocketTokenRETH.mint(depositNet, msg.sender);
// Emit deposit received event
emit DepositReceived(msg.sender, msg.value, block.timestamp);
// Process deposit
processDeposit(rocketVault, rocketDAOProtocolSettingsDeposit);
}
deposit/RocketDepositPool.sol#L73-L91
Tools Used
Manual review
Recommended Mitigation Steps
Add missing check, so user`s transactions will not be reverted when rocketpool node deposits are not enabled
function poolCanDeposit(uint256 _amount) private view returns (bool) {
address rocketDepositPoolAddress = RocketStorageInterface(
ROCKET_STORAGE_ADDRESS
).getAddress(
keccak256(
abi.encodePacked("contract.address", "rocketDepositPool")
)
);
RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(
rocketDepositPoolAddress
);
address rocketProtocolSettingsAddress = RocketStorageInterface(
ROCKET_STORAGE_ADDRESS
).getAddress(
keccak256(
abi.encodePacked(
"contract.address",
"rocketDAOProtocolSettingsDeposit"
)
)
);
RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(
rocketProtocolSettingsAddress
);
return
+ rocketDAOProtocolSettingsDeposit.getDepositEnabled() &&
rocketDepositPool.getBalance() + _amount <=
rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize() &&
_amount >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit();
}
Using Uniswap spot price as price oracle.
Lines of code
Vulnerability details
Impact
In Reth.sol
, function poolPrice
use to calc ethPerDerivative and mints shares to user. But it use Uniswap spot price, which is easy to manipulate by flashloan. malicious users can use flashloan to manipulate price in order to mint more shares. It is dangerouse.
Proof of Concept
function poolPrice() private view returns (uint256) {
address rocketTokenRETHAddress = RocketStorageInterface(
ROCKET_STORAGE_ADDRESS
).getAddress(
keccak256(
abi.encodePacked("contract.address", "rocketTokenRETH")
)
);
IUniswapV3Factory factory = IUniswapV3Factory(UNI_V3_FACTORY);
IUniswapV3Pool pool = IUniswapV3Pool(
factory.getPool(rocketTokenRETHAddress, W_ETH_ADDRESS, 500)
);
(uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
return (sqrtPriceX96 * (uint(sqrtPriceX96)) * (1e18)) >> (96 * 2);
}
Tools Used
manual review
Recommended Mitigation Steps
Recommend use Uniswap TWAP oracle to get pool price which is very hard to manpulate.
division before multiplication in SfrxEth can cause slippage to be higher than expected
Lines of code
Vulnerability details
Impact
When withdrawing from SfrxEth, slippage control is put in place when swapping sFrxEth for Eth. However, due to division before multiplication when calculating minOut, slippage control can be lower than intended, causing slippage that is more than expected.
Proof of Concept
In withdraw
of SfrxEth
minOut act as the slippage control. Notice that multiplication before division is done which can cause minOut to be lower than intended. This can cause slippage to be more than expected.
uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) *
(10 ** 18 - maxSlippage)) / 10 ** 18;
IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).exchange(
1,
0,
frxEthBalance,
minOut
);
Note: This also applies to deposit
in Reth
.
uint256 minOut = ((((rethPerEth * msg.value) / 10 ** 18) *
((10 ** 18 - maxSlippage))) / 10 ** 18);
Tools Used
Manual Review
Recommended Mitigation Steps
Recommend multiplying before dividing.
uint256 minOut = ethPerDerivative(_amount) * _amount *
(10 ** 18 - maxSlippage) / 10 ** 18 / 10 ** 18;
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.