0

Context

I wrote a contract that aims to raise uint256 investmentTarget funds, within a time period of uint32 private _raisePeriod; after the constructor has been initialised at _startTime = block.timestamp;. If the uint256 investmentTarget is not reached within that period, it should return all received investments.

So I give that contract the modifier:

modifier onlyAfterDelayAndUnderTarget() {
    console2.log("_startTime=",_startTime);
    console2.log("_raisePeriod=",_raisePeriod);
    console2.log("_cumReceivedInvestments=",_cumReceivedInvestments);
    console2.log("_investmentTarget=",_investmentTarget);
    require(block.timestamp >= _startTime + _raisePeriod, "5 weeks haven't passed yet");
    require(_cumReceivedInvestments < _investmentTarget, "Investment target reached");
    _; // Allows execution of the decorated (triggerReturnAll) function.
  }

and that modifier is applied to the return function:

  function _triggerReturnAll() internal onlyAfterDelayAndUnderTarget {
    // TODO: return all investments.
    uint256 nrOfTierInvestments = _tierInvestments.length;
    for (uint256 i = 0; i < nrOfTierInvestments; ++i) {
      // Transfer the amount to the PaymentSplitter contract
      payable(_tierInvestments[i].getInvestor()).transfer(_tierInvestments[i].getNewInvestmentAmount());
    }
    require(address(this).balance == 0, "After returning investments, there is still money in the contract.");
  }

and the contract is constructed & tested with:

function testRaisePeriodReturnSingleInvestment() public virtual override {
      console2.log("block.timestamp=",block.timestamp);

      // Simulate 3 weeks passing by
      vm.warp(block.timestamp+3 weeks);
      console2.log("3 weeks later block.timestamp=",block.timestamp);
      assertEq(address(_dim).balance, 0.5 ether, "The _dim did not contain 0.5 ether.");
      
      
      vm.warp(block.timestamp + 15 weeks);
      console2.log("15 weeks later block.timestamp=",block.timestamp);
      assertEq(address(_dim).balance, 0 ether, "The _dim did not contain 0 ether.");
      
  }

Issue

The test fails at the last assert because the balance is unchanged:

0595] MultipleInvestmentTest::testRaisePeriodReturnSingleInvestment()
    ├─ [0] VM::warp(1814400 [1.814e6])
    │   └─ ← [Return] 
    ├─ [0] VM::warp(6652800 [6.652e6])
    │   └─ ← [Return] 
    ├─ emit LogNamedString(key: "Error", value: "The _dim did not contain 0 ether.")
    ├─ emit Log(err: "Error: a == b not satisfied [uint256]")
    ├─ emit LogNamedUint256(key: "   Left", value: 500000000000000000 [5e17])
    ├─ emit LogNamedUint256(key: "  Right", value: 0)
    ├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001)
    │   └─ ← [Return] 
    └─ ← [Stop] 

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 801.30µs (38.02µs CPU time)

Ran 1 test suite in 794.98ms (801.30µs CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/unit/RaiePeriod.t.sol:MultipleInvestmentTest
[FAIL. Reason: assertion failed] testRaisePeriodReturnSingleInvestment() (gas: 40595)

I think that implies that either I am not testing the passing of time correctly, or, (I consider this more likely), I did not implement the tracking of time and the accompanying investment return trigger correctly in the main contract that is being tested.

Question

How can I ensure the contract (autonomously) triggers a function after n weeks have passed, and test that it has triggered that function, using a forge test?

1 Answer 1

0

Invalid Assumptions

In the original question I made 2 invalid assumptions:

  • The vm.warp(x) function does not warp for a duration of x seconds, but warps to the time x from the time of the first block.
  • The contract should automatically activate if a certain time is reached.

In this case, someone wants the contract to act, the investors, so 1 of them should call this function to get their money back.

Working Solution

In the main contract I added modifier:

modifier onlyAfterDelayAndUnderTarget() {
    require(block.timestamp >= _startTime + _raisePeriod, "The fund raising period has not passed yet.");
    require(_cumReceivedInvestments < _investmentTarget, "Investment target reached!");
    _; // Allows execution of the decorated (triggerReturnAll) function.
  }

and payout function:

function triggerReturnAll() public onlyAfterDelayAndUnderTarget {
    // TODO: return all investments.
    uint256 nrOfTierInvestments = _tierInvestments.length;
    for (uint256 i = 0; i < nrOfTierInvestments; ++i) {
      // Transfer the amount to the PaymentSplitter contract
      payable(_tierInvestments[i].getInvestor()).transfer(_tierInvestments[i].getNewInvestmentAmount());
    }
    require(address(this).balance == 0, "After returning investments, there is still money in the contract.");
  }

and in the test(s) I wrote:

function testRaisePeriodReturnSingleInvestment() public virtual override {
    // Simulate 3 weeks passing by
    vm.warp(block.timestamp + 3 weeks);

    vm.expectRevert(bytes("The fund raising period has not passed yet."));
    _dim._triggerReturnAll();
    assertEq(address(_dim).balance, 0.5 ether, "The _dim did not contain 0.5 ether.");

    vm.warp(block.timestamp + 15 weeks);

    _dim._triggerReturnAll();
    assertEq(address(_dim).balance, 0 ether, "The _dim did not contain 0 ether.");
  }

  function testKeepInvestmentsForSuccesfullRaise() public virtual override {
    // Simulate 3 weeks passing by
    vm.warp(block.timestamp + 3 weeks);

    vm.expectRevert(bytes("The fund raising period has not passed yet."));
    _dim._triggerReturnAll();
    assertEq(address(_dim).balance, 0.5 ether, "The _dim did not contain 0.5 ether.");

    // Set the msg.sender address to that of the _investorWallet0 for the next call.
    vm.prank(address(_investorWallet0));
    // Send investment directly from the investor wallet into the receiveInvestment function.
    _dim.receiveInvestment{ value: 2.5 ether }();

    vm.warp(block.timestamp + 15 weeks);

    vm.expectRevert(bytes("Investment target reached!"));
    _dim._triggerReturnAll();
    assertEq(address(_dim).balance, 3 ether, "The _dim did not contain 0 ether.");
  }

which pass. I hope it saves someone some time.

Room for Improvement

If someone knows how to make the contract itself payout at a certain (block) time, without being called, I would greatly appreciate your solution!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.