Solidity Exercise - TimelockEscrow/TripleNestedMapping/...

Posted by Bourne's Blog - A Full-stack & Web3 Developer on May 16, 2024

TimelockEscrow

  • The goal of this exercise is to create a Time lock escrow.
  • A buyer deposits ether into a contract, and the seller cannot withdraw it until 3 days passes. Before that, the buyer can take it back.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.13;

contract TimelockEscrow {
    address public seller;

    /**
     * The goal of this exercise is to create a Time lock escrow.
     * A buyer deposits ether into a contract, and the seller cannot withdraw it until 3 days passes. Before that, the buyer can take it back
     * Assume the owner is the seller
     */

    constructor() {
        seller = msg.sender;
    }

    // creates a buy order between msg.sender and seller
    /**
     * escrows msg.value for 3 days which buyer can withdraw at anytime before 3 days but afterwhich only seller can withdraw
     * should revert if an active escrow still exist or last escrow hasn't been withdrawn
     */
    function createBuyOrder() external payable {
        // your code here
    }

    /**
     * allows seller to withdraw after 3 days of the escrow with @param buyer has passed
     */
    function sellerWithdraw(address buyer) external {
        // your code here
    }

    /**
     * allowa buyer to withdraw at anytime before the end of the escrow (3 days)
     */
    function buyerWithdraw() external {
        // your code here
    }

    // returns the escrowed amount of @param buyer
    function buyerDeposit(address buyer) external view returns (uint256) {
        // your code here
    }
}

Problem Analysis

According the requirement, there are more than one buyers can be existed at the same time, so we need a mapping to storage the order. An order should contain the amount of ether escrow, and the timestamp the order created.

So we define a struct Order{uint256 amount; uint256 createdAt;}, and mapping(address => Order) orders to storage each order.

  • In createBuyOrder, we need to ensure there is no order exists for the current buyer(the amount and createdAt equal 0);
  • In sellerWithdraw, we need to ensure it’s 3 days later after the order created by the given buyer;
  • in buyerWithdraw, we need to ensure it’s still within 3 days after the order created by the msg.sender .

Coding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.13;

contract TimelockEscrow {
    address public seller;
    struct Order{
        uint256 amount;
        uint256 createdAt;
    }
    mapping(address => Order) orders; // buyer address => Order

    /**
     * The goal of this exercise is to create a Time lock escrow.
     * A buyer deposits ether into a contract, and the seller cannot withdraw it until 3 days passes. Before that, the buyer can take it back
     * Assume the owner is the seller
     */

    constructor() {
        seller = msg.sender;
    }

    // creates a buy order between msg.sender and seller
    /**
     * escrows msg.value for 3 days which buyer can withdraw at anytime before 3 days but afterwhich only seller can withdraw
     * should revert if an active escrow still exist or last escrow hasn't been withdrawn
     */
    function createBuyOrder() external payable {
        // your code here
        require(orders[msg.sender].amount == 0 && orders[msg.sender].createdAt == 0, "you have an active order.");
        orders[msg.sender] = Order(msg.value, block.timestamp);
    }

    /**
     * allows seller to withdraw after 3 days of the escrow with @param buyer has passed
     */
    function sellerWithdraw(address buyer) external {
        // your code here
        Order memory order = orders[buyer];
        require(block.timestamp > (order.createdAt + 3 days), "The seller needs 3 days to withdraw.");
        payable(seller).transfer(order.amount);
    }

    /**
     * allowa buyer to withdraw at anytime before the end of the escrow (3 days)
     */
    function buyerWithdraw() external {
        // your code here
        Order memory order = orders[msg.sender];
        require(block.timestamp < (order.createdAt + 3 days), "The buyer needs widthdraw within 3 days.");
        payable(msg.sender).transfer(order.amount);
    }

    // returns the escrowed amount of @param buyer
    function buyerDeposit(address buyer) external view returns (uint256) {
        // your code here
        return orders[buyer].amount;
    }
}

Test

1
2
3
4
5
6
7
8
9
10
11
12
➜  TimelockEscrow git:(main) ✗ forge test -vvv
[⠊] Compiling...
[⠃] Compiling 19 files with Solc 0.8.25
[⠊] Solc 0.8.25 finished in 848.41ms
Compiler run successful!

Ran 2 tests for test/TimeLockEscrow.t.sol:TimelockEscrowTest
[PASS] testTimelock() (gas: 75663)
[PASS] testTimelockBuyerWithdraws() (gas: 71944)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.28ms (1.91ms CPU time)

