Building with Solidity

9. Context and Safety Nets

Use Solidity global variables and safety patterns (require, custom errors, try/catch) to write robust contracts.

Prerequisites

Global Tools and Safety Patterns

Solidity gives you built‑in tools that tell you who called, when, and with how much value.​
Key globals include:

  • msg.sender: the caller of the function.

  • msg.value: how much VET was sent.

  • block.timestamp: time of the current block.​

For example, a paid registration function uses these tools directly:

function addStudent(string memory name, string memory familyName)
    public
    payable
{
    require(msg.value == 1 ether, "You must pay 1 VET to register");
    require(!students[msg.sender].registered, "You are already registered");

    students[msg.sender] = Student({
        wallet: msg.sender,
        name: name,
        familyName: familyName,
        registered: true,
        graduated: false,
        certificate: 0
    });
}
function addStudent(string memory name, string memory familyName)
    public
    payable
{
    require(msg.value == 1 ether, "You must pay 1 VET to register");
    require(!students[msg.sender].registered, "You are already registered");

    students[msg.sender] = Student({
        wallet: msg.sender,
        name: name,
        familyName: familyName,
        registered: true,
        graduated: false,
        certificate: 0
    });
}
function addStudent(string memory name, string memory familyName)
    public
    payable
{
    require(msg.value == 1 ether, "You must pay 1 VET to register");
    require(!students[msg.sender].registered, "You are already registered");

    students[msg.sender] = Student({
        wallet: msg.sender,
        name: name,
        familyName: familyName,
        registered: true,
        graduated: false,
        certificate: 0
    });
}

TO REMEMBER

require is your main safety gate: if the condition fails, execution stops and state changes are reverted.​

For more efficient, structured errors, you can define custom errors:

error UnauthorizedAccess(address caller);

modifier onlyOwner() {
    if (msg.sender != contractOwner) {
        revert UnauthorizedAccess(msg.sender);
    }
    _;
}
error UnauthorizedAccess(address caller);

modifier onlyOwner() {
    if (msg.sender != contractOwner) {
        revert UnauthorizedAccess(msg.sender);
    }
    _;
}
error UnauthorizedAccess(address caller);

modifier onlyOwner() {
    if (msg.sender != contractOwner) {
        revert UnauthorizedAccess(msg.sender);
    }
    _;
}

This lowers gas and makes it easier to decode failures in tools and explorers.​

Solidity even supports try/catch for calls that might fail:

try this.getUser(msg.sender) returns (User memory user) {
    return user;
} catch {
    // handle failure gracefully
}
try this.getUser(msg.sender) returns (User memory user) {
    return user;
} catch {
    // handle failure gracefully
}
try this.getUser(msg.sender) returns (User memory user) {
    return user;
} catch {
    // handle failure gracefully
}

Think of it as an internal fallback: instead of crashing the whole transaction, you can choose how to respond.​

Common Pitfalls

Using tx.origin for auth is dangerous; always prefer msg.sender.

Congratulations! You now know when to use msg.value, why tx.origin is rarely appropriate, and how structured errors and debugging.