Smart Accounts
Every user in Pons Network has a Smart Account on each destination chain. This account is the cornerstone of Pons security. It ensures that resolvers can only do exactly what users authorize, and nothing more.
Why Smart Accounts?
Traditional bridges transfer assets to a user's EOA (Externally Owned Account). But this creates problems:
- User needs gas tokens on the destination chain
- User must be online to execute actions
- Multiple transactions required
Smart Accounts solve this. They:
- Receive bridged assets on behalf of the user
- Execute user-authorized actions automatically
- Pay fees from the bridged assets (no gas tokens needed)
- Enforce strict security guarantees
Design Principles
Deterministic
Same address on every chain. Predictable before deployment. Derived from owner address + salt.
Counterfactual
Doesn't exist until first use. No deployment cost upfront. Created lazily when needed.
Self-Custodial
Only the owner's signature can authorize actions. No admin keys. No upgrades. No backdoors.
Security Model
How Resolvers Are Constrained
The critical question: How can we let resolvers execute actions without letting them steal funds?
The answer is cryptographic signatures combined with on-chain validation.
What the Signature Contains
When a user signs an action, they commit to exactly what will happen:
EIP-712 Signed Action
- CallsExact contract addresses and calldata to execute
- Expected AmountMinimum assets that must arrive before execution
- Fee ConfigurationMaximum fees the user agrees to pay
- DeadlineAction expires after this timestamp
- NonceUnique timestamp-based ID, prevents replay attacks
What Resolvers CANNOT Do
The Smart Account enforces strict constraints. A resolver cannot:
| Attack Vector | Protection |
|---|---|
| Execute different calls | Signature covers exact calldata |
| Take more fees than agreed | Fee amounts are signed |
| Replay an action | Each nonce can only be used once |
| Execute after deadline | Deadline check reverts late submissions |
| Execute with less assets | Expected amount check protects user |
| Drain remaining funds | Only signed calls can execute |
What Resolvers CAN Do
Resolvers have limited power:
- Submit the user's pre-signed action
- Provide gas for the transaction
- Optionally provide tokens upfront (reimbursed from fees)
That's it. They're essentially a gas-paying relay.
Smart Account Capabilities
1. Receive Bridged Assets
The Smart Account is the recipient of cross-chain transfers:
// User's Smart Account address is deterministic
const smartAccountAddress = await pons.calculateSmartAccountAddress(
userAddress, // Owner
0n // Salt
);
// Same address on every supported chain!
2. Execute Arbitrary Calls
Users can authorize any contract interaction:
const action = new ActionBuilder()
// Swap on Uniswap
.addCall(UNISWAP_ROUTER, swapCalldata)
// Stake on Lido
.addCall(LIDO_STAKING, stakeCalldata)
// Mint an NFT
.addCall(NFT_CONTRACT, mintCalldata)
.build();
3. Batch Multiple Operations
Execute complex multi-step transactions atomically:
All steps execute in one transaction, or none do.
4. Pay Fees Without Gas Tokens
No Gas Tokens Needed
The resolver pays gas in native tokens. The Smart Account reimburses the resolver from the user's bridged assets. User never needs destination chain gas.
Signature Verification Deep Dive
EIP-712 Typed Data
Actions use EIP-712 structured signing for security and UX:
const typedData = {
domain: {
name: 'PonsSmartAccount',
version: '1',
chainId: destinationChainId,
verifyingContract: smartAccountAddress,
},
types: {
Action: [
{ name: 'calls', type: 'Call[]' },
{ name: 'expectedAmount', type: 'uint256' },
{ name: 'feeConfig', type: 'FeeConfig' },
{ name: 'deadline', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
],
Call: [
{ name: 'target', type: 'address' },
{ name: 'data', type: 'bytes' },
{ name: 'value', type: 'uint256' },
],
// ...
},
message: action,
};
On-Chain Verification
The Smart Account verifies signatures on-chain:
function execute(Action calldata action, bytes calldata signature) external {
// 1. Verify signature is from owner
require(
_verifySignature(action, signature) == owner,
"Invalid signature"
);
// 2. Check deadline hasn't passed
require(block.timestamp <= action.deadline, "Expired");
// 3. Check nonce hasn't been used (prevents replay)
require(!usedNonces[action.nonce], "Nonce already used");
usedNonces[action.nonce] = true;
// 4. Check sufficient assets arrived
require(
_getBalance() >= action.expectedAmount,
"Insufficient amount"
);
// 5. Pay fees to operators
_payFees(action.feeConfig);
// 6. Execute exactly what user signed
for (uint i = 0; i < action.calls.length; i++) {
_executeCall(action.calls[i]);
}
}
Edge Cases & Protections
What if the resolver never executes?
- User can execute the action themselves
- Or wait for another resolver
- Action remains valid until deadline
What if assets arrive but action fails?
- Assets stay in the Smart Account (user's account)
- User can withdraw directly with a new signature
- User retains full control
What if resolver tries to frontrun?
- Signature is specific to exact calldata
- Resolver can't modify the action
- MEV attacks don't affect user outcome
Summary
For Users
Complete security: Your funds can only be used exactly as you authorize. Resolvers are constrained by cryptographic proofs. You never lose custody.
For Developers
Simple integration: Users sign once, everything else is automatic. No need to handle gas tokens, network switching, or multi-step flows.