In this level, there is a walletRegistry. As long as someone registries a gnosis wallet, he can get 10 DVT. Up to now, 4 guys have registered, but they haven’t called proxyCreated(). Our goal is to their DVTs in total 40.
There is only one contract providing us. In fact, it involves several other contracts, especially GnosisSafeProxyFactory and GnosisSafe.
2.theory of gnosis wallet
Creating a gnosis wallet is cheap because it uses clone pattern which doesn’t need to deploy the entire logic contract.
To solve this level, we must know how gnosis wallet works and is created.
The proxy contract is created in GnosisSafeProxyFactory contract. And we can create a gnosis wallet by createProxyWithCallback(). Attention, it contains a variable named initializer which we can exploit while it is used to initialized the wallet.
After that, it will call deployProxyWithNonce() which uses CREATE2 to create a wallet. By the way, the bytecode in CREATE2 is constant because it uses clone pattern.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
function deployProxyWithNonce( address _singleton, bytes memory initializer, uint256 saltNonce ) internal returns (GnosisSafeProxy proxy) { // If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating it bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce)); bytes memory deploymentData = abi.encodePacked(type(GnosisSafeProxy).creationCode, uint256(uint160(_singleton))); // solhint-disable-next-line no-inline-assembly assembly { proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt) } require(address(proxy) != address(0), "Create2 call failed"); }
3.attack logic
As we know, they four registered but don’t call proxyCreated() to get 10 DET yet. We help them to call because there is no limit. So we can exploit during help them :)
There are a lot of require() in it, let’s analyses it:
require(token.balanceOf(address(this)) >= TOKEN_PAYMENT, "Not enough funds to pay");: the level contract should hold enough DVT.
require(msg.sender == walletFactory, "Caller must be factory");: Only GnosisSafeProxyFactory can call it.
require(singleton == masterCopy, "Fake mastercopy used");: we can only create the same wallet which is masterCopy
require(bytes4(initializer[:4]) == GnosisSafe.setup.selector, "Wrong initialization");: the wallet must initialized by setup(). We can exploit in it! Because we can pass any data in setup()
require(GnosisSafe(walletAddress).getThreshold() == MAX_THRESHOLD, "Invalid threshold");: Threshold must be 1.
require(GnosisSafe(walletAddress).getOwners().length == MAX_OWNERS, "Invalid number of owners"); : there must be only one owner.
require(beneficiaries[walletOwner], "Owner is not registered as beneficiary");: the owner must registry in this contract.
We know we can do something in setup(). the wallet someone creates will call setup() and then it will do something with to and data. If the data contains approve(), to can use transferFrom to transfer wallet’s DVT!
function setup( address[] calldata _owners, uint256 _threshold, address to, bytes calldata data, address fallbackHandler, address paymentToken, uint256 payment, address payable paymentReceiver ) external { // setupOwners checks if the Threshold is already set, therefore preventing that this method is called twice setupOwners(_owners, _threshold); if (fallbackHandler != address(0)) internalSetFallbackHandler(fallbackHandler); // As setupOwners can only be called if the contract has not been initialized we don't need a check for setupModules setupModules(to, data);
if (payment > 0) { // To avoid running into issues with EIP-170 we reuse the handlePayment function (to avoid adjusting code of that has been verified we do not adjust the method itself) // baseGas = 0, gasPrice = 1 and gas = payment => amount = (payment + 0) * 1 = payment handlePayment(payment, 0, 1, paymentToken, paymentReceiver); } emit SafeSetup(msg.sender, _owners, _threshold, to, fallbackHandler); }
So our attack is that: build a data with approve() as initializer to create the wallet. Then we can call transferFrom() to steal DVT!
contract GnosisWalletAttacker{ GnosisSafeProxyFactory public factory; IProxyCreationCallback public callback; address[] public users; address public singleton; address token;