Platform: Code4rena
Start Date: 24/03/2023
Pot Size: $49,200 USDC
Total HM: 20
Participants: 246
Period: 6 days
Judge: Picodes
Total Solo HM: 1
Id: 226
League: ETH
Rank: 195/246
Findings: 1
Award: $13.13
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: brgltd
Also found by: 0x3b, 0xAgro, 0xGusMcCrae, 0xNorman, 0xRajkumar, 0xSmartContract, 0xTraub, 0xWagmi, 0xWaitress, 0xffchain, 0xhacksmithh, 0xkazim, 0xnev, 3dgeville, ArbitraryExecution, Aymen0909, BRONZEDISC, Bason, Bloqarl, BlueAlder, Brenzee, CodeFoxInc, CodingNameKiki, Cryptor, DadeKuma, DevABDee, Diana, Dug, Englave, Gde, Haipls, HollaDieWaldfee, Ignite, Infect3d, Jerry0x, Josiah, Kaysoft, Koko1912, KrisApostolov, Lavishq, LeoGold, Madalad, PNS, Rappie, RaymondFam, RedTiger, Rickard, Rolezn, Sathish9098, SunSec, T1MOH, UdarTeam, Udsen, Viktor_Cortess, Wander, adriro, ak1, alejandrocovrr, alexzoid, arialblack14, ayden, bin2chen, brevis, btk, c3phas, carlitox477, catellatech, ch0bu, chaduke, ck, climber2002, codeslide, descharre, dingo2077, ernestognw, fatherOfBlocks, favelanky, georgits, helios, hl_, inmarelibero, juancito, ks__xxxxx, lopotras, lukris02, m_Rassska, mahdirostami, maxper, nadin, navinavu, nemveer, p_crypt0, peanuts, pipoca, pixpi, qpzm, rbserver, reassor, roelio, rotcivegaf, scokaf, siddhpurakaran, slvDev, smaul, tnevler, tsvetanovv, turvy_fuzz, vagrant, wen, yac, zzzitron
13.1298 USDC - $13.13
SC: SafEth.sol
The core of lack is lying in function addDerivative()
where any address can be added as vault.
If in new vault has not implemented IDerivative interface or even uncorrect realization of functions - user's can't stake or stake their funds. Also there is no function to delete derivative contract.
Foundry test:
forge test --fork-url mainnet -vvv
pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../../src/SafEth/SafEth.sol"; import "../../src/SafEth/derivatives/Reth.sol"; import "../../src/SafEth/derivatives/SfrxEth.sol"; import "../../src/SafEth/derivatives/WstEth.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract SafEthAudit is Test { SafEth public safEthSC; SafEth public SafEthProxy; ERC1967Proxy public safERC1967ProxySC; Reth public rethEthSC; Reth public RethProxy; ERC1967Proxy public rethERC1967ProxySC; SfrxEth public SfrxEthSC; SfrxEth public SfrxEthProxy; ERC1967Proxy public sfrxEthERC1967ProxySC; WstEth public WstEthSC; WstEth public WstEthProxy; ERC1967Proxy public wstEthERC1967ProxySC; address eoa = vm.addr(123); address user1 = vm.addr(12347); address attacker = vm.addr(1234); function setUp() public { vm.deal(eoa, 100 ether); vm.deal(user1, 100 ether); vm.deal(attacker, 100000 ether); vm.startPrank(eoa); //SafEthProxy and SafETH deploy; safEthSC = new SafEth(); safERC1967ProxySC = new ERC1967Proxy(address(safEthSC),abi.encodeWithSelector( safEthSC.initialize.selector, "TokenName", "TSBL" )); SafEthProxy = SafEth(payable(address(safERC1967ProxySC))); //RethProxy and Reth deploy; rethEthSC = new Reth(); rethERC1967ProxySC = new ERC1967Proxy(address(rethEthSC),abi.encodeWithSelector( rethEthSC.initialize.selector,address(SafEthProxy))); RethProxy = Reth(payable(address(rethERC1967ProxySC))); //SfrxEthProxy and SfrxEth deploy; SfrxEthSC = new SfrxEth(); sfrxEthERC1967ProxySC = new ERC1967Proxy(address(SfrxEthSC),abi.encodeWithSelector( SfrxEthSC.initialize.selector,address(SafEthProxy))); SfrxEthProxy = SfrxEth(payable(address(sfrxEthERC1967ProxySC))); //WstEthProxy and WstEth deploy; WstEthSC = new WstEth(); wstEthERC1967ProxySC = new ERC1967Proxy(address(WstEthSC),abi.encodeWithSelector( WstEthSC.initialize.selector,address(SafEthProxy))); WstEthProxy = WstEth(payable(address(wstEthERC1967ProxySC))); console.log("eoa address: ",address(eoa)); console.log("SafEthProxy address: ",address(SafEthProxy)); console.log("safEthSC address impl: ",address(safEthSC)); console.log("=============================================================="); console.log("RethProxy address: ",address(RethProxy)); console.log("rethEthSC address impl: ",address(rethEthSC)); console.log("=============================================================="); console.log("SfrxEthProxy address: ",address(SfrxEthProxy)); console.log("SfrxEthSC address impl: ",address(SfrxEthSC)); console.log("=============================================================="); console.log("WstEthProxy address: ",address(WstEthProxy)); console.log("WstEthSC address impl: ",address(WstEthSC)); //Start config SafEthProxy.addDerivative(address(RethProxy),100); SafEthProxy.addDerivative(address(SfrxEthProxy),100); SafEthProxy.addDerivative(address(WstEthProxy),100); SafEthProxy.setMaxSlippage(0,1e16); SafEthProxy.setMaxSlippage(1,1e16); SafEthProxy.setMaxSlippage(2,1e16); SafEthProxy.setMinAmount(0); SafEthProxy.setMaxAmount(200e18); vm.stopPrank(); } uint256 maxSlip = 1e16; function testStake() public { vm.startPrank(user1); SafEthProxy.stake{value: 1 ether}(); uint256 balanceLP = SafEthProxy.balanceOf(user1); vm.stopPrank(); vm.prank(eoa); SafEthProxy.addDerivative(address(msg.sender),100); vm.startPrank(user1); SafEthProxy.unstake(balanceLP); } }
Add in function addDerivative
checking process:
require((bool success,) = derivative.call(abi.encodeWithSignature("deposit()")),"Incorrect Interface");
require((bool success,) = derivative.call(abi.encodeWithSignature("withdraw()")),"Incorrect Interface");
require (success)
SC: All in scope.
Despite on fact that maxSlippage
is being set during initialize()
process in every derivative contract, at SafEth.sol
there is function setMaxSlippage()
that allows to set any slippage from 0 to uint256.max. There is no require(maxSlippage < 1e17)
by default.
So there are some scenarios when user funds could be lost.
1 . If owner sets maxSlippage
more than 1e18
in WstEth.sol
the transaction will be reverted. (due to the no sign in uint);
2 . Despite on fact that setMinAmount
is setting during initialize()
process in every derivative contract, at SafEth.sol
there is function setMinAmount()
which allows to set any amount with no requirements). So user deposits 10 wei. Slippage currently 1% (1e16). Actual minOut
in withdraw()
function will be 9 wei. (9.99 round down due to the EVM principles.) What at fact 10% slippage. Or other example: user deposits 101 wei, where slippage is set to 10%. Actual slippage = 10.1, but EVM slippage = 10. User could lost more than his script configured.
Such no-value loses can actually break logic if user use scripts or other smart contract which relies to actual smart contract code.
Short test with another one example:
uint256 maxSlip = 1e16; function testFuzz_Slippage(uint256 ethDep) public { vm.assume(ethDep > 1 ether && ethDep < 200 ether); console.log("ethDep: ",ethDep); console.log("maxSlip: ",maxSlip); uint256 minOutSC = (ethDep * (10 ** 18 - maxSlip)) / 10 ** 18; uint256 minOutReal = ethDep - 1e16 * ethDep / 1e18; uint256 actualSlippage = ethDep - minOutSC; console.log("Slippace calculated in current SC: ",1e16); console.log("Slippace calculated appropriatly w/o roundring: ",actualSlippage); assertEq(1e16,actualSlippage); }
Full test with deployment process.
pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../../src/SafEth/SafEth.sol"; import "../../src/SafEth/derivatives/Reth.sol"; import "../../src/SafEth/derivatives/SfrxEth.sol"; import "../../src/SafEth/derivatives/WstEth.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract SafEthAudit is Test { SafEth public safEthSC; SafEth public SafEthProxy; ERC1967Proxy public safERC1967ProxySC; Reth public rethEthSC; Reth public RethProxy; ERC1967Proxy public rethERC1967ProxySC; SfrxEth public SfrxEthSC; SfrxEth public SfrxEthProxy; ERC1967Proxy public sfrxEthERC1967ProxySC; WstEth public WstEthSC; WstEth public WstEthProxy; ERC1967Proxy public wstEthERC1967ProxySC; address eoa = vm.addr(123); address user1 = vm.addr(12347); address attacker = vm.addr(1234); function setUp() public { vm.startPrank(eoa); //SafEthProxy and SafETH deploy; safEthSC = new SafEth(); safERC1967ProxySC = new ERC1967Proxy(address(safEthSC),abi.encodeWithSelector( safEthSC.initialize.selector, "TokenName", "TSBL" )); SafEthProxy = SafEth(payable(address(safERC1967ProxySC))); //RethProxy and Reth deploy; rethEthSC = new Reth(); rethERC1967ProxySC = new ERC1967Proxy(address(rethEthSC),abi.encodeWithSelector( rethEthSC.initialize.selector,address(SafEthProxy))); RethProxy = Reth(payable(address(rethERC1967ProxySC))); //SfrxEthProxy and SfrxEth deploy; SfrxEthSC = new SfrxEth(); sfrxEthERC1967ProxySC = new ERC1967Proxy(address(SfrxEthSC),abi.encodeWithSelector( SfrxEthSC.initialize.selector,address(SafEthProxy))); SfrxEthProxy = SfrxEth(payable(address(sfrxEthERC1967ProxySC))); //WstEthProxy and WstEth deploy; WstEthSC = new WstEth(); wstEthERC1967ProxySC = new ERC1967Proxy(address(WstEthSC),abi.encodeWithSelector( WstEthSC.initialize.selector,address(SafEthProxy))); WstEthProxy = WstEth(payable(address(wstEthERC1967ProxySC))); console.log("eoa address: ",address(eoa)); console.log("SafEthProxy address: ",address(SafEthProxy)); console.log("safEthSC address impl: ",address(safEthSC)); console.log("=============================================================="); console.log("RethProxy address: ",address(RethProxy)); console.log("rethEthSC address impl: ",address(rethEthSC)); console.log("=============================================================="); console.log("SfrxEthProxy address: ",address(SfrxEthProxy)); console.log("SfrxEthSC address impl: ",address(SfrxEthSC)); console.log("=============================================================="); console.log("WstEthProxy address: ",address(WstEthProxy)); console.log("WstEthSC address impl: ",address(WstEthSC)); //Start config SafEthProxy.addDerivative(address(RethProxy),100); SafEthProxy.addDerivative(address(SfrxEthProxy),100); SafEthProxy.addDerivative(address(WstEthProxy),100); SafEthProxy.setMaxSlippage(0,10); SafEthProxy.setMaxSlippage(1,10); SafEthProxy.setMaxSlippage(2,10); SafEthProxy.setMinAmount(5e17); SafEthProxy.setMaxAmount(200e18); } uint256 maxSlip = 1e16; function testFuzz_Slippage(uint256 ethDep) public { vm.assume(ethDep > 1 ether && ethDep < 200 ether); console.log("ethDep: ",ethDep); console.log("maxSlip: ",maxSlip); uint256 minOutSC = (ethDep * (10 ** 18 - maxSlip)) / 10 ** 18; uint256 minOutReal = ethDep - 1e16 * ethDep / 1e18; uint256 actualSlippage = ethDep - minOutSC; console.log("Slippace calculated in current SC: ",1e16); console.log("Slippace calculated appropriatly w/o roundring: ",actualSlippage); assertEq(1e16,actualSlippage); } }
As we can see difference:
Manual review
Use require(x % == 0)
definition or mantissa libraries.
SC: all
Despite on fact there is no direct function for transfer to owner or other destination there is backdoor in this implementation.
-Owner set weights for every derivative contract to 0
;
-Owner add new derivative with weight 100
with implemented functions deposit()
but custom logic.
-Owner call rebalanceToWeights
.
-All funds from vault are going to new custom derivativeContract.
Owner have to be at least multisig(better DAO for adding new derivatives);
SC: all vaults.
Let's dive into WstEth.sol
function withdraw(uint256 _amount) external onlyOwner { IWStETH(WST_ETH).unwrap(_amount); uint256 stEthBal = IERC20(STETH_TOKEN).balanceOf(address(this)); IERC20(STETH_TOKEN).approve(LIDO_CRV_POOL, stEthBal); uint256 minOut = (stEthBal * (10 ** 18 - maxSlippage)) / 10 ** 18; IStEthEthPool(LIDO_CRV_POOL).exchange(1, 0, stEthBal, minOut); // solhint-disable-next-line (bool sent, ) = address(msg.sender).call{value: address(this).balance}( "" ); require(sent, "Failed to send Ether"); }
User's trader contract before withdraw()
can calculate expect return value and use this data for other trading process. But if someone send WST
token to WST_ETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0
than return value will be more than expected what can leads to some unexpected circumstances. Because of WstEth.sol is sending whole balance of contract: (bool sent, ) = address(msg.sender).call{value: address(this).balance}
Other contracts have same logic.
Instead of value: address(this).balance
, contract could pull out msg.value by handling it in receive()
fallback function.
#0 - c4-sponsor
2023-04-10T21:07:56Z
toshiSat marked the issue as sponsor acknowledged
#1 - c4-judge
2023-04-24T18:41:06Z
Picodes marked the issue as grade-b