@trustline.id/evmsdk
v1.1.0
Published
Protects EVM-compatible smart contracts from unauthorized access and malicious transactions
Maintainers
Readme
Trustline EVM SDK
A Solidity SDK for protecting EVM-compatible smart contracts from unauthorized access and malicious transactions by integrating Trustline's Oracle with multiple on-chain data sources.
Features
- ✅ Transaction Validation - Validate blockchain transactions with customizable policies
- ✅ Sanctions Checking - Verify addresses against sanctions lists
- ✅ Multiple Validation Modes - Support for Dapp, Uniswap V4, Morpho V2, and ERC-3643 modes
- ✅ Address Verification - Check sender and recipient addresses for compliance
- ✅ Upgradeable Support - Dedicated base contract for proxy-based deployments (see Integration constraints)
- ✅ ERC20 & ERC3643 Support - Secure operations on standard token contracts
- ✅ Proxy Deployment - Automatic Validation Engine proxy deployment
- ✅ Flexible Integration - Use existing Validation Engine or deploy new instance
- ✅ Firewall intermediary - Generic forward proxy to protect existing contracts with configurable access roles
Installation
npm install @trustline.id/evmsdkArchitecture
Validation is performed through a small set of on-chain/off-chain components:
- Your contract — Inherits from
Trustlinedand callsrequireTrustline()before sensitive operations (orcheckTrustlineStatus()when a non-enforcing query is needed). It holds the address of a Validation Engine proxy. - Validation Engine proxy — An ERC1967 proxy that your contract talks to. It delegates all calls to the Validation Engine logic contract, so the implementation can be upgraded without changing your contract’s configuration. This proxy is deployed automatically when your contract is deployed or initialized, if you do not provide an existing proxy. Do not deploy this proxy manually - manual deployment risks a separate, non-atomic
initializestep and an uninitialized or misconfigured engine. - Validation Engine logic — The implementation contract that runs Trustline's transaction validation logic. It verifies certificates issued by Trustline's Oracle backend (and optionally consults other oracles) to decide whether a transaction and its addresses are authorized. Trustline deploys it on supported blockchains.
- Trustline's Oracle backend — Trustline’s off-chain service that issues validation certificates to the on-chain Validation Engine.
- Other oracles — The Validation Engine can aggregate data from additional on-chain oracles (e.g. sanctions lists) so validation uses multiple data sources.
- TrustlineFirewall — Intermediary that forwards calls to an existing target after validation. Configure the target so privileged actions are allowed only from the firewall address; callers invoke the firewall, which runs
requireTrustline()then forwards with a regularcall(the target sees the firewall asmsg.sender).
In short: your contract → Validation Engine proxy → Validation Engine logic → Trustline Oracle backend + other oracles. You only configure your contract with the logic address or an already-deployed proxy (for advanced use cases) at deploy time; the rest is handled by Trustline’s infrastructure. Alternatively, deploy a TrustlineFirewall in front of a contract you do not want to modify directly.
Quick Start
Basic Contract Integration (non-upgradeable)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
import {Trustlined} from "@trustline.id/evmsdk/contracts/Trustlined.sol";
contract MyContract is Trustlined {
constructor(
address trustlineValidationEngineLogic,
address trustlineValidationEngineProxy
) Trustlined(trustlineValidationEngineLogic, trustlineValidationEngineProxy) {}
function transfer(address recipient, uint256 amount) external {
// Validate sender only
requireTrustline();
// Your transfer logic here
// ...
}
function transferWithRecipientCheck(address recipient, uint256 amount) external {
// Validate both sender and recipient
address[] memory addresses = new address[](1);
addresses[0] = recipient;
requireTrustline(addresses);
// Your transfer logic here
// ...
}
}Use
Trustlinedfor regular (non-proxy) deployments where constructor initialization is expected.
Using Existing Validation Engine Proxy
If you already have a Validation Engine proxy that was previously deployed atomically by another Trustlined contract (via the auto-deploy path below), you can reuse it:
contract MyContract is Trustlined {
constructor(address existingValidationEngineProxy)
Trustlined(address(0), existingValidationEngineProxy) {}
// If you pass address(0) for logic, the provided proxy will be used
}Important: Never deploy a Validation Engine proxy manually (e.g. with a standalone
ERC1967Proxydeployment followed by a separateinitializecall). Only use proxies created byTrustlinedduring contract deployment, which deploys the proxy and callsinitializein a single atomic step. A manually deployed proxy may be left uninitialized or initialized by the wrong account.
Deploying New Validation Engine Proxy
To deploy a new Validation Engine proxy, pass the Trustline logic address and address(0) for the proxy. Deployment and initialization happen atomically during your contract's deployment - do not deploy the proxy yourself:
contract MyContract is Trustlined {
constructor(address validationEngineLogic)
Trustlined(validationEngineLogic, address(0)) {}
// A new Validation Engine proxy will be deployed automatically during contract deployment
}Cross-chain deployment
Validation Engine proxy addresses are not deterministic across chains. Track the addresses by reading the deployed proxies from the ValidationEngineDeployed event or by calling validationEngine() after deployment.
Integration constraints
Required
- Direct inheritance into your own contract (or upgradeable implementation). Each base gets its own slots in a single, shared layout — this works with tokens, vaults, and other standards.
- On upgradeable contracts, inherit
TrustlinedUpgradeableand call__Trustlined_initfrom yourinitializer(theTrustlinedconstructor does not run on the proxy).
Not supported
- Diamond (EIP-2535) facets, delegatecall routers, or any module whose code runs against foreign storage. There, facet layout slot 0 maps to the host's slot 0 and
validationEnginecan read or write the wrong value — validation may be bypassed or broken. - Changing inheritance order across upgrades (inserting or reordering bases shifts where
validationEnginelives and corrupts storage).
Recommended
- Keep inheritance order stable across upgrades.
- When combining with other bases that use unstructured storage (e.g.
ERC20), list them in a fixed order and treat the full layout as part of your upgrade checklist. ListingTrustlinedleftmost placesvalidationEngineat slot 0; listing it afterERC20places it at the next free slot — both are valid in direct inheritance.
// Supported — standalone dapp
contract MyDapp is Trustlined {
constructor(address logic, address proxy) Trustlined(logic, proxy) {}
}
// Supported — token; ERC20 and Trustlined each own distinct slots
contract MyToken is ERC20, Trustlined {
constructor(address logic, address proxy)
ERC20("My Token", "MYT")
Trustlined(logic, proxy)
{}
function transfer(address to, uint256 amount) public override returns (bool) {
address[] memory addresses = new address[](1);
addresses[0] = to;
requireTrustline(addresses);
return super.transfer(to, amount);
}
}
// Supported — upgradeable; Initializable uses ERC-7201 namespaced storage in OpenZeppelin v5+
contract MyDapp is Initializable, TrustlinedUpgradeable {
function initialize(address logic, address proxy) public initializer {
__Trustlined_init(logic, proxy);
}
}
// Not supported — facet code runs in the Diamond's storage, not the facet's layout
contract MyFacet is Trustlined { ... } // when used as an EIP-2535 delegatecall facetAPI Reference
Contract: Trustlined
Base abstract contract that provides transaction validation functionality. Inherit from this contract to add Trustline validation to your smart contracts.
Constructor
constructor(
address trustlineValidationEngineLogic,
address trustlineValidationEngineProxy
)Parameters:
trustlineValidationEngineLogic: The Validation Engine logic contract address. Used only iftrustlineValidationEngineProxyisaddress(0). If both are provided,trustlineValidationEngineProxytakes precedence.trustlineValidationEngineProxy: Optional Validation Engine proxy address. If provided (non-zero), it will be used directly. Ifaddress(0), a new proxy will be deployed using the logic contract.
Behavior:
- If
trustlineValidationEngineProxyis non-zero: Uses the provided proxy directly (must have been deployed atomically byTrustlined, withdefaultAdminset to the caller) - If
trustlineValidationEngineProxyisaddress(0): Deploys a new ERC1967 proxy usingtrustlineValidationEngineLogicand callsinitializein the same transaction - never deploy this proxy manually. The proxy address is chain-specific and non-deterministic across networks (see Cross-chain deployment).
Functions
requireTrustline and checkTrustlineStatus serve different roles:
| Function | Role | Enforces compliance? |
|----------|------|----------------------|
| requireTrustline(...) | Enforcing call — reverts if the transaction is not compliant | Yes |
| checkTrustlineStatus(...) | Query — returns true or false without reverting | No |
Use requireTrustline() to guard state-changing operations. Use checkTrustlineStatus() only when you need the result (e.g. a view function, or conditional branching). If you call checkTrustlineStatus() before a state-changing operation and want to block non-compliant callers, you must wrap it: require(checkTrustlineStatus(), "..."). Calling checkTrustlineStatus() alone does not prevent execution.
requireTrustline()
Enforcing call. Requires a trusted transaction and a non-sanctioned msg.sender. Reverts if the transaction is not compliant.
function requireTrustline() internalUsage:
function transfer(uint256 amount) external {
requireTrustline(); // Validates msg.sender only
// Your logic here
}requireTrustline(address[] memory addresses)
Requires a trusted transaction and non-sanctioned msg.sender + addresses. Reverts if the transaction is not compliant.
function requireTrustline(address[] memory addresses) internalParameters:
addresses: An array of addresses that will be verified by the policy (e.g., recipients, token addresses)
Usage:
function payTokens(address recipient, address token, uint256 amount) external {
address[] memory addresses = new address[](2);
addresses[0] = recipient;
addresses[1] = token;
requireTrustline(addresses); // Validates msg.sender, recipient, and token
// Your logic here
}checkTrustlineStatus()
Checks whether a transaction is trusted and verifies msg.sender against sanctions lists. Returns true if compliant, false otherwise.
function checkTrustlineStatus() internal view returns (bool)Usage:
function canTransfer() external view returns (bool) {
return checkTrustlineStatus();
}checkTrustlineStatus(address[] memory addresses)
Checks whether a transaction is trusted and verifies msg.sender + addresses against sanctions lists. Returns true if compliant, false otherwise.
function checkTrustlineStatus(address[] memory addresses) internal view returns (bool)Parameters:
addresses: An array of addresses that will be verified by the policy
Usage:
function canPay(address recipient) external view returns (bool) {
address[] memory addresses = new address[](1);
addresses[0] = recipient;
return checkTrustlineStatus(addresses);
}Public Variables
validationEngine
The Trustline ValidationEngine contract address. This is set during contract initialization.
IValidationEngine public validationEngine;Events
ValidationEngineDeployed
Emitted when a new Validation Engine proxy is deployed for this client contract.
event ValidationEngineDeployed(
address indexed client,
address indexed engineProxy,
address indexed logic,
address initialOwner
);Parameters:
client: The address of the integrating contract (i.e., the contract inheriting from Trustlined)engineProxy: The freshly deployed ERC1967 proxy address for the Validation Engine instancelogic: The Validation Engine implementation (logic) contract the proxy points to at deployment timeinitialOwner: The address passed to the engine'sinitialize(address)call (typically the deployer/initializer)
ValidationEngineAdopted
Emitted when an existing Validation Engine proxy is adopted by this client contract.
event ValidationEngineAdopted(
address indexed client,
address indexed engineProxy
);Parameters:
client: The address of the integrating contract (i.e., the contract inheriting from Trustlined)engineProxy: The Validation Engine proxy address being reused
Validation Modes
The SDK supports different validation modes for various DeFi protocols. These can be used with the advanced IValidationEngine interface methods:
Dapp(default) - Custom dapp validation modeUniswapV4- Uniswap V4 protocol validationMorphoV2- Morpho V2 protocol validationERC3643- ERC-3643 token standard validation
Examples
Payment Forwarder
A complete example that ensures all payments are compliant. See the PaymentForwarder.sol contract for the full implementation.
Trustline Firewall
Generic intermediary for protecting an existing contract without embedding Trustline in every method. Callers invoke the firewall; it runs requireTrustline(), then forwards calldata and value to a fixed target with a regular call. The target executes in its own context and sees msg.sender as the firewall address.
Typical setup: grant each firewall the privileged role it protects on the target (e.g. owner, curator, allocator), then route operations through that firewall instead of calling the target directly. When the protocol has several distinct roles, deploy one firewall per role, each with its own operators.
Access model
| Role | Scope |
|------|-------|
| owner | Single admin — setTarget, setOwner, setOperator, setPublicForward (all Trustline-protected) |
| isOperator | Mapping of addresses allowed on the protected forward path (fallback / receive) |
| publicForward | When true, any address may use the forward path (still Trustline-gated) |
The owner is not an operator by default. To let the admin forward calls, call setOperator(owner, true). To allow unrestricted callers on protected methods, call setPublicForward(true).
Deployment
import {TrustlineFirewall} from "@trustline.id/evmsdk/contracts/examples/TrustlineFirewall.sol";
new TrustlineFirewall(
targetAddress,
validationEngineLogic,
validationEngineProxy, // address(0) to auto-deploy a new proxy
adminOwner,
initialOperator, // address(0) to skip
initialPublicForward // false = operators only
);See TrustlineFirewall.sol for the full implementation.
Upgradeable Contracts
For proxy-based deployments, use TrustlinedUpgradeable. Do not use Trustlined directly in an upgradeable implementation.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {TrustlinedUpgradeable} from "@trustline.id/evmsdk/contracts/TrustlinedUpgradeable.sol";
contract UpgradeableContract is Initializable, TrustlinedUpgradeable {
function initialize(
address trustlineValidationEngineLogic,
address trustlineValidationEngineProxy
) public initializer {
__Trustlined_init(trustlineValidationEngineLogic, trustlineValidationEngineProxy);
}
function transfer(address to, uint256 amount) external {
address[] memory addresses = new address[](1);
addresses[0] = to;
requireTrustline(addresses);
// Your logic here
}
}Which base contract should I use?
- Use
Trustlinedfor non-upgradeable contracts deployed directly. - Use
TrustlinedUpgradeablefor contracts deployed behind a proxy (UUPS/Transparent pattern). - Always call
__Trustlined_init(...)from your contract initializer when usingTrustlinedUpgradeable.
Build
Build the SDK:
npm run buildThis generates:
artifacts/- Compiled contract artifactsdist/bundle.js- Browser bundle (for JavaScript usage)
Compile contracts only:
npm run compileSecurity Considerations
- Never deploy a Validation Engine proxy manually - always let
Trustlineddeploy and initialize it atomically, or reuse a proxy previously created that way - Integrate via direct inheritance only — not as a Diamond facet or delegatecall module (see Integration constraints)
- Validation Engine proxy addresses are chain-specific; do not assume cross-chain address parity when auto-deploying (see Cross-chain deployment)
- Validation Engine logic and proxy addresses must be genuine contracts — EIP-7702 delegated EOAs (code prefix
0xef0100) are rejected - Always validate addresses that receive funds or tokens
- Use
requireTrustline(addresses[])when checking recipients - Use
requireTrustline()for sender-only validation when appropriate - The Validation Engine must be properly configured and deployed
License
MIT
Links
- Homepage: https://www.trustline.id
- Repository: https://github.com/TrustLine-id/evmsdk
- Issues: https://github.com/TrustLine-id/evmsdk/issues
Support
Not sure how to get started? Contact us at [email protected]