Ran 1 test suite in 193.51ms (5.28ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

Reference: Solidity Exercise - TimelockEscrow

TripleNestedMapping

This exercise assumes you know how mappings work.

  • Create a public TRIPLE nested mapping of (string(_name) => uint256(_password) => uint256(_pin) => bool).
  • The name of the mapping must be isLoggedIn and it should be public.
  • Set the boolean value of the arguments to true in the ‘setLogin’ function.

Coding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.13;

contract TripleNestedMapping {
    /* 
        This exercise assumes you know how mappings work.
        1. Create a public TRIPLE nested mapping of 
           (string(_name) => uint256(_password) => uint256(_pin) => bool).
        2. The name of the mapping must be `isLoggedIn` and it should be public.
        3. Set the boolean value of the arguments to `true` in the 'setLogin' function.
    */
    mapping(string => mapping(uint256 => mapping(uint256 => bool))) public isLoggedIn;

    function setLogin(
        string memory _name,
        uint256 _password,
        uint256 _pin
    ) public {
        // your code here
        isLoggedIn[_name][_password][_pin] = true;
    }
}

Test

1
2
3
4
5
6
7
8
9
10
11
➜  TripleNestedMapping git:(main) ✗ forge test -vvv          
[⠊] Compiling...
[⠃] Compiling 19 files with Solc 0.8.25
[⠊] Solc 0.8.25 finished in 837.30ms
Compiler run successful!

Ran 1 test for test/TripleNestedMapping.t.sol:TripleNestedMappingTest
[PASS] testTripleNestedMapping() (gas: 81004)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.69ms (269.50µs CPU time)

Ran 1 test suite in 200.25ms (6.69ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Reference: Solidity Exercise - TripleNestedMapping

TupleDore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.13;

contract Tupledore {
    /* This exercise assumes you know about tuples/struct in solidity.
        1. Create a struct named `UserInfo` with types address 
           and uint256.
        2. Create a variable of type UserInfo, named `userInfo`.
        3. Create a function called `setTuple` that takes in 
           a address and uint256 and sets the all values 
           the `userInfo` variable you created above.
        4. Create a function called `returnTuple`, 
           that returns `userInfo` (as a tuple)
    */
   struct UserInfo{
      address addr;
      uint256 age;
   }
   UserInfo public userInfo;
   function setTuple(address _addr, uint256 _age) public {
      userInfo.addr = _addr;
      userInfo.age = _age;
   }

   function returnTuple() public view returns(UserInfo memory){
      return userInfo;
   }
}

Test

1
2
3
4
5
6
7
8
9
10
11
➜  Tupledore git:(main) ✗ forge test -vvv
[⠊] Compiling...
[⠘] Compiling 19 files with Solc 0.8.25
[⠊] Solc 0.8.25 finished in 825.11ms
Compiler run successful!

Ran 1 test for test/Tupledore.t.sol:TupledoreTest
[PASS] testTupledore() (gas: 52787)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.90ms (288.50µs CPU time)

Ran 1 test suite in 193.49ms (4.90ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Unchecked

unchecked keyword tell the compiler the code is not result in any issues, and skip security checkings, like array bounds checking, integer overflow checking, and division by zero checking, etc. this can significantly reduce the gas cost, but also lead to security risk potentials. You need very carefaul while using uncheck keyword.

Problem Analysis

In this case, it’s obviously the x passed to getNumber should be greater than 100, otherwise, the return value would be negative (since the return type is unsigned, it will be a huge astronomical number).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.13;

contract Unchecked {
    /*
        This exercise assumes you understand what unchecked keyword is.
        1. The `getNumber` function reverts when called, you need to make the function stop
           reverting and return underflow value.
    */

    function getNumber(uint256 x) public pure returns (uint256) {
        unchecked{
            return x - 100;
        }
    }
}

Test

Let add debug to the test case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/Unchecked.sol";
import "forge-std/console.sol";

contract UncheckedTest is Test {
    Unchecked public uncheckedContract;

    function setUp() public {
        uncheckedContract = new Unchecked();
    }

    function testUnchecked() external {
        uint256 res = uncheckedContract.getNumber(10);
        console.log("getNumber(10) => ", res);
        uint256 res1 = uncheckedContract.getNumber(0);
        console.log("getNumber(0) => ", res1);
        uint256 res2 = uncheckedContract.getNumber(50);
        console.log("getNumber(50) => ", res2);
        uint256 res3 = uncheckedContract.getNumber(20);
        console.log("getNumber(20) => ", res3);

        uint256 res4 = uncheckedContract.getNumber(120);
        console.log("getNumber(120) => ", res4);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
➜  Unchecked git:(main) ✗ forge test -vvv
[⠊] Compiling...
[⠘] Compiling 19 files with Solc 0.8.25
[⠃] Solc 0.8.25 finished in 785.44ms
Compiler run successful with warnings:
Warning (2018): Function state mutability can be restricted to view
  --> test/Unchecked.t.sol:15:5:
   |
15 |     function testUnchecked() external {
   |     ^ (Relevant source part starts here and spans across multiple lines).


Ran 1 test for test/Unchecked.t.sol:UncheckedTest
[PASS] testUnchecked() (gas: 14378)
Logs:
  getNumber(10) =>  115792089237316195423570985008687907853269984665640564039457584007913129639846
  getNumber(0) =>  115792089237316195423570985008687907853269984665640564039457584007913129639836
  getNumber(50) =>  115792089237316195423570985008687907853269984665640564039457584007913129639886
  getNumber(20) =>  115792089237316195423570985008687907853269984665640564039457584007913129639856
  getNumber(120) =>  20

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.46ms (1.21ms CPU time)

Ran 1 test suite in 194.77ms (6.46ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

We can see from the test output, even the test case passed as we expected, but the results are abnormal while the number passed to getNumber were less than 100.

Reference: Solidity Exercise - Unchecked