diff --git a/contracts/GateSeal.vy b/contracts/GateSeal.vy index 1479543..f7668c9 100644 --- a/contracts/GateSeal.vy +++ b/contracts/GateSeal.vy @@ -159,11 +159,16 @@ def seal(_sealables: DynArray[address, MAX_SEALABLES]): assert sealable in self.sealables, "sealables: includes a non-sealable" success: bool = False + response: Bytes[32] = b"" # using `raw_call` to catch external revert and continue execution - success = raw_call( + # capturing `response` to keep the compiler from acting out but will not be checking it + # as different sealables may return different values if anything at all + # for details, see https://docs.vyperlang.org/en/stable/built-in-functions.html#raw_call + success, response = raw_call( sealable, _abi_encode(SEAL_DURATION_SECONDS, method_id=method_id("pauseFor(uint256)")), + max_outsize=32, revert_on_failure=False ) diff --git a/contracts/test_helpers/SealableMock.vy b/contracts/test_helpers/SealableMock.vy index 7034c9e..547e03c 100644 --- a/contracts/test_helpers/SealableMock.vy +++ b/contracts/test_helpers/SealableMock.vy @@ -16,6 +16,13 @@ def isPaused() -> bool: return self._is_paused() +@external +def __force_pause_for(_duration: uint256): + # pause ignoring any checks + # required to simulate cases where Sealable is already paused but the seal() reverts + self.resumed_timestamp = block.timestamp + _duration + + @external def pauseFor(_duration: uint256): assert not self.reverts, "simulating revert" diff --git a/tests/test_gate_seal.py b/tests/test_gate_seal.py index 9b69639..cfc755e 100644 --- a/tests/test_gate_seal.py +++ b/tests/test_gate_seal.py @@ -428,3 +428,46 @@ def test_cannot_seal_twice_in_one_tx(gate_seal, sealables, sealing_committee): gate_seal.seal(sealables, sender=sealing_committee) with reverts("gate seal: expired"): gate_seal.seal(sealables, sender=sealing_committee) + + +def test_raw_call_success_should_be_false_when_sealable_reverts_on_pause(project, deployer, generate_sealables, sealing_committee, seal_duration_seconds, expiry_timestamp): + """ + `raw_call` without `max_outsize` and with `revert_on_failure=True` for some reason returns the boolean value of memory[0] :^) + + Which is why we specify `max_outsize=32`, even though don't actually use it. + + To test that `success` returns actual value instead of returning bool of memory[0], + we need to pause the contract before the sealing, + so that the condition `success and is_paused()` is false (i.e `False and True`), see GateSeal.vy::seal() + + For that, we use `__force_pause_for` on SealableMock to ignore any checks and forcefully pause the contract. + After calling this function, the SealableMock is paused but the call to `pauseFor` will still revert, + thus the returned `success` should be False, the condition fails and the call reverts altogether. + + Without `max_outsize=32`, the transaction would not revert. + """ + + unpausable = False + should_revert = True + # we'll use only 1 sealable + sealables = generate_sealables(1, unpausable, should_revert) + + # deploy GateSeal + gate_seal = project.GateSeal.deploy( + sealing_committee, + seal_duration_seconds, + sealables, + expiry_timestamp, + sender=deployer, + ) + + # making sure we have the right contract + assert gate_seal.get_sealables() == sealables + + # forcefully pause the sealable before sealing + sealables[0].__force_pause_for(seal_duration_seconds, sender=deployer) + assert sealables[0].isPaused(), "should be paused now" + + # seal() should revert because `raw_call` to sealable returns `success=False`, even though isPaused() is True. + with reverts("0"): + gate_seal.seal(sealables, sender=sealing_committee)